某线上Phoenix业务有28台RegionServer,实现类似Kylin的功能,存储多维聚合数据供下游查询。之前偶尔出现写入任务执行失败,查询响应时间长的问题。现在该业务要做机房迁移,但新机房资源不足,预计只能提供10台RegionServer服务器。经过优化,迁移后RegionServer降为16台,并将逐步降到10台(迁移是按照10台规模规划的,为了加快数据迁移速度临时借调几台),服务也比之前更稳定,查询速度更快,本文详细介绍Phoenix业务迁移优化过程:
迁移优化目标
最核心的目标是减机器数,降成本。次要目标是让服务更稳定,查询更快,吞吐能力更强。写入失败,查询响应时间长的问题,也要一并解决。
减少存储空间
做优化,首先是找到系统瓶颈在哪。而减机器,直接导致存储空间成为瓶颈。10台服务器所能提供的存储容量不够,需要减少数据量。要减少数据量,核心是业务层面做取舍,减少数据维度,缩短TTL,将数据量降到10台服务器所能支持的存储容量以内。那么技术层面可以做哪些优化呢?
替换压缩算法
将压缩算法由SNAPPY换成GZ,存储总量下降了30%,读写吞吐,延时没发现有变化。SNAPPY是谷歌极力推荐的数据压缩算法,在大数据领域广泛应用。而GZ是Phoenix官方推荐的大表压缩策略。两者有什么区别?SNAPPY的设计初衷是兼顾压缩速度和压缩率,两手抓两手都要硬。而GZ先使用L777算法压缩,压缩结果再用静态Huffman编码输出,压缩时间更长,压缩率更高。
为什么大数据领域,特别是离线业务,都在用SNAPPY?这是因为离线集群一般使用SATA盘,存储空间充足,而CPU更容易成为瓶颈,集群负载高时CPU利用率几乎打满。离线任务大多是连续的扫盘/写盘,执行时间受压缩/解压速度影响很大。由于SNAPPY压缩速度快,省CPU,所以较为适合HBase大表的场景。对应的,HBase集群现在一般使用SSD的服务器。SSD存储较贵,存储空间容易成为瓶颈。另外,因为用户写操作会在Memstore里做聚合,异步落盘(同步写的HLog一般不压缩),用户读操作大多能命中内存中的BlockCache,所以用户读写操作基本不受压缩/解压速度影响。HBase是轻计算的服务,CPU利用率经常在20%以下。所以使用GZ压缩策略,让空闲的CPU换昂贵的存储资源是非常划算的事情。
序列化行/列名
如果表的每行数据只写入一次,写入后不修改,那么可以将其定义为不可变表,对应表属 IMMUTABLE_ROWS=true。不可变表可以极大简化索引表的写入逻辑。不可变表有一个默认属性是SINGLE_CELL_ARRAY_WITH_OFFSETS,将一行的数据序列化后存到HBase的一个Cell里,在列较多时能显著降低存储量。官方建议如果大部分列有值非Null不经常修改,可以使用SINGLE_CELL_ARRAY_WITH_OFFSETS属性。但用户的写入逻辑会有多个写入任务写同一行数据的不同列。要变成一个任务写入全部列,修改代价较大,所以不可变表和行序列化的优化暂时无法使用。对列名进行编码也能降低存储量,之前用户用的Phoenix版本是不支持列名编码的,这次借这次迁移升级客户端的版本。COLUMN_ENCODED_BYTES = 1 最多支持255个列,由于业务场景使用大量动态列,列数超过225列,所以设置为2,支持255的2次方个列。
写入优化
写入场景要解决的核心问题是什么?
写热点引起的写入失败。
写入场景是Hive作业在凌晨那段时间集中往HBase里面灌数据。由于日期是业务查询唯一的必选项,所以表设计将日期字段放在Rowkey第一列。日期放在第一列就存在写热点的问题。虽然用64Salted打散,几千个Region每次写入热点集中在64个Region上面。而Region分配算法无法保证每次写的64个Region在RegionServer上均匀分布,有时出现某个RegionServer有较多的写热点Region,而且加上老集群的配置资源利用率不高,Flush刷入磁盘很多小文件。当RegionServer出现写热点时,热点Region生成很多HFile->Compact排队合并不过来->很多Region达到HFile数上限堵塞写->写任务执行失败。所以写入优化最重要的是解决写热点的问题,提高资源利用率。
1.修改RegionServer配置
前文HBase优化总结篇介绍相关参数优化内容,主要是hbase-site和hbase-env的配置优化,此业务做同样的优化。
2.修改表属性
SALT_BUCKETS = 256
MEMSTORE_FLUSHSIZE = 268435456
MAX_FILESIZE = 85899345920
hbase.hstore.engine.class = org.apache.hadoop.hbase.regionserver.DateTieredStoreEngine
既然64Salted依然有热点,那么调大到256。因为盐的数据结构是1byte前缀,所以Phoenix的最大盐是256。曾调研过Salted能否大于256,调研结果是因为大量逻辑默认Salted是1byte,要支持2byte需要修改50几处代码所以保留256的上限。那么Salted到底设置多少合适呢?加盐是为了缓解写热点现象。解决写热点的前提下,Salted应该尽可能小,也有观点认为加盐增大读并发,可以提高查询速度,所以盐应该越大越好,但是个人认为为了提高读并发而调大盐并不是一个正确的方向。查询的并发任务数应该由统计信息控制,按数据的逻辑分片切分,这样并发数只跟数据量有关。然而Salted过大在较精确查询时产生太多的小Scan查询反而变慢。特别是过滤条件含有in(大量Rowkey前缀字段枚举)的场景,枚举数 * Salted的并发数很可观。可能一个SQL实际要扫描的数据量并不多,但Scan数却达到了几千甚至上万个,查询时间反而变长。单条SQL的Scan数量太多影响Phoenix客户端整体吞吐能力,同时能执行的SQL数量下降,因此盐不宜过大。由于同时只有256个Region在写,调大Flush Size进一步缓解写放大现象。调高MAX_FILESIZE是为了避免分裂太多的Region。日期是前缀,每次Split Region的时候Rowkey较小的在TTL过期后就变成空Region,而Rowkey较大的继续分裂导致Region数持续膨胀。调大Region能够避免此问题,过大的配置可能影响查询,一定影响Compact,故而Compact策略也要调整。因为日期范围是查询的必选条件,并且越靠近现在的数据被查询的可能性越高,所以非常适合用DateTiered Compact策略,此策略将数据按时间窗口分层存储,近期数据查询速度更快。对于写入场景,DateTiered策略将HFile按时间窗口分层,Compact只需要合并同一层的HFile缓解Flush过快引起的Compact压力大避免写堵塞。需要注意的是,DateTiered相关参数在Phoenix建表时无法指定,需要先创建好Phoenix表,再用HBase的alter configuration命令添加。既然使用DateTiered策略,为了提高查询性能Phoenix层面需要使用ROW_TIMESTAMP类型主键,具体细节在下一章节介绍。
读优化
读优化就是要提高单条SQL的查询速度,提高系统吞吐能力,支持客户端服务同时运行更多的SQL。
1.修改表属性
GUIDE_POSTS_WIDTH = 838860800
UPDATE_CACHE_FREQUENCY = 3600000
逻辑分片由之前的400MB调整为800MB。对大表来说,逻辑分片会非常多,统计信息数据需要较多的缓存空间,甚至会引发性能问题(性能问题细节在客户端参数优化章节介绍),需要控制逻辑分片数量。分片调大需要考虑一个问题:是否会影响查询性能?由于95%的查询命中读缓存,在内存中数据扫描速度是非常快的,扫描800MB不是问题。即使个别查询需要扫描整个分片且没命中缓存要读HFile,问题也不大。分片计算的是未压缩数据大小,该业务GZ压缩率大概在10%左右,800MB逻辑数据对应的HFile数据大小在100MB以内。SSD读百兆文件的速度较快,并不是查询响应的瓶颈能够接受。测试发现由于单条SQL分片数减少一半,并发Scan也减少一半,缓解线程池队列的排队,查询速度反而有提升,系统吞吐能力也提升,应该调大分片。由于大表元数据较大,每次缓存失效后,重新加载元数据总会引起一个慢SQL。所以调大缓存刷新时间为1小时来缓解该问题。
2.使用ROW_TIMESTAMP字段类型
经常作为滤条件、时间类型的主键字段,可以定义成ROW_TIMESTAMP类型,Phoenix将该字段和timestamp绑定。查询条件如果有该字段的过滤,Scan调用setTimeRange方法设置时间戳的上下限,可以用时间戳过滤HFile文件,配合DateTiered策略使用效果最佳。所以将日期字段定义为ROW_TIMESTAMP。目前使用的Phoenix版本,当过滤条件有in(大量row_timestamp字段枚举值)时,计算min/max timestamp有问题,查询结果不正确。需要将日期的过滤条件修改为大于小于。和使用日期枚举值相比,用大于小于可以降低并发Scan数。但对查询较精确Rowkey前缀的场景,用日期枚举可以大幅缩小每个Scan扫描数据的范围。测试发现由于数据调整、参数优化的关系,BlockCache缓存能够存放近期数据,即使是较精确查询的场景,查询速度依然比之前快。所以对日期字段,暂时使用大于小于来代替in枚举过滤。
3.物理执行计划优化
某些场景优化后有几百倍性能提升,详见Phoenix优化的上一篇。
4.客户端参数优化
phoenix.query.threadPoolSize = 1024
phoenix.query.queueSize = 100000
phoenix.stats.cache.maxSize = 536870912
Phoenix客户端是业务方的服务,给下游用户提供在线查询功能。将查询线程数调大到1024,可以并发运行更多的Scan,提高单条SQL执行速度,也提高了系统的吞吐能力。优化前,并发查询任务较多时会报查询队列满的异常,几次参数调整,最终队列长度调到10万。统计信息缓存最大尺寸这个配置很有意思。调查慢SQL现象时发现,大表的统计信息大小会超过256MB默认值。执行查询SQL时,如果缓存中没有统计信息,会先查询其统计信息表。由于大表的统计信息超过了最大缓存限制,每次查询完统计信息就从缓存中移除。所以每次查询都要重新加载一遍统计信息,即使是explain操作也要10秒以上。调大缓存可以避免大表缓存被移除,从而提高查询响应速度。前面调大单个分片,减少数据量的操作,也可以减少统计信息的数据量。
遗留问题
有几个遗留问题待其他同事解决。
1.ROW_TIMESTAMP字段时间戳上下限计算错误
过滤条件含有in(较多天数)时,timestamp上下限计算的逻辑有bug,查询结果不正确。应该是简单的逻辑计算错误,需要修改相应的源码。如果社区master分支也有该问题,可以给社区提个Patch。
2.修改ROW_TIMESTAMP字段在Rowkey中的位置
之前的表设计把日期字段放在Rowkey第一位。但既然使用了ROW TIMESTAMP类型,尝试使用timestamp过滤文件的方式代替Rowkey前缀匹配。如果查询的日期范围和时间窗口分层一致,timestamp过滤和日期前缀过滤效果应该是差不多的,这样日期字段就不需要放在Rowkey的第一位。把日期字段放在rowkey相对靠后的位置,可以缓解写热点现象,也就不用设置较大的Salted。
3.元数据缓存失效定期产生一条慢SQL
缓存过期时间不宜太长,否则元数据更新后不能及时同步。但缓存过期后,下一条查询SQL需要重新加载元数据缓存。大表有几百MB的统计信息加载较慢,会定期产生一条慢SQL。Phoenix客户端需要提供自动更新元数据缓存的功能,定期更新指定表的缓存,避免影响用户查询。
4.group by逻辑错误
Phoenix将group操作下推到RegionServer上执行,其协处理器GroupedAggregateRegionObserver有两个逻辑分支,Key有序和无序。有序说明key的顺序和分组完全一致,处理逻辑只需按照Rowkey顺序扫描数据。相同分组就直接合并,遇到新分组说明上一个分组的数据已经扫描完成,将上一个分组的汇总数据返回给客户端,继续合并下一个分组。有序处理不需要缓存太多中间数据,空间代价较低。无序说明处理逻辑需要维护包含所有分组信息的map。如果分组太多超过了内存限制,Shuffle过程还需要落盘,反复数据置换,代价较高。Phoenix尽可能使用有序逻辑的设计理念是对的,但把很多不能保证Key有序的场景误认为有序,这会带来正确性问题,比如主键顺序是rowkey1, rowkey2, rowkey3..., 那么group by rowkey1, rowkey3是不能保证Key有序的,按照Key有序逻辑处理存在问题。再比如group by rowkey2,rowkey1,也是不能保证Key有序的。目前前者已经通过修改源码解决,后者让业务方修改sql为group by rowkey1, rowkey2绕过,需要源码层面做进一步优化。
5.hive/spark查询Phoenix时谓词下推
使用外部引擎查询Phoenix表,只下推查询列没有下推查询条件。很多查询从逻辑上来说是可以下推Rowkey前缀过滤条件,只扫描部分数据的。但没有下推只能全表扫描需要优化源码将Rowkey字段的过滤谓词推下去。如果要查询索引表,非Rowkey字段的谓词也需要尽可能推下去。
大家工作学习遇到HBase技术问题,把问题发布到HBase技术社区论坛http://hbase.group,欢迎大家论坛上面提问留言讨论。想了解更多HBase技术关注HBase技术社区公众号(微信号:hbasegroup),非常欢迎大家积极投稿。
技术社群
【HBase生态+Spark社区大群】
群福利:群内每周进行群直播技术分享及问答加入方式1:
https://dwz.cn/Fvqv066s?spm=a2c4e.11153940.blogcont688191.19.1fcd1351nOOPvI
加入方式2:钉钉扫码加入
免费试用
HBase初学者的福利来袭