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