hbase优化之旅(四)-regionserver从17优化到10台的小结

本文是前几篇文章的小结,介绍如何通过技术手段,挖掘hbase服务的潜力,将分组的服务器数从17台降低到10台。
首发于专栏 :https://zhuanlan.zhihu.com/p/60357239 

目录
确定优化目标
系统资源瓶颈和优化效果
cpu利用率
内存使用
网络IO
磁盘IO
存储量
稳定性
综述
优化细节
合并region
hbase参数调整
未来的工作
近期工作
中远期工作

确定优化目标
沟通交流后,业务方更看重降低成本。数据量梳理后略有降低,保证吞吐,无长期请求堆积前提下可以放宽延时要求。为了更快的进行优化,放宽稳定性可以要求接受短期波动。
另外,该分组的regionserver之前存在不稳定的问题,这次优化也一并解决。

系统资源瓶颈和优化效果
降低成本,即省机器,就是用更少的服务器满足业务需求。需要搞清楚单台服务器的瓶颈在哪。下面分析单台服务器的指标和优化效果。
cpu利用率
因为hbase是重IO,轻计算的服务,一般来说hbase服务cpu利用率较低。优化前单台cpu利用率平均在5%,峰值在10%左右。直接机器减半也没有压力。
空闲的cpu利用率,可以用来置换更高的存储压缩比,节约存储空间。更多的业务线程和gc线程,提高吞吐能力,减少gc对系统吞吐和延时毛刺的影响。
峰值cpu在40%以下,说明cpu资源充足富裕,可以减机器,用cpu换存储空间,提高业务吞吐。


内存使用
256GB内存,datanode 4g,regionserver堆内50g,堆外100g,空闲大概100g。可以提高单机regionserver内存到堆内100g,堆外120g,留10%的空闲。
提高内存的好处很多。单机负载不变的前提下,增加堆内内存,可以降低gc频率减少毛刺出现频率,降低gc时间占比提高吞吐(另外如果mixed gc周期能超过大多数region flush的周期,回收会更有效率)。增加memstore写缓存,可以降低写放大效应,从而降低网络和磁盘IO。降低写放大,还可以减少compact,提高缓存有效性从而增加缓存命中率,进一步降低读IO,减少读请求延时。增加堆外内存,可以缓存命中率。
举个例子,一台服务器提高内存使用量,同时region数变为其他服务器的2倍。堆外内存增加20%而region数翻倍,单纯数字上来看缓存命令率应该下降,实际稳定后命中率保持在96%高于其他服务器的94%,这就是降低compact提高命中率的收益。
增大内存的坏处是,单次gc时间会更长,极端情况下请求延时的毛刺会更明显。另外一些hbase内部的数据结构,如memstore的ConcurrentSkipListMap,对象数太多性能会下降的很厉害,需要结合用户需求,在region数,hbase-site参数,gc参数上综合考虑,做一个权衡。


网络IO
网络In 20MB,Out 40到100MB波动。万兆网卡峰值也就百MB以内,机器数砍半没压力。

磁盘IO
非major compact时段,磁盘读流量较高,高峰期200MB到300MB,主要来自compact。写流量50MB/s。磁盘流量较高是个瓶颈,需要技术优化。
磁盘读流量主要是本地regionserver 业务读和compact读,写流量主要是本地regionserver写wal,flush,compact,以及其他datanode的写副本。
网络in主要是业务写入请求和其他datanode写副本请求,网络out主要是业务查询响应和regionserver 写wal,flush,compact写其他datanode副本的请求。
由于业务查询主要走内存缓存(95%),hifile又有很高的压缩比(1:5左右),如果不考虑服务端filter过滤的情况,业务读引起的磁盘IO应该只是网络查询响应IO的百分之一。
用户的业务场景没用filter,所以可以推论读IO主要来自compact ,验证了写放大效应明显。
经过参数优化降低写放大效应,region数不变,单机磁盘读IO下降到之前的1/3,80MB左右。
又经过region合并,参数继续优化,单机两倍region负载,读Io保持在150MB到200MB之间,比之前单倍region负载还要低很多。

存储量
集群存储峰值大概有三个时间点,每周major compact前,每周major compact后,major compact中(大概是major compact前存储*(1+并发major region数/总region数))。
目前没有明确用哪个存储量衡量峰值,一般定期检查,发现用到差不多了就和用户沟通加机器。不允许独立分组存储总量大于分组机器所提供的存储总量。
在major compact前取了数据的总量,刚好跟10台的存储量差不多,可以降低到10台。

稳定性
如系列文章上一篇gc执行细节和参数调优方法论详所说,分组之前存在稳定性的问题。出现问题时需要人工操作迁移region,重启。当机器数减到17台,直接减机器立刻加剧了问题出现的频率。经分析,问题主要出在gc参数上,详见上一篇文章。

综述
通过瓶颈分析,解决了磁盘IO问题后,10台服务器是存储量瓶颈,所以第一步的目标是降到10台。



优化细节
合并region

之前分组17台,单机region数大概500上下,region数太多导致flush都是小文件,写放大罪魁祸首。合并小于10g 的region,将region数降低到单机130+,写放大效应立刻降低了,可以减少机器数了。
一个批量写请求,regionserver内写多个region是串行的,这个角度减少region数可以改善写延时。同时单个region memstore太大,ConcurrentSkipListMap的结构插入性能会降低。目前没发现减少region后明显的写延时差别。杭州的同事分享过阿里对memstore数据结构的优化,版本升级到1.4.8后可以评估能否用的到。
另外,大region会导致major compact压力更大。这个可以通过修改compact policy来解决。

hbase参数调整
增大内存

堆内存 50g->100g
堆外内存 100g->120g
大内存降低写放大效应,提高读缓存命中率,支持更高的吞吐能力。改善了gc表现。


基础配置
hbase.regionserver.handler.count 192->384
hbase.ipc.server.callqueue.read.share 无->0.4

hbase.ipc.server.callqueue.handler.factor 无->0.2
两倍region后,监控看当前handler经常达到192上限,按其他公司经验调大一倍。有时能打到上限。
线程多可以减少队列等待时间,可能增加请求处理时间,监控看,调大能显著减少队列等待时间p99,请求时间p99没有明显变化。
读写分离主要是能降低p99,避免慢写入/高并发scan等耗时长的操作堵塞句柄。192个句柄时,等待队列的p99有时会持续在500ms-1s左右。调大到384,增加读写分离,等待队列p99就维持在100ms以下了。
5个句柄使用一个队列,是为了降低高并发时的锁资源争抢。需要权衡队列比例,如果1,2个句柄就使用一个队列,很容易几个慢请求就把队列里的后续请求都堵住了。目前设置为5:1.

memstrore
hbase.hstore.flusher.count 15-->8 flush线程数
base.regionserver.optionalcacheflushinterval 无->7200000 (1小时到2小时)
hbase.regionserver.hlog.blocksize 无 -> 268435456 (实际值没变,由使用hdfs块大小变为显式设置)
hbase.regionserver.maxlogs 52 -> 200

除了regionserver重启时memstore flush外,有几种情况会触发memstore flush,分别是单个region的memstore达到上限,单个region的memstore距离上次flush过了刷新周期,hlogs达到上限flush涉及的还未flush的region,总memstore达到内存设置上限。这几种情况按照从好到差顺序排列,越靠后对系统的稳定性影响越高,应尽量避免。

堆内存的增加,直接增大了memstore内存上限,增大了单region flush的容量,可以刷大文件减少写放大效应。同时尽量让region写满128MB再flush,可以错开flush时间,错开compact时间,降低磁盘IO峰值,减少flush和compact排队现象。
10台均分region,调整后单台的memstore在10g到30g波动。为了尽量128MB在刷,其他几种flush方式的参数要跟着调整。

虽然我们要避免后面几种flush情况出现,但当业务突然有写入热点,或机器重启各region的memstore重置,可能会触发定时刷新/达到hlogs上限引起集中flush。为降低同时flush并发高引起的问题,降低了并发flush数。由于memstore足够大,单次flush周期长,即使控制flush并发,也不会充暴memstore内存造成写堵塞。
此外,控制flush数可以间接控制minor compact的压力。

按10台regionserver计算规模和请求量,白天绝大多数region在2个内memstore达到128MB,2小时刷新可以确保绝大多数region写满128MB自动刷新,减少写放大。如果按默认1小时,重启后的前2,3天里,会有很多region在相同时间flush,触发compact,系统压力很大。2,3天后,靠着刷新时间每次的随机波动才能慢慢分散开。所以应确保白天峰值写入量时,大多数region都能在刷新周期里写满flush。

hlogs过少会造成刷新周期太短。以之前经常延时变长的一台服务器为例,平均5到10分钟强刷一次,刷的都是小文件,带来了严重的读写放大后果。之前region数三倍于现在,和gc参数不合适一起造成了偶发的gc时间占比高影响业务的问题。 另外,目前hlogs 50的配置会造成同时flush大量region,同时compact,系统压力大,造成请求和吞吐的毛刺。
maxlogs的配置多大合适?maxlogs 从90,到120,150,200,250,300,350,400都实验过,越大的hlogs对缓解region写入不均,调大flush尺寸和周期越有利 。
单纯按照流传的公式来看, 下限 50 * 1024/(256*0.95)= 210,上限60 * 1024/(256*0.95)= 252,应该在210到252之间。实际由于各region会陆续触发flush,hlogs即使到达252,memstore总内存可能只有10-20g,依然可以调大。
hlogs上限提高能解决重启后同时flush密集的问题。重启后,各region memstore都是从0开始缓存,hlogs到上限时如果大量region没写满,会触发大量region同时 flush,这些region的memstore再次清零了,下个周期依然有大量region同时flush,打散速度慢于刷新周期触发的flush。hlogs上限出发的同时flush,compact对gc压力很大,请求延时会周期性显著提升。如果256MB flush,8台服务器,hlogs需要到400。128MB则200即可。
hlogs过多可能有什么影响?1.重启时间变长。重试时间长本质是总memstore增大,flush时间长造成的。之前17台规模单台重启大概1分钟,8台规模单台大概1分50秒,总时间并没有增加。再就是如果某台regionserver挂掉,集群要重读未flush的hlogs,hlogs多了会增加重读的量,增加集群负担。

综上所述,最终flush size定为128MB,hlogs上限定为200。

读cache
hfile.block.cache.size 0.19 ->0.2
hbase.bucketcache.size 102400 -> 132000
hbase.bucketcache.percentage.in.combinedcache 0.9 -> 0.85

缓存大小配合堆内堆外内存增加的调整,缓存变成堆外上限112g,堆内20g。L1和L2的比例按实际线上情况由0.9调整为0.85.


compact
hbase.regionserver.thread.compaction.small 6 -> 8
hbase.regionserver.thread.compaction.large 3 -> 4

略微提高minor compact 和major compact速度,尤其是major compact速度,以便机器减少到一半时,夜里能major完。调到12,gc压力过大,所以只是微调。

hdfs
dfs.client.hedged.read.threadpool.size 50 ->300 
dfs.client.hedged.read.threshold.millis 150->500
hbase.regionserver.hlog.slowsync.ms 无->400
compact负载一高,200线程池会报大量的线程池满,资源不够用,所以调到300。
我们用多路读是为了当磁盘故障时,可以读其他副本。如果超时时间太低,可以读本地的去读了远程副本,显著增大集群网络和磁盘IO。读包括compact的读,是轻延时重吞吐的,集群磁盘IO负载高,延时增加,触发多路读又增大了集群的IO压力。尤其是本地化不是100%时,会读其他机器上的副本,400毫秒也依然容易超时,所以超时时间改为500,确保在一般的高负载和非本地化场景中,也不会给集群额外的压力,只有磁盘真正故障堵塞读的时候再读其他副本上。由于95%以上的读都来自内存缓存,500毫秒的最大超时时间并不会造成显著的读请求延时升高,毕竟常态的gc也要几百ms时间。
负载稍微高点,日志文件满屏都是wal log slow,淹没了其他需要关注的问题。由于wal是单线程顺序写单文件,写入速度是有瓶颈的,调到400,只有负载较高时才会打印。

gc参数调整
请看上一篇,本文不再叙述。

未来的工作
近期工作
打散major compact执行时间

现在集群内所有业务分组同一天晚上进行major compact ,集群网络和磁盘IO压力大,存储量也会同时飙升。major compact即将打散到每周7天的晚上,以降低集群的压力。

换compact policy
compact policy优化,需要用户配合做客户端代码优化,和业务方暂定4月初共建。
现在所有业务用的都是默认的default policy,中规中矩。而该项目业务场景非常适合用Date Tiered Compaction,该策略能极大降低compact写放大效应尤其是major compact的压力,并且提升近期热点数据查询速度。
更换该策略,需要业务方略微修改客户端代码,读写时增加TTL的设置,否则可能会降低查询速度。

换压缩算法
换高压缩比算法gz能进一步减少存储总量,已有经验是可以降低30%左右。即存储量视角,可以降到7台服务器规模。换压缩算法会增加cpu利用率,可能对用户读写造成未知的影响,待4月和用户共建。


中远期工作
2副本

好处是存储量立刻降低1/3,坏处是集群同时坏2块盘的概率比坏3块高得多,更容易丢数据。一般是偏离线,稳定性要求不高,存储量偏高的业务,数据在hive有一份,即使有数据缺失可以很快从hive导一份。需要和用户进一步确定业务模式是否能采用。

超卖
目前成本分摊策略,独立业务分组按regionserver服务器数分摊成本,如果独立业务分组存储总量大于分组机器所提供的存储总量,即认为存储量达到瓶颈。就必须要增加机器,不同的业务瓶颈不一样,按存储量严格限制,不利于提升集群资源的整体利用率。
所以从整个集群的资源利用率来看,允许部分吞吐,延时要求不高的业务使用更多的存储,即存储超卖,可以更有效提高集群资源利用率,释放数据引擎潜力,降低业务方使用成本,为公司省钱。
超卖涉及三个维度。超卖存储每TB价格,超卖存储量如何计算。超卖比例,
现在每TB成本是整机打包折算的,而超卖的存储只涉及纯磁盘的折算成本。要超卖存储,需要额外提供超卖存储的每TB价格,这个价格应该比整机的每TB成本低一些。
超卖存储量可以用定时程序收集分组的hdfs存储量,保留最高值,除0.9作为该月实际存储用量,减去分组机器数提供的存储就是超卖量。
超卖比例,前期可以先允许超卖分组存储的20%,并监控集群整体磁盘利用率情况。超卖试运行一段时间后,如果集群存储依然空闲,可以尝试提高超卖比例上限。
目前成本分摊的方式,只支持业务分组按整机打包折算,没有超卖的分摊方式。待评估实际需求后,再来决定是否允许超卖。
如果有业务方需要过高的超卖比例,不适用于现在的集群架构,需要考虑ssd和sata混部的架构。

ssd和sata混部
目前业务按数据量和期望延时的不同分了两个集群,1个是ssd集群,一个是sata盘集群。ssd读写更快,但成本高,存储量有限。
业内最顶尖的做法,是允许ssd和sata盘混部,好处有2。一个是3副本一个是ssd2个是sata盘,保证性能的同时降低存储成本。另一个是时间相关数据按时间分块存储在不同介质上,例如支付宝账单,3个月内ssd,1年内sata盘,一年前的历史数据就存储在更便宜的存储介质上了。历史类数据都适合用这种方式。
混部的方式,在软件,集群架构,机器采购方面都有颇多挑战,是个长期的工作。
继续阅读 »
本文是前几篇文章的小结,介绍如何通过技术手段,挖掘hbase服务的潜力,将分组的服务器数从17台降低到10台。
首发于专栏 :https://zhuanlan.zhihu.com/p/60357239 

目录
确定优化目标
系统资源瓶颈和优化效果
cpu利用率
内存使用
网络IO
磁盘IO
存储量
稳定性
综述
优化细节
合并region
hbase参数调整
未来的工作
近期工作
中远期工作

确定优化目标
沟通交流后,业务方更看重降低成本。数据量梳理后略有降低,保证吞吐,无长期请求堆积前提下可以放宽延时要求。为了更快的进行优化,放宽稳定性可以要求接受短期波动。
另外,该分组的regionserver之前存在不稳定的问题,这次优化也一并解决。

系统资源瓶颈和优化效果
降低成本,即省机器,就是用更少的服务器满足业务需求。需要搞清楚单台服务器的瓶颈在哪。下面分析单台服务器的指标和优化效果。
cpu利用率
因为hbase是重IO,轻计算的服务,一般来说hbase服务cpu利用率较低。优化前单台cpu利用率平均在5%,峰值在10%左右。直接机器减半也没有压力。
空闲的cpu利用率,可以用来置换更高的存储压缩比,节约存储空间。更多的业务线程和gc线程,提高吞吐能力,减少gc对系统吞吐和延时毛刺的影响。
峰值cpu在40%以下,说明cpu资源充足富裕,可以减机器,用cpu换存储空间,提高业务吞吐。


内存使用
256GB内存,datanode 4g,regionserver堆内50g,堆外100g,空闲大概100g。可以提高单机regionserver内存到堆内100g,堆外120g,留10%的空闲。
提高内存的好处很多。单机负载不变的前提下,增加堆内内存,可以降低gc频率减少毛刺出现频率,降低gc时间占比提高吞吐(另外如果mixed gc周期能超过大多数region flush的周期,回收会更有效率)。增加memstore写缓存,可以降低写放大效应,从而降低网络和磁盘IO。降低写放大,还可以减少compact,提高缓存有效性从而增加缓存命中率,进一步降低读IO,减少读请求延时。增加堆外内存,可以缓存命中率。
举个例子,一台服务器提高内存使用量,同时region数变为其他服务器的2倍。堆外内存增加20%而region数翻倍,单纯数字上来看缓存命令率应该下降,实际稳定后命中率保持在96%高于其他服务器的94%,这就是降低compact提高命中率的收益。
增大内存的坏处是,单次gc时间会更长,极端情况下请求延时的毛刺会更明显。另外一些hbase内部的数据结构,如memstore的ConcurrentSkipListMap,对象数太多性能会下降的很厉害,需要结合用户需求,在region数,hbase-site参数,gc参数上综合考虑,做一个权衡。


网络IO
网络In 20MB,Out 40到100MB波动。万兆网卡峰值也就百MB以内,机器数砍半没压力。

磁盘IO
非major compact时段,磁盘读流量较高,高峰期200MB到300MB,主要来自compact。写流量50MB/s。磁盘流量较高是个瓶颈,需要技术优化。
磁盘读流量主要是本地regionserver 业务读和compact读,写流量主要是本地regionserver写wal,flush,compact,以及其他datanode的写副本。
网络in主要是业务写入请求和其他datanode写副本请求,网络out主要是业务查询响应和regionserver 写wal,flush,compact写其他datanode副本的请求。
由于业务查询主要走内存缓存(95%),hifile又有很高的压缩比(1:5左右),如果不考虑服务端filter过滤的情况,业务读引起的磁盘IO应该只是网络查询响应IO的百分之一。
用户的业务场景没用filter,所以可以推论读IO主要来自compact ,验证了写放大效应明显。
经过参数优化降低写放大效应,region数不变,单机磁盘读IO下降到之前的1/3,80MB左右。
又经过region合并,参数继续优化,单机两倍region负载,读Io保持在150MB到200MB之间,比之前单倍region负载还要低很多。

存储量
集群存储峰值大概有三个时间点,每周major compact前,每周major compact后,major compact中(大概是major compact前存储*(1+并发major region数/总region数))。
目前没有明确用哪个存储量衡量峰值,一般定期检查,发现用到差不多了就和用户沟通加机器。不允许独立分组存储总量大于分组机器所提供的存储总量。
在major compact前取了数据的总量,刚好跟10台的存储量差不多,可以降低到10台。

稳定性
如系列文章上一篇gc执行细节和参数调优方法论详所说,分组之前存在稳定性的问题。出现问题时需要人工操作迁移region,重启。当机器数减到17台,直接减机器立刻加剧了问题出现的频率。经分析,问题主要出在gc参数上,详见上一篇文章。

综述
通过瓶颈分析,解决了磁盘IO问题后,10台服务器是存储量瓶颈,所以第一步的目标是降到10台。



优化细节
合并region

之前分组17台,单机region数大概500上下,region数太多导致flush都是小文件,写放大罪魁祸首。合并小于10g 的region,将region数降低到单机130+,写放大效应立刻降低了,可以减少机器数了。
一个批量写请求,regionserver内写多个region是串行的,这个角度减少region数可以改善写延时。同时单个region memstore太大,ConcurrentSkipListMap的结构插入性能会降低。目前没发现减少region后明显的写延时差别。杭州的同事分享过阿里对memstore数据结构的优化,版本升级到1.4.8后可以评估能否用的到。
另外,大region会导致major compact压力更大。这个可以通过修改compact policy来解决。

hbase参数调整
增大内存

堆内存 50g->100g
堆外内存 100g->120g
大内存降低写放大效应,提高读缓存命中率,支持更高的吞吐能力。改善了gc表现。


基础配置
hbase.regionserver.handler.count 192->384
hbase.ipc.server.callqueue.read.share 无->0.4

hbase.ipc.server.callqueue.handler.factor 无->0.2
两倍region后,监控看当前handler经常达到192上限,按其他公司经验调大一倍。有时能打到上限。
线程多可以减少队列等待时间,可能增加请求处理时间,监控看,调大能显著减少队列等待时间p99,请求时间p99没有明显变化。
读写分离主要是能降低p99,避免慢写入/高并发scan等耗时长的操作堵塞句柄。192个句柄时,等待队列的p99有时会持续在500ms-1s左右。调大到384,增加读写分离,等待队列p99就维持在100ms以下了。
5个句柄使用一个队列,是为了降低高并发时的锁资源争抢。需要权衡队列比例,如果1,2个句柄就使用一个队列,很容易几个慢请求就把队列里的后续请求都堵住了。目前设置为5:1.

memstrore
hbase.hstore.flusher.count 15-->8 flush线程数
base.regionserver.optionalcacheflushinterval 无->7200000 (1小时到2小时)
hbase.regionserver.hlog.blocksize 无 -> 268435456 (实际值没变,由使用hdfs块大小变为显式设置)
hbase.regionserver.maxlogs 52 -> 200

除了regionserver重启时memstore flush外,有几种情况会触发memstore flush,分别是单个region的memstore达到上限,单个region的memstore距离上次flush过了刷新周期,hlogs达到上限flush涉及的还未flush的region,总memstore达到内存设置上限。这几种情况按照从好到差顺序排列,越靠后对系统的稳定性影响越高,应尽量避免。

堆内存的增加,直接增大了memstore内存上限,增大了单region flush的容量,可以刷大文件减少写放大效应。同时尽量让region写满128MB再flush,可以错开flush时间,错开compact时间,降低磁盘IO峰值,减少flush和compact排队现象。
10台均分region,调整后单台的memstore在10g到30g波动。为了尽量128MB在刷,其他几种flush方式的参数要跟着调整。

虽然我们要避免后面几种flush情况出现,但当业务突然有写入热点,或机器重启各region的memstore重置,可能会触发定时刷新/达到hlogs上限引起集中flush。为降低同时flush并发高引起的问题,降低了并发flush数。由于memstore足够大,单次flush周期长,即使控制flush并发,也不会充暴memstore内存造成写堵塞。
此外,控制flush数可以间接控制minor compact的压力。

按10台regionserver计算规模和请求量,白天绝大多数region在2个内memstore达到128MB,2小时刷新可以确保绝大多数region写满128MB自动刷新,减少写放大。如果按默认1小时,重启后的前2,3天里,会有很多region在相同时间flush,触发compact,系统压力很大。2,3天后,靠着刷新时间每次的随机波动才能慢慢分散开。所以应确保白天峰值写入量时,大多数region都能在刷新周期里写满flush。

hlogs过少会造成刷新周期太短。以之前经常延时变长的一台服务器为例,平均5到10分钟强刷一次,刷的都是小文件,带来了严重的读写放大后果。之前region数三倍于现在,和gc参数不合适一起造成了偶发的gc时间占比高影响业务的问题。 另外,目前hlogs 50的配置会造成同时flush大量region,同时compact,系统压力大,造成请求和吞吐的毛刺。
maxlogs的配置多大合适?maxlogs 从90,到120,150,200,250,300,350,400都实验过,越大的hlogs对缓解region写入不均,调大flush尺寸和周期越有利 。
单纯按照流传的公式来看, 下限 50 * 1024/(256*0.95)= 210,上限60 * 1024/(256*0.95)= 252,应该在210到252之间。实际由于各region会陆续触发flush,hlogs即使到达252,memstore总内存可能只有10-20g,依然可以调大。
hlogs上限提高能解决重启后同时flush密集的问题。重启后,各region memstore都是从0开始缓存,hlogs到上限时如果大量region没写满,会触发大量region同时 flush,这些region的memstore再次清零了,下个周期依然有大量region同时flush,打散速度慢于刷新周期触发的flush。hlogs上限出发的同时flush,compact对gc压力很大,请求延时会周期性显著提升。如果256MB flush,8台服务器,hlogs需要到400。128MB则200即可。
hlogs过多可能有什么影响?1.重启时间变长。重试时间长本质是总memstore增大,flush时间长造成的。之前17台规模单台重启大概1分钟,8台规模单台大概1分50秒,总时间并没有增加。再就是如果某台regionserver挂掉,集群要重读未flush的hlogs,hlogs多了会增加重读的量,增加集群负担。

综上所述,最终flush size定为128MB,hlogs上限定为200。

读cache
hfile.block.cache.size 0.19 ->0.2
hbase.bucketcache.size 102400 -> 132000
hbase.bucketcache.percentage.in.combinedcache 0.9 -> 0.85

缓存大小配合堆内堆外内存增加的调整,缓存变成堆外上限112g,堆内20g。L1和L2的比例按实际线上情况由0.9调整为0.85.


compact
hbase.regionserver.thread.compaction.small 6 -> 8
hbase.regionserver.thread.compaction.large 3 -> 4

略微提高minor compact 和major compact速度,尤其是major compact速度,以便机器减少到一半时,夜里能major完。调到12,gc压力过大,所以只是微调。

hdfs
dfs.client.hedged.read.threadpool.size 50 ->300 
dfs.client.hedged.read.threshold.millis 150->500
hbase.regionserver.hlog.slowsync.ms 无->400
compact负载一高,200线程池会报大量的线程池满,资源不够用,所以调到300。
我们用多路读是为了当磁盘故障时,可以读其他副本。如果超时时间太低,可以读本地的去读了远程副本,显著增大集群网络和磁盘IO。读包括compact的读,是轻延时重吞吐的,集群磁盘IO负载高,延时增加,触发多路读又增大了集群的IO压力。尤其是本地化不是100%时,会读其他机器上的副本,400毫秒也依然容易超时,所以超时时间改为500,确保在一般的高负载和非本地化场景中,也不会给集群额外的压力,只有磁盘真正故障堵塞读的时候再读其他副本上。由于95%以上的读都来自内存缓存,500毫秒的最大超时时间并不会造成显著的读请求延时升高,毕竟常态的gc也要几百ms时间。
负载稍微高点,日志文件满屏都是wal log slow,淹没了其他需要关注的问题。由于wal是单线程顺序写单文件,写入速度是有瓶颈的,调到400,只有负载较高时才会打印。

gc参数调整
请看上一篇,本文不再叙述。

未来的工作
近期工作
打散major compact执行时间

现在集群内所有业务分组同一天晚上进行major compact ,集群网络和磁盘IO压力大,存储量也会同时飙升。major compact即将打散到每周7天的晚上,以降低集群的压力。

换compact policy
compact policy优化,需要用户配合做客户端代码优化,和业务方暂定4月初共建。
现在所有业务用的都是默认的default policy,中规中矩。而该项目业务场景非常适合用Date Tiered Compaction,该策略能极大降低compact写放大效应尤其是major compact的压力,并且提升近期热点数据查询速度。
更换该策略,需要业务方略微修改客户端代码,读写时增加TTL的设置,否则可能会降低查询速度。

换压缩算法
换高压缩比算法gz能进一步减少存储总量,已有经验是可以降低30%左右。即存储量视角,可以降到7台服务器规模。换压缩算法会增加cpu利用率,可能对用户读写造成未知的影响,待4月和用户共建。


中远期工作
2副本

好处是存储量立刻降低1/3,坏处是集群同时坏2块盘的概率比坏3块高得多,更容易丢数据。一般是偏离线,稳定性要求不高,存储量偏高的业务,数据在hive有一份,即使有数据缺失可以很快从hive导一份。需要和用户进一步确定业务模式是否能采用。

超卖
目前成本分摊策略,独立业务分组按regionserver服务器数分摊成本,如果独立业务分组存储总量大于分组机器所提供的存储总量,即认为存储量达到瓶颈。就必须要增加机器,不同的业务瓶颈不一样,按存储量严格限制,不利于提升集群资源的整体利用率。
所以从整个集群的资源利用率来看,允许部分吞吐,延时要求不高的业务使用更多的存储,即存储超卖,可以更有效提高集群资源利用率,释放数据引擎潜力,降低业务方使用成本,为公司省钱。
超卖涉及三个维度。超卖存储每TB价格,超卖存储量如何计算。超卖比例,
现在每TB成本是整机打包折算的,而超卖的存储只涉及纯磁盘的折算成本。要超卖存储,需要额外提供超卖存储的每TB价格,这个价格应该比整机的每TB成本低一些。
超卖存储量可以用定时程序收集分组的hdfs存储量,保留最高值,除0.9作为该月实际存储用量,减去分组机器数提供的存储就是超卖量。
超卖比例,前期可以先允许超卖分组存储的20%,并监控集群整体磁盘利用率情况。超卖试运行一段时间后,如果集群存储依然空闲,可以尝试提高超卖比例上限。
目前成本分摊的方式,只支持业务分组按整机打包折算,没有超卖的分摊方式。待评估实际需求后,再来决定是否允许超卖。
如果有业务方需要过高的超卖比例,不适用于现在的集群架构,需要考虑ssd和sata混部的架构。

ssd和sata混部
目前业务按数据量和期望延时的不同分了两个集群,1个是ssd集群,一个是sata盘集群。ssd读写更快,但成本高,存储量有限。
业内最顶尖的做法,是允许ssd和sata盘混部,好处有2。一个是3副本一个是ssd2个是sata盘,保证性能的同时降低存储成本。另一个是时间相关数据按时间分块存储在不同介质上,例如支付宝账单,3个月内ssd,1年内sata盘,一年前的历史数据就存储在更便宜的存储介质上了。历史类数据都适合用这种方式。
混部的方式,在软件,集群架构,机器采购方面都有颇多挑战,是个长期的工作。 收起阅读 »

(1.3版)hbase优化之旅(三)-regionserver g1 gc执行细节和参数调优方法论详解

release note
1.3版:调整章节顺序,略微修改概述和标题
1.2版:增加目录信息
1.1版 :增加问题12,补充gc问题挂进程和优化方法论总结的章节,解释最终优化内容。
 
 
目录
本文亮点
gc调优效果
学习的起点-知道自己不知道what
g1 gc执行细节
参数调优方式论
增加日志打印
分析统计信息
分析gc日志
gc问题挂进程
1.写入过猛引起的进程挂
2.参数问题导致mixed gc连续出现长时间暂停
优化方法论小结
最终优化内容

 
本文亮点

讲g1原理和参数语义的文章很多,随便谷歌百度一下无数。但大多数都是对官方介绍的翻译转述。很少有文章介绍参数间彼此的影响,调整参数牵一发动全身的影响有哪些。参数调优的方法论,更是没人提及。

本文目标受众是对g1原理和参数有所了解,想进一步了解参数彼此间联系,想在参数调优方面更进一步的同学。

通过一个线上gc优化的实际案例,带你熟悉gc执行细节,了解我总结的参数调优方法论。

如果对g1不了解,可以先搜索了解一下g1原理和参数后再来看本文,会事半功倍。


gc调优效果

线上某分组的regionserver之前存在不稳定的问题。一两个月内总会随机出现1,2台机器,突然cpu飚高,写延时变高引起堆积,hbase日志和监控找不到原因,只能将问题regionserver移走换一台服务器代替。后来发现regionserver直接重启也能解决,怀疑regionserver配置有问题。

经调研和论证,确定是gc参数配置问题,参数优化后彻底解决了该问题。

由于服务器上还部署了datanode,存在一定的资源抢占,regionserver gc时间,间隔有一些波动,但大体是可衡量的。

之前50g堆,young区最大15g,6秒一次young gc,16线程暂停时间100ms左右。 现在100g堆,控制 20g young区,间隔8秒一次young gc,32线程暂停时间大概90ms。

优化前,大概7分钟一次mixed gc周期,优化后大概半小时。

优化前,mixed gc周期内头几次mixed gc之后,mixed gc快速恶化到400-1500毫秒之间,收尾的几次gc基本要秒级以上,gc时间占比短期内达到80%。优化后,mixed gc绝大多数在400ms以内,百分之几的概率在500-900毫秒范围,gc时间基本不会触发超过10%的告警日志打印。


学习的起点-知道自己不知道what 

看过文档,知道参数语义了,就可以进行参数调优了吗?

学习有这么几个阶段,什么都不知道,不知道自己不知道,知道自己不知道,知道自己知道。

如果在“不知道自己不知道”的阶段,误以为自己什么都知道,贸然调优,事倍功半,优化结果南辕北辙。

请看下面的问题,是否有明确的答案?


1.-XX:G1NewSizePercent=5,-XX:G1MaxNewSizePercent=60 是young区起始比例和最大比例的默认值。那么young区有最小比例吗,最小比例是多少?

2.young区的动态大小除了受-XX:MaxGCPauseMillis=100 单次gc最大暂停时间影响,受-XX:G1MaxNewSizePercent=60上限限制,还被其他因素影响吗?

3.-XX:InitiatingHeapOccupancyPercent=45是启动并发mixed gc的已用内存比例阈值,该比例的分母是当前堆内存。那么分子是old区已用,是young+old区已用,还是堆内存已用?

4.一次mixed gc周期内,mixed gc之间的间隔是怎样的,立刻执行下次,有固定时间间隔,还是受其他条件影响?

5.-XX:G1OldCSetRegionThresholdPercent=10 是单次mixed gc扫old区region数的比例。该比例的分母是old区已用region,young+old区已用region,还是堆总region?该比例设置10就会扫10%的region吗,是否受其他条件影响?

6.-XX:G1MixedGCCountTarget=8 一次mixed gc周期mixed gc的目标次数,该数值实际逻辑是什么,8就是8次mixed gc?

7.-XX:G1MixedGCLiveThresholdPercent=85 (其他版本默认值有可能90)要回收region的存活对象内存占比,存活越低回收效果越好。如果存活比例超过85%了,这次mixed gc,这次mixed迭代周期内就一定不会回收了吗?

8.很多技术文章都翻译了官方文档的这句话,mixed gc从可回收比例高的region开始回收。实际执行上是怎么做的?

9.-XX:G1MixedGCCountTarget=8 是一次mixed gc周期mixed gc的次数, -XX:G1OldCSetRegionThresholdPercent=10 是最大扫old区region数的比例,-XX:MaxGCPauseMillis=100 是期待的单次gc最大暂停时间。-XX:G1MixedGCLiveThresholdPercent=85 (其他版本默认值有可能90)是回收region的存活对象内存占比。这几个参数如何决定单次mixed gc清理多少region,一次mixed周期执行多少次mixed gc?

10.其他条件一样,-XX:MaxGCPauseMillis=100 一定比 -XX:MaxGCPauseMillis=200 的单次gc时间短吗?

11.阅读理解,下面三行mixed gc执行日志,说明存在哪些问题?

6949.921: [G1Ergonomics (CSet Construction) finish adding old regions to CSet, reason: predicted time is too high, predicted time: 6.95 ms, remaining time: 0.00 ms, old: 76 regions, min: 76 regions]
6949.921: [G1Ergonomics (CSet Construction) added expensive regions to CSet, reason: old CSet region num not reached min, old: 76 regions, expensive: 70 regions, min: 76 regions, remaining time: 0.00 ms]
6949.921: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 79 regions, survivors: 1 regions, old: 76 regions, predicted pause time: 770.76 ms, target pause time: 100.00 ms]

 
12.坊间传闻,g1内存越大回收效率越差,越不稳定。即使机器资源够分配100g的堆,也只分配50g。这么做有道理吗?

上面各个参数都标注了网上常见的参数解释,了解参数解释你能回答以上的问题吗?如果有疑惑,那么请看下一节。

 


g1 gc执行细节

-XX:G1NewSizePercent同时是young区最小比例。

young区上限还受old区大小影响,最大不超过90%(默认保留10%)-当前old区大小。当old区朝90%接近时,young区会持续减少直到下限。

-XX:InitiatingHeapOccupancyPercent比例的分子是old区已用。

mixed gc之间是以触发下限young区gc为间隔的,即下限eden区写满时,mixed gc同时清理young区和old区。

-XX:G1OldCSetRegionThresholdPercent比例的分母是堆总region数,100g堆32MB大小region,则总数3200个region,10%就是320个。该比例决定单次mixed gc扫描region数的上限。如果预期扫region时间高于预期剩余时间,则只会扫少量region计算活跃对象比例。

-XX:G1MixedGCCountTarget=8语义是,用触发mixed gc周期时old区的已用region数,除以8,作为每次mixed gc逻辑上最少回收的region数。

一次mixed gc扫描的region中,活跃对象比例高于-XX:G1MixedGCLiveThresholdPercent的region,叫做expensive region,清理代价高。

如果不昂贵的region数不小于最小回收数,那么回收所有不昂贵region,昂贵region按活跃比例在mixed gc周期内整体排序。如果扫描完所有old区region,垃圾比例依然高于-XX:G1HeapWastePercent比例,那么按活跃对象比逆序清理昂贵region,直到比例降低到阈值。

如果总region数大于最小region数但不昂贵region数不够min,则回收min个region,昂贵region中活跃对象比最低的region填补min的缺。

如果min大于实际扫的region,会回收本次mixed gc所有扫瞄过的region数,即使region的活跃对象比超过阈值。

如果-XX:MaxGCPauseMillis过低,预期扫region时间远大于预期剩余时间,那么实际扫的region会小于min数,即使扫的都是昂贵region,依然会全部回收,造成数秒级gc暂停,实际暂停时间反而比-XX:MaxGCPauseMillis大一些要长。

若干次mixed gc后,如果可回收占堆内存比例小于等于-XX:G1HeapWastePercent,本轮mixed gc周期结束。

 

综上所述,参数有密切的关联关系,调优需要全局权衡。

最后一个问题的日志,由于-XX:MaxGCPauseMillis过低只扫描了少量region,-XX:G1MixedGCCountTarget过低min region数高,昂贵region依然要被回收,暂停时间远大于预期到达秒级,下次扫的region更少,回收昂贵region难以释放内存,持续恶化。堆50g,young区下限2.5g ,间隔不到1秒一次mixed gc,gc时间占比很快超过80%。再加上偶发的memstore内存接近峰值,加上L1读cache,加上静态对象占用的,总不可释放内存比例很高,而-XX:InitiatingHeapOccupancyPercent比例过低,触发mixed gc周期时几乎拷贝了一遍old区所有region,内存也没释放多少空间,regionserver表现出持续的吞吐能力降低,服务不可用现象。
 
目前只遗留了一个问题,g1是否可以用大堆。容我卖个关子,读者可以结合上面的执行细节,先自己认真思考一下,再往下看。

 
参数调优方式论

授人以鱼不如授人以渔,我们先来探讨调优的方法论。

调优方法论是,先整体分析gc运行状况,找到瓶颈点或怀疑有问题的地方。仔细翻阅问题发生时间的gc日志,找到有问题的信息,调优。继续观察。


增加日志打印

调优首先要清楚gc运行状况,上一篇gc探索分享里介绍了如何加打印参数,以及如何通过gceasy可视化统计信息。如果没阅读请先看上一篇相关内容。需要额外注意的是,gceasy对g1支持的有点小bug,gc暂停时间图把mixed gc统计到young gc次数里了。如有图里有暂停时间比-XX:MaxGCPauseMillis高一个数量级的暂停时间,都是mixed gc的。

 
分析统计信息

通过gceasy,我们可以看到gc平均暂停时间,最大暂停时间,应用吞吐占比。gc暂停时间分布,gc前后堆内存变化,gc暂停时间按时间周期的分布图,单次gc垃圾回收内存量的分布图,young区容量分布图,old区容量分布图,young区 promoted分布图,gc内部各阶段时间统计对比,对象创建和升代速度。

通过这些统计信息我们可以了解系统的运行情况,如young 区在多大范围波动,平均young区 gc间隔大概是多久,升代量是多少,mixed gc周期的间隔,每次mixed gc周期回收的old 区内存,等等。掌握这些统计信息,我们就对系统运行情况有了基本了解,可以对系统是否健康做一个初步判断。

不健康主要有两大类,单次暂停时间过长,gc时间占比高。这两个问题又可以引申出单次gc暂停时间过长的次数太多,gc时间占比过高的频率高,mixed gc频率高,每次mixed回收old区内存量太低,等等。

我的经验是,50g或100g的堆内存,如果gc时间占比超过5%,或者gc最大暂停时间超过5秒出现很多次,都是有问题的,需要优化。

如果gc日志跨越好几天,周期性gc时间占比高,但平均gc时间占比可能并不高。如果某一时间段吞吐,延时有问题,可以将这个时间段前后半小时的gc日志截出来,单独上传gceasy。

只看统计信息还不够。本人遇到过日志周期跨越好几天,平均gc时间占比不到2%,最大暂停时间1秒110毫秒,但图表上来看1秒上下的暂停总是连续出现,伴随着周期性业务响应变慢。仔细查看gc日志,gc时间占比在短时间内达到了70%以上,周期性出现。所以需要进一步分析gc日志。


分析gc日志

gc日志内容很多,包括每次gc前堆情况,gc后堆情况,young和mixed gc执行情况。那么我们如何分析呢?

还是要对症下药,找问题出现时前后的日志。

如果暂停时间周期性或偶发出现比预期高一个数量级,可以根据统计信息看到的长时间暂停时间。搜日志,如搜索 real=5找暂停时间5秒到6秒的 ,找到gc上下文,分析为什么慢。


常见以下几种问题

1. max小于等于min

2194074.642: [G1Ergonomics (CSet Construction) finish adding old regions to CSet, reason: old CSet region num reached max, old: 80 regions, max: 80 regions]
2194074.642: [G1Ergonomics (CSet Construction) added expensive regions to CSet, reason: old CSet region num not reached min, old: 80 regions, expensive: 62 regions, min: 105 regions, remaining time: 0.00 ms]
2194074.642: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 79 regions, survivors: 1 regions, old: 80 regions, predicted pause time: 374.58 ms, target pause time: 100.00 ms]

没啥说的,改大-XX:G1MixedGCCountTarget,提高max/min的比例吧。我的经验是min最好控制在max的1/4以内,留冗余给昂贵region。

 

2.predicted time is too high

 60998.873: [G1Ergonomics (CSet Construction) finish adding old regions to CSet, reason: predicted time is too high, predicted time: 308.81 ms, remaining time: 0.00 ms, old: 28 regions, min: 28 regions]
 
 60998.873: [G1Ergonomics (CSet Construction) added expensive regions to CSet, reason: old CSet region num not reached min, old: 28 regions, expensive: 27 regions, min: 28 regions, remaining time:0.00 ms]

 

由于期望gc执行时间短,预期时间太长,只会扫很少的old区region,甚至可能比min region还少,当遇到连续的高代价region,即使是100%活跃的region也要拷贝,执行时间更长,下次预期时间更长,扫的region数更少,进入恶性循环。单次mixed gc释放不了多少内存,gc时间占比越来越高,有zk超时风险。

本质上,这是设置期望时间太短反而造成暂停时间更长,需要放宽期望gc执行时间,减少young 区最小值,以增大回收old区的可用时间。降低-XX:G1OldCSetRegionThresholdPercent比例以降低预期时间。内存使用上,让不可回收内存比例低一些,避免高存活比例region连续出现的概率,即增大堆内存,增大old区回收阈值,控制memstore,block cache L1的尺寸。要注意的是,memstore增大可以降低写放大,降低磁盘读写IO,增大L1缓存可以提高读缓存命中率。所以这不单单是gc自己的问题,要系统性综合考虑,确定系统的瓶颈究竟是什么,优化哪个问题更重要。

 

3.reclaimable percentage not over threshold

61007.913: [G1Ergonomics (CSet Construction) finish adding old regions to CSet, reason: reclaimable percentage not over threshold, old: 24 regions, max: 320 regions, reclaimable: 16101191472 bytes (15.00 %), threshold: 15.00 %]
 
 61007.913: [G1Ergonomics (CSet Construction) added expensive regions to CSet, reason: old CSet region num not reached min, old: 24 regions, expensive: 24 regions, min: 28 regions, remaining time:0.00 ms]

 
到达垃圾保留比例,最后一次mixed gc只会扫很少的region,如果正好都是昂贵的region,则拷贝代价很高。
运气不好的话,这几乎是不可避免的。调大-XX:G1OldCSetRegionThresholdPercent理论上可以让最后一次扫region的平均数量变大,但会造成predicted time is too high更频繁出现。增加堆内存上限和old区回收阈值,提高-XX:G1HeapWastePercent比例,可以更早结束垃圾mixed gc周期,最后一次扫描都是昂贵region的概率也降低了。调大-XX:G1MixedGCCountTarget 让min region更少,可能每次回收量减少一次回收周期时间拉长,需要配合更高的垃圾浪费率和更低的-XX:G1MixedGCLiveThresholdPercent比例,达到快速清理的效果。


gc问题挂进程

gc问题的极端后果是进程挂掉。一般经验认为,内存增加比释放快,内存不足,full gc, oom,进程就挂了。

我遇到过多次gc引起进程挂掉,但目前还没遇到过g1的oom,甚至都没遇到过g1的full gc。这是因为regionserver内存模型young区升代比例很低,另外g1在恶劣条件下gc时间占比很高,即使regionserver压力很大,还没到full gc,就gc时间占比过高引起zk session超时退出了。下面举两个例子。

1.写入过猛引起的进程挂

业务方补一年的数据,hadoop作业写入过猛,还有热点,flush一直排队,compact一直排队,甚至触发hfile上限堵塞写了。写的p99一度飙升到30秒,young gc一次升old区很多,old区内存增长比正常快。结果还没到old区触发mixed gc,由于young gc达到了1秒2,3次,gc时间占比一度超过了95%,开始出现zk session超时,regionserver自动退出。

可以调大region hfile数上限来缓解,但治标不治本。需要控制用户写入,加quota来限制。

 

2.参数问题导致mixed gc连续出现长时间暂停

regionserver有一定压力,在承受范围内而进程挂了。这是由于参数设置有问题,由于期待暂停时间过低扫的region数不够多,又都是不可回收region,暂停时间越来越长,几次达到8,9秒暂停后,zk session超时退出。

这个按上面的redicted time is too high问题来优化即可。

 

优化方法论小结

1.-XX:ParallelGCThreads官方推荐是逻辑cpu核数的5/8,注意逻辑cpu核数是物理核的2倍,所以24核可以开到32,young和mixed gc确实变快了。

2.参数调整确保每轮mixed gc的max region数是min region数的4倍以上,降低都是昂贵region的几率。

3.适量增加-XX:MaxGCPauseMillis反而可以降低mixed gc的暂停时间。目的是留给扫描region充足时间,确保每轮mixed gc扫描的region数和期待的max region数相似。

4.如果不想young gc时间也同步变长,可以通过-XX:G1MaxNewSizePercent降低young区最大比例来控制young gc时间。

5.降低最小young 区比例,可以降低mixed gc时回收young 区的时间,从而增加扫描old区region时间,确保扫描更多region。

6.触发mixed gc周期时,old区可回收内存比例越高,越不容易遇到连续昂贵ergion,回收越有效率。所以应该调大堆内存,调高mixed gc触发阈值,控制不可回收内存比例(即memstore和L1 block cache)。

7.当前面条件都满足时,每次mixed gc周期可回收内存比例很高,每轮mixed gc扫描的region数几倍于min region有充足的region挑选不昂贵ergion,可以调高-XX:G1HeapWastePercent比例让本轮mixed gc尽快结束,降低-XX:G1MixedGCLiveThresholdPercent优先回收活跃对象更少的region。


最终优化内容

-Xmx100g -Xms100g   50g -> 100g
-XX:MaxDirectMemorySize= 100g -> 120g
-XX:ConcGCThreads= 4 -> 8
-XX:ParallelGCThreads= 16 -> 32
-XX:G1NewSizePercent= 5 -> 3
-XX:G1MaxNewSizePercent= 60 -> 20
-XX:G1MixedGCCountTarget= 8 -> 64
-XX:G1OldCSetRegionThresholdPercent= 10 -> 4
-XX:InitiatingHeapOccupancyPercent=   65 ->80
-XX:G1HeapWastePercent= 5 -> 20
-XX:G1MixedGCLiveThresholdPercent= 85 -> 80

-XX:G1HeapWastePercent= 5 -> 20
-XX:G1MixedGCLiveThresholdPercent= 85 -> 80

 

 

 
 
继续阅读 »
release note
1.3版:调整章节顺序,略微修改概述和标题
1.2版:增加目录信息
1.1版 :增加问题12,补充gc问题挂进程和优化方法论总结的章节,解释最终优化内容。
 
 
目录
本文亮点
gc调优效果
学习的起点-知道自己不知道what
g1 gc执行细节
参数调优方式论
增加日志打印
分析统计信息
分析gc日志
gc问题挂进程
1.写入过猛引起的进程挂
2.参数问题导致mixed gc连续出现长时间暂停
优化方法论小结
最终优化内容

 
本文亮点

讲g1原理和参数语义的文章很多,随便谷歌百度一下无数。但大多数都是对官方介绍的翻译转述。很少有文章介绍参数间彼此的影响,调整参数牵一发动全身的影响有哪些。参数调优的方法论,更是没人提及。

本文目标受众是对g1原理和参数有所了解,想进一步了解参数彼此间联系,想在参数调优方面更进一步的同学。

通过一个线上gc优化的实际案例,带你熟悉gc执行细节,了解我总结的参数调优方法论。

如果对g1不了解,可以先搜索了解一下g1原理和参数后再来看本文,会事半功倍。


gc调优效果

线上某分组的regionserver之前存在不稳定的问题。一两个月内总会随机出现1,2台机器,突然cpu飚高,写延时变高引起堆积,hbase日志和监控找不到原因,只能将问题regionserver移走换一台服务器代替。后来发现regionserver直接重启也能解决,怀疑regionserver配置有问题。

经调研和论证,确定是gc参数配置问题,参数优化后彻底解决了该问题。

由于服务器上还部署了datanode,存在一定的资源抢占,regionserver gc时间,间隔有一些波动,但大体是可衡量的。

之前50g堆,young区最大15g,6秒一次young gc,16线程暂停时间100ms左右。 现在100g堆,控制 20g young区,间隔8秒一次young gc,32线程暂停时间大概90ms。

优化前,大概7分钟一次mixed gc周期,优化后大概半小时。

优化前,mixed gc周期内头几次mixed gc之后,mixed gc快速恶化到400-1500毫秒之间,收尾的几次gc基本要秒级以上,gc时间占比短期内达到80%。优化后,mixed gc绝大多数在400ms以内,百分之几的概率在500-900毫秒范围,gc时间基本不会触发超过10%的告警日志打印。


学习的起点-知道自己不知道what 

看过文档,知道参数语义了,就可以进行参数调优了吗?

学习有这么几个阶段,什么都不知道,不知道自己不知道,知道自己不知道,知道自己知道。

如果在“不知道自己不知道”的阶段,误以为自己什么都知道,贸然调优,事倍功半,优化结果南辕北辙。

请看下面的问题,是否有明确的答案?


1.-XX:G1NewSizePercent=5,-XX:G1MaxNewSizePercent=60 是young区起始比例和最大比例的默认值。那么young区有最小比例吗,最小比例是多少?

2.young区的动态大小除了受-XX:MaxGCPauseMillis=100 单次gc最大暂停时间影响,受-XX:G1MaxNewSizePercent=60上限限制,还被其他因素影响吗?

3.-XX:InitiatingHeapOccupancyPercent=45是启动并发mixed gc的已用内存比例阈值,该比例的分母是当前堆内存。那么分子是old区已用,是young+old区已用,还是堆内存已用?

4.一次mixed gc周期内,mixed gc之间的间隔是怎样的,立刻执行下次,有固定时间间隔,还是受其他条件影响?

5.-XX:G1OldCSetRegionThresholdPercent=10 是单次mixed gc扫old区region数的比例。该比例的分母是old区已用region,young+old区已用region,还是堆总region?该比例设置10就会扫10%的region吗,是否受其他条件影响?

6.-XX:G1MixedGCCountTarget=8 一次mixed gc周期mixed gc的目标次数,该数值实际逻辑是什么,8就是8次mixed gc?

7.-XX:G1MixedGCLiveThresholdPercent=85 (其他版本默认值有可能90)要回收region的存活对象内存占比,存活越低回收效果越好。如果存活比例超过85%了,这次mixed gc,这次mixed迭代周期内就一定不会回收了吗?

8.很多技术文章都翻译了官方文档的这句话,mixed gc从可回收比例高的region开始回收。实际执行上是怎么做的?

9.-XX:G1MixedGCCountTarget=8 是一次mixed gc周期mixed gc的次数, -XX:G1OldCSetRegionThresholdPercent=10 是最大扫old区region数的比例,-XX:MaxGCPauseMillis=100 是期待的单次gc最大暂停时间。-XX:G1MixedGCLiveThresholdPercent=85 (其他版本默认值有可能90)是回收region的存活对象内存占比。这几个参数如何决定单次mixed gc清理多少region,一次mixed周期执行多少次mixed gc?

10.其他条件一样,-XX:MaxGCPauseMillis=100 一定比 -XX:MaxGCPauseMillis=200 的单次gc时间短吗?

11.阅读理解,下面三行mixed gc执行日志,说明存在哪些问题?

6949.921: [G1Ergonomics (CSet Construction) finish adding old regions to CSet, reason: predicted time is too high, predicted time: 6.95 ms, remaining time: 0.00 ms, old: 76 regions, min: 76 regions]
6949.921: [G1Ergonomics (CSet Construction) added expensive regions to CSet, reason: old CSet region num not reached min, old: 76 regions, expensive: 70 regions, min: 76 regions, remaining time: 0.00 ms]
6949.921: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 79 regions, survivors: 1 regions, old: 76 regions, predicted pause time: 770.76 ms, target pause time: 100.00 ms]

 
12.坊间传闻,g1内存越大回收效率越差,越不稳定。即使机器资源够分配100g的堆,也只分配50g。这么做有道理吗?

上面各个参数都标注了网上常见的参数解释,了解参数解释你能回答以上的问题吗?如果有疑惑,那么请看下一节。

 


g1 gc执行细节

-XX:G1NewSizePercent同时是young区最小比例。

young区上限还受old区大小影响,最大不超过90%(默认保留10%)-当前old区大小。当old区朝90%接近时,young区会持续减少直到下限。

-XX:InitiatingHeapOccupancyPercent比例的分子是old区已用。

mixed gc之间是以触发下限young区gc为间隔的,即下限eden区写满时,mixed gc同时清理young区和old区。

-XX:G1OldCSetRegionThresholdPercent比例的分母是堆总region数,100g堆32MB大小region,则总数3200个region,10%就是320个。该比例决定单次mixed gc扫描region数的上限。如果预期扫region时间高于预期剩余时间,则只会扫少量region计算活跃对象比例。

-XX:G1MixedGCCountTarget=8语义是,用触发mixed gc周期时old区的已用region数,除以8,作为每次mixed gc逻辑上最少回收的region数。

一次mixed gc扫描的region中,活跃对象比例高于-XX:G1MixedGCLiveThresholdPercent的region,叫做expensive region,清理代价高。

如果不昂贵的region数不小于最小回收数,那么回收所有不昂贵region,昂贵region按活跃比例在mixed gc周期内整体排序。如果扫描完所有old区region,垃圾比例依然高于-XX:G1HeapWastePercent比例,那么按活跃对象比逆序清理昂贵region,直到比例降低到阈值。

如果总region数大于最小region数但不昂贵region数不够min,则回收min个region,昂贵region中活跃对象比最低的region填补min的缺。

如果min大于实际扫的region,会回收本次mixed gc所有扫瞄过的region数,即使region的活跃对象比超过阈值。

如果-XX:MaxGCPauseMillis过低,预期扫region时间远大于预期剩余时间,那么实际扫的region会小于min数,即使扫的都是昂贵region,依然会全部回收,造成数秒级gc暂停,实际暂停时间反而比-XX:MaxGCPauseMillis大一些要长。

若干次mixed gc后,如果可回收占堆内存比例小于等于-XX:G1HeapWastePercent,本轮mixed gc周期结束。

 

综上所述,参数有密切的关联关系,调优需要全局权衡。

最后一个问题的日志,由于-XX:MaxGCPauseMillis过低只扫描了少量region,-XX:G1MixedGCCountTarget过低min region数高,昂贵region依然要被回收,暂停时间远大于预期到达秒级,下次扫的region更少,回收昂贵region难以释放内存,持续恶化。堆50g,young区下限2.5g ,间隔不到1秒一次mixed gc,gc时间占比很快超过80%。再加上偶发的memstore内存接近峰值,加上L1读cache,加上静态对象占用的,总不可释放内存比例很高,而-XX:InitiatingHeapOccupancyPercent比例过低,触发mixed gc周期时几乎拷贝了一遍old区所有region,内存也没释放多少空间,regionserver表现出持续的吞吐能力降低,服务不可用现象。
 
目前只遗留了一个问题,g1是否可以用大堆。容我卖个关子,读者可以结合上面的执行细节,先自己认真思考一下,再往下看。

 
参数调优方式论

授人以鱼不如授人以渔,我们先来探讨调优的方法论。

调优方法论是,先整体分析gc运行状况,找到瓶颈点或怀疑有问题的地方。仔细翻阅问题发生时间的gc日志,找到有问题的信息,调优。继续观察。


增加日志打印

调优首先要清楚gc运行状况,上一篇gc探索分享里介绍了如何加打印参数,以及如何通过gceasy可视化统计信息。如果没阅读请先看上一篇相关内容。需要额外注意的是,gceasy对g1支持的有点小bug,gc暂停时间图把mixed gc统计到young gc次数里了。如有图里有暂停时间比-XX:MaxGCPauseMillis高一个数量级的暂停时间,都是mixed gc的。

 
分析统计信息

通过gceasy,我们可以看到gc平均暂停时间,最大暂停时间,应用吞吐占比。gc暂停时间分布,gc前后堆内存变化,gc暂停时间按时间周期的分布图,单次gc垃圾回收内存量的分布图,young区容量分布图,old区容量分布图,young区 promoted分布图,gc内部各阶段时间统计对比,对象创建和升代速度。

通过这些统计信息我们可以了解系统的运行情况,如young 区在多大范围波动,平均young区 gc间隔大概是多久,升代量是多少,mixed gc周期的间隔,每次mixed gc周期回收的old 区内存,等等。掌握这些统计信息,我们就对系统运行情况有了基本了解,可以对系统是否健康做一个初步判断。

不健康主要有两大类,单次暂停时间过长,gc时间占比高。这两个问题又可以引申出单次gc暂停时间过长的次数太多,gc时间占比过高的频率高,mixed gc频率高,每次mixed回收old区内存量太低,等等。

我的经验是,50g或100g的堆内存,如果gc时间占比超过5%,或者gc最大暂停时间超过5秒出现很多次,都是有问题的,需要优化。

如果gc日志跨越好几天,周期性gc时间占比高,但平均gc时间占比可能并不高。如果某一时间段吞吐,延时有问题,可以将这个时间段前后半小时的gc日志截出来,单独上传gceasy。

只看统计信息还不够。本人遇到过日志周期跨越好几天,平均gc时间占比不到2%,最大暂停时间1秒110毫秒,但图表上来看1秒上下的暂停总是连续出现,伴随着周期性业务响应变慢。仔细查看gc日志,gc时间占比在短时间内达到了70%以上,周期性出现。所以需要进一步分析gc日志。


分析gc日志

gc日志内容很多,包括每次gc前堆情况,gc后堆情况,young和mixed gc执行情况。那么我们如何分析呢?

还是要对症下药,找问题出现时前后的日志。

如果暂停时间周期性或偶发出现比预期高一个数量级,可以根据统计信息看到的长时间暂停时间。搜日志,如搜索 real=5找暂停时间5秒到6秒的 ,找到gc上下文,分析为什么慢。


常见以下几种问题

1. max小于等于min

2194074.642: [G1Ergonomics (CSet Construction) finish adding old regions to CSet, reason: old CSet region num reached max, old: 80 regions, max: 80 regions]
2194074.642: [G1Ergonomics (CSet Construction) added expensive regions to CSet, reason: old CSet region num not reached min, old: 80 regions, expensive: 62 regions, min: 105 regions, remaining time: 0.00 ms]
2194074.642: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 79 regions, survivors: 1 regions, old: 80 regions, predicted pause time: 374.58 ms, target pause time: 100.00 ms]

没啥说的,改大-XX:G1MixedGCCountTarget,提高max/min的比例吧。我的经验是min最好控制在max的1/4以内,留冗余给昂贵region。

 

2.predicted time is too high

 60998.873: [G1Ergonomics (CSet Construction) finish adding old regions to CSet, reason: predicted time is too high, predicted time: 308.81 ms, remaining time: 0.00 ms, old: 28 regions, min: 28 regions]
 
 60998.873: [G1Ergonomics (CSet Construction) added expensive regions to CSet, reason: old CSet region num not reached min, old: 28 regions, expensive: 27 regions, min: 28 regions, remaining time:0.00 ms]

 

由于期望gc执行时间短,预期时间太长,只会扫很少的old区region,甚至可能比min region还少,当遇到连续的高代价region,即使是100%活跃的region也要拷贝,执行时间更长,下次预期时间更长,扫的region数更少,进入恶性循环。单次mixed gc释放不了多少内存,gc时间占比越来越高,有zk超时风险。

本质上,这是设置期望时间太短反而造成暂停时间更长,需要放宽期望gc执行时间,减少young 区最小值,以增大回收old区的可用时间。降低-XX:G1OldCSetRegionThresholdPercent比例以降低预期时间。内存使用上,让不可回收内存比例低一些,避免高存活比例region连续出现的概率,即增大堆内存,增大old区回收阈值,控制memstore,block cache L1的尺寸。要注意的是,memstore增大可以降低写放大,降低磁盘读写IO,增大L1缓存可以提高读缓存命中率。所以这不单单是gc自己的问题,要系统性综合考虑,确定系统的瓶颈究竟是什么,优化哪个问题更重要。

 

3.reclaimable percentage not over threshold

61007.913: [G1Ergonomics (CSet Construction) finish adding old regions to CSet, reason: reclaimable percentage not over threshold, old: 24 regions, max: 320 regions, reclaimable: 16101191472 bytes (15.00 %), threshold: 15.00 %]
 
 61007.913: [G1Ergonomics (CSet Construction) added expensive regions to CSet, reason: old CSet region num not reached min, old: 24 regions, expensive: 24 regions, min: 28 regions, remaining time:0.00 ms]

 
到达垃圾保留比例,最后一次mixed gc只会扫很少的region,如果正好都是昂贵的region,则拷贝代价很高。
运气不好的话,这几乎是不可避免的。调大-XX:G1OldCSetRegionThresholdPercent理论上可以让最后一次扫region的平均数量变大,但会造成predicted time is too high更频繁出现。增加堆内存上限和old区回收阈值,提高-XX:G1HeapWastePercent比例,可以更早结束垃圾mixed gc周期,最后一次扫描都是昂贵region的概率也降低了。调大-XX:G1MixedGCCountTarget 让min region更少,可能每次回收量减少一次回收周期时间拉长,需要配合更高的垃圾浪费率和更低的-XX:G1MixedGCLiveThresholdPercent比例,达到快速清理的效果。


gc问题挂进程

gc问题的极端后果是进程挂掉。一般经验认为,内存增加比释放快,内存不足,full gc, oom,进程就挂了。

我遇到过多次gc引起进程挂掉,但目前还没遇到过g1的oom,甚至都没遇到过g1的full gc。这是因为regionserver内存模型young区升代比例很低,另外g1在恶劣条件下gc时间占比很高,即使regionserver压力很大,还没到full gc,就gc时间占比过高引起zk session超时退出了。下面举两个例子。

1.写入过猛引起的进程挂

业务方补一年的数据,hadoop作业写入过猛,还有热点,flush一直排队,compact一直排队,甚至触发hfile上限堵塞写了。写的p99一度飙升到30秒,young gc一次升old区很多,old区内存增长比正常快。结果还没到old区触发mixed gc,由于young gc达到了1秒2,3次,gc时间占比一度超过了95%,开始出现zk session超时,regionserver自动退出。

可以调大region hfile数上限来缓解,但治标不治本。需要控制用户写入,加quota来限制。

 

2.参数问题导致mixed gc连续出现长时间暂停

regionserver有一定压力,在承受范围内而进程挂了。这是由于参数设置有问题,由于期待暂停时间过低扫的region数不够多,又都是不可回收region,暂停时间越来越长,几次达到8,9秒暂停后,zk session超时退出。

这个按上面的redicted time is too high问题来优化即可。

 

优化方法论小结

1.-XX:ParallelGCThreads官方推荐是逻辑cpu核数的5/8,注意逻辑cpu核数是物理核的2倍,所以24核可以开到32,young和mixed gc确实变快了。

2.参数调整确保每轮mixed gc的max region数是min region数的4倍以上,降低都是昂贵region的几率。

3.适量增加-XX:MaxGCPauseMillis反而可以降低mixed gc的暂停时间。目的是留给扫描region充足时间,确保每轮mixed gc扫描的region数和期待的max region数相似。

4.如果不想young gc时间也同步变长,可以通过-XX:G1MaxNewSizePercent降低young区最大比例来控制young gc时间。

5.降低最小young 区比例,可以降低mixed gc时回收young 区的时间,从而增加扫描old区region时间,确保扫描更多region。

6.触发mixed gc周期时,old区可回收内存比例越高,越不容易遇到连续昂贵ergion,回收越有效率。所以应该调大堆内存,调高mixed gc触发阈值,控制不可回收内存比例(即memstore和L1 block cache)。

7.当前面条件都满足时,每次mixed gc周期可回收内存比例很高,每轮mixed gc扫描的region数几倍于min region有充足的region挑选不昂贵ergion,可以调高-XX:G1HeapWastePercent比例让本轮mixed gc尽快结束,降低-XX:G1MixedGCLiveThresholdPercent优先回收活跃对象更少的region。


最终优化内容

-Xmx100g -Xms100g   50g -> 100g
-XX:MaxDirectMemorySize= 100g -> 120g
-XX:ConcGCThreads= 4 -> 8
-XX:ParallelGCThreads= 16 -> 32
-XX:G1NewSizePercent= 5 -> 3
-XX:G1MaxNewSizePercent= 60 -> 20
-XX:G1MixedGCCountTarget= 8 -> 64
-XX:G1OldCSetRegionThresholdPercent= 10 -> 4
-XX:InitiatingHeapOccupancyPercent=   65 ->80
-XX:G1HeapWastePercent= 5 -> 20
-XX:G1MixedGCLiveThresholdPercent= 85 -> 80

-XX:G1HeapWastePercent= 5 -> 20
-XX:G1MixedGCLiveThresholdPercent= 85 -> 80

 

 

 
  收起阅读 »

hbase优化之旅(二)regionserver的G1 GC优化探索

优化的最终目的是保障现有用户体验的同时,减少机器,节约成本。

为了更好的编写本文和后续文章,花费20美金。欢迎去附录链接专栏赞赏支持。

g1介绍
g1特点


g1原理见附录官方文档,本文假设读者对jvm gc和g1原理有基本的了解。

g1特点是内存分片(一般1024片),支持动态调整young区大小,old区使用mixed gc方式分成多次小gc,尽量减少单次gc STW(stop the world)暂停时间,让gc对应用延迟的影响在预期范围内。

g1适用场景

对平均响应时间,最大响应时间有严格要求的应用系统,如hbase regionserver。

优化原则
先优化业务层和应用层


系统调优是从业务到实现,从整体到局部,从架构到具体组件的。在进行gc调优之前,我们应该确保业务层和应用层已经评估优化过。业务层和应用层的优化一般来说更容易有收益,我们不能指望一个架构设计有缺陷,应用层代码有很多已知问题的系统,通过gc调优一劳永逸。

gc调优3选2原则

先来看一下衡量gc的指标有哪些。对应用吞吐量的影响(一般是gc对cpu的消耗),对延迟的影响,总内存占用(gc触发时留有内存业务可以继续,留有内存做对象拷贝碎片整理等操作,不能oom)。

GC调优3选2原则: 在吞吐量、延迟、内存占用上,我们只能选择其中两个进行调优,无法三者兼得。

hbase已有业务regionserver的调优目标

在调优之前,必须要有明确的性能优化目标, 然后找到未达到该目标的性能瓶颈。再针对瓶颈做优化。通过各种监控和统计工具,确认调优后的应用是否已经达到相关目标。

hbase集群启用了group分组,重要业务有独立的regionserver分组。

重要业务regionserver的调优目标是,在满足业务延迟要求的基础上,用尽量低的成本,满足业务吞吐量的峰值需求。

也就是说,总吞吐量固定,延迟要求固定,单机cpu和内存固定,求最小机器数。

再转换一下,对单机来说,延迟指标确定,将单机吞吐在单机cpu和内存允许的范围内调整到最大。

需要说明的是,单机能承担多少吞吐,跟业务访问模型,region数,读写缓存参数,网络IO,磁盘IO都有关系。业务和hbase参数的调整应该在gc优化之前进行,网络和磁盘IO一般是应用层优化的。所以下文假设业务层和应用层已优化完毕,网络和磁盘都不是瓶颈,只聚焦在gc参数调优。

本文假设我们换算后的延迟目标是平均gc暂停时间100ms,最大暂停时间2s,gc时间占比3%以内。实际达到这个目标后,还要通过regionserver监控确定请求的延时要是否在用户用户的要求范围内。

影响延迟的因素

gc的时间占比。平均stw gc时间,频率。毛刺stw gc时间,频率。峰值stw gc时间,频率。

一般来说,regionserver应该避免full gc。

新生代越大,单次young gc时间越长,频率越低。

mixed gc受gc触发时机,gc并发线程数,预期迭代次数,每个迭代回收分片比例等多个参数影响,详见附录官方文档。

关于JVM版本

目前生产环境用1.8.0_77, 小米hbase环境用1.8.0_111, Oracle jdk的8最新版本是8u201。

intel性能测试见附录,jdk7不同版本间g1性能差距很大。Jdk7u21升级到jdk7u60,gc以及stw gc的平均时间,最大时间,时间分布都有大幅优化。

所以应该尽量用最新版本的JDK。

优化步骤

需要有方法论判断当前是否应该继续优化。

根据业务对延迟的需求,比较现在的请求延迟和gc情况,预估能接受的平均gc暂停时间,最大gc 暂停时间范围。

关掉自动balance,给一台regionserver少量增加region从而增加单机吞吐。当请求延迟超过用户要求的警戒线后,分析gc日志,找到瓶颈,优化降低gc延迟从而降低请求延迟,以便继续增加region。

当单机region数过多(可以考虑合并region),cpu负载过高,请求延迟无法降下来,任意一个条件满足,单机优化结束。稳定运行一段时间后,尝试将优化推广到整个业务分组。

增加日志


要分析gc情况一定要有gc日志。之前的日志参数如下
-XX:+PrintGCDetails gc细节
-XX:+PrintGCDateStamps 时间戳
-Xloggc:${HBASE_LOG_DIR}/gc-`date +'%Y%m%d%H%M'` gc文件格式
-XX:+UseGCLogFileRotation gc文件循环
-XX:NumberOfGCLogFiles=10 文件数
-XX:GCLogFileSize=512M 文件大小
-XX:+HeapDumpOnOutOfMemoryError oom时堆dump
-XX:HeapDumpPath=${HBASE_LOG_DIR}/hbase.heapdump dump目录
-XX:ErrorFile=${HBASE_LOG_DIR}/hs_err_pid%p.log
-XX:+PrintAdaptiveSizePolicy 打印自适应收集的大小
-XX:+PrintFlagsFinal 打印参数值

参考其他优化的文章,增加打印参数
-XX:+PrintGCApplicationStoppedTime    打印垃圾回收期间程序暂停的时间
-XX:+PrintTenuringDistribution https://www.jianshu.com/p/e634955f3bbb survivor分布情况
-XX:+PrintHeapAtGC gc前后打印堆信息
-XX:+PrintSafepointStatistics https://blog.csdn.net/u0119182 ... 47159 分析安全点统计信息,优化gc参考
-XX:PrintSafepointStatisticsCount=1
-XX:PrintFLSStatistics=1 打印每次GC前后内存碎片的统计信息,统计信息主要包括3个维度:Free Space、Max Chunk Size和Num Chunks。似乎cms更有用

gc日志太多可能会影响性能,目前没有日志对性能影响的数据,暂不考虑日志对性能的影响。


可视化

有很多gc可视化的工具。比如在线的gceasy https://gceasy.io/index.jsp#banner,上传gc日志可以进行分析。免费功能有各种图表展示。20美金一个月可以使用高级功能。

本文用gceasy做例子,其他可视化工具介绍见附录。

下面从前文优化过的节点开始,分析gc日志。








 
问题分析
gceasy优化建议


不花钱怎么变强?开启gceasy高级功能。


单次gc暂停最长到5秒了。链接给出了优化建议,见附录。

首先是优化程序降低对象创建速度。现在平均对象创建速度是1.22gb/sec。减少对象创建速度主要是更多用堆外内存,优化写放大,本文重点是gc优化,不展开。

再是年轻代太小,增大年轻代。这规则适用于其他gc,而g1 young gc是动态的,mixed gc是分次迭代的,young gc不能太大,否则young gc反而会比单次mixed慢。

再就是设置期待的最大暂停时间,目前设置100ms,暂不修改。

服务器上进程多,内存交换,服务器无这个情况。

gc线程不够多,有可能。但gc3秒以上的只有6次,差不多每天一次。如果是线程数明显不够次数应该更多,先放放。

服务器高IO导致的等待时间长。有可能,有datanode,需要进一步分析gc日志和IO监控。

没有禁用system.gc()。gc case表格来看没有相关调用。

堆太大。有可能,毕竟80GB堆。但大堆对吞吐有好处。

看5秒暂停gc的具体日志,
{Heap before GC invocations=50103 (full 0):
garbage-first heap total 83886080K, used 48760053K [0x00007faec4000000, 0x00007faec6005000, 0x00007fc2c4000000)
region size 32768K, 128 young (4194304K), 5 survivors (163840K)
Metaspace used 50945K, capacity 51410K, committed 52168K, reserved 53248K
2019-02-18T17:48:53.570+0800: 365568.567: [GC pause (G1 Evacuation Pause) (mixed)
Desired survivor size 268435456 bytes, new threshold 1 (max 1)
- age 1: 87997400 bytes, 87997400 total
365568.567: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 3313913, predicted base time: 1151.36 ms, remaining time: 0.00 ms, target pause time: 100.00 ms]
365568.567: [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 123 regions, survivors: 5 regions, predicted young region time: 50.17 ms]
365568.583: [G1Ergonomics (CSet Construction) finish adding old regions to CSet, reason: reclaimable percentage not over threshold, old: 70 regions, max: 256 regions, reclaimable: 8557735352 bytes (9.96 %), threshold: 10.00 %]
365568.583: [G1Ergonomics (CSet Construction) added expensive regions to CSet, reason: old CSet region num not reached min, old: 70 regions, expensive: 70 regions, min: 80 regions, remaining time: 0.00 ms]
365568.583: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 123 regions, survivors: 5 regions, old: 70 regions, predicted pause time: 4353.85 ms, target pause time: 100.00 ms]
365573.866: [G1Ergonomics (Heap Sizing) attempt heap expansion, reason: recent GC overhead higher than threshold after GC, recent GC overhead: 40.96 %, threshold: 10.00 %, uncommitted: 0 bytes, calculated expansion amount: 0 bytes (20.00 %)]
365573.866: [G1Ergonomics (Mixed GCs) do not continue mixed GCs, reason: reclaimable percentage not over threshold, candidate old regions: 407 regions, reclaimable: 8557735352 bytes (9.96 %), threshold: 10.00 %]
, 5.2999017 secs]
[Parallel Time: 5227.7 ms, GC Workers: 16]
[GC Worker Start (ms): Min: 365568584.3, Avg: 365568584.3, Max: 365568584.4, Diff: 0.1]
[Ext Root Scanning (ms): Min: 1.3, Avg: 1.6, Max: 3.9, Diff: 2.7, Sum: 25.1]
[Update RS (ms): Min: 1065.2, Avg: 1067.2, Max: 1067.8, Diff: 2.6, Sum: 17075.4]
[Processed Buffers: Min: 739, Avg: 831.5, Max: 879, Diff: 140, Sum: 13304]
[Scan RS (ms): Min: 3610.8, Avg: 3611.6, Max: 3612.6, Diff: 1.8, Sum: 57786.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.3, Diff: 0.3, Sum: 0.5]
[Object Copy (ms): Min: 545.2, Avg: 546.3, Max: 547.2, Diff: 2.0, Sum: 8741.5]
[Termination (ms): Min: 0.1, Avg: 0.6, Max: 0.8, Diff: 0.7, Sum: 9.8]
[Termination Attempts: Min: 1, Avg: 465.3, Max: 552, Diff: 551, Sum: 7445]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.0, Sum: 0.4]
[GC Worker Total (ms): Min: 5227.3, Avg: 5227.4, Max: 5227.5, Diff: 0.2, Sum: 83638.8]
[GC Worker End (ms): Min: 365573811.8, Avg: 365573811.8, Max: 365573811.8, Diff: 0.0]
[Code Root Fixup: 0.4 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 3.4 ms]
[Other: 68.4 ms]
[Choose CSet: 16.3 ms]
[Ref Proc: 3.6 ms]
[Ref Enq: 0.2 ms]
[Redirty Cards: 8.7 ms]
[Humongous Register: 0.1 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 31.6 ms]
[Eden: 3936.0M(3936.0M)->0.0B(3904.0M) Survivors: 160.0M->192.0M Heap: 46.5G(80.0G)->42.3G(80.0G)]
Heap after GC invocations=50104 (full 0):
garbage-first heap total 83886080K, used 44323410K [0x00007faec4000000, 0x00007faec6005000, 0x00007fc2c4000000)
region size 32768K, 6 young (196608K), 6 survivors (196608K)
Metaspace used 50945K, capacity 51410K, committed 52168K, reserved 53248K
}
[Times: user=85.30 sys=0.00, real=5.30 secs]
2019-02-18T17:48:58.871+0800: 365573.867: Total time for which application threads were stopped: 5.3131110 seconds, Stopping threads took: 0.0005771 seconds
2019-02-18T17:48:58.896+0800: 365573.893: Total time for which application threads were stopped: 0.0186168 seconds, Stopping threads took: 0.0005541 seconds
2019-02-18T17:48:58.906+0800: 365573.902: Total time for which application threads were stopped: 0.0078098 seconds, Stopping threads took: 0.0005988 seconds
2019-02-18T17:48:58.915+0800: 365573.912: Total time for which application threads were stopped: 0.0075271 seconds, Stopping threads took: 0.0003996 seconds
2019-02-18T17:48:58.923+0800: 365573.919: Total time for which application threads were stopped: 0.0072014 seconds, Stopping threads took: 0.0003114 seconds
2019-02-18T17:48:58.934+0800: 365573.930: Total time for which application threads were stopped: 0.0078706 seconds, Stopping threads took: 0.0005490 seconds
2019-02-18T17:48:58.942+0800: 365573.938: Total time for which application threads were stopped: 0.0073198 seconds, Stopping threads took: 0.0003927 seconds
2019-02-18T17:48:58.964+0800: 365573.960: Total time for which application threads were stopped: 0.0078810 seconds, Stopping threads took: 0.0007641 seconds
2019-02-18T17:48:58.972+0800: 365573.969: Total time for which application threads were stopped: 0.0075881 seconds, Stopping threads took: 0.0006277 seconds
2019-02-18T17:48:58.981+0800: 365573.978: Total time for which application threads were stopped: 0.0081290 seconds, Stopping threads took: 0.0011246 seconds
2019-02-18T17:48:58.992+0800: 365573.989: Total time for which application threads were stopped: 0.0076556 seconds, Stopping threads took: 0.0005358 seconds
2019-02-18T17:48:59.015+0800: 365574.011: Total time for which application threads were stopped: 0.0076750 seconds, Stopping threads took: 0.0005602 seconds
2019-02-18T17:48:59.026+0800: 365574.022: Total time for which application threads were stopped: 0.0089086 seconds, Stopping threads took: 0.0006000 seconds
2019-02-18T17:48:59.044+0800: 365574.041: Total time for which application threads were stopped: 0.0087554 seconds, Stopping threads took: 0.0006332 seconds
2019-02-18T17:48:59.054+0800: 365574.050: Total time for which application threads were stopped: 0.0084038 seconds, Stopping threads took: 0.0004326 seconds
{Heap before GC invocations=50104 (full 0):
garbage-first heap total 83886080K, used 48321106K [0x00007faec4000000, 0x00007faec6005000, 0x00007fc2c4000000)
region size 32768K, 128 young (4194304K), 6 survivors (196608K)
Metaspace used 50946K, capacity 51410K, committed 52168K, reserved 53248K

从日志看,期望survivor区有大概256MB,实际只有32*5=160MB,可能会将eden区直接提到old区。

近期gc时间占比过高,到40%了。

清理young 区的gc线程耗时过长,清理后可回收内存低于10%,mixed gc结束,其他gc线程空闲。说明mixed gc触发阈值设置低了,young gc参数也需要调整。

看暂停时间大于2秒的其他日志,都是触发mixed gc,但只有一个清理young和个别old区region的线程工作。确实说明mixed gc触发阈值设置低了,young gc过大了。
 
 


有41次gc的系统时间大于用户时间,大概3小时一次。

操作系统层面有可能是操作系统层异常,或者系统太忙了。出现频率大概3小时一次,看ganglia日志问题出现时cpu负载并不高。并且系统时间只是略高于用户时间不是明显有问题的数量级差距。先不考虑操作系统和JVM有bug的情况,暂不处理。

 

还是说有5秒的gc,同上。


修改参数
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:+UnlockExperimentalVMOptions
-XX:+ParallelRefProcEnabled
-XX:ConcGCThreads=4
-XX:ParallelGCThreads=16
-XX:G1NewSizePercent=5
-XX:G1MaxNewSizePercent=20
-XX:MaxTenuringThreshold=1
-XX:G1HeapRegionSize=32m
-XX:G1MixedGCCountTarget=16
-XX:InitiatingHeapOccupancyPercent=60
-XX:MaxDirectMemorySize=110g
-XX:G1OldCSetRegionThresholdPercent=10
-XX:G1HeapWastePercent=10
-Xmx80g
-Xms80g


从gc前后堆内存图来看,-XX:InitiatingHeapOccupancyPercent=65,52GB更合适。

暂停时间平均135ms,从暂停时间图来看,young gc时间较长,需要调小young gc区,小米经验改初始值,由5改成2。-XX:G1NewSizePercent=2

mixed gc 大多数一轮不到8次,暂停时间基本都超过100ms了。期待每轮16次的参数没起作用,这是由于每轮清理的region数过多。-XX:G1OldCSetRegionThresholdPercent=5,由10改成5,减少单次mixed gc时间。

survivor区应该大于young日志中期待的,Set SurvivorRatio from 8 (default) to 4。
-XX:InitiatingHeapOccupancyPercent=65
-XX:G1NewSizePercent=2
-XX:G1OldCSetRegionThresholdPercent=5
-XX:SurvivorRatio=4



设置其他必要参数

参考各类优化资料,增加以下参数
-XX:+UseStringDeduplication 字符串去重,提高性能
-XX:-ResizePLAB 减少gc线程间通信的东西,关闭动态提升本地buffer
-XX:+PerfDisableSharedMem 关掉统计信息的内存映射。开启在某些特殊场景下,会极大增加gc暂停时间

优化结果




平均暂停时间,最大暂停时间,暂停时间分布,gc吞吐占比都有提高,优化成功。

接下来迭代提高这台服务器region数,做gc优化。

 
遗留问题

偶发系统时间大于用户时间的问题,依然存在。发生时系统负载并不高,有可能操作系统和JVM层面有优化空间。

gceasy建议不要用-XX:+UseGCLogFileRotation,理由是历史会看不到,文件顺序不容易分辨,多个文件不利于gc分析。但这个参数意义是控制总文件大小,所以依然保留。

附录
g1官方介绍


https://www.oracle.com/technet ... .html

其他可视化工具介绍

https://blog.csdn.net/qq_32447 ... 67984

https://blog.csdn.net/xuelinme ... 90115

https://blog.csdn.net/renfufei ... 78064

gceasy优化建议

https://blog.gceasy.io/2016/11 ... uses/

https://blog.gceasy.io/2016/12 ... time/

https://blog.gceasy.io/2016/12 ... pped/

https://blog.gceasy.io/2018/12 ... tion/

https://blog.gceasy.io/2019/01 ... tion/

优化参考

https://www.oracle.com/technet ... .html

https://blogs.apache.org/hbase ... hbase

https://software.intel.com/en- ... hbase

http://openinx.github.io/ppt/h ... 8.pdf

https://blog.csdn.net/songhaif ... 77612

https://blog.csdn.net/maosijun ... 62489

https://dzone.com/articles/g1g ... ags-1

http://www.evanjones.ca/jvm-mmap-pause.html

之前的优化探索
 http://www.hbase.group/article/192
 
专栏地址
 
https://zhuanlan.zhihu.com/c_178811296
继续阅读 »
优化的最终目的是保障现有用户体验的同时,减少机器,节约成本。

为了更好的编写本文和后续文章,花费20美金。欢迎去附录链接专栏赞赏支持。

g1介绍
g1特点


g1原理见附录官方文档,本文假设读者对jvm gc和g1原理有基本的了解。

g1特点是内存分片(一般1024片),支持动态调整young区大小,old区使用mixed gc方式分成多次小gc,尽量减少单次gc STW(stop the world)暂停时间,让gc对应用延迟的影响在预期范围内。

g1适用场景

对平均响应时间,最大响应时间有严格要求的应用系统,如hbase regionserver。

优化原则
先优化业务层和应用层


系统调优是从业务到实现,从整体到局部,从架构到具体组件的。在进行gc调优之前,我们应该确保业务层和应用层已经评估优化过。业务层和应用层的优化一般来说更容易有收益,我们不能指望一个架构设计有缺陷,应用层代码有很多已知问题的系统,通过gc调优一劳永逸。

gc调优3选2原则

先来看一下衡量gc的指标有哪些。对应用吞吐量的影响(一般是gc对cpu的消耗),对延迟的影响,总内存占用(gc触发时留有内存业务可以继续,留有内存做对象拷贝碎片整理等操作,不能oom)。

GC调优3选2原则: 在吞吐量、延迟、内存占用上,我们只能选择其中两个进行调优,无法三者兼得。

hbase已有业务regionserver的调优目标

在调优之前,必须要有明确的性能优化目标, 然后找到未达到该目标的性能瓶颈。再针对瓶颈做优化。通过各种监控和统计工具,确认调优后的应用是否已经达到相关目标。

hbase集群启用了group分组,重要业务有独立的regionserver分组。

重要业务regionserver的调优目标是,在满足业务延迟要求的基础上,用尽量低的成本,满足业务吞吐量的峰值需求。

也就是说,总吞吐量固定,延迟要求固定,单机cpu和内存固定,求最小机器数。

再转换一下,对单机来说,延迟指标确定,将单机吞吐在单机cpu和内存允许的范围内调整到最大。

需要说明的是,单机能承担多少吞吐,跟业务访问模型,region数,读写缓存参数,网络IO,磁盘IO都有关系。业务和hbase参数的调整应该在gc优化之前进行,网络和磁盘IO一般是应用层优化的。所以下文假设业务层和应用层已优化完毕,网络和磁盘都不是瓶颈,只聚焦在gc参数调优。

本文假设我们换算后的延迟目标是平均gc暂停时间100ms,最大暂停时间2s,gc时间占比3%以内。实际达到这个目标后,还要通过regionserver监控确定请求的延时要是否在用户用户的要求范围内。

影响延迟的因素

gc的时间占比。平均stw gc时间,频率。毛刺stw gc时间,频率。峰值stw gc时间,频率。

一般来说,regionserver应该避免full gc。

新生代越大,单次young gc时间越长,频率越低。

mixed gc受gc触发时机,gc并发线程数,预期迭代次数,每个迭代回收分片比例等多个参数影响,详见附录官方文档。

关于JVM版本

目前生产环境用1.8.0_77, 小米hbase环境用1.8.0_111, Oracle jdk的8最新版本是8u201。

intel性能测试见附录,jdk7不同版本间g1性能差距很大。Jdk7u21升级到jdk7u60,gc以及stw gc的平均时间,最大时间,时间分布都有大幅优化。

所以应该尽量用最新版本的JDK。

优化步骤

需要有方法论判断当前是否应该继续优化。

根据业务对延迟的需求,比较现在的请求延迟和gc情况,预估能接受的平均gc暂停时间,最大gc 暂停时间范围。

关掉自动balance,给一台regionserver少量增加region从而增加单机吞吐。当请求延迟超过用户要求的警戒线后,分析gc日志,找到瓶颈,优化降低gc延迟从而降低请求延迟,以便继续增加region。

当单机region数过多(可以考虑合并region),cpu负载过高,请求延迟无法降下来,任意一个条件满足,单机优化结束。稳定运行一段时间后,尝试将优化推广到整个业务分组。

增加日志


要分析gc情况一定要有gc日志。之前的日志参数如下
-XX:+PrintGCDetails gc细节
-XX:+PrintGCDateStamps 时间戳
-Xloggc:${HBASE_LOG_DIR}/gc-`date +'%Y%m%d%H%M'` gc文件格式
-XX:+UseGCLogFileRotation gc文件循环
-XX:NumberOfGCLogFiles=10 文件数
-XX:GCLogFileSize=512M 文件大小
-XX:+HeapDumpOnOutOfMemoryError oom时堆dump
-XX:HeapDumpPath=${HBASE_LOG_DIR}/hbase.heapdump dump目录
-XX:ErrorFile=${HBASE_LOG_DIR}/hs_err_pid%p.log
-XX:+PrintAdaptiveSizePolicy 打印自适应收集的大小
-XX:+PrintFlagsFinal 打印参数值

参考其他优化的文章,增加打印参数
-XX:+PrintGCApplicationStoppedTime    打印垃圾回收期间程序暂停的时间
-XX:+PrintTenuringDistribution https://www.jianshu.com/p/e634955f3bbb survivor分布情况
-XX:+PrintHeapAtGC gc前后打印堆信息
-XX:+PrintSafepointStatistics https://blog.csdn.net/u0119182 ... 47159 分析安全点统计信息,优化gc参考
-XX:PrintSafepointStatisticsCount=1
-XX:PrintFLSStatistics=1 打印每次GC前后内存碎片的统计信息,统计信息主要包括3个维度:Free Space、Max Chunk Size和Num Chunks。似乎cms更有用

gc日志太多可能会影响性能,目前没有日志对性能影响的数据,暂不考虑日志对性能的影响。


可视化

有很多gc可视化的工具。比如在线的gceasy https://gceasy.io/index.jsp#banner,上传gc日志可以进行分析。免费功能有各种图表展示。20美金一个月可以使用高级功能。

本文用gceasy做例子,其他可视化工具介绍见附录。

下面从前文优化过的节点开始,分析gc日志。








 
问题分析
gceasy优化建议


不花钱怎么变强?开启gceasy高级功能。


单次gc暂停最长到5秒了。链接给出了优化建议,见附录。

首先是优化程序降低对象创建速度。现在平均对象创建速度是1.22gb/sec。减少对象创建速度主要是更多用堆外内存,优化写放大,本文重点是gc优化,不展开。

再是年轻代太小,增大年轻代。这规则适用于其他gc,而g1 young gc是动态的,mixed gc是分次迭代的,young gc不能太大,否则young gc反而会比单次mixed慢。

再就是设置期待的最大暂停时间,目前设置100ms,暂不修改。

服务器上进程多,内存交换,服务器无这个情况。

gc线程不够多,有可能。但gc3秒以上的只有6次,差不多每天一次。如果是线程数明显不够次数应该更多,先放放。

服务器高IO导致的等待时间长。有可能,有datanode,需要进一步分析gc日志和IO监控。

没有禁用system.gc()。gc case表格来看没有相关调用。

堆太大。有可能,毕竟80GB堆。但大堆对吞吐有好处。

看5秒暂停gc的具体日志,
{Heap before GC invocations=50103 (full 0):
garbage-first heap total 83886080K, used 48760053K [0x00007faec4000000, 0x00007faec6005000, 0x00007fc2c4000000)
region size 32768K, 128 young (4194304K), 5 survivors (163840K)
Metaspace used 50945K, capacity 51410K, committed 52168K, reserved 53248K
2019-02-18T17:48:53.570+0800: 365568.567: [GC pause (G1 Evacuation Pause) (mixed)
Desired survivor size 268435456 bytes, new threshold 1 (max 1)
- age 1: 87997400 bytes, 87997400 total
365568.567: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 3313913, predicted base time: 1151.36 ms, remaining time: 0.00 ms, target pause time: 100.00 ms]
365568.567: [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 123 regions, survivors: 5 regions, predicted young region time: 50.17 ms]
365568.583: [G1Ergonomics (CSet Construction) finish adding old regions to CSet, reason: reclaimable percentage not over threshold, old: 70 regions, max: 256 regions, reclaimable: 8557735352 bytes (9.96 %), threshold: 10.00 %]
365568.583: [G1Ergonomics (CSet Construction) added expensive regions to CSet, reason: old CSet region num not reached min, old: 70 regions, expensive: 70 regions, min: 80 regions, remaining time: 0.00 ms]
365568.583: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 123 regions, survivors: 5 regions, old: 70 regions, predicted pause time: 4353.85 ms, target pause time: 100.00 ms]
365573.866: [G1Ergonomics (Heap Sizing) attempt heap expansion, reason: recent GC overhead higher than threshold after GC, recent GC overhead: 40.96 %, threshold: 10.00 %, uncommitted: 0 bytes, calculated expansion amount: 0 bytes (20.00 %)]
365573.866: [G1Ergonomics (Mixed GCs) do not continue mixed GCs, reason: reclaimable percentage not over threshold, candidate old regions: 407 regions, reclaimable: 8557735352 bytes (9.96 %), threshold: 10.00 %]
, 5.2999017 secs]
[Parallel Time: 5227.7 ms, GC Workers: 16]
[GC Worker Start (ms): Min: 365568584.3, Avg: 365568584.3, Max: 365568584.4, Diff: 0.1]
[Ext Root Scanning (ms): Min: 1.3, Avg: 1.6, Max: 3.9, Diff: 2.7, Sum: 25.1]
[Update RS (ms): Min: 1065.2, Avg: 1067.2, Max: 1067.8, Diff: 2.6, Sum: 17075.4]
[Processed Buffers: Min: 739, Avg: 831.5, Max: 879, Diff: 140, Sum: 13304]
[Scan RS (ms): Min: 3610.8, Avg: 3611.6, Max: 3612.6, Diff: 1.8, Sum: 57786.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.3, Diff: 0.3, Sum: 0.5]
[Object Copy (ms): Min: 545.2, Avg: 546.3, Max: 547.2, Diff: 2.0, Sum: 8741.5]
[Termination (ms): Min: 0.1, Avg: 0.6, Max: 0.8, Diff: 0.7, Sum: 9.8]
[Termination Attempts: Min: 1, Avg: 465.3, Max: 552, Diff: 551, Sum: 7445]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.0, Sum: 0.4]
[GC Worker Total (ms): Min: 5227.3, Avg: 5227.4, Max: 5227.5, Diff: 0.2, Sum: 83638.8]
[GC Worker End (ms): Min: 365573811.8, Avg: 365573811.8, Max: 365573811.8, Diff: 0.0]
[Code Root Fixup: 0.4 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 3.4 ms]
[Other: 68.4 ms]
[Choose CSet: 16.3 ms]
[Ref Proc: 3.6 ms]
[Ref Enq: 0.2 ms]
[Redirty Cards: 8.7 ms]
[Humongous Register: 0.1 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 31.6 ms]
[Eden: 3936.0M(3936.0M)->0.0B(3904.0M) Survivors: 160.0M->192.0M Heap: 46.5G(80.0G)->42.3G(80.0G)]
Heap after GC invocations=50104 (full 0):
garbage-first heap total 83886080K, used 44323410K [0x00007faec4000000, 0x00007faec6005000, 0x00007fc2c4000000)
region size 32768K, 6 young (196608K), 6 survivors (196608K)
Metaspace used 50945K, capacity 51410K, committed 52168K, reserved 53248K
}
[Times: user=85.30 sys=0.00, real=5.30 secs]
2019-02-18T17:48:58.871+0800: 365573.867: Total time for which application threads were stopped: 5.3131110 seconds, Stopping threads took: 0.0005771 seconds
2019-02-18T17:48:58.896+0800: 365573.893: Total time for which application threads were stopped: 0.0186168 seconds, Stopping threads took: 0.0005541 seconds
2019-02-18T17:48:58.906+0800: 365573.902: Total time for which application threads were stopped: 0.0078098 seconds, Stopping threads took: 0.0005988 seconds
2019-02-18T17:48:58.915+0800: 365573.912: Total time for which application threads were stopped: 0.0075271 seconds, Stopping threads took: 0.0003996 seconds
2019-02-18T17:48:58.923+0800: 365573.919: Total time for which application threads were stopped: 0.0072014 seconds, Stopping threads took: 0.0003114 seconds
2019-02-18T17:48:58.934+0800: 365573.930: Total time for which application threads were stopped: 0.0078706 seconds, Stopping threads took: 0.0005490 seconds
2019-02-18T17:48:58.942+0800: 365573.938: Total time for which application threads were stopped: 0.0073198 seconds, Stopping threads took: 0.0003927 seconds
2019-02-18T17:48:58.964+0800: 365573.960: Total time for which application threads were stopped: 0.0078810 seconds, Stopping threads took: 0.0007641 seconds
2019-02-18T17:48:58.972+0800: 365573.969: Total time for which application threads were stopped: 0.0075881 seconds, Stopping threads took: 0.0006277 seconds
2019-02-18T17:48:58.981+0800: 365573.978: Total time for which application threads were stopped: 0.0081290 seconds, Stopping threads took: 0.0011246 seconds
2019-02-18T17:48:58.992+0800: 365573.989: Total time for which application threads were stopped: 0.0076556 seconds, Stopping threads took: 0.0005358 seconds
2019-02-18T17:48:59.015+0800: 365574.011: Total time for which application threads were stopped: 0.0076750 seconds, Stopping threads took: 0.0005602 seconds
2019-02-18T17:48:59.026+0800: 365574.022: Total time for which application threads were stopped: 0.0089086 seconds, Stopping threads took: 0.0006000 seconds
2019-02-18T17:48:59.044+0800: 365574.041: Total time for which application threads were stopped: 0.0087554 seconds, Stopping threads took: 0.0006332 seconds
2019-02-18T17:48:59.054+0800: 365574.050: Total time for which application threads were stopped: 0.0084038 seconds, Stopping threads took: 0.0004326 seconds
{Heap before GC invocations=50104 (full 0):
garbage-first heap total 83886080K, used 48321106K [0x00007faec4000000, 0x00007faec6005000, 0x00007fc2c4000000)
region size 32768K, 128 young (4194304K), 6 survivors (196608K)
Metaspace used 50946K, capacity 51410K, committed 52168K, reserved 53248K

从日志看,期望survivor区有大概256MB,实际只有32*5=160MB,可能会将eden区直接提到old区。

近期gc时间占比过高,到40%了。

清理young 区的gc线程耗时过长,清理后可回收内存低于10%,mixed gc结束,其他gc线程空闲。说明mixed gc触发阈值设置低了,young gc参数也需要调整。

看暂停时间大于2秒的其他日志,都是触发mixed gc,但只有一个清理young和个别old区region的线程工作。确实说明mixed gc触发阈值设置低了,young gc过大了。
 
 


有41次gc的系统时间大于用户时间,大概3小时一次。

操作系统层面有可能是操作系统层异常,或者系统太忙了。出现频率大概3小时一次,看ganglia日志问题出现时cpu负载并不高。并且系统时间只是略高于用户时间不是明显有问题的数量级差距。先不考虑操作系统和JVM有bug的情况,暂不处理。

 

还是说有5秒的gc,同上。


修改参数
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:+UnlockExperimentalVMOptions
-XX:+ParallelRefProcEnabled
-XX:ConcGCThreads=4
-XX:ParallelGCThreads=16
-XX:G1NewSizePercent=5
-XX:G1MaxNewSizePercent=20
-XX:MaxTenuringThreshold=1
-XX:G1HeapRegionSize=32m
-XX:G1MixedGCCountTarget=16
-XX:InitiatingHeapOccupancyPercent=60
-XX:MaxDirectMemorySize=110g
-XX:G1OldCSetRegionThresholdPercent=10
-XX:G1HeapWastePercent=10
-Xmx80g
-Xms80g


从gc前后堆内存图来看,-XX:InitiatingHeapOccupancyPercent=65,52GB更合适。

暂停时间平均135ms,从暂停时间图来看,young gc时间较长,需要调小young gc区,小米经验改初始值,由5改成2。-XX:G1NewSizePercent=2

mixed gc 大多数一轮不到8次,暂停时间基本都超过100ms了。期待每轮16次的参数没起作用,这是由于每轮清理的region数过多。-XX:G1OldCSetRegionThresholdPercent=5,由10改成5,减少单次mixed gc时间。

survivor区应该大于young日志中期待的,Set SurvivorRatio from 8 (default) to 4。
-XX:InitiatingHeapOccupancyPercent=65
-XX:G1NewSizePercent=2
-XX:G1OldCSetRegionThresholdPercent=5
-XX:SurvivorRatio=4



设置其他必要参数

参考各类优化资料,增加以下参数
-XX:+UseStringDeduplication 字符串去重,提高性能
-XX:-ResizePLAB 减少gc线程间通信的东西,关闭动态提升本地buffer
-XX:+PerfDisableSharedMem 关掉统计信息的内存映射。开启在某些特殊场景下,会极大增加gc暂停时间

优化结果




平均暂停时间,最大暂停时间,暂停时间分布,gc吞吐占比都有提高,优化成功。

接下来迭代提高这台服务器region数,做gc优化。

 
遗留问题

偶发系统时间大于用户时间的问题,依然存在。发生时系统负载并不高,有可能操作系统和JVM层面有优化空间。

gceasy建议不要用-XX:+UseGCLogFileRotation,理由是历史会看不到,文件顺序不容易分辨,多个文件不利于gc分析。但这个参数意义是控制总文件大小,所以依然保留。

附录
g1官方介绍


https://www.oracle.com/technet ... .html

其他可视化工具介绍

https://blog.csdn.net/qq_32447 ... 67984

https://blog.csdn.net/xuelinme ... 90115

https://blog.csdn.net/renfufei ... 78064

gceasy优化建议

https://blog.gceasy.io/2016/11 ... uses/

https://blog.gceasy.io/2016/12 ... time/

https://blog.gceasy.io/2016/12 ... pped/

https://blog.gceasy.io/2018/12 ... tion/

https://blog.gceasy.io/2019/01 ... tion/

优化参考

https://www.oracle.com/technet ... .html

https://blogs.apache.org/hbase ... hbase

https://software.intel.com/en- ... hbase

http://openinx.github.io/ppt/h ... 8.pdf

https://blog.csdn.net/songhaif ... 77612

https://blog.csdn.net/maosijun ... 62489

https://dzone.com/articles/g1g ... ags-1

http://www.evanjones.ca/jvm-mmap-pause.html

之前的优化探索
 http://www.hbase.group/article/192
 
专栏地址
 
https://zhuanlan.zhihu.com/c_178811296 收起阅读 »

hbase优化之旅(一)探索regionserver参数优化

要有目录和章节标题功能,图片能直接粘贴过来就好了。
 优化的目的
我们线上hbase集群使用了group分组功能,但没有针对不同业务分组的特点做特殊优化,hbase服务能力没有彻底激发出来。
本文记录了对某个业务分组参数优化的探索,借此机会深入了解不同配置对regionserver监控指标和机器负载的影响。 优化后,单台regionserver查询延迟更低,磁盘IO降低,系统更稳定。从而提高吞吐能力,进而减少机器,提升资源利用率的能力,节约成本。
 
要解决那些问题
目前该业务分组发现的问题主要在写入。虽然资源利用率不高,但时不时会rpcp99变慢。从监控可以看到,队列等待时间会间隔性变长。看regionserver日志,hlogs上限触发同时刷太多region,flush和compact压力集中。刷了好多小文件,写放大效果明显。
 
问题分析
分组有17台regionserver,单台有大概520个region,白天请求量较为平稳。分析一台的使用情况。
 
物理资源使用情况
机器类型
2*E5-2650v4(2*12核),8*32G,2*150G(120G) SATA SSD+12*800G(960G) SATA SSD
 
机器负载
cpu和load并不高。



内存看,由于应用内存分配较为保守,regionserver堆内堆外共150g,datanode 4g,很大一块给了操作系统读cache。但regionserver本身也配置了100g的读cache,操作系统读cache没什么用。


磁盘读流量较高,写流量50MB/s。网络In 20MB,Out 40到80MB波动。



磁盘读流量主要是本地regionserver 业务读和compact读,写流量主要是本地regionserver写wal,flush,compact,以及其他datanode的写副本。
网络in主要是业务写入请求和其他datanode写副本请求,网络out主要是业务查询响应和regionserver 写wal,flush,compact写其他datanode副本的请求。
由于业务查询主要走内存缓存(95%),hifile又有很高的压缩比(1:5),如果不考虑服务端数据过滤的情况,业务读引起的磁盘IO应该只是网络查询响应IO的百分之一。
当然服务端数据过滤的影响是需要考虑的,但相比网络out磁盘读流量太高了不正常。compact 读小文件写大文件压缩率提高了,这也是磁盘读流量大于网络OUT的可能。 磁盘读流量很大应该是compact读引起的,这个时间段又没有major compacted,说明可能小文件很多引起了写放大。

regionserver使用情况
regionserver配置
版本0.98.21,配置见附录一。
 
指标监控
队列等待时间p99最多有150ms,执行时间p99(ProcessCallTime_99th_percentile)在45ms内。
 
0QueueCallTime.png


0rpc99.png


 
读缓存命中率95%,还ok


 

有一些慢写入
 
0slowput.png



大概20分钟memstore整体刷一次,slowput的出现在刷memstore时
 
0memstore.png


0hlongfilecount.png



 
compaction排队的频率和memstore刷新基本一致,无major,说明是memstore定期大flush引起。
 
0compactionQueue.png


0compactedcellsize.png


0major.png


 
 

flush排队也发生在定期memstore flush时
 
0flushqueue.png



看regionserver日志,固定间隔就会hlogs上限触发同时刷很多region,22MB的memstore刷5MB hifile,同时刷了好多小文件,又马上compact,写放大效果明显。
09:34:04,075 INFO  [regionserver60020.logRoller] wal.FSHLog: Too many hlogs: logs=53, maxlogs=52; forcing flush of 69 regions(s) ...

09:34:27,339 INFO [regionserver60020.logRoller] wal.FSHLog: Too many hlogs: logs=53, maxlogs=52; forcing flush of 222 regions(s) ...

...

09:34:27,601 INFO [MemStoreFlusher.1] regionserver.DefaultStoreFlusher: Flushed, sequenceid=311232252, memsize=22.6 M, hasBloomFilter=true, into tmp file ...

09:34:27,608 INFO [MemStoreFlusher.1] regionserver.HStore: Added ...., entries=27282, sequenceid=311232252, filesize=5.4 M

09:34:27,608 INFO [MemStoreFlusher.1] regionserver.HRegion: Finished memstore flush of ~23.6 M/24788698, currentsize=0/0 for region .... in 267ms, sequenceid=311232252, compaction requested=true


堆内存和gc情况,波动主要发生在周期刷memstore时。
 

 
问题总结
1.hbase.regionserver.maxlogs太少,
按照hlog和memstore的关系公式
hbase.regionserver.hlog.blocksize * hbase.reionserver.logroll.mutiplier*hbase.regionserver.maxlogs 
>= hbase.regionserver.global.memstore.lowerLimit * HBASE_HEAPSIZE
至少应该是95。配置太低频繁由于hlog上限触发几百个region flush,flush的都是小文件,flush和compact压力都大。

2.内存空闲太多,预留20%空闲就可以了。但要注意g1内存过大可能导致gc时间变长。

3.region数太多,大部分region的memstore没有128MB就强制flush了。正常应尽量保证每个 region 能够分配到 Flushsize 大小的内存,尽可能的 flush 大文件,从而减少后续 Compaction 开销。region数可以通过region合并减少,但这次先不优化,通过增大内存方式缓解。

参数优化探索
第一次修改
期望优化hlogs触发强刷memstore。
hbase.regionserver.maxlogs 52改为95.
重启regionserver。

变成47分钟触发hlog上限刷一次memstore了。

1hlog.png


1memstore.png


日志看,47分钟左右hlogs上限触发257 region同时刷新。和之前强刷一样,很多memstore20MB,压缩后的hfile5MB。总memstore峰值最多18.35GB,部分region 128MB提前flush了,所以到不了理论上限22.5GB。
 
 
compaction排队的频率降低了

1compact.png


 
队列等待时间波峰的间隔变长,有所改善。
 
1queuecalltime.png


1rpc.png


 

gc次数和时间平常值略有降低,波峰提高。
 
1memstore.png


1gc.png


 

slowput略有减少。
1slowput.png


优化有一定效果。
 
 
第二次修改
由于region数太多,单个region可能不到128MB就触发hlog的强刷了。调大内存从而增加memstore和hlogs上限。计算预计在hlog上限前肯定能触发每小时一刷。调大了handler线程数。
hbase-env.sh
1.jvm XX:MaxDirectMemoryize 调为110GB 2.堆内Xmx调为80g
hbase-site.xml
3.hbase.bucketcache.size 112640 110g,hfile.block.cache.size 0.14 4.hbase.regionserver.global.memstore.upperLimit 0.56 44.8g, hbase.regionserver.global.memstore.lowerLimit 0.5 40g, 5.hbase.regionserver.hlog.blocksize 268435456 256MB,显示配置,默认和hdfs块大小一致,也是配置的这个值。
 hbase.regionserver.maxlogs 177
6.hbase.regionserver.handler.count 384







重启regionserver,观察监控。

memstore1小时一降,并不是hlog上限触发的,是每小时定时刷新触发的。
 
2memstore.png


2hlog.png

 

看日志,16:31陆续有region128MB flush。16:59 memstore 17.5GB, hlogs 125时,大部分region触发1小时刷新,delay几秒到几十秒。刷新时间还是较为集中,有flush和compact排队。其中6点的compactsize大且持续,应该是很多region需要compact的hfile变大了。
 

2flush_compact.png


2compactsize.png




慢put变多,rpc执行时间峰值变高了,队列等待时间p99最长居然到2s!

2slowput.png


2rpc.png


2queuecalltime.png

  
 

原因是gc峰值变高了,影响regionserver业务了。
 
2gc.png



堆内存变大了,gc回收的变多,gc时间就变长了。
 
2memheap.png



优化有些副作用,要消除。


第三次修改
期望优化增大堆内存引起的gc问题。memstore刷新间隔改为2小时,刷新size改为256MB
1.XX:InitiatingHeapOccupancyPercent=65 调整为XX:InitiatingHeapOccupancyPercent=75 之前hmaxlogs52限制实际memstore最大12g左右,堆内读写缓存加起来0.45左右。现在读写缓存加起来最多0.7了,所以调大触发gc的上限(bad modify) 
2. -XX:ConcGCThreads=8,-XX:ParallelGCThreads=30,-XX:G1MixedGCCountTarget=32 增大gc并发,文档说建议逻辑核的5/8,48核30。并发标记跟着调大。拆解gc次数降低单次时间。 3.hbase.regionserver.optionalcacheflushinterval 改 7200000 4.hbase.hregion.memstore.flush.size 268435456 128MB的压缩会也就22MB hfile没多大,所以单个memstore内存改为256MB。

重启观察。

发现由于同时调大间隔时间和flush size,又变成1小时40分钟hlog上限触发强刷了


 
看日志,同时刷的region数少了,170个,flush和compact排队有所改善。



rpc和队列时间p99峰值还是太高,慢put不少




整体gc count减少,gc时间峰值变多变高


看gc的日志,调整gc并发,gc的系统时间依然很长,需要继续调整。
 
第四五次修改
gc触发阈值降低,提前gc。gc并发改小,一次最大10%的region。间隔时间改回1小时。改了两次,最终修改如下
1.XX:InitiatingHeapOccupancyPercent=65, 又改到60 提前 
2. -XX:ConcGCThreads=4,-XX:ParallelGCThreads=16,-XX:G1OldCSetRegionThresholdPercent=10 提高一次gc最多回收的region数。
3. hbase.regionserver.optionalcacheflushinterval 去掉,改回一小时。
重启观察。

memstore又是1小时一刷很多region,由于启动时间较短只执行了几个周期,还没有分散开。


flush和compact排队还是按小时出现


 

慢put少了,rpc和队列等待p99降低了,但希望能更低一些。




gc变多了,而时间少了一些但峰值依然挺高,看gc日志young gc时间也有挺长的,mixed gc时间依然挺长。


需要继续调优。
 
 
第六次修改
继续调gc参数,young gc内存控制大小。继续增大memstore配置让其能2小时一刷。由于region数太多,去掉flush 256MB设置。
-XX:G1MixedGCCountTarget=16 增大一次全局标记后mixed gc最多执行的次数 
-XX:G1HeapWastePercent=10 允许保留10%垃圾
-XX:G1MaxNewSizePercent=20 young 上限20%,避免young区太大gc慢
hbase.regionserver.global.memstore.upperLimit 0.6 调大上限
hbase.regionserver.global.memstore.lowerLimit 0.54
hbase.regionserver.maxlogs 200 调大上限
hbase.regionserver.optionalcacheflushinterval 7200000 2小时一刷
hbase.hregion.memstore.flush.size 去掉用默认128MB

regionserver指标
2小时一刷memstore


flush和compact排队都少了

 

慢put减少,rpc和队列等待时间p99整体降低,峰值也降了很多



 

gc更平衡,峰值时间更短

 

机器负载
cpu load依然很低



磁盘读IO下降到1/4,网络out有所下降。



内存还有20%+的空闲,还可以多用点。

优化有效果。
 
 
总结
优化成果
本次优化通过调整regionserver参数和gc参数,减少小文件flush,降低compact写放大。平均响应时间降低,响应时间高峰次数减少。磁盘读写IO减少很多,从而增加了磁盘的使用时间。
 
后续优化方向
减少region数
单台regionserver上的region偏多。
region都是预分区,预分区时按预估总数据大小计算的分区数,但没考虑hfile压缩。几张大表单region的存储量都是region hfile上限容量的几分之一,可以合并region,减少region数。
 
增大内存配置
保留10%空闲即可
 
gc深入调优
这次gc参数调优没有明确目标,没有优化方法论,双盲优化,事倍功半,要深入gc参数细节,用明确的方法论,朝明确的目标优化。敬请期待。
 
减少服务器
瓶颈可能在存储容量上。hdfs逻辑存储共36.0 T,17台regionserver可以支持50TB逻辑存储容量,可以迭代减少几台,达到节约成本的目的。
 
附录
hbase-site.xml原始配置


hbase-env.sh原始配置
export HBASE_REGIONSERVER_OPTS="-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:${HBASE_LOG_DIR}/gc-`date +'%Y%m%d%H%M'` -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=512M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${HBASE_LOG_DIR}/hbase.heapdump -XX:ErrorFile=${HBASE_LOG_DIR}/hs_err_pid%p.log -XX:+PrintAdaptiveSizePolicy -XX:+PrintFlagsFinal -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+UnlockExperimentalVMOptions -XX:+ParallelRefProcEnabled -XX:ConcGCThreads=4 -XX:ParallelGCThreads=16 -XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60 -XX:MaxTenuringThreshold=1 -XX:G1HeapRegionSize=32m -XX:G1MixedGCCountTarget=8 -XX:InitiatingHeapOccupancyPercent=65 -XX:MaxDirectMemorySize=100g -XX:G1OldCSetRegionThresholdPercent=5 -Xmx50g -Xms50g"




g1参考资料
https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html 
 
专栏地址
https://zhuanlan.zhihu.com/p/57438971
 
 
 
 
 
 
 
 
 
 
 
 
==============分割线=============附件删了就编辑不过了==================
 
继续阅读 »
要有目录和章节标题功能,图片能直接粘贴过来就好了。
 优化的目的
我们线上hbase集群使用了group分组功能,但没有针对不同业务分组的特点做特殊优化,hbase服务能力没有彻底激发出来。
本文记录了对某个业务分组参数优化的探索,借此机会深入了解不同配置对regionserver监控指标和机器负载的影响。 优化后,单台regionserver查询延迟更低,磁盘IO降低,系统更稳定。从而提高吞吐能力,进而减少机器,提升资源利用率的能力,节约成本。
 
要解决那些问题
目前该业务分组发现的问题主要在写入。虽然资源利用率不高,但时不时会rpcp99变慢。从监控可以看到,队列等待时间会间隔性变长。看regionserver日志,hlogs上限触发同时刷太多region,flush和compact压力集中。刷了好多小文件,写放大效果明显。
 
问题分析
分组有17台regionserver,单台有大概520个region,白天请求量较为平稳。分析一台的使用情况。
 
物理资源使用情况
机器类型
2*E5-2650v4(2*12核),8*32G,2*150G(120G) SATA SSD+12*800G(960G) SATA SSD
 
机器负载
cpu和load并不高。



内存看,由于应用内存分配较为保守,regionserver堆内堆外共150g,datanode 4g,很大一块给了操作系统读cache。但regionserver本身也配置了100g的读cache,操作系统读cache没什么用。


磁盘读流量较高,写流量50MB/s。网络In 20MB,Out 40到80MB波动。



磁盘读流量主要是本地regionserver 业务读和compact读,写流量主要是本地regionserver写wal,flush,compact,以及其他datanode的写副本。
网络in主要是业务写入请求和其他datanode写副本请求,网络out主要是业务查询响应和regionserver 写wal,flush,compact写其他datanode副本的请求。
由于业务查询主要走内存缓存(95%),hifile又有很高的压缩比(1:5),如果不考虑服务端数据过滤的情况,业务读引起的磁盘IO应该只是网络查询响应IO的百分之一。
当然服务端数据过滤的影响是需要考虑的,但相比网络out磁盘读流量太高了不正常。compact 读小文件写大文件压缩率提高了,这也是磁盘读流量大于网络OUT的可能。 磁盘读流量很大应该是compact读引起的,这个时间段又没有major compacted,说明可能小文件很多引起了写放大。

regionserver使用情况
regionserver配置
版本0.98.21,配置见附录一。
 
指标监控
队列等待时间p99最多有150ms,执行时间p99(ProcessCallTime_99th_percentile)在45ms内。
 
0QueueCallTime.png


0rpc99.png


 
读缓存命中率95%,还ok


 

有一些慢写入
 
0slowput.png



大概20分钟memstore整体刷一次,slowput的出现在刷memstore时
 
0memstore.png


0hlongfilecount.png



 
compaction排队的频率和memstore刷新基本一致,无major,说明是memstore定期大flush引起。
 
0compactionQueue.png


0compactedcellsize.png


0major.png


 
 

flush排队也发生在定期memstore flush时
 
0flushqueue.png



看regionserver日志,固定间隔就会hlogs上限触发同时刷很多region,22MB的memstore刷5MB hifile,同时刷了好多小文件,又马上compact,写放大效果明显。
09:34:04,075 INFO  [regionserver60020.logRoller] wal.FSHLog: Too many hlogs: logs=53, maxlogs=52; forcing flush of 69 regions(s) ...

09:34:27,339 INFO [regionserver60020.logRoller] wal.FSHLog: Too many hlogs: logs=53, maxlogs=52; forcing flush of 222 regions(s) ...

...

09:34:27,601 INFO [MemStoreFlusher.1] regionserver.DefaultStoreFlusher: Flushed, sequenceid=311232252, memsize=22.6 M, hasBloomFilter=true, into tmp file ...

09:34:27,608 INFO [MemStoreFlusher.1] regionserver.HStore: Added ...., entries=27282, sequenceid=311232252, filesize=5.4 M

09:34:27,608 INFO [MemStoreFlusher.1] regionserver.HRegion: Finished memstore flush of ~23.6 M/24788698, currentsize=0/0 for region .... in 267ms, sequenceid=311232252, compaction requested=true


堆内存和gc情况,波动主要发生在周期刷memstore时。
 

 
问题总结
1.hbase.regionserver.maxlogs太少,
按照hlog和memstore的关系公式
hbase.regionserver.hlog.blocksize * hbase.reionserver.logroll.mutiplier*hbase.regionserver.maxlogs 
>= hbase.regionserver.global.memstore.lowerLimit * HBASE_HEAPSIZE
至少应该是95。配置太低频繁由于hlog上限触发几百个region flush,flush的都是小文件,flush和compact压力都大。

2.内存空闲太多,预留20%空闲就可以了。但要注意g1内存过大可能导致gc时间变长。

3.region数太多,大部分region的memstore没有128MB就强制flush了。正常应尽量保证每个 region 能够分配到 Flushsize 大小的内存,尽可能的 flush 大文件,从而减少后续 Compaction 开销。region数可以通过region合并减少,但这次先不优化,通过增大内存方式缓解。

参数优化探索
第一次修改
期望优化hlogs触发强刷memstore。
hbase.regionserver.maxlogs 52改为95.
重启regionserver。

变成47分钟触发hlog上限刷一次memstore了。

1hlog.png


1memstore.png


日志看,47分钟左右hlogs上限触发257 region同时刷新。和之前强刷一样,很多memstore20MB,压缩后的hfile5MB。总memstore峰值最多18.35GB,部分region 128MB提前flush了,所以到不了理论上限22.5GB。
 
 
compaction排队的频率降低了

1compact.png


 
队列等待时间波峰的间隔变长,有所改善。
 
1queuecalltime.png


1rpc.png


 

gc次数和时间平常值略有降低,波峰提高。
 
1memstore.png


1gc.png


 

slowput略有减少。
1slowput.png


优化有一定效果。
 
 
第二次修改
由于region数太多,单个region可能不到128MB就触发hlog的强刷了。调大内存从而增加memstore和hlogs上限。计算预计在hlog上限前肯定能触发每小时一刷。调大了handler线程数。
hbase-env.sh
1.jvm XX:MaxDirectMemoryize 调为110GB 2.堆内Xmx调为80g
hbase-site.xml
3.hbase.bucketcache.size 112640 110g,hfile.block.cache.size 0.14 4.hbase.regionserver.global.memstore.upperLimit 0.56 44.8g, hbase.regionserver.global.memstore.lowerLimit 0.5 40g, 5.hbase.regionserver.hlog.blocksize 268435456 256MB,显示配置,默认和hdfs块大小一致,也是配置的这个值。
 hbase.regionserver.maxlogs 177
6.hbase.regionserver.handler.count 384







重启regionserver,观察监控。

memstore1小时一降,并不是hlog上限触发的,是每小时定时刷新触发的。
 
2memstore.png


2hlog.png

 

看日志,16:31陆续有region128MB flush。16:59 memstore 17.5GB, hlogs 125时,大部分region触发1小时刷新,delay几秒到几十秒。刷新时间还是较为集中,有flush和compact排队。其中6点的compactsize大且持续,应该是很多region需要compact的hfile变大了。
 

2flush_compact.png


2compactsize.png




慢put变多,rpc执行时间峰值变高了,队列等待时间p99最长居然到2s!

2slowput.png


2rpc.png


2queuecalltime.png

  
 

原因是gc峰值变高了,影响regionserver业务了。
 
2gc.png



堆内存变大了,gc回收的变多,gc时间就变长了。
 
2memheap.png



优化有些副作用,要消除。


第三次修改
期望优化增大堆内存引起的gc问题。memstore刷新间隔改为2小时,刷新size改为256MB
1.XX:InitiatingHeapOccupancyPercent=65 调整为XX:InitiatingHeapOccupancyPercent=75 之前hmaxlogs52限制实际memstore最大12g左右,堆内读写缓存加起来0.45左右。现在读写缓存加起来最多0.7了,所以调大触发gc的上限(bad modify) 
2. -XX:ConcGCThreads=8,-XX:ParallelGCThreads=30,-XX:G1MixedGCCountTarget=32 增大gc并发,文档说建议逻辑核的5/8,48核30。并发标记跟着调大。拆解gc次数降低单次时间。 3.hbase.regionserver.optionalcacheflushinterval 改 7200000 4.hbase.hregion.memstore.flush.size 268435456 128MB的压缩会也就22MB hfile没多大,所以单个memstore内存改为256MB。

重启观察。

发现由于同时调大间隔时间和flush size,又变成1小时40分钟hlog上限触发强刷了


 
看日志,同时刷的region数少了,170个,flush和compact排队有所改善。



rpc和队列时间p99峰值还是太高,慢put不少




整体gc count减少,gc时间峰值变多变高


看gc的日志,调整gc并发,gc的系统时间依然很长,需要继续调整。
 
第四五次修改
gc触发阈值降低,提前gc。gc并发改小,一次最大10%的region。间隔时间改回1小时。改了两次,最终修改如下
1.XX:InitiatingHeapOccupancyPercent=65, 又改到60 提前 
2. -XX:ConcGCThreads=4,-XX:ParallelGCThreads=16,-XX:G1OldCSetRegionThresholdPercent=10 提高一次gc最多回收的region数。
3. hbase.regionserver.optionalcacheflushinterval 去掉,改回一小时。
重启观察。

memstore又是1小时一刷很多region,由于启动时间较短只执行了几个周期,还没有分散开。


flush和compact排队还是按小时出现


 

慢put少了,rpc和队列等待p99降低了,但希望能更低一些。




gc变多了,而时间少了一些但峰值依然挺高,看gc日志young gc时间也有挺长的,mixed gc时间依然挺长。


需要继续调优。
 
 
第六次修改
继续调gc参数,young gc内存控制大小。继续增大memstore配置让其能2小时一刷。由于region数太多,去掉flush 256MB设置。
-XX:G1MixedGCCountTarget=16 增大一次全局标记后mixed gc最多执行的次数 
-XX:G1HeapWastePercent=10 允许保留10%垃圾
-XX:G1MaxNewSizePercent=20 young 上限20%,避免young区太大gc慢
hbase.regionserver.global.memstore.upperLimit 0.6 调大上限
hbase.regionserver.global.memstore.lowerLimit 0.54
hbase.regionserver.maxlogs 200 调大上限
hbase.regionserver.optionalcacheflushinterval 7200000 2小时一刷
hbase.hregion.memstore.flush.size 去掉用默认128MB

regionserver指标
2小时一刷memstore


flush和compact排队都少了

 

慢put减少,rpc和队列等待时间p99整体降低,峰值也降了很多



 

gc更平衡,峰值时间更短

 

机器负载
cpu load依然很低



磁盘读IO下降到1/4,网络out有所下降。



内存还有20%+的空闲,还可以多用点。

优化有效果。
 
 
总结
优化成果
本次优化通过调整regionserver参数和gc参数,减少小文件flush,降低compact写放大。平均响应时间降低,响应时间高峰次数减少。磁盘读写IO减少很多,从而增加了磁盘的使用时间。
 
后续优化方向
减少region数
单台regionserver上的region偏多。
region都是预分区,预分区时按预估总数据大小计算的分区数,但没考虑hfile压缩。几张大表单region的存储量都是region hfile上限容量的几分之一,可以合并region,减少region数。
 
增大内存配置
保留10%空闲即可
 
gc深入调优
这次gc参数调优没有明确目标,没有优化方法论,双盲优化,事倍功半,要深入gc参数细节,用明确的方法论,朝明确的目标优化。敬请期待。
 
减少服务器
瓶颈可能在存储容量上。hdfs逻辑存储共36.0 T,17台regionserver可以支持50TB逻辑存储容量,可以迭代减少几台,达到节约成本的目的。
 
附录
hbase-site.xml原始配置


hbase-env.sh原始配置
export HBASE_REGIONSERVER_OPTS="-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:${HBASE_LOG_DIR}/gc-`date +'%Y%m%d%H%M'` -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=512M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${HBASE_LOG_DIR}/hbase.heapdump -XX:ErrorFile=${HBASE_LOG_DIR}/hs_err_pid%p.log -XX:+PrintAdaptiveSizePolicy -XX:+PrintFlagsFinal -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+UnlockExperimentalVMOptions -XX:+ParallelRefProcEnabled -XX:ConcGCThreads=4 -XX:ParallelGCThreads=16 -XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60 -XX:MaxTenuringThreshold=1 -XX:G1HeapRegionSize=32m -XX:G1MixedGCCountTarget=8 -XX:InitiatingHeapOccupancyPercent=65 -XX:MaxDirectMemorySize=100g -XX:G1OldCSetRegionThresholdPercent=5 -Xmx50g -Xms50g"




g1参考资料
https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html 
 
专栏地址
https://zhuanlan.zhihu.com/p/57438971
 
 
 
 
 
 
 
 
 
 
 
 
==============分割线=============附件删了就编辑不过了==================
  收起阅读 »

中国HBase技术社区第十届meetup--HBase生态实践 (杭州站)

报名地址:点我报名
 
演讲主题及嘉宾
  • Topic1:基于HBase和Spark构建企业级数据处理平台

           首先介绍下Spark的发展之路,HBase结合Spark如何构建企业级数据处理平台,最后介绍下相关的案例。
  • Topic2:HBase在有赞的平台实践和应用

          介绍HBase在有赞的部署实践,管控平台实践,我们在社区1.2.6版本之上做了哪些改造,修复了哪些bug,以及结合在有赞的业务场景,介绍HBase的应用实践。
  • Topic3:云HBase之OpenTSDB时序引擎压缩优化

          1.OpenTSDB设计介绍
          2.介绍云HBase OpenTSDB 压缩优化
          3.介绍云HBase OpenTSDB 使用模式
  • Topic4:Flink+HBase实时计算场景

         1.Flink简介
         2.Flink优势
         3.Flink典型应用场景
         4.Flink和HBase结合的案例分享

议程安排

13:30-14:00  签到
14:00-14:40  基于HBase和Spark构建企业级数据处理平台   讲师:沐远
14:40-15:20  HBase在有赞的平台实践和应用 讲师:赵原
15:20-15:30  抽奖环节
15:30-16:10  云HBase之OpenTSDB时序引擎压缩优化  讲师:郭泽晖
16:10-16:50  Flink+HBase实时计算场景 讲师:吾与

*主办:中国HBase技术社区;云栖社区

*协办:阿里云飞天八部多模型数据库组;DataFun社区

*合作伙伴:云栖TechDay;开源中国;SegmentFault;掘金;活动行;示说网
 
议程图-2.jpg

 
继续阅读 »
报名地址:点我报名
 
演讲主题及嘉宾
  • Topic1:基于HBase和Spark构建企业级数据处理平台

           首先介绍下Spark的发展之路,HBase结合Spark如何构建企业级数据处理平台,最后介绍下相关的案例。
  • Topic2:HBase在有赞的平台实践和应用

          介绍HBase在有赞的部署实践,管控平台实践,我们在社区1.2.6版本之上做了哪些改造,修复了哪些bug,以及结合在有赞的业务场景,介绍HBase的应用实践。
  • Topic3:云HBase之OpenTSDB时序引擎压缩优化

          1.OpenTSDB设计介绍
          2.介绍云HBase OpenTSDB 压缩优化
          3.介绍云HBase OpenTSDB 使用模式
  • Topic4:Flink+HBase实时计算场景

         1.Flink简介
         2.Flink优势
         3.Flink典型应用场景
         4.Flink和HBase结合的案例分享

议程安排

13:30-14:00  签到
14:00-14:40  基于HBase和Spark构建企业级数据处理平台   讲师:沐远
14:40-15:20  HBase在有赞的平台实践和应用 讲师:赵原
15:20-15:30  抽奖环节
15:30-16:10  云HBase之OpenTSDB时序引擎压缩优化  讲师:郭泽晖
16:10-16:50  Flink+HBase实时计算场景 讲师:吾与

*主办:中国HBase技术社区;云栖社区

*协办:阿里云飞天八部多模型数据库组;DataFun社区

*合作伙伴:云栖TechDay;开源中国;SegmentFault;掘金;活动行;示说网
 
议程图-2.jpg

  收起阅读 »

HBase社区福利 | HBase初学者的福利来袭

HBase 是一个分布式存储、数据库引擎,可以支持千万的QPS、PB级别的存储,这些都已经在生产环境验证,并且在广大的公司有大量的应用。根据目前公开的数据,阿里巴巴 HBase 集群规模为 12000 多台;京东 HBase 集群规模为 5000 多台;360 HBase 规模为 12500 台;爱奇艺 6000 多台。

HBase 可以说是一个数据库,也可以说是一个存储。拥有双重属性的 HBase 天生就具备广阔的应用场景。在2.0中,引入了OffHeap降低了延迟,可以满足在线的需求。引入MOB,可以存储10M左右的对象,完全适应了对象存储。另外由于自身的并发能力、存储能力,可以说是一个很有竞争力的引擎。HBase 在各大公司的应用场景包括以下几点:
 
  • 推荐画像:特别是用户的画像,是一个比较大的稀疏矩阵,蚂蚁的风控就是构建在HBase之上
  • 对象存储:我们知道不少的头条类、新闻类的的新闻、网页、图片存储在HBase之中,一些病毒公司的病毒库也是存储在HBase之中
  • 时序数据:HBase之上有OpenTSDB模块,可以满足时序类场景的需求
  • 时空数据:主要是轨迹、气象网格之类,滴滴打车的轨迹数据主要存在HBase之中,另外在技术所有大一点的数据量的车联网企业,数据都是存在HBase之中
  • CubeDB OLAP:Kylin一个cube分析工具,底层的数据就是存储在HBase之中,不少客户自己基于离线计算构建cube存储在hbase之中,满足在线报表查询的需求
  • 消息/订单:在电信领域、银行领域,不少的订单查询底层的存储,另外不少通信、消息同步的应用构建在HBase之上
  • Feeds流:典型的应用就是xx朋友圈类似的应用
  • NewSQL:之上有Phoenix的插件,可以满足二级索引、SQL的需求,对接传统数据需要SQL非事务的需求。


HBase 有着如此多的应用,越来越多的公司都在使用它,这就导致越来越多的开发者们需要学习 HBase。但是HBase是一种分布式的数据库,其正常运行需要部署 HDFS、Zookeeper以及HBase等相关组件;这对于一个新手来说特别复杂,间接增加了我们的学习成本。其实对大部分用户来说,只是想简单的使用 HBase,比如使用它来读写数据,验证某些场景的可行性。对这部分用户,其实他们并不想花太多时间在部署各种组件上面。

基于这些原因,中国HBase技术社区联合阿里云HBase数据库团队为大家带来了福利。阿里云HBase为我们提供了免费的HBase环境,如下:
微信截图_20190220134510.png

我们可以通过 https://promotion.aliyun.com/ntms/act/hbasefree.html 链接去申请入。
继续阅读 »
HBase 是一个分布式存储、数据库引擎,可以支持千万的QPS、PB级别的存储,这些都已经在生产环境验证,并且在广大的公司有大量的应用。根据目前公开的数据,阿里巴巴 HBase 集群规模为 12000 多台;京东 HBase 集群规模为 5000 多台;360 HBase 规模为 12500 台;爱奇艺 6000 多台。

HBase 可以说是一个数据库,也可以说是一个存储。拥有双重属性的 HBase 天生就具备广阔的应用场景。在2.0中,引入了OffHeap降低了延迟,可以满足在线的需求。引入MOB,可以存储10M左右的对象,完全适应了对象存储。另外由于自身的并发能力、存储能力,可以说是一个很有竞争力的引擎。HBase 在各大公司的应用场景包括以下几点:
 
  • 推荐画像:特别是用户的画像,是一个比较大的稀疏矩阵,蚂蚁的风控就是构建在HBase之上
  • 对象存储:我们知道不少的头条类、新闻类的的新闻、网页、图片存储在HBase之中,一些病毒公司的病毒库也是存储在HBase之中
  • 时序数据:HBase之上有OpenTSDB模块,可以满足时序类场景的需求
  • 时空数据:主要是轨迹、气象网格之类,滴滴打车的轨迹数据主要存在HBase之中,另外在技术所有大一点的数据量的车联网企业,数据都是存在HBase之中
  • CubeDB OLAP:Kylin一个cube分析工具,底层的数据就是存储在HBase之中,不少客户自己基于离线计算构建cube存储在hbase之中,满足在线报表查询的需求
  • 消息/订单:在电信领域、银行领域,不少的订单查询底层的存储,另外不少通信、消息同步的应用构建在HBase之上
  • Feeds流:典型的应用就是xx朋友圈类似的应用
  • NewSQL:之上有Phoenix的插件,可以满足二级索引、SQL的需求,对接传统数据需要SQL非事务的需求。


HBase 有着如此多的应用,越来越多的公司都在使用它,这就导致越来越多的开发者们需要学习 HBase。但是HBase是一种分布式的数据库,其正常运行需要部署 HDFS、Zookeeper以及HBase等相关组件;这对于一个新手来说特别复杂,间接增加了我们的学习成本。其实对大部分用户来说,只是想简单的使用 HBase,比如使用它来读写数据,验证某些场景的可行性。对这部分用户,其实他们并不想花太多时间在部署各种组件上面。

基于这些原因,中国HBase技术社区联合阿里云HBase数据库团队为大家带来了福利。阿里云HBase为我们提供了免费的HBase环境,如下:
微信截图_20190220134510.png

我们可以通过 https://promotion.aliyun.com/ntms/act/hbasefree.html 链接去申请入。 收起阅读 »

2019 HBase Meetup 演讲者和议题征集

HBase Meetup 会议由 HBase技术社区主办,在全国各大城市举办。在过去的2018年,我们在北京、上海、杭州、深圳以及武汉等城市举办了9场 HBase Meetup 会议,来自各大公司的 HBase PMC、committer 以及 HBase 开发者共聚一堂,为大家分享了 HBase 技术解析及应用实践。 
 
2019年,我们继续在全国举办 HBase 线下交流会。现向大家征集这几次会议的大会演讲者和议题,如果大家有意来分享,可以到 http://hbasegroup.mikecrm.com/zh19LHN 参加调查问卷,欢迎大家踊跃参与。同时愿意主动主办的公司,也可以来联系我。
 
议题范围:HBase、Spark、Phoenix、Solr、时序、时空以及图等使用案例,相关经验等。
投送议题时需要说明议题名称和简介,PPT可后续完成。
 
报名咨询:
微信:iteblog (备注 HBase Meetup)
钉钉:rvix4rb

各地 Meetup 举办时间:
 
继续阅读 »
HBase Meetup 会议由 HBase技术社区主办,在全国各大城市举办。在过去的2018年,我们在北京、上海、杭州、深圳以及武汉等城市举办了9场 HBase Meetup 会议,来自各大公司的 HBase PMC、committer 以及 HBase 开发者共聚一堂,为大家分享了 HBase 技术解析及应用实践。 
 
2019年,我们继续在全国举办 HBase 线下交流会。现向大家征集这几次会议的大会演讲者和议题,如果大家有意来分享,可以到 http://hbasegroup.mikecrm.com/zh19LHN 参加调查问卷,欢迎大家踊跃参与。同时愿意主动主办的公司,也可以来联系我。
 
议题范围:HBase、Spark、Phoenix、Solr、时序、时空以及图等使用案例,相关经验等。
投送议题时需要说明议题名称和简介,PPT可后续完成。
 
报名咨询:
微信:iteblog (备注 HBase Meetup)
钉钉:rvix4rb

各地 Meetup 举办时间:
  收起阅读 »

HBase Meetup 2019 年计划安排

2018年我们在全国各地举办了9场HBase Meetup会议。2019年,我们将继续为大家举办 HBase Meetup 会议,目前计划安排如下:
 
2018年我们在全国各地举办了9场HBase Meetup会议。2019年,我们将继续为大家举办 HBase Meetup 会议,目前计划安排如下:
 

2018年HBase生态社群画像 +最全资料汇总下载

d31742f41fe14a640fcc4b3b527f04985e00dd2c.png

f77a3002db4fae240559d8cc3fe8b92c64ed4937.png

26e49202a039fe1e3eb1adf6470f07d87716769f.png

40cc47db9c4e7f674843066f3ec2633979d75a72.png

388942cd244f470b6f655d62d3f576757dbff8af.png

a65ddc521b8fadf40f57094b44a917a9179ef60f.png

0f1570d0d4240064e6de0815d4441e0c61d988b5.png

27d414dbdcbb2d680461513a5b255b417b3fc70c.png

钉群直播全部资料下载:下载
 
e774dbc14e69ff6895ec38b329b89a30650882ec.png

2200687ad1a586fd54358ae539511f8c0ea8c15c.png

9届Meetup视频和PPT下载:下载

0ae287f151e65e060e273905f916f76805e6f5b4.png


《58HBase平台实践和应用 -平台建设篇》
何良均/张祥 58同城
查看
《HBase Rowkey 设计指南》
吴阳平 阿里巴巴HBase业务架 构师
查看
《HBase 2.0之修复工具HBCK2 运维指南》
田竞云 小米HBase Committer
查看
《从NoSQL到NewSQL,凤 凰涅槃成就Phoenix》
张赟 阿里巴巴 Phoenix Committer
查看
《消灭毛刺!HBase2.0全链路offheap效果拔群》
杨文龙 阿里巴巴技术专家 HBase Committer&HBase PMC
查看
《解读HBase2.0新功能 AssignmentManagerV2》
杨文龙 阿里巴巴技术专家 HBase Committer&Hbase PMC
查看
《深入解读HBase2.0新功能 高可用读Region Replica》
杨文龙 阿里巴巴技术专家 HBase Committer&Hbase PMC
查看
《HBase最佳实践-读性能 优化策略》
范欣欣 网易 HBase 资深开发工程师
查看
《HBase2.0新特性之In- Memory Compaction》
陆豪 阿里巴巴技术专家
查看
《HBase Coprocessor的实现与应用》
叶铿 烽火 大数据平台负责人
查看
《HBase实战之MOB使用指南》
查看
《HBase在新能源汽车监控系统中的应用》
颜禹 重庆博尼施CTO
查看
《HBase在爱奇艺的应用实践》
郑昊南 爱奇艺 资深研发工程师
查看
《HBase在滴滴出行的应用场景和最佳实践》
李扬 滴滴
查看
《HBase在人工智能场景的使用》
吴阳平 阿里HBase业务架构师
查看
《车纷享:基于阿里云 HBase构建车联网平台实践》
查看
《HBase基本知识介绍及典型 案例分析》
吴阳平 阿里HBase业务架构师
查看

9daceab1cd0cfec37d5122c10d424251cddaea94.png

89d798bac2565ae1ea89845a5f2b34f2b4555443.png

4a5346ef14879ecd66262f0f6bfbf9d178a1e6da.png

5a613c85a5a65c4777f339ad0a2a6700c3cced98.png


5d6d269caa4deb5ea85092c36ac5aa461ae8d6b2.png


HBase生态+Spark社区 云栖号:https://yq.aliyun.com/teams/382
中国 HBase 技术社区网站:http://hbase.group
继续阅读 »
d31742f41fe14a640fcc4b3b527f04985e00dd2c.png

f77a3002db4fae240559d8cc3fe8b92c64ed4937.png

26e49202a039fe1e3eb1adf6470f07d87716769f.png

40cc47db9c4e7f674843066f3ec2633979d75a72.png

388942cd244f470b6f655d62d3f576757dbff8af.png

a65ddc521b8fadf40f57094b44a917a9179ef60f.png

0f1570d0d4240064e6de0815d4441e0c61d988b5.png

27d414dbdcbb2d680461513a5b255b417b3fc70c.png

钉群直播全部资料下载:下载
 
e774dbc14e69ff6895ec38b329b89a30650882ec.png

2200687ad1a586fd54358ae539511f8c0ea8c15c.png

9届Meetup视频和PPT下载:下载

0ae287f151e65e060e273905f916f76805e6f5b4.png


《58HBase平台实践和应用 -平台建设篇》
何良均/张祥 58同城
查看
《HBase Rowkey 设计指南》
吴阳平 阿里巴巴HBase业务架 构师
查看
《HBase 2.0之修复工具HBCK2 运维指南》
田竞云 小米HBase Committer
查看
《从NoSQL到NewSQL,凤 凰涅槃成就Phoenix》
张赟 阿里巴巴 Phoenix Committer
查看
《消灭毛刺!HBase2.0全链路offheap效果拔群》
杨文龙 阿里巴巴技术专家 HBase Committer&HBase PMC
查看
《解读HBase2.0新功能 AssignmentManagerV2》
杨文龙 阿里巴巴技术专家 HBase Committer&Hbase PMC
查看
《深入解读HBase2.0新功能 高可用读Region Replica》
杨文龙 阿里巴巴技术专家 HBase Committer&Hbase PMC
查看
《HBase最佳实践-读性能 优化策略》
范欣欣 网易 HBase 资深开发工程师
查看
《HBase2.0新特性之In- Memory Compaction》
陆豪 阿里巴巴技术专家
查看
《HBase Coprocessor的实现与应用》
叶铿 烽火 大数据平台负责人
查看
《HBase实战之MOB使用指南》
查看
《HBase在新能源汽车监控系统中的应用》
颜禹 重庆博尼施CTO
查看
《HBase在爱奇艺的应用实践》
郑昊南 爱奇艺 资深研发工程师
查看
《HBase在滴滴出行的应用场景和最佳实践》
李扬 滴滴
查看
《HBase在人工智能场景的使用》
吴阳平 阿里HBase业务架构师
查看
《车纷享:基于阿里云 HBase构建车联网平台实践》
查看
《HBase基本知识介绍及典型 案例分析》
吴阳平 阿里HBase业务架构师
查看

9daceab1cd0cfec37d5122c10d424251cddaea94.png

89d798bac2565ae1ea89845a5f2b34f2b4555443.png

4a5346ef14879ecd66262f0f6bfbf9d178a1e6da.png

5a613c85a5a65c4777f339ad0a2a6700c3cced98.png


5d6d269caa4deb5ea85092c36ac5aa461ae8d6b2.png


HBase生态+Spark社区 云栖号:https://yq.aliyun.com/teams/382
中国 HBase 技术社区网站:http://hbase.group 收起阅读 »

HBase疑难杂症填坑指南

1.如何避免HBase写入过快引起的各种问题?
首先我们简单回顾下整个写入流程
client api ==> RPC ==> server IPC ==> RPC queue ==> RPC handler ==> write WAL ==> write memstore ==> flush to filesystem
1.png

整个写入流程从客户端调用API开始,数据会通过protobuf编码成一个请求,通过socket实现的IPC模块被送达server的RPC队列中。最后由负责处理RPC的handler取出请求完成写入操作。写入会先写WAL文件,然后再写一份到内存中,也就是memstore模块,当满足条件时,memstore才会被flush到底层文件系统,形成HFile。
当写入过快时会遇见什么问题?
写入过快时,memstore的水位会马上被推高。
你可能会看到以下类似日志:
RegionTooBusyException: Above memstore limit, regionName=xxxxx ...
这个是Region的memstore占用内存大小超过正常的4倍,这时候会抛异常,写入请求会被拒绝,客户端开始重试请求。当达到128M的时候会触发flush memstore,当达到128M * 4还没法触发flush时候会抛异常来拒绝写入。两个相关参数的默认值如下:
hbase.hregion.memstore.flush.size=128M 
hbase.hregion.memstore.block.multiplier=4
或者这样的日志:
regionserver.MemStoreFlusher: Blocking updates on hbase.example.host.com,16020,1522286703886: the global memstore size 1.3 G is >= than blocking 1.3 G size 
regionserver.MemStoreFlusher: Memstore is above high water mark and block 528ms
这是所有region的memstore内存总和开销超过配置上限,默认是配置heap的40%,这会导致写入被阻塞。目的是等待flush的线程把内存里的数据flush下去,否则继续允许写入memestore会把内存写爆
hbase.regionserver.global.memstore.upperLimit=0.4 # 较旧版本,新版本兼容 
hbase.regionserver.global.memstore.size=0.4 # 新版本
当写入请求由于达到memstore上限而被阻塞,队列会开始积压,如果运气不好最后会导致OOM,你可能会发现JVM由于OOM crash或者看到如下类似日志:
ipc.RpcServer: /192.168.x.x:16020 is unable to read call parameter from client 10.47.x.x 
java.lang.OutOfMemoryError: Java heap space
HBase这里我认为有个很不好的设计,捕获了OOM异常却没有终止进程。这时候进程可能已经没法正常运行下去了,你还会在日志里发现很多其它线程也抛OOM异常。比如stop可能根本stop不了,RS可能会处于一种僵死状态。
如何避免RS OOM?
一种是加快flush速度
hbase.hstore.blockingWaitTime = 90000 ms 
hbase.hstore.flusher.count = 2
hbase.hstore.blockingStoreFiles = 10
当达到hbase.hstore.blockingStoreFiles配置上限时,会导致flush阻塞等到compaction工作完成。阻塞时间是hbase.hstore.blockingWaitTime,可以改小这个时间。hbase.hstore.flusher.count可以根据机器型号去配置,可惜这个数量不会根据写压力去动态调整,配多了,非导入数据多场景也没用,改配置还得重启。
同样的道理,如果flush加快,意味这compaction也要跟上,不然文件会越来越多,这样scan性能会下降,开销也会增大。
hbase.regionserver.thread.compaction.small = 1 
hbase.regionserver.thread.compaction.large = 1
增加compaction线程会增加CPU和带宽开销,可能会影响正常的请求。如果不是导入数据,一般而言是够了。好在这个配置在云HBase内是可以动态调整的,不需要重启。
上述配置都需要人工干预,如果干预不及时server可能已经OOM了,这时候有没有更好的控制方法?
hbase.ipc.server.max.callqueue.size = 1024 * 1024 * 1024 # 1G
直接限制队列堆积的大小。当堆积到一定程度后,事实上后面的请求等不到server端处理完,可能客户端先超时了。并且一直堆积下去会导致OOM,1G的默认配置需要相对大内存的型号。当达到queue上限,客户端会收到CallQueueTooBigException 然后自动重试。通过这个可以防止写入过快时候把server端写爆,有一定反压作用。线上使用这个在一些小型号稳定性控制上效果不错。
填坑链接:如何避免HBase写入过快引起的各种问题
继续阅读 »
1.如何避免HBase写入过快引起的各种问题?
首先我们简单回顾下整个写入流程
client api ==> RPC ==> server IPC ==> RPC queue ==> RPC handler ==> write WAL ==> write memstore ==> flush to filesystem
1.png

整个写入流程从客户端调用API开始,数据会通过protobuf编码成一个请求,通过socket实现的IPC模块被送达server的RPC队列中。最后由负责处理RPC的handler取出请求完成写入操作。写入会先写WAL文件,然后再写一份到内存中,也就是memstore模块,当满足条件时,memstore才会被flush到底层文件系统,形成HFile。
当写入过快时会遇见什么问题?
写入过快时,memstore的水位会马上被推高。
你可能会看到以下类似日志:
RegionTooBusyException: Above memstore limit, regionName=xxxxx ...
这个是Region的memstore占用内存大小超过正常的4倍,这时候会抛异常,写入请求会被拒绝,客户端开始重试请求。当达到128M的时候会触发flush memstore,当达到128M * 4还没法触发flush时候会抛异常来拒绝写入。两个相关参数的默认值如下:
hbase.hregion.memstore.flush.size=128M 
hbase.hregion.memstore.block.multiplier=4
或者这样的日志:
regionserver.MemStoreFlusher: Blocking updates on hbase.example.host.com,16020,1522286703886: the global memstore size 1.3 G is >= than blocking 1.3 G size 
regionserver.MemStoreFlusher: Memstore is above high water mark and block 528ms
这是所有region的memstore内存总和开销超过配置上限,默认是配置heap的40%,这会导致写入被阻塞。目的是等待flush的线程把内存里的数据flush下去,否则继续允许写入memestore会把内存写爆
hbase.regionserver.global.memstore.upperLimit=0.4 # 较旧版本,新版本兼容 
hbase.regionserver.global.memstore.size=0.4 # 新版本
当写入请求由于达到memstore上限而被阻塞,队列会开始积压,如果运气不好最后会导致OOM,你可能会发现JVM由于OOM crash或者看到如下类似日志:
ipc.RpcServer: /192.168.x.x:16020 is unable to read call parameter from client 10.47.x.x 
java.lang.OutOfMemoryError: Java heap space
HBase这里我认为有个很不好的设计,捕获了OOM异常却没有终止进程。这时候进程可能已经没法正常运行下去了,你还会在日志里发现很多其它线程也抛OOM异常。比如stop可能根本stop不了,RS可能会处于一种僵死状态。
如何避免RS OOM?
一种是加快flush速度
hbase.hstore.blockingWaitTime = 90000 ms 
hbase.hstore.flusher.count = 2
hbase.hstore.blockingStoreFiles = 10
当达到hbase.hstore.blockingStoreFiles配置上限时,会导致flush阻塞等到compaction工作完成。阻塞时间是hbase.hstore.blockingWaitTime,可以改小这个时间。hbase.hstore.flusher.count可以根据机器型号去配置,可惜这个数量不会根据写压力去动态调整,配多了,非导入数据多场景也没用,改配置还得重启。
同样的道理,如果flush加快,意味这compaction也要跟上,不然文件会越来越多,这样scan性能会下降,开销也会增大。
hbase.regionserver.thread.compaction.small = 1 
hbase.regionserver.thread.compaction.large = 1
增加compaction线程会增加CPU和带宽开销,可能会影响正常的请求。如果不是导入数据,一般而言是够了。好在这个配置在云HBase内是可以动态调整的,不需要重启。
上述配置都需要人工干预,如果干预不及时server可能已经OOM了,这时候有没有更好的控制方法?
hbase.ipc.server.max.callqueue.size = 1024 * 1024 * 1024 # 1G
直接限制队列堆积的大小。当堆积到一定程度后,事实上后面的请求等不到server端处理完,可能客户端先超时了。并且一直堆积下去会导致OOM,1G的默认配置需要相对大内存的型号。当达到queue上限,客户端会收到CallQueueTooBigException 然后自动重试。通过这个可以防止写入过快时候把server端写爆,有一定反压作用。线上使用这个在一些小型号稳定性控制上效果不错。
填坑链接:如何避免HBase写入过快引起的各种问题 收起阅读 »

中国HBase技术社区第九届meetup-HBase典型应用场景与实践

会议时间
2018年12月23日 14:00 ~ 2018年12月23日 18:00  (北京朝阳)360公司 A座一层发布厅,报名地址:http://www.huodongxing.com/event/1469391290300

head_image.jpg

 本期嘉宾介绍

微信图片_20181218193751.jpg

本期活动主题

  13:30-14:00  

   签到

  14:00-14:40 

  HBase 2.0 在360的技术改进与应用实践       

  讲师:王小勇——360系统部分布式存储方向架构师

  介绍内容:HBase在360的使用现状和发展历程,以及在升级HBase2.0的过程中发现的问题与改进。

  14:40-15:20   

   HBase 基本知识介绍及典型案例分析

   讲师:吴阳平——阿里云HBase业务架构师

   介绍内容:HBase基础知识介绍,Rowkey设计技巧,HBase企业级特性及组件介绍,HBase+Spark典型案例分析。

  15:20-15:30   

   抽奖环节 送360、阿里、京东、58专属定制的礼品

  15:30-16:10    

   HBase在无界零售中的应用

   讲师:诸葛子房——京东大数据工程师

   介绍内容:Hbase存储的优势;Hbase案例分享;Hbase数据分析

  16:10-16:50    

   图数据库hgraphdb介绍

   讲师:陈江——阿里高级技术专家

   介绍内容:HGraphDB 是一个使用 HBase 作为底层存储的图数据库, 是 Apache TinkerPop 3 接口的实现。


  16:50-17:30   

   HBase在58的实践和应用

   讲师:何良均——58大数据工程师

   介绍内容:HBase在58的实践和应用,包括平台建设、生态建设、平台监控、平台运营等


主办:中国HBase技术社区

协办:360技术委员会;阿里云飞天八部多模型数据库组;云栖社区;360大学;360系统部,DataFun社区

合作伙伴:开源中国;SegmentFault;掘金;示说网

活动官方报名平台:活动行


技术社群及公众号推荐:


【HBase中国钉钉技术交流社群】:

为了更好的服务HBase开发者,我们启用了钉钉企业号。并每周在群内进行【技术分享直播】和【在线回答技术问题】。

欢迎大家入群,点击link 入群:  https://dwz.cn/Fvqv066s
继续阅读 »
会议时间
2018年12月23日 14:00 ~ 2018年12月23日 18:00  (北京朝阳)360公司 A座一层发布厅,报名地址:http://www.huodongxing.com/event/1469391290300

head_image.jpg

 本期嘉宾介绍

微信图片_20181218193751.jpg

本期活动主题

  13:30-14:00  

   签到

  14:00-14:40 

  HBase 2.0 在360的技术改进与应用实践       

  讲师:王小勇——360系统部分布式存储方向架构师

  介绍内容:HBase在360的使用现状和发展历程,以及在升级HBase2.0的过程中发现的问题与改进。

  14:40-15:20   

   HBase 基本知识介绍及典型案例分析

   讲师:吴阳平——阿里云HBase业务架构师

   介绍内容:HBase基础知识介绍,Rowkey设计技巧,HBase企业级特性及组件介绍,HBase+Spark典型案例分析。

  15:20-15:30   

   抽奖环节 送360、阿里、京东、58专属定制的礼品

  15:30-16:10    

   HBase在无界零售中的应用

   讲师:诸葛子房——京东大数据工程师

   介绍内容:Hbase存储的优势;Hbase案例分享;Hbase数据分析

  16:10-16:50    

   图数据库hgraphdb介绍

   讲师:陈江——阿里高级技术专家

   介绍内容:HGraphDB 是一个使用 HBase 作为底层存储的图数据库, 是 Apache TinkerPop 3 接口的实现。


  16:50-17:30   

   HBase在58的实践和应用

   讲师:何良均——58大数据工程师

   介绍内容:HBase在58的实践和应用,包括平台建设、生态建设、平台监控、平台运营等


主办:中国HBase技术社区

协办:360技术委员会;阿里云飞天八部多模型数据库组;云栖社区;360大学;360系统部,DataFun社区

合作伙伴:开源中国;SegmentFault;掘金;示说网

活动官方报名平台:活动行


技术社群及公众号推荐:


【HBase中国钉钉技术交流社群】:

为了更好的服务HBase开发者,我们启用了钉钉企业号。并每周在群内进行【技术分享直播】和【在线回答技术问题】。

欢迎大家入群,点击link 入群:  https://dwz.cn/Fvqv066s
收起阅读 »

使用 Spark 分析 HBase 的数据

HBase具有很好的在线入库和查询能力,不过在分析上面有比较大的欠缺,这篇文章主要介绍如何使用Spark对HBase中数据做复杂分析。

HBase查询分析的现状
 
  • HBase原生API:HBase原生API适合基于row key做点查,这个是HBase最擅长的查询场景
  • Phoenix:Phoenix作为HBase的SQL层,使用二级索引的技术,擅长多条件组合查询;Phoenix没有自己的计算资源,类似groupby这样的复杂查询需要借助HBase的协处理器来完成,这样一方面性能不好,同时会影响HBase集群的稳定性;
  • Spark:具有丰富的算子支持复杂分析,使用Spark集群的计算资源,通过并发分析的方式可以提高性能,同时不影响HBase集群的稳定性。


Spark分析HBase的方式对比

Spark分析HBase数据有“RDD API”、“SQL API”、“HFILE”三种方式,相关对比如下:

menu.saveimg_.savepath20181128110650_.jpg

 
对于数据动态更新增加的小表推荐使用SQL API的方式,可以有效的优化分析,减少对HBase集群稳定性的影响;对于静态表或者全量静态表的分析推荐使用分析HFILE的方式直读HDFS,这样可以完全不影响HBase集群稳定性;不推荐使用RDD API 的方式,这种方式一方没有优化性能差,同时在高并发以及表数据量大时,会严重影响HBase集群的稳定性,从而影响在线业务。

 三种方式的具体使用

阿里云HBase团队为大家提供了一个github项目供大家参考使用上面的三种方式来开发Spark分析HBase的程序,项目地址:

https://github.com/lw309637554/alicloud-hbase-spark-examples

依赖项:需要下载云HBase及云Phoenix的client包
分析HFILE:
  • 需要先开通云HBase的HDFS访问权限,参考文档
  • 在hbase shell中对表生成snapshot表“snapshot 'sourceTable', ‘snapshotName'”
  • 在项目中配置自己的hdfs-sit.xml文件,然后通过直读HDFS的方式分析snapshot表


具体的example
  • RDD API对应:org.apache.spark.hbase.NativeRDDAnalyze
  • SQL API对应:org.apache.spark.sql.execution.datasources.hbase.SqlAnalyze
  • 分析HFILE对应:org.apache.spark.hfile.SparkAnalyzeHFILE

 
继续阅读 »
HBase具有很好的在线入库和查询能力,不过在分析上面有比较大的欠缺,这篇文章主要介绍如何使用Spark对HBase中数据做复杂分析。

HBase查询分析的现状
 
  • HBase原生API:HBase原生API适合基于row key做点查,这个是HBase最擅长的查询场景
  • Phoenix:Phoenix作为HBase的SQL层,使用二级索引的技术,擅长多条件组合查询;Phoenix没有自己的计算资源,类似groupby这样的复杂查询需要借助HBase的协处理器来完成,这样一方面性能不好,同时会影响HBase集群的稳定性;
  • Spark:具有丰富的算子支持复杂分析,使用Spark集群的计算资源,通过并发分析的方式可以提高性能,同时不影响HBase集群的稳定性。


Spark分析HBase的方式对比

Spark分析HBase数据有“RDD API”、“SQL API”、“HFILE”三种方式,相关对比如下:

menu.saveimg_.savepath20181128110650_.jpg

 
对于数据动态更新增加的小表推荐使用SQL API的方式,可以有效的优化分析,减少对HBase集群稳定性的影响;对于静态表或者全量静态表的分析推荐使用分析HFILE的方式直读HDFS,这样可以完全不影响HBase集群稳定性;不推荐使用RDD API 的方式,这种方式一方没有优化性能差,同时在高并发以及表数据量大时,会严重影响HBase集群的稳定性,从而影响在线业务。

 三种方式的具体使用

阿里云HBase团队为大家提供了一个github项目供大家参考使用上面的三种方式来开发Spark分析HBase的程序,项目地址:

https://github.com/lw309637554/alicloud-hbase-spark-examples

依赖项:需要下载云HBase及云Phoenix的client包
分析HFILE:
  • 需要先开通云HBase的HDFS访问权限,参考文档
  • 在hbase shell中对表生成snapshot表“snapshot 'sourceTable', ‘snapshotName'”
  • 在项目中配置自己的hdfs-sit.xml文件,然后通过直读HDFS的方式分析snapshot表


具体的example
  • RDD API对应:org.apache.spark.hbase.NativeRDDAnalyze
  • SQL API对应:org.apache.spark.sql.execution.datasources.hbase.SqlAnalyze
  • 分析HFILE对应:org.apache.spark.hfile.SparkAnalyzeHFILE

  收起阅读 »

openTSDB源码详解之rowKey生成

openTSDB的一个非常好的设计就是其rowKey。下面笔者主要通过源码的角度来详细介绍一下其rowKey的生成步骤。
1. 相关处理类
openTSDB往hbase中写入数据的处理过程,我之前就已经分析过,主要涉及的类有:- `addPointInternal(...)`
1.png

这里主要讲解的是,一个row key是如何生成的。

2. 具体步骤
  •  2.1 row_size的确定

       2.1.1 `SALT_WIDTH`
首先对于一个rowKey,肯定是有大小的,但是这个rowKey的大小该怎么确定呢?因为这个rowKey是需要写入到HBase中的,也就是需要通过网络的传输才能到达HBase端,所以不可能无限长,即需要精确到一个具体的长度。
openTSDB中的rowKey的大小用变量row_size表示。其具体的生成规则如下图所示:
2.png

其中的 `SALT_WIDTH()`函数返回的是SALT WIDTH,它是一个static修饰的int型变量,初始值为0,如下图所示:
3.png

    2.1.2 `metric_width`,`tag_name_width`,`tag_value_width`
`metric_width` 代表的就是`metric`的长度。在openTSDB系统中,默认的长度是3。该值来自`tsdb.metrics.width()`函数的返回值。
4.png

`width()` 方法如下:
5.png

而id_width的值设置过程如下:
6.png

7.png

调用构造函数`UniqueId(...)`的过程如下:
8.png

最终可以看到这些值默认为3
9.png

针对笔者传输的数据,可以推导出最后生成的row_size的大小就是`row_size = 0 +  3 +  4 + 3*1 + 3*1 = 13`,经过debug验证,也可以看到的确就是13。
  •  2.2 字节数组`row`

10.png

  •  2.3 `pos`变量

11.png

问题1:使用`pos`变量的原因是,控制对字节数组row的复制控制【每将一个值复制到row之后,就需要动态改变row的起始位置,这个位置就是pos定义的】
因为我没有开启 加盐处理功能,所以这里的pos的初始值就是0。
  •  2.4 为字节数组`row`赋值

12.png

上述copyInRowKey(...)的功能就是将metric对应的`uid`值放入到`row`中。而其中的 `getOrCreateId()`方法则是获取metric对应的`uid`值。该方法主要调用的其它方法如下:
- `getIdAsync()` 方法
13.png

- 从缓存中获取uid
14.png

- Deferred 类的构造方法
15.png

- `copyInRowKey()`方法
16.png

注意上面一张图中row和下面一张图的row的变化。这个变化就体现了将metric的id放到了rowKey中。
17.png

  •  2.5 `pos`值的变化

18.png

因为已经处理了metric,所以需要将其pos自增一下。
同时,因为暂时不着急处理timestamp,所以再次将pos自增处理。
  •  2.6 `tag pair`的处理

19.png

最后,需要在row中放入 tag pair,方式是采用for循环处理。
主要调用方法如下:
- `resolveOrCreateAll()`
20.png

- `resolveAllInternal()`方法
21.png

  •  2.7 `row`的大致部分

经过上述几个步骤的处理,则会生成一个row值:
22.png

  •  2.8 `rowKey`的完善

上述的步骤只是生成了一个rowKey的框架,但是还没有完全生成一个rowKey,因为还有最后一个步骤。接着会进入`WriteCB`这个回调类,如下:
23.png

注意这里的offset的取值:`metrics.width() + Const.SALT_WIDTH()`这个是用于将当前value所在小时的时间戳放入到row中。所以需要偏移`metrics.width() + Const.SALT_WIDTH()` 这样的长度。这里使用base_time = 1542096000举例,在未执行Bytes.setInt()方法之前,row的值如下图示:
24.png

使用`Bytes`类的`setInt()`方法修改row的值。`setInt()`主要的是`>>>`运算,这个运算符的意思是指:右移指定的位数,左边的空白位置0。得到的row值如下:
25.png

可能有人会问,为什么使用的是`Bytes.setInt()`这个方法?要知道,openTSDB只是根据timestamp(实质是 `base_time`,这个`base_time`我在后面会补充介绍)去生成rowKey的一部分,所以这一部分是什么样的逻辑不重要了,重要的是这个逻辑需要保持相同的`base_time` 映射到固定的`byte` 值。而openTSDB在`rowKey`的生成过程中采取的方法就是使用`>>>`去处理(timestamp)`base_time` ,从而保证`rowKey`在`base_time`相同时,其`base_time`对应的`rowKey`部分也相同。
到这里,一个完整的rowKey就生成了。

3.其它
  • 3.1 `base_time`字段的生成

这个`base_time`代表的值就是当前value传递过来所在的timestamp。例如:对于一个timestamp=1542088140,处理过程就是取该timestamp所在小时 0分0秒的timestamp值。这也能解释为什么opentsdb中的存储按照每小时进行行存储,而不是每个时间戳一行。
26.png

这个`Const.MAX_TIMESPAN`定义如下:
  /** Max time delta (in seconds) we can store in a column qualifier.  */
public static final short MAX_TIMESPAN = 3600;
代表的是一个小时的秒跨度。对其`MAX_TIMESPAN` 取余之后,该余数就是秒数,再减去这个秒数就得到hh:00:00的值。上面的值处理之后,得到的base_time值变为了1542085200。如下示:
27_.png

 
继续阅读 »
openTSDB的一个非常好的设计就是其rowKey。下面笔者主要通过源码的角度来详细介绍一下其rowKey的生成步骤。
1. 相关处理类
openTSDB往hbase中写入数据的处理过程,我之前就已经分析过,主要涉及的类有:- `addPointInternal(...)`
1.png

这里主要讲解的是,一个row key是如何生成的。

2. 具体步骤
  •  2.1 row_size的确定

       2.1.1 `SALT_WIDTH`
首先对于一个rowKey,肯定是有大小的,但是这个rowKey的大小该怎么确定呢?因为这个rowKey是需要写入到HBase中的,也就是需要通过网络的传输才能到达HBase端,所以不可能无限长,即需要精确到一个具体的长度。
openTSDB中的rowKey的大小用变量row_size表示。其具体的生成规则如下图所示:
2.png

其中的 `SALT_WIDTH()`函数返回的是SALT WIDTH,它是一个static修饰的int型变量,初始值为0,如下图所示:
3.png

    2.1.2 `metric_width`,`tag_name_width`,`tag_value_width`
`metric_width` 代表的就是`metric`的长度。在openTSDB系统中,默认的长度是3。该值来自`tsdb.metrics.width()`函数的返回值。
4.png

`width()` 方法如下:
5.png

而id_width的值设置过程如下:
6.png

7.png

调用构造函数`UniqueId(...)`的过程如下:
8.png

最终可以看到这些值默认为3
9.png

针对笔者传输的数据,可以推导出最后生成的row_size的大小就是`row_size = 0 +  3 +  4 + 3*1 + 3*1 = 13`,经过debug验证,也可以看到的确就是13。
  •  2.2 字节数组`row`

10.png

  •  2.3 `pos`变量

11.png

问题1:使用`pos`变量的原因是,控制对字节数组row的复制控制【每将一个值复制到row之后,就需要动态改变row的起始位置,这个位置就是pos定义的】
因为我没有开启 加盐处理功能,所以这里的pos的初始值就是0。
  •  2.4 为字节数组`row`赋值

12.png

上述copyInRowKey(...)的功能就是将metric对应的`uid`值放入到`row`中。而其中的 `getOrCreateId()`方法则是获取metric对应的`uid`值。该方法主要调用的其它方法如下:
- `getIdAsync()` 方法
13.png

- 从缓存中获取uid
14.png

- Deferred 类的构造方法
15.png

- `copyInRowKey()`方法
16.png

注意上面一张图中row和下面一张图的row的变化。这个变化就体现了将metric的id放到了rowKey中。
17.png

  •  2.5 `pos`值的变化

18.png

因为已经处理了metric,所以需要将其pos自增一下。
同时,因为暂时不着急处理timestamp,所以再次将pos自增处理。
  •  2.6 `tag pair`的处理

19.png

最后,需要在row中放入 tag pair,方式是采用for循环处理。
主要调用方法如下:
- `resolveOrCreateAll()`
20.png

- `resolveAllInternal()`方法
21.png

  •  2.7 `row`的大致部分

经过上述几个步骤的处理,则会生成一个row值:
22.png

  •  2.8 `rowKey`的完善

上述的步骤只是生成了一个rowKey的框架,但是还没有完全生成一个rowKey,因为还有最后一个步骤。接着会进入`WriteCB`这个回调类,如下:
23.png

注意这里的offset的取值:`metrics.width() + Const.SALT_WIDTH()`这个是用于将当前value所在小时的时间戳放入到row中。所以需要偏移`metrics.width() + Const.SALT_WIDTH()` 这样的长度。这里使用base_time = 1542096000举例,在未执行Bytes.setInt()方法之前,row的值如下图示:
24.png

使用`Bytes`类的`setInt()`方法修改row的值。`setInt()`主要的是`>>>`运算,这个运算符的意思是指:右移指定的位数,左边的空白位置0。得到的row值如下:
25.png

可能有人会问,为什么使用的是`Bytes.setInt()`这个方法?要知道,openTSDB只是根据timestamp(实质是 `base_time`,这个`base_time`我在后面会补充介绍)去生成rowKey的一部分,所以这一部分是什么样的逻辑不重要了,重要的是这个逻辑需要保持相同的`base_time` 映射到固定的`byte` 值。而openTSDB在`rowKey`的生成过程中采取的方法就是使用`>>>`去处理(timestamp)`base_time` ,从而保证`rowKey`在`base_time`相同时,其`base_time`对应的`rowKey`部分也相同。
到这里,一个完整的rowKey就生成了。

3.其它
  • 3.1 `base_time`字段的生成

这个`base_time`代表的值就是当前value传递过来所在的timestamp。例如:对于一个timestamp=1542088140,处理过程就是取该timestamp所在小时 0分0秒的timestamp值。这也能解释为什么opentsdb中的存储按照每小时进行行存储,而不是每个时间戳一行。
26.png

这个`Const.MAX_TIMESPAN`定义如下:
  /** Max time delta (in seconds) we can store in a column qualifier.  */
public static final short MAX_TIMESPAN = 3600;
代表的是一个小时的秒跨度。对其`MAX_TIMESPAN` 取余之后,该余数就是秒数,再减去这个秒数就得到hh:00:00的值。上面的值处理之后,得到的base_time值变为了1542085200。如下示:
27_.png

  收起阅读 »

如何确保多步操作的事务性?HBase基础框架级特性Procedure解读

标题中提及"事务"可能会给大家带来误解,这篇文章不是在讨论HBase如何支持分布式事务能力的,而是介绍HBase用来处理内部事务操作的特性,这个特性被称之为Procedure V2,也是2.0版本的主打特性之一。
本文内容基于HBase 2.1版本,整体内容组织结构为:
1. Procedure特性的设计初衷。
2. Procedure的完整生命周期。
3. Procedure框架关键角色。
4. Procedure重点模块实现细节。
关于HBase基础原理系列文章:
开篇内容
介绍HBase的数据模型、适用场景、集群关键角色、建表流程以及所涉及的HBase基础概念。
Writer全流程
介绍写数据的接口,RowKey定义,数据在客户端的组装,数据路由,打包分发,以及RegionServer侧将数据写入到Region中的全部流程。
Flush与Compaction
阐述Flush与Compaction流程,讲述了Compaction所面临的本质问题,介绍了HBase现有的几种Compaction策略以及各自的适用场景。
Read全流程
首先介绍HBase的两种读取模式(Get与Scan),而后详细介绍Scan的详细实现流程。
HFile详解
详细介绍HBase底层数据文件格式HFile的实现细节。
关于Procedure V2介绍文章:
Procedure V2介绍
回顾·Procedure V2介绍
主要介绍Procedure V2的设计和结构,以及为什么用Procedure V2能比较容易实现出正确的AssignmentManager以及在2.1分支上对一些Procedure实现修正和改进。
以下是正文内容:
Procedure的设计初衷
我们先来看一下Procedure特性的设计初衷。以建表为例: 建表过程,包含如下几个关键的操作:
1.初始化所有的RegionInfos。
2.建表前的必要检查。
3.创建表的文件目录布局。
4.将Region写到Meta表中。
5. ......
从使用者看来,一个建表操作要么是成功的,要么是失败的,并不存在一种中间状态,因此,用"事务"来描述这种操作可以更容易理解。如果曾使用过1.0或更早的版本,相信一定曾被这个问题"折磨"过:“一个表明明没有创建成功,当使用者尝试再次重建的时候,却被告知该表已存在”,究其根因,在于建表遇到异常后,没有进行合理的Rollback操作,导致集群处于一种不一致的状态。
类似的需求,在HBase内部司空见惯:Disable/Enable表,修改表,RegionServer Failover处理,Snapshot, Assign Region, Flush Table...这些操作,都具备两个特点:多步操作,有限状态机(或简单或复杂)。因此,可以实现一个公共能力,这样可避免每个特性各自为营,导致大量难以维护的冗余代码。
最初版本的Procedure,由Online Snapshot特性引入(HBASE-7290),主要用来协调分布式请求。而最新版本的Procedure(称之为Procedure V2),受Accumulo的Fault-Tolerant Executor (FATE)的启发而重新设计,该特性已经在HBase内部得到广泛的应用,因此,可以将其称之为一个基础框架级特性,了解该特性的设计原理是学习2.x源码的基础。本文范畴内所探讨的Procedure,均指Procedure V2。
Procedure的生命周期
一个Procedure由一个或一系列操作构成。一个Procedure的执行结果,要么是成功,要么是失败,失败后不会让集群处于一种不一致的状态。
接下来,我们将以Create Table操作为例,来介绍一个Procedure的完整的生命周期。
创建
Master收到一个Create Table的请求后会创建一个CreateTableProcedure实例。
CreateTableProcedure涉及到一系列的操作,而每一个操作都关联了一个操作状态。在CreateTableState中,定义了与之相关的所有操作状态:
CreateTableProcedure内部定义了自身的初始状态为CREATE_TABLE_PRE_OPERATION,而且定义了每一种状态时对应的处理操作以及当前这个状态完成后应该要切换至哪个状态。如下表列出了当前状态与成功后的退出状态:
Procedure本身还有一个运行状态,这套运行状态的定义如下:
正是为了有所区分,本文将CreateTableProcedure内部定义的私有状态,称之为操作状态,而运行状态则是所有的Procedure都拥有的状态信息,从每一个状态的定义很容易看出它的作用。
提交
Master将创建出来的CreateTableProcedure实例,提交给ProcedureExecutor。
ProcedureExecutor关于一个新提交的Procedure,做如下几步处理:
  1. 必要检查(初始状态,确保无Parent Procedure,确保有Owner信息)。
  2. 设置Nonce Key(如果存在)以及Procedure ID。
  3. 将新的Procedure写入到ProcedureStore中,持久化。
  4. 将新的Procedure添加到ProcedureScheduler的调度队列的尾部。

运行
ProcedureExecutor初始化阶段,启动了若干个WorkerThread,具体数量可配置。
WorkerThread不断从ProcedureScheduler中poll出新的待执行的Procedure,而后:
  1. 获取IdLock: 获取Procedure关联的IdLock,避免同一个Procedure在多个线程中同时处理。
  2. 获取资源锁: 调用Procedure内部定义的Acquire Lock请求,获取Procedure自身所需的资源锁。IdLock是为了确保一个Procedure只被一个线程调用,而这里的Lock是为了确保这一个Table只能被一个Procedure处理,这里需要获取Namespace的共享锁,以及当前这个Table的互斥锁,这里其实是一个分布式锁的需求,容易想到用ZooKeeper实现,事实上,这个Lock也可以用Procedure来实现,在后续章节中将会讲到这一点。
  3. 处理当前操作状态:执行Procedure初始状态所定义的处理逻辑,处理完后会返回当前这个Procedure对象。每一步运行完,都将最新的状态持久化到ProcedureStore中。
  4. 处理下一操作状态:如果返回对象依然是原来的Procedure,而且未失败,则意味着需要继续下一步处理。
  5. 循环处理:循环3,4步即可处理完所有CreateTableProcedure内部定义的所有处理。

       释放锁资源。
完成
处理每一个操作状态时,都产生一个状态返回值(Flow),如果还有下一个待处理的状态,则返回Flow.HAS_MORE_STATE,如果全部执行完成,则返回Flow.NO_MORE_STATE,借此可以判断一个Procedure是否执行完成。一个执行成功后的Procedure,运行状态被设置为SUCCESS。
完成后的Procedure,需要在ProcedureStore中进行标记删除。
Procedure框架关键角色
通过上一章节的内容,我们已经知道了,在一个Procedure执行过程中,涉及到如下几个关键角色:
ProcedureExecutor
负责提交、执行Procedure。Procedure的执行操作,主要由其负责的多个WorkerThread来完成。
Procedure的持久化,由ProcedureExecutor提交给ProcedureStore。后续的每一次状态更新,也由ProcedureExecutor向ProcedureStore发起Update请求。
新的Procedure持久化后将被提交给ProcedureScheduler,由ProcedureScheduler完成调度。ProcedureExecutor中的WorkerThread从ProcedureScheduler中获取待执行的Procedure。
ProcedureStore
用来持久化新提交的Procedure以及后续的每一次状态更新值。
ProcedureStore的默认实现类为WALProcedureStore,基于日志文件来持久化Procedure信息,虽然称之为WAL,但与HBase自身的WAL日志文件的实现完全不同,类似点在于:
  • 当超过一定大小后或者超过一定的时间周期后,需要roll一个新的WAL文件出来,避免一个WAL文件过大。
  • 需要实现一套关于无用WAL日志文件的跟踪清理机制,避免WAL文件占用过大的存储空间。
  • 实现了一套类似于RingBuffer的机制,通过打包sync并发的写入请求,来提升写入吞吐量。

ProcedureScheduler
负责调度一个集群内的各种类型的Procedure请求,支持按优先级调度,相同优先级的Procedure则支持公平调度。
我们先来看看Procedure的几大类型:
  • Meta Procedure:唯一的一种类型为RecoverMetaProcedure,该类型已被废弃。
  • Server Procedure:目前也只有一种类型:ServerCrashProcedure,用来负责RegionServer进程故障后的处理。
  • Peer Procedure:与Replication相关,如AddPeerProcedure, RemovePeerProcedure等等。
  • Table Procedure: Table Procedure的类型最为丰富,如CreateTableProcedure, DisableTableProcedure, EnableTableProcedure, AssignProcedure, SplitTableRegionProcedure,.....涵盖表级别、Region级别的各类操作。

在ProcedureScheduler中,需要同时调度这几种类型的Procedure,调度的优先级顺序(由高到低)为:
    Meta -> Server -> Peer -> Table。
在每一种类型内部,又有内部的优先级定义。以Table Procedure为例,Meta Table的优先级最高,System Table(如acl表)其次,普通用户表的优先级最低。
重点模块实现细节
看到这里,你也许会认为,"Procedure特性原来如此简单!",但如果仔细阅读这部分代码,就会深刻体会到它在实现上的复杂度,导致这种复杂度的客观原因总结起来有如下几点:
  1. 要实现一个统一的状态机管理框架本身就比较复杂,可以说将Assign Region/Create Table/Split Region等流程的复杂度转嫁了过来。
  2. 需要支持优先级调度与公平调度
  3. WALProcedureStore无用WAL文件的跟踪与清理,重启后的回放,均需要严谨的处理。
  4. 涉及复杂的拓扑结构:一个Procedure中间运行过程可能会产生多个Sub-Procedures,这过程需要协调。
  5. 不依赖于ZooKeeper的分布式锁机制。
  6. 跨节点Rpc请求协调。

在实际实现中,还内部维护几个私有的数据结构,如Avl-Tree, FairQueue以及Bitmap,这也是导致实现复杂度过高的一大原因。接下来选择了三点内容来展开讲解,这三点内容也算是Procedure框架里的难点部分,分别为:WAL清理机制,Procedure调度策略以及分布式锁与事件通知机制。
WAL清理机制
WALProcedureStore也存在WAL日志文件的roll机制,这样就会产生多个WAL文件。对于一个旧的WAL文件,如何认定它可以被安全清理了?这就需要在WALProcedureStore中设计一个合理的关于Procedure状态的更新机制。
WALProcedureStore使用ProcedureStoreTracker对象来跟踪Procedure的写入/更新与删除操作,这个对象被称之为storeTracker
在ProcedureStoreTracker中,只需要用ProcID(long类型)来表示一个Procedure。即使只记录大量的ProcIDs,也会占用大量的内存空间,因此,在ProcedureStoreTracker内部实现了一个简单的Bitmap,用一个BIT来表示一个ProcID,这个Bitmap采用了分区、弹性扩展的设计:每一个分区称之为一个BitsetNode,每一个BitsetNode有一个起始值(Start),使用一个long数组来表示这个分区对应的Bitmap,每一个long数值包含64位,因此可以用来表示64个ProcIDs。一个BitsetNode应该可以包含X个long数值,这样就可以表示从Start值开始的X * 64个ProcIDs,但可惜,现在的代码实现还是存在问题的(应该是BUG),导致一个BitsetNode只能包含1个long值。
如果了解过Java Bitset的原理,或者是RoaringBitmap,就会发现这个实现并无任何新颖之处。
在一个BitsetNode内部,其实包含两个Bitmap: 一个Bitmap(modified)用来记录Insert/Update的ProcIDs,另一个Bitmap(Deleted)用来记录已经被Delete的ProcIDs。例如,如果Proc Y在Bitmap(modified)所对应的BIT为1,在Bitmap(Deleted)中所对应的BIT为0,则意味着这个Procedure仍然存活(或许刚刚被写入,或许刚刚被更新)。如果在Bitmap(Modified)中对应的BIT为1,但在Bitmap(Deleted)中所对应的BIT为1,则意味着这个Procedure已被删除了。
如果一个旧的WAL文件所关联的所有的Procedures,都已经被更新过(每一次更新,意味着Procedure的状态已经发生变化,则旧日志记录则已失去意义),或者都已经被删除,则这个WAL文件就可以被删除了。在实现上,同样可以用另外一个ProcedureStoreTracker对象(称之为holdingCleanupTracker)来跟踪最老的WAL中的Procedure的状态,每当有新的Procedure发生更新或者被删除,都同步删除holdingCleanupTracker中对应的ProcID即可。当然,还得考虑另外一种情形,如果有个别Procedure迟迟未更新如何处理? 这时,只要强制触发这些Procedures的更新操作即可。
这样描述起来似乎很简单,但这里却容易出错,而且一旦出错,可能会导致WAL日志被误删,或者堆积大量的日志文件无法被清理,出现这样的问题都是致命的。
Procedure调度策略
关于调度策略的基础需求,可以简单被表述为:
  1. 不同类型的Procedure优先级不同,如Server Procedure要优先于Table Procedure被调度。
  2. 即使同为Table Procedure类型,也需要按照Table的类型进行优先级调度,对于相同的优先级类型,则采用公平调度策略。

MasterProcedureScheduler是默认的ProcedureScheduler实现,接下来,我们看一下它的内部实现。
MasterProcedureScheduler将同一类型的Procedure,放在一个被称之为FairQueue的队列中,这样,共有四种类型的FairQueue队列,这四个队列分别被称之为MetaRunQueue, ServerRunQueue, PeerRunQueue, TableRunQueue),在调度时,按照上述罗列的顺序进行调度,这样,就确保了不同类型间的整体调度顺序。
简单起见,我们假设这四个队列中,仅有TableRunQueue有数据,其它皆空,这样确保会调度到TableRunQueue中的Procedure。在TableRunQueue中,本身会涉及多个Table,而每一个Table也可能会涉及多个Procedures:
TableA -> {ProcA1, ProcA2, ProcA3}
TableB -> {ProcB1, ProcB2, ProcB3, ProcB4, ProcB5}
TableC -> {ProcC1, ProcC2, ProcC3, ProcC4}

每一个Table以及所涉及到的Procedure列表,被封装成了一个TableQueue对象,在TableQueue中,使用了一个双向队列(Deque)来存储Procedures列表,Deque的特点是既可以在队列两端进行插入。在这个Deque中的顺序,直接决定了同一个Table的Procedures之间的调度顺序。
当需要为TableB写入一个新的Procedure时,需要首先快速获取TableB所关联的TableQueue对象,常见的思路是将所有的TableQueue存储在一个ConcurrentHashMap中,以TableName为Key,然而,这里却没有采用ConcurrentHashMap,而是实现了一个Avl-Tree(自动平衡二叉树),这样设计的考虑点为:ConcurrentHashMap中的写入会创建额外的Tree Node对象,当对象的写入与删除非常频繁时对于GC的压力较大(请参考AvlUtil.java)。Avl-Tree利于快速获取检索,但写入性能却慢于红黑树,因为涉及到过多的翻转操作。这样,基于TableName,可以快速从这个Avl-Tree中获取对应的TableQueue对象,而后就可以将这个新的Procedure写入到这个TableQueue的Deque中,写入时还可以指定写入到头部还是尾端。
现在我们已经了解了TableQueue对象,而且知道了多个TableQueue被存储在了一个类似于Map的数据结构中,还有一个关键问题没有解决:如何实现不同Table间的调度?
所有的TableQueue,都存放在TableRunQueue(再强调一下,这是一个FairQueue对象)中,而且按Table的优先级顺序组织。每当有一个新的TableQueue对象产生时,都会按照该TableQueue所关联的Table的优先级,插入到TableRunQueue中的合适位置。
当从这个TableRunQueue中poll一个新的TableQueue时,高优先级的TableQueue先被poll出来。如果被poll出来的TableQueue为普通优先级(priority值为1),为了维持公平调度的原则,在TabelRunQueue中将这个TableQueue从头部移到尾部,这样下一次将会调度到其它的TableQueue。
再简单总结一下:TableQueue对象用来描述一个Table所关联的Procedures队列,TableQueue对象存在于两个数据结构中,一个为Avl-Tree,这样可以基于TableName快速获取对应的TableQueue,以便快速写入;另一个数据结构为FairQueue,这是为了实现多Table间的调度。
分布式锁与事件通知机制
同样围绕Create Table的例子,说明一下关于分布式锁的需求:
  • 两个相同表的CreateTableProcedure不应该被同时执行
  • 同一个Namespace下的多个不同表的CreateTableProcedure允许被同时执行
  • 只要存在未完成的CreateTableProcedure,所关联的Namespace不允许被删除

实现上述需求,Procedure框架采用了共享锁/互斥锁方案:
  • 当CreateTableProcedure执行时,需要获取对应Namespace的共享锁,以及所要创建的Table的互斥锁。
  • 删除一个Namespace则需要获取这个Namespace的互斥锁,这意味着只要有一个Procedure持有该Namespace的共享锁,则无法被删除。

当一个Procedure X试图去获取一个Table的互斥锁时,碰巧该Table的互斥锁被其它Procedure持有,此时,Procedure X需要加到这个Table的锁的等待队列中,一旦该锁被释放,Procedure X需要被唤醒。
回顾一下Procedure的执行过程:
  1. Acquire Lock
  2. Execute

获取锁资源的操作,只需要在"Acquire Lock"步骤完成即可。
MasterProcedureScheduler中,使用一个SchemaLocking的对象来维护所有的锁资源,如Server Lock, Namespace Lock,Table Lock等等。以Table Lock资源为例:一个Table的锁资源,使用一个LockAndQueue对象进行抽象,顾名思义,在这个对象中,既有Lock,又有关于这个锁资源的Procedure等待队列;多个Table的LockAndQueue对象被组织在一个Map中,以TableName为Key。
同时,还可以将获取锁资源的操作封装成一个Procedure,称之为LockProcedure,以供Procedure框架之外的特性使用,如TakeSnapshotHandler,可以利用该机制来获取Table的互斥锁。
Procedure框架就是这样没有依赖于ZooKeeper,实现自身的分布式锁与消息通知机制。
总结
本文先从Procedure的设计初衷着手,而后以Create Table为例介绍了一个Procedure的生命周期,通过这个过程,可以简单了解整个框架所涉及到的几个角色,因此,在接下来的章节中,进一步细化Procedure框架中的几个角色。最后一部分,选择整个框架中比较复杂的几个模块,展开实现细节。受限于篇幅,有几部分内容未涉及到:
  1. WAL数据格式与WAL回放机制
  2. Notification-Bus(当然这部分也只实现了一小部分)
  3. 复杂Procedure拓扑结构情形
  4. Procedure超时处理

作为应用Procedure框架的最典型流程Region Assignment,在此文范畴内几乎未涉及。因为关于Region Assignment的故事太精彩,又太揪心,所以会放在一篇独立的文章中专门讲解。
参考信息
http://hbase.apache.org/book.html#pv2
http://hbase.apache.org/book.html#amv2
HBASE-13439: Procedure Framework(Pv2)
Accumulo FATE
Procedure V2介绍
HBASE-13203 Procedure v2 - master create/delete table
HBASE-14837: Procedure Queue Improvement
HBASE-20828: Finish-up AMv2 Design/List of Tenets/Specification of operation
HBASE-20338: WALProcedureStore#recoverLease() should have fixed sleeps for retrying rollWriter()
HBASE-21354: Procedure may be deleted improperly during master restarts resulting in "corrupt"
HBASE-20973: ArrayIndexOutOfBoundsException when rolling back procedure...
HBASE-19756: Master NPE during completed failed eviction
HBASE-19953: Avoid calling post* hook when procedure fails
HBASE-19996: Some nonce procs might not be cleaned up(follow up HBASE-19756)
继续阅读 »
标题中提及"事务"可能会给大家带来误解,这篇文章不是在讨论HBase如何支持分布式事务能力的,而是介绍HBase用来处理内部事务操作的特性,这个特性被称之为Procedure V2,也是2.0版本的主打特性之一。
本文内容基于HBase 2.1版本,整体内容组织结构为:
1. Procedure特性的设计初衷。
2. Procedure的完整生命周期。
3. Procedure框架关键角色。
4. Procedure重点模块实现细节。
关于HBase基础原理系列文章:
开篇内容
介绍HBase的数据模型、适用场景、集群关键角色、建表流程以及所涉及的HBase基础概念。
Writer全流程
介绍写数据的接口,RowKey定义,数据在客户端的组装,数据路由,打包分发,以及RegionServer侧将数据写入到Region中的全部流程。
Flush与Compaction
阐述Flush与Compaction流程,讲述了Compaction所面临的本质问题,介绍了HBase现有的几种Compaction策略以及各自的适用场景。
Read全流程
首先介绍HBase的两种读取模式(Get与Scan),而后详细介绍Scan的详细实现流程。
HFile详解
详细介绍HBase底层数据文件格式HFile的实现细节。
关于Procedure V2介绍文章:
Procedure V2介绍
回顾·Procedure V2介绍
主要介绍Procedure V2的设计和结构,以及为什么用Procedure V2能比较容易实现出正确的AssignmentManager以及在2.1分支上对一些Procedure实现修正和改进。
以下是正文内容:
Procedure的设计初衷
我们先来看一下Procedure特性的设计初衷。以建表为例: 建表过程,包含如下几个关键的操作:
1.初始化所有的RegionInfos。
2.建表前的必要检查。
3.创建表的文件目录布局。
4.将Region写到Meta表中。
5. ......
从使用者看来,一个建表操作要么是成功的,要么是失败的,并不存在一种中间状态,因此,用"事务"来描述这种操作可以更容易理解。如果曾使用过1.0或更早的版本,相信一定曾被这个问题"折磨"过:“一个表明明没有创建成功,当使用者尝试再次重建的时候,却被告知该表已存在”,究其根因,在于建表遇到异常后,没有进行合理的Rollback操作,导致集群处于一种不一致的状态。
类似的需求,在HBase内部司空见惯:Disable/Enable表,修改表,RegionServer Failover处理,Snapshot, Assign Region, Flush Table...这些操作,都具备两个特点:多步操作,有限状态机(或简单或复杂)。因此,可以实现一个公共能力,这样可避免每个特性各自为营,导致大量难以维护的冗余代码。
最初版本的Procedure,由Online Snapshot特性引入(HBASE-7290),主要用来协调分布式请求。而最新版本的Procedure(称之为Procedure V2),受Accumulo的Fault-Tolerant Executor (FATE)的启发而重新设计,该特性已经在HBase内部得到广泛的应用,因此,可以将其称之为一个基础框架级特性,了解该特性的设计原理是学习2.x源码的基础。本文范畴内所探讨的Procedure,均指Procedure V2。
Procedure的生命周期
一个Procedure由一个或一系列操作构成。一个Procedure的执行结果,要么是成功,要么是失败,失败后不会让集群处于一种不一致的状态。
接下来,我们将以Create Table操作为例,来介绍一个Procedure的完整的生命周期。
创建
Master收到一个Create Table的请求后会创建一个CreateTableProcedure实例。
CreateTableProcedure涉及到一系列的操作,而每一个操作都关联了一个操作状态。在CreateTableState中,定义了与之相关的所有操作状态:
CreateTableProcedure内部定义了自身的初始状态为CREATE_TABLE_PRE_OPERATION,而且定义了每一种状态时对应的处理操作以及当前这个状态完成后应该要切换至哪个状态。如下表列出了当前状态与成功后的退出状态:
Procedure本身还有一个运行状态,这套运行状态的定义如下:
正是为了有所区分,本文将CreateTableProcedure内部定义的私有状态,称之为操作状态,而运行状态则是所有的Procedure都拥有的状态信息,从每一个状态的定义很容易看出它的作用。
提交
Master将创建出来的CreateTableProcedure实例,提交给ProcedureExecutor。
ProcedureExecutor关于一个新提交的Procedure,做如下几步处理:
  1. 必要检查(初始状态,确保无Parent Procedure,确保有Owner信息)。
  2. 设置Nonce Key(如果存在)以及Procedure ID。
  3. 将新的Procedure写入到ProcedureStore中,持久化。
  4. 将新的Procedure添加到ProcedureScheduler的调度队列的尾部。

运行
ProcedureExecutor初始化阶段,启动了若干个WorkerThread,具体数量可配置。
WorkerThread不断从ProcedureScheduler中poll出新的待执行的Procedure,而后:
  1. 获取IdLock: 获取Procedure关联的IdLock,避免同一个Procedure在多个线程中同时处理。
  2. 获取资源锁: 调用Procedure内部定义的Acquire Lock请求,获取Procedure自身所需的资源锁。IdLock是为了确保一个Procedure只被一个线程调用,而这里的Lock是为了确保这一个Table只能被一个Procedure处理,这里需要获取Namespace的共享锁,以及当前这个Table的互斥锁,这里其实是一个分布式锁的需求,容易想到用ZooKeeper实现,事实上,这个Lock也可以用Procedure来实现,在后续章节中将会讲到这一点。
  3. 处理当前操作状态:执行Procedure初始状态所定义的处理逻辑,处理完后会返回当前这个Procedure对象。每一步运行完,都将最新的状态持久化到ProcedureStore中。
  4. 处理下一操作状态:如果返回对象依然是原来的Procedure,而且未失败,则意味着需要继续下一步处理。
  5. 循环处理:循环3,4步即可处理完所有CreateTableProcedure内部定义的所有处理。

       释放锁资源。
完成
处理每一个操作状态时,都产生一个状态返回值(Flow),如果还有下一个待处理的状态,则返回Flow.HAS_MORE_STATE,如果全部执行完成,则返回Flow.NO_MORE_STATE,借此可以判断一个Procedure是否执行完成。一个执行成功后的Procedure,运行状态被设置为SUCCESS。
完成后的Procedure,需要在ProcedureStore中进行标记删除。
Procedure框架关键角色
通过上一章节的内容,我们已经知道了,在一个Procedure执行过程中,涉及到如下几个关键角色:
ProcedureExecutor
负责提交、执行Procedure。Procedure的执行操作,主要由其负责的多个WorkerThread来完成。
Procedure的持久化,由ProcedureExecutor提交给ProcedureStore。后续的每一次状态更新,也由ProcedureExecutor向ProcedureStore发起Update请求。
新的Procedure持久化后将被提交给ProcedureScheduler,由ProcedureScheduler完成调度。ProcedureExecutor中的WorkerThread从ProcedureScheduler中获取待执行的Procedure。
ProcedureStore
用来持久化新提交的Procedure以及后续的每一次状态更新值。
ProcedureStore的默认实现类为WALProcedureStore,基于日志文件来持久化Procedure信息,虽然称之为WAL,但与HBase自身的WAL日志文件的实现完全不同,类似点在于:
  • 当超过一定大小后或者超过一定的时间周期后,需要roll一个新的WAL文件出来,避免一个WAL文件过大。
  • 需要实现一套关于无用WAL日志文件的跟踪清理机制,避免WAL文件占用过大的存储空间。
  • 实现了一套类似于RingBuffer的机制,通过打包sync并发的写入请求,来提升写入吞吐量。

ProcedureScheduler
负责调度一个集群内的各种类型的Procedure请求,支持按优先级调度,相同优先级的Procedure则支持公平调度。
我们先来看看Procedure的几大类型:
  • Meta Procedure:唯一的一种类型为RecoverMetaProcedure,该类型已被废弃。
  • Server Procedure:目前也只有一种类型:ServerCrashProcedure,用来负责RegionServer进程故障后的处理。
  • Peer Procedure:与Replication相关,如AddPeerProcedure, RemovePeerProcedure等等。
  • Table Procedure: Table Procedure的类型最为丰富,如CreateTableProcedure, DisableTableProcedure, EnableTableProcedure, AssignProcedure, SplitTableRegionProcedure,.....涵盖表级别、Region级别的各类操作。

在ProcedureScheduler中,需要同时调度这几种类型的Procedure,调度的优先级顺序(由高到低)为:
    Meta -> Server -> Peer -> Table。
在每一种类型内部,又有内部的优先级定义。以Table Procedure为例,Meta Table的优先级最高,System Table(如acl表)其次,普通用户表的优先级最低。
重点模块实现细节
看到这里,你也许会认为,"Procedure特性原来如此简单!",但如果仔细阅读这部分代码,就会深刻体会到它在实现上的复杂度,导致这种复杂度的客观原因总结起来有如下几点:
  1. 要实现一个统一的状态机管理框架本身就比较复杂,可以说将Assign Region/Create Table/Split Region等流程的复杂度转嫁了过来。
  2. 需要支持优先级调度与公平调度
  3. WALProcedureStore无用WAL文件的跟踪与清理,重启后的回放,均需要严谨的处理。
  4. 涉及复杂的拓扑结构:一个Procedure中间运行过程可能会产生多个Sub-Procedures,这过程需要协调。
  5. 不依赖于ZooKeeper的分布式锁机制。
  6. 跨节点Rpc请求协调。

在实际实现中,还内部维护几个私有的数据结构,如Avl-Tree, FairQueue以及Bitmap,这也是导致实现复杂度过高的一大原因。接下来选择了三点内容来展开讲解,这三点内容也算是Procedure框架里的难点部分,分别为:WAL清理机制,Procedure调度策略以及分布式锁与事件通知机制。
WAL清理机制
WALProcedureStore也存在WAL日志文件的roll机制,这样就会产生多个WAL文件。对于一个旧的WAL文件,如何认定它可以被安全清理了?这就需要在WALProcedureStore中设计一个合理的关于Procedure状态的更新机制。
WALProcedureStore使用ProcedureStoreTracker对象来跟踪Procedure的写入/更新与删除操作,这个对象被称之为storeTracker
在ProcedureStoreTracker中,只需要用ProcID(long类型)来表示一个Procedure。即使只记录大量的ProcIDs,也会占用大量的内存空间,因此,在ProcedureStoreTracker内部实现了一个简单的Bitmap,用一个BIT来表示一个ProcID,这个Bitmap采用了分区、弹性扩展的设计:每一个分区称之为一个BitsetNode,每一个BitsetNode有一个起始值(Start),使用一个long数组来表示这个分区对应的Bitmap,每一个long数值包含64位,因此可以用来表示64个ProcIDs。一个BitsetNode应该可以包含X个long数值,这样就可以表示从Start值开始的X * 64个ProcIDs,但可惜,现在的代码实现还是存在问题的(应该是BUG),导致一个BitsetNode只能包含1个long值。
如果了解过Java Bitset的原理,或者是RoaringBitmap,就会发现这个实现并无任何新颖之处。
在一个BitsetNode内部,其实包含两个Bitmap: 一个Bitmap(modified)用来记录Insert/Update的ProcIDs,另一个Bitmap(Deleted)用来记录已经被Delete的ProcIDs。例如,如果Proc Y在Bitmap(modified)所对应的BIT为1,在Bitmap(Deleted)中所对应的BIT为0,则意味着这个Procedure仍然存活(或许刚刚被写入,或许刚刚被更新)。如果在Bitmap(Modified)中对应的BIT为1,但在Bitmap(Deleted)中所对应的BIT为1,则意味着这个Procedure已被删除了。
如果一个旧的WAL文件所关联的所有的Procedures,都已经被更新过(每一次更新,意味着Procedure的状态已经发生变化,则旧日志记录则已失去意义),或者都已经被删除,则这个WAL文件就可以被删除了。在实现上,同样可以用另外一个ProcedureStoreTracker对象(称之为holdingCleanupTracker)来跟踪最老的WAL中的Procedure的状态,每当有新的Procedure发生更新或者被删除,都同步删除holdingCleanupTracker中对应的ProcID即可。当然,还得考虑另外一种情形,如果有个别Procedure迟迟未更新如何处理? 这时,只要强制触发这些Procedures的更新操作即可。
这样描述起来似乎很简单,但这里却容易出错,而且一旦出错,可能会导致WAL日志被误删,或者堆积大量的日志文件无法被清理,出现这样的问题都是致命的。
Procedure调度策略
关于调度策略的基础需求,可以简单被表述为:
  1. 不同类型的Procedure优先级不同,如Server Procedure要优先于Table Procedure被调度。
  2. 即使同为Table Procedure类型,也需要按照Table的类型进行优先级调度,对于相同的优先级类型,则采用公平调度策略。

MasterProcedureScheduler是默认的ProcedureScheduler实现,接下来,我们看一下它的内部实现。
MasterProcedureScheduler将同一类型的Procedure,放在一个被称之为FairQueue的队列中,这样,共有四种类型的FairQueue队列,这四个队列分别被称之为MetaRunQueue, ServerRunQueue, PeerRunQueue, TableRunQueue),在调度时,按照上述罗列的顺序进行调度,这样,就确保了不同类型间的整体调度顺序。
简单起见,我们假设这四个队列中,仅有TableRunQueue有数据,其它皆空,这样确保会调度到TableRunQueue中的Procedure。在TableRunQueue中,本身会涉及多个Table,而每一个Table也可能会涉及多个Procedures:
TableA -> {ProcA1, ProcA2, ProcA3}
TableB -> {ProcB1, ProcB2, ProcB3, ProcB4, ProcB5}
TableC -> {ProcC1, ProcC2, ProcC3, ProcC4}

每一个Table以及所涉及到的Procedure列表,被封装成了一个TableQueue对象,在TableQueue中,使用了一个双向队列(Deque)来存储Procedures列表,Deque的特点是既可以在队列两端进行插入。在这个Deque中的顺序,直接决定了同一个Table的Procedures之间的调度顺序。
当需要为TableB写入一个新的Procedure时,需要首先快速获取TableB所关联的TableQueue对象,常见的思路是将所有的TableQueue存储在一个ConcurrentHashMap中,以TableName为Key,然而,这里却没有采用ConcurrentHashMap,而是实现了一个Avl-Tree(自动平衡二叉树),这样设计的考虑点为:ConcurrentHashMap中的写入会创建额外的Tree Node对象,当对象的写入与删除非常频繁时对于GC的压力较大(请参考AvlUtil.java)。Avl-Tree利于快速获取检索,但写入性能却慢于红黑树,因为涉及到过多的翻转操作。这样,基于TableName,可以快速从这个Avl-Tree中获取对应的TableQueue对象,而后就可以将这个新的Procedure写入到这个TableQueue的Deque中,写入时还可以指定写入到头部还是尾端。
现在我们已经了解了TableQueue对象,而且知道了多个TableQueue被存储在了一个类似于Map的数据结构中,还有一个关键问题没有解决:如何实现不同Table间的调度?
所有的TableQueue,都存放在TableRunQueue(再强调一下,这是一个FairQueue对象)中,而且按Table的优先级顺序组织。每当有一个新的TableQueue对象产生时,都会按照该TableQueue所关联的Table的优先级,插入到TableRunQueue中的合适位置。
当从这个TableRunQueue中poll一个新的TableQueue时,高优先级的TableQueue先被poll出来。如果被poll出来的TableQueue为普通优先级(priority值为1),为了维持公平调度的原则,在TabelRunQueue中将这个TableQueue从头部移到尾部,这样下一次将会调度到其它的TableQueue。
再简单总结一下:TableQueue对象用来描述一个Table所关联的Procedures队列,TableQueue对象存在于两个数据结构中,一个为Avl-Tree,这样可以基于TableName快速获取对应的TableQueue,以便快速写入;另一个数据结构为FairQueue,这是为了实现多Table间的调度。
分布式锁与事件通知机制
同样围绕Create Table的例子,说明一下关于分布式锁的需求:
  • 两个相同表的CreateTableProcedure不应该被同时执行
  • 同一个Namespace下的多个不同表的CreateTableProcedure允许被同时执行
  • 只要存在未完成的CreateTableProcedure,所关联的Namespace不允许被删除

实现上述需求,Procedure框架采用了共享锁/互斥锁方案:
  • 当CreateTableProcedure执行时,需要获取对应Namespace的共享锁,以及所要创建的Table的互斥锁。
  • 删除一个Namespace则需要获取这个Namespace的互斥锁,这意味着只要有一个Procedure持有该Namespace的共享锁,则无法被删除。

当一个Procedure X试图去获取一个Table的互斥锁时,碰巧该Table的互斥锁被其它Procedure持有,此时,Procedure X需要加到这个Table的锁的等待队列中,一旦该锁被释放,Procedure X需要被唤醒。
回顾一下Procedure的执行过程:
  1. Acquire Lock
  2. Execute

获取锁资源的操作,只需要在"Acquire Lock"步骤完成即可。
MasterProcedureScheduler中,使用一个SchemaLocking的对象来维护所有的锁资源,如Server Lock, Namespace Lock,Table Lock等等。以Table Lock资源为例:一个Table的锁资源,使用一个LockAndQueue对象进行抽象,顾名思义,在这个对象中,既有Lock,又有关于这个锁资源的Procedure等待队列;多个Table的LockAndQueue对象被组织在一个Map中,以TableName为Key。
同时,还可以将获取锁资源的操作封装成一个Procedure,称之为LockProcedure,以供Procedure框架之外的特性使用,如TakeSnapshotHandler,可以利用该机制来获取Table的互斥锁。
Procedure框架就是这样没有依赖于ZooKeeper,实现自身的分布式锁与消息通知机制。
总结
本文先从Procedure的设计初衷着手,而后以Create Table为例介绍了一个Procedure的生命周期,通过这个过程,可以简单了解整个框架所涉及到的几个角色,因此,在接下来的章节中,进一步细化Procedure框架中的几个角色。最后一部分,选择整个框架中比较复杂的几个模块,展开实现细节。受限于篇幅,有几部分内容未涉及到:
  1. WAL数据格式与WAL回放机制
  2. Notification-Bus(当然这部分也只实现了一小部分)
  3. 复杂Procedure拓扑结构情形
  4. Procedure超时处理

作为应用Procedure框架的最典型流程Region Assignment,在此文范畴内几乎未涉及。因为关于Region Assignment的故事太精彩,又太揪心,所以会放在一篇独立的文章中专门讲解。
参考信息
http://hbase.apache.org/book.html#pv2
http://hbase.apache.org/book.html#amv2
HBASE-13439: Procedure Framework(Pv2)
Accumulo FATE
Procedure V2介绍
HBASE-13203 Procedure v2 - master create/delete table
HBASE-14837: Procedure Queue Improvement
HBASE-20828: Finish-up AMv2 Design/List of Tenets/Specification of operation
HBASE-20338: WALProcedureStore#recoverLease() should have fixed sleeps for retrying rollWriter()
HBASE-21354: Procedure may be deleted improperly during master restarts resulting in "corrupt"
HBASE-20973: ArrayIndexOutOfBoundsException when rolling back procedure...
HBASE-19756: Master NPE during completed failed eviction
HBASE-19953: Avoid calling post* hook when procedure fails
HBASE-19996: Some nonce procs might not be cleaned up(follow up HBASE-19756) 收起阅读 »

OpenTSDB(HBase)时序数据存储模型

OpenTSDB基于HBase存储时序数据,在HBase层面设计RowKey规则为:metric+timestamp+datasource(tags)。HBase是一个KV数据库,一个时序数据(point)如果以KV的形式表示,那么其中的V必然是point的具体数值,而K就自然而然是唯一确定point数值的datasource+metric+timestamp。这种规律不仅适用于HBase,还适用于其他KV数据库,比如Kudu。

既然HBase中K是由datasource、metric以及timestamp三者构成,现在我们可以简单认为rowkey就为这三者的组合,那问题来了:这三者的组合顺序是怎么样的呢?

首先来看哪个应该排在首位。因为HBase中一张表的数据组织方式是按照rowkey的字典序顺序排列的,为了将同一种指标的所有数据集中放在一起,HBase将将metric放在了rowkey的最前面。假如将timestamp放在最前面,同一时刻的数据必然会写入同一个数据分片,无法起到散列的效果;而如果将datasource(即tags)放在最前面的话,这里有个更大的问题,就是datasource本身由多个标签组成,如果用户指定其中部分标签查找,而且不是前缀标签的话,在HBase里面将会变成大范围的扫描过滤查询,查询效率非常之低。举个上面的例子,如果将datasource放在最前面,那rowkey就可以表示为publisher=ultrarimfast.com&advertiser:google.com&gender:Male&country:USA_impressions_20110101000000,此时用户想查找20110101000000这个时间点所有发布在USA的所有广告的浏览量,即只根据country=USA这样一个维度信息查找指定时间点的某个指标,而且这个维度不是前缀维度,就会扫描大量的记录进行过滤。

确定了metric放在最前面之后,再来看看接下来应该将datasource放在中间呢还是应该将timestamp放在中间?将metric放在前面已经可以解决请求均匀分布(散列)的要求,因此HBase将timestamp放在中间,将datasource放在最后。试想,如果将datasource放在中间,也会遇到上文中说到的后缀维度查找的问题。

因此,OpenTSDB中rowkey的设计为:metric+timestamp+datasource,好了,那HBase就可以只设置一个columnfamily和一个column。那问题来了,OpenTSDB的这种设计有什么问题?在了解设计问题之前需要简单看看HBase在文件中存储KV的方式,即一系列时序数据在文件、内存中的存储方式,如下图所示:

v2-f5949de041cae2639dd7db6d1ca8dc29_r.jpg

上图是HBase中一个存储KeyValue(KV)数据的数据块结构,一个数据块由多个KeyValue数据组成,在我们的事例中KeyValue就是一个时序数据点(point)。其中Value结构很简单,就是一个数值。而Key就比较复杂了,rowkey+columnfamily+column+timestamp+keytype组成,其中rowkey等于metric+timestamp+datasource。

问题一:存在很多无用的字段。一个KeyValue中只有rowkey是有用的,其他字段诸如columnfamily、column、timestamp以及keytype从理论上来讲都没有任何实际意义,但在HBase的存储体系里都必须存在,因而耗费了很大的存储成本。

问题二:数据源和采集指标冗余。KeyValue中rowkey等metric+timestamp+datasource,试想同一个数据源的同一个采集指标,随着时间的流逝不断吐出采集数据,这些数据理论上共用同一个数据源(datasource)和采集指标(metric),但在HBase的这套存储体系下,共用是无法体现的,因此存在大量的数据冗余,主要是数据源冗余以及采集指标冗余。

问题三:无法有效的压缩。HBase提供了块级别的压缩算法-snappy、gzip等,这些通用压缩算法并没有针对时序数据进行设置,压缩效率比较低。HBase同样提供了一些编码算法,比如FastDiff等等,可以起到一定的压缩效果,但是效果并不佳。效果不佳的主要原因是HBase没有数据类型的概念,没有schema的概念,不能针对特定数据类型进行特定编码,只能选择通用的编码,效果可想而知。

问题四:不能完全保证多维查询能力。HBase本身没有schema,目前没有实现倒排索引机制,所有查询必须指定metric、timestamp以及完整的tags或者前缀tags进行查询,对于后缀维度查询也勉为其难。

虽说有这样那样的问题,但是OpenTSDB还是针对存储模型做了两个方面的优化:

优化一:timestamp并不是想象中细粒度到秒级或毫秒级,而是精确到小时级别,然后将小时中每一秒设置到列上。这样一行就会有3600列,每一列表示一小时的一秒。这样设置据说可以有效的取出一小时整的数据。

优化二:所有metrics以及所有标签信息(tags)都使用了全局编码将标签值编码成更短的bit,减少rowkey的存储数据量。上文分析HBase这种存储方式的弊端是说道会存在大量的数据源(tags)冗余以及指标(metric)冗余,有冗余是吧,那我就搞个编码,将string编码成bit,尽最大努力减少冗余。虽说这样的全局编码可以有效降低数据的存储量,但是因为全局编码字典需要存储在内存中,因此在很多时候(海量标签值),字典所需内存都会非常之大。

上述两个优化可以参考OpenTSDB这张经典的示意图:

v2-1e76d8c875d96b6f7b1331997542c44f_hd.jpg

 
继续阅读 »
OpenTSDB基于HBase存储时序数据,在HBase层面设计RowKey规则为:metric+timestamp+datasource(tags)。HBase是一个KV数据库,一个时序数据(point)如果以KV的形式表示,那么其中的V必然是point的具体数值,而K就自然而然是唯一确定point数值的datasource+metric+timestamp。这种规律不仅适用于HBase,还适用于其他KV数据库,比如Kudu。

既然HBase中K是由datasource、metric以及timestamp三者构成,现在我们可以简单认为rowkey就为这三者的组合,那问题来了:这三者的组合顺序是怎么样的呢?

首先来看哪个应该排在首位。因为HBase中一张表的数据组织方式是按照rowkey的字典序顺序排列的,为了将同一种指标的所有数据集中放在一起,HBase将将metric放在了rowkey的最前面。假如将timestamp放在最前面,同一时刻的数据必然会写入同一个数据分片,无法起到散列的效果;而如果将datasource(即tags)放在最前面的话,这里有个更大的问题,就是datasource本身由多个标签组成,如果用户指定其中部分标签查找,而且不是前缀标签的话,在HBase里面将会变成大范围的扫描过滤查询,查询效率非常之低。举个上面的例子,如果将datasource放在最前面,那rowkey就可以表示为publisher=ultrarimfast.com&advertiser:google.com&gender:Male&country:USA_impressions_20110101000000,此时用户想查找20110101000000这个时间点所有发布在USA的所有广告的浏览量,即只根据country=USA这样一个维度信息查找指定时间点的某个指标,而且这个维度不是前缀维度,就会扫描大量的记录进行过滤。

确定了metric放在最前面之后,再来看看接下来应该将datasource放在中间呢还是应该将timestamp放在中间?将metric放在前面已经可以解决请求均匀分布(散列)的要求,因此HBase将timestamp放在中间,将datasource放在最后。试想,如果将datasource放在中间,也会遇到上文中说到的后缀维度查找的问题。

因此,OpenTSDB中rowkey的设计为:metric+timestamp+datasource,好了,那HBase就可以只设置一个columnfamily和一个column。那问题来了,OpenTSDB的这种设计有什么问题?在了解设计问题之前需要简单看看HBase在文件中存储KV的方式,即一系列时序数据在文件、内存中的存储方式,如下图所示:

v2-f5949de041cae2639dd7db6d1ca8dc29_r.jpg

上图是HBase中一个存储KeyValue(KV)数据的数据块结构,一个数据块由多个KeyValue数据组成,在我们的事例中KeyValue就是一个时序数据点(point)。其中Value结构很简单,就是一个数值。而Key就比较复杂了,rowkey+columnfamily+column+timestamp+keytype组成,其中rowkey等于metric+timestamp+datasource。

问题一:存在很多无用的字段。一个KeyValue中只有rowkey是有用的,其他字段诸如columnfamily、column、timestamp以及keytype从理论上来讲都没有任何实际意义,但在HBase的存储体系里都必须存在,因而耗费了很大的存储成本。

问题二:数据源和采集指标冗余。KeyValue中rowkey等metric+timestamp+datasource,试想同一个数据源的同一个采集指标,随着时间的流逝不断吐出采集数据,这些数据理论上共用同一个数据源(datasource)和采集指标(metric),但在HBase的这套存储体系下,共用是无法体现的,因此存在大量的数据冗余,主要是数据源冗余以及采集指标冗余。

问题三:无法有效的压缩。HBase提供了块级别的压缩算法-snappy、gzip等,这些通用压缩算法并没有针对时序数据进行设置,压缩效率比较低。HBase同样提供了一些编码算法,比如FastDiff等等,可以起到一定的压缩效果,但是效果并不佳。效果不佳的主要原因是HBase没有数据类型的概念,没有schema的概念,不能针对特定数据类型进行特定编码,只能选择通用的编码,效果可想而知。

问题四:不能完全保证多维查询能力。HBase本身没有schema,目前没有实现倒排索引机制,所有查询必须指定metric、timestamp以及完整的tags或者前缀tags进行查询,对于后缀维度查询也勉为其难。

虽说有这样那样的问题,但是OpenTSDB还是针对存储模型做了两个方面的优化:

优化一:timestamp并不是想象中细粒度到秒级或毫秒级,而是精确到小时级别,然后将小时中每一秒设置到列上。这样一行就会有3600列,每一列表示一小时的一秒。这样设置据说可以有效的取出一小时整的数据。

优化二:所有metrics以及所有标签信息(tags)都使用了全局编码将标签值编码成更短的bit,减少rowkey的存储数据量。上文分析HBase这种存储方式的弊端是说道会存在大量的数据源(tags)冗余以及指标(metric)冗余,有冗余是吧,那我就搞个编码,将string编码成bit,尽最大努力减少冗余。虽说这样的全局编码可以有效降低数据的存储量,但是因为全局编码字典需要存储在内存中,因此在很多时候(海量标签值),字典所需内存都会非常之大。

上述两个优化可以参考OpenTSDB这张经典的示意图:

v2-1e76d8c875d96b6f7b1331997542c44f_hd.jpg

  收起阅读 »

中国HBase技术社区第八届MeetUp ——HBase典型应用场景与实践(南京站)

HBase—Hadoop Database是一个分布式的、面向列的开源数据库,该技术来源于 Fay Chang 所撰写的Google论文“Bigtable:一个结构化数据的分布式存储系统”。HBase的特点是高可靠性、高性能、面向列、可伸缩的分布式存储系统,如今HBase已经广泛应用于各互联网行业。那么我们如何熟练掌握HBase技术及应用呢?

2018年11月17号,由中国HBase技术社区、DataFun社区联合氪空间主办的中国第八届HBase Meetup将来到南京,届时来自阿里云、毕马威、苏宁等公司HBase的专家们,将为大家分享HBase的应用实践。

主办方:中国HBase技术社区、DataFun社区
联合主办方:氪空间
合作伙伴:云栖社区、掘金社区
时间:2018.11.17,13:00-18:00
地点:南京市玄武区同仁西街7号南楼三层氪空间(地铁珠江路附近)
注:报名通过审核后,请持有效票二维码参会。
微信图片_20181106142054.jpg

议程安排:
1.jpg

分享介绍:
1.png

玄陵(郭超) 阿里云 工程师
嘉宾介绍:玄陵(郭超),阿里云工程师,了解常见分布式大数据数据库,并有一定实战经验。
分享主题:阿里云HBase备份恢复的原理以及实践
内容概要: 主要介绍阿里云HBase的备份恢复的设计背景,原理以及实现,以及与业内大部分的分布式大数据数据库备份恢复的异同。

2.jpg

蒋晓明  毕马威智能创新空间 技术经理
嘉宾介绍:蒋晓明  现就职于毕马威智能创新空间,主要从事数据平台的建设,热衷于大数据相关技术的研究
分享主题:HBase在审计行业的应用
内容概要:a. 企业财务详情查询,HBase提供多维度快速查询;b. 企业自动化电子对账,HBase提供多维度匹配。

32.jpg


张立明 苏宁高级研发工程师&HBase组件负责人
嘉宾介绍:张立明,苏宁高级研发工程师,HBase组件负责人,负责HBase平台的可用性和稳定性建设,热衷于大数据相关技术。
分享主题: HBase在苏宁的应用和实践
内容概要:介绍HBase在苏宁的主要使用场景,围绕使用场景介绍HBase在苏宁的使用现状及发展历程、功能增强及性能优化、运维监控。

a. 使用现状及发展历程主要包括:集群规模、版本升级、集群部署方式;
b. 功能增强及性能优化主要包括:HA,全局限流,性能优化经验介绍;
c. 运维监控主要包括:系统稳定性和性能方面监控,以及根据监控信息快速定位问题经验介绍,表健康状态监控及自动优化。

主办方介绍:

中国HBase技术社区:为了让众多HBase相关从业人员及爱好者有一个自由交流HBase相关技术的社区,由阿里巴巴、小米、网易、滴滴、知乎等公司的HBase技术研究人员共同发起了组建:中国HBase技术社区(Chinese HBase Technical Community 简称CHTC)。

我们非常欢迎对HBase有技术激情的同学一起加入探讨HBase技术,同时诚邀广大HBase技术爱好者加入。大家在工作学习遇到HBase技术问题,可以把问题发布到中国HBase技术社区论坛http://hbase.group,欢迎大家论坛上面提问留言讨论。想了解更多HBase技术,关注HBase技术社区公众号(微信号:hbasegroup),邀请进社区的讨论群。
4.jpg

DataFun定位于最“实用”的数据科学社区,主要形式为线下的深度沙龙、线上的内容整理。希望将工业界专家在各自场景下的实践经验,通过DataFun的平台传播和扩散,对即将或已经开始相关尝试的同学有启发和借鉴。DataFun的愿景是:为大数据、人工智能从业者和爱好者打造一个分享、交流、学习、成长的平台,让数据科学领域的知识和经验更好的传播和落地产生价值。

DataFun社区成立至今,已经成功在全国范围内举办数十场线下技术沙龙,有超过一百位的业内专家参与分享,聚集了万余大数据、算法相关领域从业者。
menu.saveimg_.savepath20181106143601_.jpg

联合主办方:
6.png

合作社区:
C80F6D6B-479D-4d5f-97FC-C5FA8B0159AD.png

8.jpg

9.png
继续阅读 »
HBase—Hadoop Database是一个分布式的、面向列的开源数据库,该技术来源于 Fay Chang 所撰写的Google论文“Bigtable:一个结构化数据的分布式存储系统”。HBase的特点是高可靠性、高性能、面向列、可伸缩的分布式存储系统,如今HBase已经广泛应用于各互联网行业。那么我们如何熟练掌握HBase技术及应用呢?

2018年11月17号,由中国HBase技术社区、DataFun社区联合氪空间主办的中国第八届HBase Meetup将来到南京,届时来自阿里云、毕马威、苏宁等公司HBase的专家们,将为大家分享HBase的应用实践。

主办方:中国HBase技术社区、DataFun社区
联合主办方:氪空间
合作伙伴:云栖社区、掘金社区
时间:2018.11.17,13:00-18:00
地点:南京市玄武区同仁西街7号南楼三层氪空间(地铁珠江路附近)
注:报名通过审核后,请持有效票二维码参会。
微信图片_20181106142054.jpg

议程安排:
1.jpg

分享介绍:
1.png

玄陵(郭超) 阿里云 工程师
嘉宾介绍:玄陵(郭超),阿里云工程师,了解常见分布式大数据数据库,并有一定实战经验。
分享主题:阿里云HBase备份恢复的原理以及实践
内容概要: 主要介绍阿里云HBase的备份恢复的设计背景,原理以及实现,以及与业内大部分的分布式大数据数据库备份恢复的异同。

2.jpg

蒋晓明  毕马威智能创新空间 技术经理
嘉宾介绍:蒋晓明  现就职于毕马威智能创新空间,主要从事数据平台的建设,热衷于大数据相关技术的研究
分享主题:HBase在审计行业的应用
内容概要:a. 企业财务详情查询,HBase提供多维度快速查询;b. 企业自动化电子对账,HBase提供多维度匹配。

32.jpg


张立明 苏宁高级研发工程师&HBase组件负责人
嘉宾介绍:张立明,苏宁高级研发工程师,HBase组件负责人,负责HBase平台的可用性和稳定性建设,热衷于大数据相关技术。
分享主题: HBase在苏宁的应用和实践
内容概要:介绍HBase在苏宁的主要使用场景,围绕使用场景介绍HBase在苏宁的使用现状及发展历程、功能增强及性能优化、运维监控。

a. 使用现状及发展历程主要包括:集群规模、版本升级、集群部署方式;
b. 功能增强及性能优化主要包括:HA,全局限流,性能优化经验介绍;
c. 运维监控主要包括:系统稳定性和性能方面监控,以及根据监控信息快速定位问题经验介绍,表健康状态监控及自动优化。

主办方介绍:

中国HBase技术社区:为了让众多HBase相关从业人员及爱好者有一个自由交流HBase相关技术的社区,由阿里巴巴、小米、网易、滴滴、知乎等公司的HBase技术研究人员共同发起了组建:中国HBase技术社区(Chinese HBase Technical Community 简称CHTC)。

我们非常欢迎对HBase有技术激情的同学一起加入探讨HBase技术,同时诚邀广大HBase技术爱好者加入。大家在工作学习遇到HBase技术问题,可以把问题发布到中国HBase技术社区论坛http://hbase.group,欢迎大家论坛上面提问留言讨论。想了解更多HBase技术,关注HBase技术社区公众号(微信号:hbasegroup),邀请进社区的讨论群。
4.jpg

DataFun定位于最“实用”的数据科学社区,主要形式为线下的深度沙龙、线上的内容整理。希望将工业界专家在各自场景下的实践经验,通过DataFun的平台传播和扩散,对即将或已经开始相关尝试的同学有启发和借鉴。DataFun的愿景是:为大数据、人工智能从业者和爱好者打造一个分享、交流、学习、成长的平台,让数据科学领域的知识和经验更好的传播和落地产生价值。

DataFun社区成立至今,已经成功在全国范围内举办数十场线下技术沙龙,有超过一百位的业内专家参与分享,聚集了万余大数据、算法相关领域从业者。
menu.saveimg_.savepath20181106143601_.jpg

联合主办方:
6.png

合作社区:
C80F6D6B-479D-4d5f-97FC-C5FA8B0159AD.png

8.jpg

9.png
收起阅读 »

HBase Meetup 成都站 PPT下载

2018年11月3号,由中国HBase技术社区、DataFun社区、爱奇艺主办的中国第七届HBase Meetup来到成都。各位嘉宾分享了 HBase 相关的知识。
 
本次会议的分享议题如下:
 
  1. 分享主题:HBase2.0重新定义小对象实时存取,PPT下载:http://hbase.group/slides/167

 
嘉宾介绍:天引,专注在大数据领域,拥有多年分布式、高并发、大规模系统的研发与实践经验,先后参与hbase、phoenix、lindorm等产品的内核引擎研发,目前负责阿里上万节点的HBase As a Service的发展与落地。
内容概要:小对象,特别指1K~10MB范围的数据,比如图片,短视频,文档等广泛的存在于人工智能,医疗,教育,生活分享,电子商务等领域。HBase2.0在MOB技术的加持下重新定义小对象实时存取,具有低延迟,读写强一致,检索能力强,水平易扩展等关键能力。本文介绍了MOB特性的原理与实现,以及与经典对象存储相比,MOB带来的差异性与优势。

       2. 分享主题:HBase在爱奇艺的应用实践,PPT下载:http://hbase.group/slides/168 

嘉宾介绍:郑浩南,爱奇艺资深研发工程师,专注于大数据领域,负责Hadoop服务的运维研究以及DevOps平台开发。
内容概要:随着大数据存储计算对延时吞吐要求越来越高,需求日益复杂化,HBase在爱奇艺中被广泛应用和实践以应对多样化的业务场景。本次演讲将介绍HBase在爱奇艺的部署模式和使用场景,以及在爱奇艺私有云环境下的运维策略。
 
      3. 分享主题:Hbase在车联网的应用与实践,PPT下载:http://hbase.group/slides/169 

嘉宾介绍:巨鹏,g7高级数据运维工程师,负责g7的数据维护以及中间件的稳定性建设。
内容概要:为大家分享在大数据量的IOT车联网环境中,是如何发挥HBase优势解决实际问题以及他稳定性如何保障。
继续阅读 »
2018年11月3号,由中国HBase技术社区、DataFun社区、爱奇艺主办的中国第七届HBase Meetup来到成都。各位嘉宾分享了 HBase 相关的知识。
 
本次会议的分享议题如下:
 
  1. 分享主题:HBase2.0重新定义小对象实时存取,PPT下载:http://hbase.group/slides/167

 
嘉宾介绍:天引,专注在大数据领域,拥有多年分布式、高并发、大规模系统的研发与实践经验,先后参与hbase、phoenix、lindorm等产品的内核引擎研发,目前负责阿里上万节点的HBase As a Service的发展与落地。
内容概要:小对象,特别指1K~10MB范围的数据,比如图片,短视频,文档等广泛的存在于人工智能,医疗,教育,生活分享,电子商务等领域。HBase2.0在MOB技术的加持下重新定义小对象实时存取,具有低延迟,读写强一致,检索能力强,水平易扩展等关键能力。本文介绍了MOB特性的原理与实现,以及与经典对象存储相比,MOB带来的差异性与优势。

       2. 分享主题:HBase在爱奇艺的应用实践,PPT下载:http://hbase.group/slides/168 

嘉宾介绍:郑浩南,爱奇艺资深研发工程师,专注于大数据领域,负责Hadoop服务的运维研究以及DevOps平台开发。
内容概要:随着大数据存储计算对延时吞吐要求越来越高,需求日益复杂化,HBase在爱奇艺中被广泛应用和实践以应对多样化的业务场景。本次演讲将介绍HBase在爱奇艺的部署模式和使用场景,以及在爱奇艺私有云环境下的运维策略。
 
      3. 分享主题:Hbase在车联网的应用与实践,PPT下载:http://hbase.group/slides/169 

嘉宾介绍:巨鹏,g7高级数据运维工程师,负责g7的数据维护以及中间件的稳定性建设。
内容概要:为大家分享在大数据量的IOT车联网环境中,是如何发挥HBase优势解决实际问题以及他稳定性如何保障。 收起阅读 »

HBase Coprocessor的实现与应用

本文来自于中国HBase技术社区武汉站HBase MeetUp线下交流会的烽火大数据平台研发负责人叶铿(云端浪子)。
HBase Coprocessor的实现与应用PPT下载:http://hbase.group/slides/159

搜狗截图20181031141018.png

本次分享的内容主要分为以下五点:
 
  1. Coprocessor简介
  2. Endpoint服务端实现
  3. Endpoint客户端实现
  4. Observer实现二级索引
  5. Coprocessor应用场景


1.Coprocessor简介

HBase协处理器的灵感来自于Jeff Dean 09年的演讲,根据该演讲实现类似于Bigtable的协处理器,包括以下特性:每个表服务器的任意子表都可以运行代码客户端的高层调用接口(客户端能够直接访问数据表的行地址,多行读写会自动分片成多个并行的RPC调用),提供一个非常灵活的、可用于建立分布式服务的数据模型,能够自动化扩展、负载均衡、应用请求路由。HBase的协处理器灵感来自Bigtable,但是实现细节不尽相同。HBase建立框架为用户提供类库和运行时环境,使得代码能够在HBase Region Server和Master上面进行处理。

menu.saveimg_.savepath20181031141144_.jpg

(1)实现目的
 
  1. HBase无法轻易建立“二级索引”;
  2. 执行求和、计数、排序等操作比较困难,必须通过MapReduce/Spark实现,对于简单的统计或聚合计算时,可能会因为网络与IO开销大而带来性能问题。


(2)灵感来源

灵感来源于Bigtable的协处理器,包含如下特性:
  1. 每个表服务器的任意子表都可以运行代码;
  2. 客户端能够直接访问数据表的行,多行读写会自动分片成多个并行的RPC调用。


(3)提供接口
 
  1. RegionObserver:提供客户端的数据操纵事件钩子:Get、Put、Delete、Scan等;
  2. WALObserver:提供WAL相关操作钩子;
  3. MasterObserver:提供DDL-类型的操作钩子。如创建、删除、修改数据表等;
  4. Endpoint:终端是动态RPC插件的接口,它的实现代码被安装在服务器端,能够通过HBase RPC调用唤醒。


(4)应用范围
 
  1. 通过使用RegionObserver接口可以实现二级索引的创建和维护;
  2. 通过使用Endpoint接口,在对数据进行简单排序和sum,count等统计操作时,能够极大提高性能。


本文将通过具体实例来演示两种协处理器的开发方法的详细实现过程。

2.Endpoint服务端实现

在传统关系型数据库里面,可以随时的对某列进行求和sum,但是目前HBase目前所提供的接口,直接求和是比较困难的,所以先编写好服务端代码,并加载到对应的Table上,加载协处理器有几种方法,可以通过HTableDescriptor的addCoprocessor方法直接加载,同理也可以通过removeCoprocessor方法卸载协处理器。

Endpoint协处理器类似传统数据库的存储过程,客户端调用Endpoint协处理器执行一段Server端代码,并将Server端代码的结果返回给Client进一步处理,最常见的用法就是进行聚合操作。举个例子说明:如果没有协处理器,当用户需要找出一张表中的最大数据即max聚合操作,必须进行全表扫描,客户端代码遍历扫描结果并执行求max操作,这样的方法无法利用底层集群的并发能力,而将所有计算都集中到Client端统一执行, 效率非常低。但是使用Coprocessor,用户将求max的代码部署到HBase Server端,HBase将利用底层Cluster的多个节点并行执行求max的操作即在每个Region范围内执行求最大值逻辑,将每个Region的最大值在Region Server端计算出,仅仅将该max值返回给客户端。客户端进一步将多个Region的max进一步处理而找到其中的max,这样整体执行效率提高很多。但是一定要注意的是Coprocessor一定要写正确,否则导致RegionServer宕机。

menu.saveimg_.savepath20181031141342_.jpg

 
Protobuf定义

如前所述,客户端和服务端之间需要进行RPC通信,所以两者间需要确定接口,当前版本的HBase的协处理器是通过Google Protobuf协议来实现数据交换的,所以需要通过Protobuf来定义接口。

如下所示:
option java_package = "com.my.hbase.protobuf.generated";
option java_outer_classname = "AggregateProtos";
option java_generic_services = true;
option java_generate_equals_and_hash = true;
option optimize_for = SPEED;

import "Client.proto";

message AggregateRequest {
required string interpreter_class_name = 1;
required Scan scan = 2;
optional bytes interpreter_specific_bytes = 3;
}

message AggregateResponse {
repeated bytes first_part = 1;
optional bytes second_part = 2;
}

service AggregateService {
rpc GetMax (AggregateRequest) returns (AggregateResponse);
rpc GetMin (AggregateRequest) returns (AggregateResponse);
rpc GetSum (AggregateRequest) returns (AggregateResponse);
rpc GetRowNum (AggregateRequest) returns (AggregateResponse);
rpc GetAvg (AggregateRequest) returns (AggregateResponse);
rpc GetStd (AggregateRequest) returns (AggregateResponse);
rpc GetMedian (AggregateRequest) returns (AggregateResponse);
}
可以看到这里定义7个聚合服务RPC,名字分别叫做GetMax、GetMin、GetSum等,本文通过GetSum进行举例,其他的聚合RPC也是类似的内部实现。RPC有一个入口参数,用消息AggregateRequest表示;RPC的返回值用消息AggregateResponse表示。Service是一个抽象概念,RPC的Server端可以看作一个用来提供服务的Service。在HBase Coprocessor中Service就是Server端需要提供的Endpoint Coprocessor服务,主要用来给HBase的Client提供服务。AggregateService.java是由Protobuf软件通过终端命令“protoc filename.proto--java_out=OUT_DIR”自动生成的,其作用是将.proto文件定义的消息结构以及服务转换成对应接口的RPC实现,其中包括如何构建request消息和response响应以及消息包含的内容的处理方式,并且将AggregateService包装成一个抽象类,具体的服务以类的方法的形式提供。AggregateService.java定义Client端与Server端通信的协议,代码中包含请求信息结构AggregateRequest、响应信息结构AggregateResponse、提供的服务种类AggregateService,其中AggregateRequest中的interpreter_class_name指的是column interpreter的类名,此类的作用在于将数据格式从存储类型解析成所需类型。AggregateService.java由于代码太长,在这里就不贴出来了。

下面我们来讲一下服务端的架构,

首先,Endpoint Coprocessor是一个Protobuf Service的实现,因此需要它必须继承某个ProtobufService。我们在前面已经通过proto文件定义Service,命名为AggregateService,因此Server端代码需要重载该类,其次作为HBase的协处理器,Endpoint 还必须实现HBase定义的协处理器协议,用Java的接口来定义。具体来说就是CoprocessorService和Coprocessor,这些HBase接口负责将协处理器和HBase 的RegionServer等实例联系起来以便协同工作。Coprocessor接口定义两个接口函数:start和stop。

加载Coprocessor之后Region打开的时候被RegionServer自动加载,并会调用器start 接口完成初始化工作。一般情况该接口函数仅仅需要将协处理器的运行上下文环境变量CoprocessorEnvironment保存到本地即可。

CoprocessorEnvironment保存协处理器的运行环境,每个协处理器都是在一个RegionServer进程内运行并隶属于某个Region。通过该变量获取Region的实例等 HBase运行时环境对象。

Coprocessor接口还定义stop()接口函数,该函数在Region被关闭时调用,用来进行协处理器的清理工作。本文里我们没有进行任何清理工作,因此该函数什么也不干。

我们的协处理器还需要实现CoprocessorService接口。该接口仅仅定义一个接口函数 getService()。我们仅需要将本实例返回即可。HBase的Region Server在接收到客户端的调用请求时,将调用该接口获取实现RPCService的实例,因此本函数一般情况下就是返回自身实例即可。

完成以上三个接口函数之后,Endpoint的框架代码就已完成。每个Endpoint协处理器都必须实现这些框架代码而且写法雷同。

menu.saveimg_.savepath20181031141452_.jpg

Server端的代码就是一个Protobuf RPC的Service实现,即通过Protobuf提供的某种服务。其开发内容主要包括:
 
  1. 实现Coprocessor的基本框架代码
  2. 实现服务的RPC具体代码


Endpoint 协处理的基本框架

Endpoint 是一个Server端Service的具体实现,其实现有一些框架代码,这些框架代码与具体的业务需求逻辑无关。仅仅是为了和HBase运行时环境协同工作而必须遵循和完成的一些粘合代码。因此多数情况下仅仅需要从一个例子程序拷贝过来并进行命名修改即可。不过我们还是完整地对这些粘合代码进行粗略的讲解以便更好地理解代码。
public Service getService() {
return this;
}

public void start(CoprocessorEnvironment env) throws IOException {
if(env instanceof RegionCoprocessorEnvironment) {
this.env = (RegionCoprocessorEnvironment)env;
} else {
throw new CoprocessorException("Must be loaded on a table region!");
}
}

public void stop(CoprocessorEnvironment env) throws IOException {
}
Endpoint协处理器真正的业务代码都在每一个RPC函数的具体实现中。


在本文中,我们的Endpoint协处理器仅提供一个RPC函数即getSUM。我将分别介绍编写该函数的几个主要工作:了解函数的定义,参数列表;处理入口参数;实现业务逻辑;设置返回参数。
public void getSum(RpcController controller, AggregateRequest request, RpcCallbackdone) {
AggregateResponse response = null;
RegionScanner scanner = null;
long sum = 0L;
try {
ColumnInterpreter ignored = this.constructColumnInterpreterFromRequest(request);
Object sumVal = null;
Scan scan = ProtobufUtil.toScan(request.getScan());
scanner = this.env.getRegion().getScanner(scan);
byte[] colFamily = scan.getFamilies()[0];
NavigableSet qualifiers = (NavigableSet) scan.getFamilyMap().get(colFamily);
byte[] qualifier = null;
if (qualifiers != null && !qualifiers.isEmpty()) {
qualifier = (byte[]) qualifiers.pollFirst();
}

ArrayList results = new ArrayList();
boolean hasMoreRows = false;

do {
hasMoreRows = scanner.next(results);
int listSize = results.size();

for (int i = 0; i < listSize; ++i) {
//取出列值
Object temp = ignored.getValue(colFamily, qualifier,
(Cell) results.get(i));
if (temp != null) {
sumVal = ignored.add(sumVal, ignored.castToReturnType(temp));
}
}

results.clear();
} while (hasMoreRows);

if (sumVal != null) {
response = AggregateResponse.newBuilder().addFirstPart(
ignored.getProtoForPromotedType(sumVal).toByteString()).build();
}
} catch (IOException var27) {
ResponseConverter.setControllerException(controller, var27);
} finally {
if (scanner != null) {
try {
scanner.close();
} catch (IOException var26) {
;
}
}

}

log.debug("Sum from this region is " +
this.env.getRegion().getRegionInfo().getRegionNameAsString() + ": " + sum);
done.run(response);
}
Endpoint类比于数据库的存储过程,其触发服务端的基于Region的同步运行再将各个结果在客户端搜集后归并计算。特点类似于传统的MapReduce框架,服务端Map客户端Reduce。

3.Endpoint客户端实现

HBase提供客户端Java包org.apache.hadoop.hbase.client.HTable,提供以下三种方法来调用协处理器提供的服务:
 
  1. coprocessorService(byte[])
  2. coprocessorService(Class, byte[], byte[],Batch.Call),
  3. coprocessorService(Class, byte[], byte[],Batch.Call, Batch.Callback)

 

menu.saveimg_.savepath20181031141634_.jpg

该方法采用rowkey指定Region。这是因为HBase客户端很少会直接操作Region,一般不需要知道Region的名字;况且在HBase中Region名会随时改变,所以用rowkey来指定Region是最合理的方式。使用rowkey可以指定唯一的一个Region,如果给定的Rowkey并不存在,只要在某个Region的rowkey范围内依然用来指定该Region。比如Region 1处理[row1, row100]这个区间内的数据,则rowkey=row1就由Region 1来负责处理,换句话说我们可以用row1来指定Region 1,无论rowkey等于”row1”的记录是否存在。CoprocessorService方法返回类型为CoprocessorRpcChannel的对象,该 RPC通道连接到由rowkey指定的Region上面,通过此通道可以调用该Region上面部署的协处理器RPC。

menu.saveimg_.savepath20181031141702_.jpg

有时候客户端需要调用多个 Region上的同一个协处理器,比如需要统计整个Table的sum,在这种情况下,需要所有的Region都参与进来,分别统计自身Region内部的sum并返回客户端,最终客户端将所有Region的返回结果汇总,就可以得到整张表的sum。


这意味着该客户端同时和多个Region进行批处理交互。一个可行的方法是,收集每个 Region的startkey,然后循环调用第一种coprocessorService方法:用每一个Region的startkey 作为入口参数,获得RPC通道创建stub对象,进而逐一调用每个Region上的协处理器RPC。这种做法需要写很多的代码,为此HBase提供两种更加简单的 coprocessorService方法来处理多个Region的协处理器调用。先来看第一种方法 coprocessorService(Class, byte[],byte[],Batch.Call),

该方法有 4 个入口参数。第一个参数是实现RPC的Service 类,即前文中的AggregateService类。通过它,HBase就可以找到相应的部署在Region上的协处理器,一个Region上可以部署多个协处理器,客户端必须通过指定Service类来区分究竟需要调用哪个协处理器提供的服务。

要调用哪些Region上的服务则由startkey和endkey来确定,通过 rowkey范围即可确定多个 Region。为此,coprocessorService方法的第二个和第三个参数分别是 startkey和endkey,凡是落在[startkey,endkey]区间内的Region都会参与本次调用。

第四个参数是接口类Batch.Call。它定义了如何调用协处理器,用户通过重载该接口的call()方法来实现客户端的逻辑。在call()方法内,可以调用RPC,并对返回值进行任意处理。即前文代码清单1中所做的事情。coprocessorService将负责对每个 Region调用这个call()方法。

coprocessorService方法的返回值是一个Map类型的集合。该集合的key是Region名字,value是Batch.Call.call方法的返回值。该集合可以看作是所有Region的协处理器 RPC 返回的结果集。客户端代码可以遍历该集合对所有的结果进行汇总处理。

这种coprocessorService方法的大体工作流程如下。首先它分析startkey和 endkey,找到该区间内的所有Region,假设存放在regionList中。然后,遍历regionList,为每一个Region调用Batch.Call,在该接口内,用户定义具体的RPC调用逻辑。最后coprocessorService将所有Batch.Call.call()的返回值加入结果集合并返回。

menu.saveimg_.savepath20181031141730_.jpg

coprocessorService的第三种方法比第二个方法多了一个参数callback。coprocessorService第二个方法内部使用HBase自带的缺省callback,该缺省 callback将每个Region的返回结果都添加到一个Map类型的结果集中,并将该集合作为coprocessorService方法的返回值。

HBase 提供第三种coprocessorService方法允许用户定义callback行为,coprocessorService 会为每一个RPC返回结果调用该callback,用户可以在callback 中执行需要的逻辑,比如执行sum累加。用第二种方法的情况下,每个Region协处理器RPC的返回结果先放入一个列表,所有的 Region 都返回后,用户代码再从该列表中取出每一个结果进行累加;用第三种方法,直接在callback中进行累加,省掉了创建结果集合和遍历该集合的开销,效率会更高一些。

因此我们只需要额外定义一个callback即可,callback是一个Batch.Callback接口类,用户需要重载其update方法。
 
public S sum(final HTable table, final ColumnInterpreter<R, S, P, Q, T> ci,final Scan scan)throws Throwable {

final AggregateRequest requestArg = validateArgAndGetPB(scan, ci, false);

class SumCallBack implements Batch.Callback {

S sumVal = null;

public S getSumResult() {
return sumVal;
}

@Override
public synchronized void update(byte[] region, byte[] row, S result) {
sumVal = ci.add(sumVal, result);
}}

SumCallBack sumCallBack = new SumCallBack();
table.coprocessorService(AggregateService.class, scan.getStartRow(), scan.getStopRow(),
new Batch.Call<AggregateService, S>() {
@Override
public S call(AggregateService instance) throws IOException {
ServerRpcController controller = new ServerRpcController();
BlockingRpcCallback<AggregateResponse> rpcCallback =
new BlockingRpcCallback<AggregateResponse>();
//RPC 调用
instance.getSum(controller, requestArg, rpcCallback);
AggregateResponse response = rpcCallback.get();
if (controller.failedOnException()) {
throw controller.getFailedOn();
}
if (response.getFirstPartCount() == 0) {
return null;
}
ByteString b = response.getFirstPart(0);
T t = ProtobufUtil.getParsedGenericInstance(ci.getClass(), 4, b);
S s = ci.getPromotedValueFromProto(t);
return s;
}
}, sumCallBack);
return sumCallBack.getSumResult();
4.Observer实现二级索引

Observer类似于传统数据库中的触发器,当发生某些事件的时候这类协处理器会被 Server 端调用。Observer Coprocessor是一些散布在HBase Server端代码的 hook钩子, 在固定的事件发生时被调用。比如:put操作之前有钩子函数prePut,该函数在pu 操作执 行前会被Region Server调用;在put操作之后则有postPut 钩子函数。
menu.saveimg_.savepath20181031141817_.jpg

 
RegionObserver工作原理

RegionObserver提供客户端的数据操纵事件钩子,Get、Put、Delete、Scan,使用此功能能够解决主表以及多个索引表之间数据一致性的问题

menu.saveimg_.savepath20181031141844_.jpg

 
  1. 客户端发出put请求;
  2. 该请求被分派给合适的RegionServer和Region;
  3. coprocessorHost拦截该请求,然后在该表上登记的每个 RegionObserver 上调用prePut();
  4. 如果没有被preGet()拦截,该请求继续送到 region,然后进行处理;
  5. Region产生的结果再次被CoprocessorHost拦截,调用postGet();
  6. 假如没有postGet()拦截该响应,最终结果被返回给客户端;

 

menu.saveimg_.savepath20181031141924_.jpg

如上图所示,HBase可以根据rowkey很快的检索到数据,但是如果根据column检索数据,首先要根据rowkey减小范围,再通过列过滤器去过滤出数据,如果使用二级索引,可以先查基于column的索引表,获取到rowkey后再快速的检索到数据。

menu.saveimg_.savepath20181031141955_.jpg

如图所示首先继承BaseRegionObserver类,重写postPut,postDelete方法,在postPut方法体内中写Put索引表数据的代码,在postDelete方法里面写Delete索引表数据,这样可以保持数据的一致性。

在Scan表的时候首先判断是否先查索引表,如果不查索引直接scan主表,如果走索引表通过索引表获取主表的rowkey再去查主表。

使用Elastic Search建立二级索引也是一样。

我们在同一个主机集群上同时建立了HBase集群和Elastic Search集群,存储到HBase的数据必须实时地同步到Elastic Search。而恰好HBase和Elastic Search都没有更新的概念,我们的需求可以简化为两步:
 
  1. 当一个新的Put操作产生时,将Put数据转化为json,索引到ElasticSearch,并把RowKey作为新文档的ID;
  2. 当一个新的Delete操作产生时获取Delete数据的rowkey删除Elastic Search中对应的ID。


5.协处理的主要应用场景 
  1. Observer允许集群在正常的客户端操作过程中可以有不同的行为表现;
  2. Endpoint允许扩展集群的能力,对客户端应用开放新的运算命令;
  3. Observer类似于RDBMS的触发器,主要在服务端工作;
  4. Endpoint类似于RDBMS的存储过程,主要在服务端工作;
  5. Observer可以实现权限管理、优先级设置、监控、ddl控制、二级索引等功能;
  6. Endpoint可以实现min、max、avg、sum、distinct、group by等功能。


例如HBase源码org.apache.hadoop.hbase.security.access.AccessController利用Observer实现对HBase进行了权限控制,有兴趣的读者可以看看相关代码。
继续阅读 »
本文来自于中国HBase技术社区武汉站HBase MeetUp线下交流会的烽火大数据平台研发负责人叶铿(云端浪子)。
HBase Coprocessor的实现与应用PPT下载:http://hbase.group/slides/159

搜狗截图20181031141018.png

本次分享的内容主要分为以下五点:
 
  1. Coprocessor简介
  2. Endpoint服务端实现
  3. Endpoint客户端实现
  4. Observer实现二级索引
  5. Coprocessor应用场景


1.Coprocessor简介

HBase协处理器的灵感来自于Jeff Dean 09年的演讲,根据该演讲实现类似于Bigtable的协处理器,包括以下特性:每个表服务器的任意子表都可以运行代码客户端的高层调用接口(客户端能够直接访问数据表的行地址,多行读写会自动分片成多个并行的RPC调用),提供一个非常灵活的、可用于建立分布式服务的数据模型,能够自动化扩展、负载均衡、应用请求路由。HBase的协处理器灵感来自Bigtable,但是实现细节不尽相同。HBase建立框架为用户提供类库和运行时环境,使得代码能够在HBase Region Server和Master上面进行处理。

menu.saveimg_.savepath20181031141144_.jpg

(1)实现目的
 
  1. HBase无法轻易建立“二级索引”;
  2. 执行求和、计数、排序等操作比较困难,必须通过MapReduce/Spark实现,对于简单的统计或聚合计算时,可能会因为网络与IO开销大而带来性能问题。


(2)灵感来源

灵感来源于Bigtable的协处理器,包含如下特性:
  1. 每个表服务器的任意子表都可以运行代码;
  2. 客户端能够直接访问数据表的行,多行读写会自动分片成多个并行的RPC调用。


(3)提供接口
 
  1. RegionObserver:提供客户端的数据操纵事件钩子:Get、Put、Delete、Scan等;
  2. WALObserver:提供WAL相关操作钩子;
  3. MasterObserver:提供DDL-类型的操作钩子。如创建、删除、修改数据表等;
  4. Endpoint:终端是动态RPC插件的接口,它的实现代码被安装在服务器端,能够通过HBase RPC调用唤醒。


(4)应用范围
 
  1. 通过使用RegionObserver接口可以实现二级索引的创建和维护;
  2. 通过使用Endpoint接口,在对数据进行简单排序和sum,count等统计操作时,能够极大提高性能。


本文将通过具体实例来演示两种协处理器的开发方法的详细实现过程。

2.Endpoint服务端实现

在传统关系型数据库里面,可以随时的对某列进行求和sum,但是目前HBase目前所提供的接口,直接求和是比较困难的,所以先编写好服务端代码,并加载到对应的Table上,加载协处理器有几种方法,可以通过HTableDescriptor的addCoprocessor方法直接加载,同理也可以通过removeCoprocessor方法卸载协处理器。

Endpoint协处理器类似传统数据库的存储过程,客户端调用Endpoint协处理器执行一段Server端代码,并将Server端代码的结果返回给Client进一步处理,最常见的用法就是进行聚合操作。举个例子说明:如果没有协处理器,当用户需要找出一张表中的最大数据即max聚合操作,必须进行全表扫描,客户端代码遍历扫描结果并执行求max操作,这样的方法无法利用底层集群的并发能力,而将所有计算都集中到Client端统一执行, 效率非常低。但是使用Coprocessor,用户将求max的代码部署到HBase Server端,HBase将利用底层Cluster的多个节点并行执行求max的操作即在每个Region范围内执行求最大值逻辑,将每个Region的最大值在Region Server端计算出,仅仅将该max值返回给客户端。客户端进一步将多个Region的max进一步处理而找到其中的max,这样整体执行效率提高很多。但是一定要注意的是Coprocessor一定要写正确,否则导致RegionServer宕机。

menu.saveimg_.savepath20181031141342_.jpg

 
Protobuf定义

如前所述,客户端和服务端之间需要进行RPC通信,所以两者间需要确定接口,当前版本的HBase的协处理器是通过Google Protobuf协议来实现数据交换的,所以需要通过Protobuf来定义接口。

如下所示:
option java_package = "com.my.hbase.protobuf.generated";
option java_outer_classname = "AggregateProtos";
option java_generic_services = true;
option java_generate_equals_and_hash = true;
option optimize_for = SPEED;

import "Client.proto";

message AggregateRequest {
required string interpreter_class_name = 1;
required Scan scan = 2;
optional bytes interpreter_specific_bytes = 3;
}

message AggregateResponse {
repeated bytes first_part = 1;
optional bytes second_part = 2;
}

service AggregateService {
rpc GetMax (AggregateRequest) returns (AggregateResponse);
rpc GetMin (AggregateRequest) returns (AggregateResponse);
rpc GetSum (AggregateRequest) returns (AggregateResponse);
rpc GetRowNum (AggregateRequest) returns (AggregateResponse);
rpc GetAvg (AggregateRequest) returns (AggregateResponse);
rpc GetStd (AggregateRequest) returns (AggregateResponse);
rpc GetMedian (AggregateRequest) returns (AggregateResponse);
}
可以看到这里定义7个聚合服务RPC,名字分别叫做GetMax、GetMin、GetSum等,本文通过GetSum进行举例,其他的聚合RPC也是类似的内部实现。RPC有一个入口参数,用消息AggregateRequest表示;RPC的返回值用消息AggregateResponse表示。Service是一个抽象概念,RPC的Server端可以看作一个用来提供服务的Service。在HBase Coprocessor中Service就是Server端需要提供的Endpoint Coprocessor服务,主要用来给HBase的Client提供服务。AggregateService.java是由Protobuf软件通过终端命令“protoc filename.proto--java_out=OUT_DIR”自动生成的,其作用是将.proto文件定义的消息结构以及服务转换成对应接口的RPC实现,其中包括如何构建request消息和response响应以及消息包含的内容的处理方式,并且将AggregateService包装成一个抽象类,具体的服务以类的方法的形式提供。AggregateService.java定义Client端与Server端通信的协议,代码中包含请求信息结构AggregateRequest、响应信息结构AggregateResponse、提供的服务种类AggregateService,其中AggregateRequest中的interpreter_class_name指的是column interpreter的类名,此类的作用在于将数据格式从存储类型解析成所需类型。AggregateService.java由于代码太长,在这里就不贴出来了。

下面我们来讲一下服务端的架构,

首先,Endpoint Coprocessor是一个Protobuf Service的实现,因此需要它必须继承某个ProtobufService。我们在前面已经通过proto文件定义Service,命名为AggregateService,因此Server端代码需要重载该类,其次作为HBase的协处理器,Endpoint 还必须实现HBase定义的协处理器协议,用Java的接口来定义。具体来说就是CoprocessorService和Coprocessor,这些HBase接口负责将协处理器和HBase 的RegionServer等实例联系起来以便协同工作。Coprocessor接口定义两个接口函数:start和stop。

加载Coprocessor之后Region打开的时候被RegionServer自动加载,并会调用器start 接口完成初始化工作。一般情况该接口函数仅仅需要将协处理器的运行上下文环境变量CoprocessorEnvironment保存到本地即可。

CoprocessorEnvironment保存协处理器的运行环境,每个协处理器都是在一个RegionServer进程内运行并隶属于某个Region。通过该变量获取Region的实例等 HBase运行时环境对象。

Coprocessor接口还定义stop()接口函数,该函数在Region被关闭时调用,用来进行协处理器的清理工作。本文里我们没有进行任何清理工作,因此该函数什么也不干。

我们的协处理器还需要实现CoprocessorService接口。该接口仅仅定义一个接口函数 getService()。我们仅需要将本实例返回即可。HBase的Region Server在接收到客户端的调用请求时,将调用该接口获取实现RPCService的实例,因此本函数一般情况下就是返回自身实例即可。

完成以上三个接口函数之后,Endpoint的框架代码就已完成。每个Endpoint协处理器都必须实现这些框架代码而且写法雷同。

menu.saveimg_.savepath20181031141452_.jpg

Server端的代码就是一个Protobuf RPC的Service实现,即通过Protobuf提供的某种服务。其开发内容主要包括:
 
  1. 实现Coprocessor的基本框架代码
  2. 实现服务的RPC具体代码


Endpoint 协处理的基本框架

Endpoint 是一个Server端Service的具体实现,其实现有一些框架代码,这些框架代码与具体的业务需求逻辑无关。仅仅是为了和HBase运行时环境协同工作而必须遵循和完成的一些粘合代码。因此多数情况下仅仅需要从一个例子程序拷贝过来并进行命名修改即可。不过我们还是完整地对这些粘合代码进行粗略的讲解以便更好地理解代码。
public Service getService() {
return this;
}

public void start(CoprocessorEnvironment env) throws IOException {
if(env instanceof RegionCoprocessorEnvironment) {
this.env = (RegionCoprocessorEnvironment)env;
} else {
throw new CoprocessorException("Must be loaded on a table region!");
}
}

public void stop(CoprocessorEnvironment env) throws IOException {
}
Endpoint协处理器真正的业务代码都在每一个RPC函数的具体实现中。


在本文中,我们的Endpoint协处理器仅提供一个RPC函数即getSUM。我将分别介绍编写该函数的几个主要工作:了解函数的定义,参数列表;处理入口参数;实现业务逻辑;设置返回参数。
public void getSum(RpcController controller, AggregateRequest request, RpcCallbackdone) {
AggregateResponse response = null;
RegionScanner scanner = null;
long sum = 0L;
try {
ColumnInterpreter ignored = this.constructColumnInterpreterFromRequest(request);
Object sumVal = null;
Scan scan = ProtobufUtil.toScan(request.getScan());
scanner = this.env.getRegion().getScanner(scan);
byte[] colFamily = scan.getFamilies()[0];
NavigableSet qualifiers = (NavigableSet) scan.getFamilyMap().get(colFamily);
byte[] qualifier = null;
if (qualifiers != null && !qualifiers.isEmpty()) {
qualifier = (byte[]) qualifiers.pollFirst();
}

ArrayList results = new ArrayList();
boolean hasMoreRows = false;

do {
hasMoreRows = scanner.next(results);
int listSize = results.size();

for (int i = 0; i < listSize; ++i) {
//取出列值
Object temp = ignored.getValue(colFamily, qualifier,
(Cell) results.get(i));
if (temp != null) {
sumVal = ignored.add(sumVal, ignored.castToReturnType(temp));
}
}

results.clear();
} while (hasMoreRows);

if (sumVal != null) {
response = AggregateResponse.newBuilder().addFirstPart(
ignored.getProtoForPromotedType(sumVal).toByteString()).build();
}
} catch (IOException var27) {
ResponseConverter.setControllerException(controller, var27);
} finally {
if (scanner != null) {
try {
scanner.close();
} catch (IOException var26) {
;
}
}

}

log.debug("Sum from this region is " +
this.env.getRegion().getRegionInfo().getRegionNameAsString() + ": " + sum);
done.run(response);
}
Endpoint类比于数据库的存储过程,其触发服务端的基于Region的同步运行再将各个结果在客户端搜集后归并计算。特点类似于传统的MapReduce框架,服务端Map客户端Reduce。

3.Endpoint客户端实现

HBase提供客户端Java包org.apache.hadoop.hbase.client.HTable,提供以下三种方法来调用协处理器提供的服务:
 
  1. coprocessorService(byte[])
  2. coprocessorService(Class, byte[], byte[],Batch.Call),
  3. coprocessorService(Class, byte[], byte[],Batch.Call, Batch.Callback)

 

menu.saveimg_.savepath20181031141634_.jpg

该方法采用rowkey指定Region。这是因为HBase客户端很少会直接操作Region,一般不需要知道Region的名字;况且在HBase中Region名会随时改变,所以用rowkey来指定Region是最合理的方式。使用rowkey可以指定唯一的一个Region,如果给定的Rowkey并不存在,只要在某个Region的rowkey范围内依然用来指定该Region。比如Region 1处理[row1, row100]这个区间内的数据,则rowkey=row1就由Region 1来负责处理,换句话说我们可以用row1来指定Region 1,无论rowkey等于”row1”的记录是否存在。CoprocessorService方法返回类型为CoprocessorRpcChannel的对象,该 RPC通道连接到由rowkey指定的Region上面,通过此通道可以调用该Region上面部署的协处理器RPC。

menu.saveimg_.savepath20181031141702_.jpg

有时候客户端需要调用多个 Region上的同一个协处理器,比如需要统计整个Table的sum,在这种情况下,需要所有的Region都参与进来,分别统计自身Region内部的sum并返回客户端,最终客户端将所有Region的返回结果汇总,就可以得到整张表的sum。


这意味着该客户端同时和多个Region进行批处理交互。一个可行的方法是,收集每个 Region的startkey,然后循环调用第一种coprocessorService方法:用每一个Region的startkey 作为入口参数,获得RPC通道创建stub对象,进而逐一调用每个Region上的协处理器RPC。这种做法需要写很多的代码,为此HBase提供两种更加简单的 coprocessorService方法来处理多个Region的协处理器调用。先来看第一种方法 coprocessorService(Class, byte[],byte[],Batch.Call),

该方法有 4 个入口参数。第一个参数是实现RPC的Service 类,即前文中的AggregateService类。通过它,HBase就可以找到相应的部署在Region上的协处理器,一个Region上可以部署多个协处理器,客户端必须通过指定Service类来区分究竟需要调用哪个协处理器提供的服务。

要调用哪些Region上的服务则由startkey和endkey来确定,通过 rowkey范围即可确定多个 Region。为此,coprocessorService方法的第二个和第三个参数分别是 startkey和endkey,凡是落在[startkey,endkey]区间内的Region都会参与本次调用。

第四个参数是接口类Batch.Call。它定义了如何调用协处理器,用户通过重载该接口的call()方法来实现客户端的逻辑。在call()方法内,可以调用RPC,并对返回值进行任意处理。即前文代码清单1中所做的事情。coprocessorService将负责对每个 Region调用这个call()方法。

coprocessorService方法的返回值是一个Map类型的集合。该集合的key是Region名字,value是Batch.Call.call方法的返回值。该集合可以看作是所有Region的协处理器 RPC 返回的结果集。客户端代码可以遍历该集合对所有的结果进行汇总处理。

这种coprocessorService方法的大体工作流程如下。首先它分析startkey和 endkey,找到该区间内的所有Region,假设存放在regionList中。然后,遍历regionList,为每一个Region调用Batch.Call,在该接口内,用户定义具体的RPC调用逻辑。最后coprocessorService将所有Batch.Call.call()的返回值加入结果集合并返回。

menu.saveimg_.savepath20181031141730_.jpg

coprocessorService的第三种方法比第二个方法多了一个参数callback。coprocessorService第二个方法内部使用HBase自带的缺省callback,该缺省 callback将每个Region的返回结果都添加到一个Map类型的结果集中,并将该集合作为coprocessorService方法的返回值。

HBase 提供第三种coprocessorService方法允许用户定义callback行为,coprocessorService 会为每一个RPC返回结果调用该callback,用户可以在callback 中执行需要的逻辑,比如执行sum累加。用第二种方法的情况下,每个Region协处理器RPC的返回结果先放入一个列表,所有的 Region 都返回后,用户代码再从该列表中取出每一个结果进行累加;用第三种方法,直接在callback中进行累加,省掉了创建结果集合和遍历该集合的开销,效率会更高一些。

因此我们只需要额外定义一个callback即可,callback是一个Batch.Callback接口类,用户需要重载其update方法。
 
public S sum(final HTable table, final ColumnInterpreter<R, S, P, Q, T> ci,final Scan scan)throws Throwable {

final AggregateRequest requestArg = validateArgAndGetPB(scan, ci, false);

class SumCallBack implements Batch.Callback {

S sumVal = null;

public S getSumResult() {
return sumVal;
}

@Override
public synchronized void update(byte[] region, byte[] row, S result) {
sumVal = ci.add(sumVal, result);
}}

SumCallBack sumCallBack = new SumCallBack();
table.coprocessorService(AggregateService.class, scan.getStartRow(), scan.getStopRow(),
new Batch.Call<AggregateService, S>() {
@Override
public S call(AggregateService instance) throws IOException {
ServerRpcController controller = new ServerRpcController();
BlockingRpcCallback<AggregateResponse> rpcCallback =
new BlockingRpcCallback<AggregateResponse>();
//RPC 调用
instance.getSum(controller, requestArg, rpcCallback);
AggregateResponse response = rpcCallback.get();
if (controller.failedOnException()) {
throw controller.getFailedOn();
}
if (response.getFirstPartCount() == 0) {
return null;
}
ByteString b = response.getFirstPart(0);
T t = ProtobufUtil.getParsedGenericInstance(ci.getClass(), 4, b);
S s = ci.getPromotedValueFromProto(t);
return s;
}
}, sumCallBack);
return sumCallBack.getSumResult();
4.Observer实现二级索引

Observer类似于传统数据库中的触发器,当发生某些事件的时候这类协处理器会被 Server 端调用。Observer Coprocessor是一些散布在HBase Server端代码的 hook钩子, 在固定的事件发生时被调用。比如:put操作之前有钩子函数prePut,该函数在pu 操作执 行前会被Region Server调用;在put操作之后则有postPut 钩子函数。
menu.saveimg_.savepath20181031141817_.jpg

 
RegionObserver工作原理

RegionObserver提供客户端的数据操纵事件钩子,Get、Put、Delete、Scan,使用此功能能够解决主表以及多个索引表之间数据一致性的问题

menu.saveimg_.savepath20181031141844_.jpg

 
  1. 客户端发出put请求;
  2. 该请求被分派给合适的RegionServer和Region;
  3. coprocessorHost拦截该请求,然后在该表上登记的每个 RegionObserver 上调用prePut();
  4. 如果没有被preGet()拦截,该请求继续送到 region,然后进行处理;
  5. Region产生的结果再次被CoprocessorHost拦截,调用postGet();
  6. 假如没有postGet()拦截该响应,最终结果被返回给客户端;

 

menu.saveimg_.savepath20181031141924_.jpg

如上图所示,HBase可以根据rowkey很快的检索到数据,但是如果根据column检索数据,首先要根据rowkey减小范围,再通过列过滤器去过滤出数据,如果使用二级索引,可以先查基于column的索引表,获取到rowkey后再快速的检索到数据。

menu.saveimg_.savepath20181031141955_.jpg

如图所示首先继承BaseRegionObserver类,重写postPut,postDelete方法,在postPut方法体内中写Put索引表数据的代码,在postDelete方法里面写Delete索引表数据,这样可以保持数据的一致性。

在Scan表的时候首先判断是否先查索引表,如果不查索引直接scan主表,如果走索引表通过索引表获取主表的rowkey再去查主表。

使用Elastic Search建立二级索引也是一样。

我们在同一个主机集群上同时建立了HBase集群和Elastic Search集群,存储到HBase的数据必须实时地同步到Elastic Search。而恰好HBase和Elastic Search都没有更新的概念,我们的需求可以简化为两步:
 
  1. 当一个新的Put操作产生时,将Put数据转化为json,索引到ElasticSearch,并把RowKey作为新文档的ID;
  2. 当一个新的Delete操作产生时获取Delete数据的rowkey删除Elastic Search中对应的ID。


5.协处理的主要应用场景 
  1. Observer允许集群在正常的客户端操作过程中可以有不同的行为表现;
  2. Endpoint允许扩展集群的能力,对客户端应用开放新的运算命令;
  3. Observer类似于RDBMS的触发器,主要在服务端工作;
  4. Endpoint类似于RDBMS的存储过程,主要在服务端工作;
  5. Observer可以实现权限管理、优先级设置、监控、ddl控制、二级索引等功能;
  6. Endpoint可以实现min、max、avg、sum、distinct、group by等功能。


例如HBase源码org.apache.hadoop.hbase.security.access.AccessController利用Observer实现对HBase进行了权限控制,有兴趣的读者可以看看相关代码。 收起阅读 »

中国HBase技术社区微信公众号:
hbasegroup

欢迎加入HBase生态+Spark社区钉钉大群