hbase执行手动大合并的时候,执行了一周还是没有结束,是怎么回事?

回复

hbase果农 发起了问题 • 1 人关注 • 0 个回复 • 252 次浏览 • 2019-04-22 08:50 • 来自相关话题

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

hbasechuxiao 发表了文章 • 3 个评论 • 623 次浏览 • 2019-03-25 18:47 • 来自相关话题

本文是前几篇文章的小结,介绍如何通过技术手段,挖掘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执行细节和参数调优方法论详解

hbasechuxiao 发表了文章 • 0 个评论 • 453 次浏览 • 2019-03-14 17:43 • 来自相关话题

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优化探索

hbasechuxiao 发表了文章 • 1 个评论 • 547 次浏览 • 2019-03-06 17:09 • 来自相关话题

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

为了更好的编写本文和后续文章,花费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疑难杂症填坑指南

hbasehbasegroup 发表了文章 • 1 个评论 • 1152 次浏览 • 2018-12-28 18:06 • 来自相关话题

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



整个写入流程从客户端调用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 spaceHBase这里我认为有个很不好的设计,捕获了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 Major Compaction没有清除Delete标志

hbasestigahuang 回复了问题 • 5 人关注 • 3 个回复 • 771 次浏览 • 2018-11-20 10:37 • 来自相关话题

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

hbasehbasegroup 发表了文章 • 0 个评论 • 383 次浏览 • 2018-11-12 15:49 • 来自相关话题

标题中提及"事务"可能会给大家带来误解,这篇文章不是在讨论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,做如下几步处理:
必要检查(初始状态,确保无Parent Procedure,确保有Owner信息)。设置Nonce Key(如果存在)以及Procedure ID。将新的Procedure写入到ProcedureStore中,持久化。将新的Procedure添加到ProcedureScheduler的调度队列的尾部。
运行
ProcedureExecutor初始化阶段,启动了若干个WorkerThread,具体数量可配置。
WorkerThread不断从ProcedureScheduler中poll出新的待执行的Procedure,而后:
获取IdLock: 获取Procedure关联的IdLock,避免同一个Procedure在多个线程中同时处理。获取资源锁: 调用Procedure内部定义的Acquire Lock请求,获取Procedure自身所需的资源锁。IdLock是为了确保一个Procedure只被一个线程调用,而这里的Lock是为了确保这一个Table只能被一个Procedure处理,这里需要获取Namespace的共享锁,以及当前这个Table的互斥锁,这里其实是一个分布式锁的需求,容易想到用ZooKeeper实现,事实上,这个Lock也可以用Procedure来实现,在后续章节中将会讲到这一点。处理当前操作状态:执行Procedure初始状态所定义的处理逻辑,处理完后会返回当前这个Procedure对象。每一步运行完,都将最新的状态持久化到ProcedureStore中。处理下一操作状态:如果返回对象依然是原来的Procedure,而且未失败,则意味着需要继续下一步处理。循环处理:循环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特性原来如此简单!",但如果仔细阅读这部分代码,就会深刻体会到它在实现上的复杂度,导致这种复杂度的客观原因总结起来有如下几点:
要实现一个统一的状态机管理框架本身就比较复杂,可以说将Assign Region/Create Table/Split Region等流程的复杂度转嫁了过来。需要支持优先级调度与公平调度。WALProcedureStore无用WAL文件的跟踪与清理,重启后的回放,均需要严谨的处理。涉及复杂的拓扑结构:一个Procedure中间运行过程可能会产生多个Sub-Procedures,这过程需要协调。不依赖于ZooKeeper的分布式锁机制。跨节点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调度策略
关于调度策略的基础需求,可以简单被表述为:
不同类型的Procedure优先级不同,如Server Procedure要优先于Table Procedure被调度。即使同为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的执行过程:
Acquire LockExecute
获取锁资源的操作,只需要在"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框架中的几个角色。最后一部分,选择整个框架中比较复杂的几个模块,展开实现细节。受限于篇幅,有几部分内容未涉及到:
WAL数据格式与WAL回放机制Notification-Bus(当然这部分也只实现了一小部分)复杂Procedure拓扑结构情形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)

spark streaming实时程序 进程总是运行三四个小时左右不知道原因挂掉,求大佬分析原因何在。。

spark刘狗 回复了问题 • 5 人关注 • 3 个回复 • 1019 次浏览 • 2019-08-22 17:20 • 来自相关话题

HBase 生态(多模式)

hbase过往记忆 发表了文章 • 0 个评论 • 1130 次浏览 • 2018-11-07 15:18 • 来自相关话题

经过近十年的发展,目前 HBase 已经支持了许多组件用于解决不同场景的问题,这组件包括:
Phoenix : SQL on HBaseSpark : OLAP、Streaming、SQLSolr:二级索引、全文索引OpenTSDB:时序HGraphDB:图GeoMesa:时空 查看全部
经过近十年的发展,目前 HBase 已经支持了许多组件用于解决不同场景的问题,这组件包括:
  1. Phoenix : SQL on HBase
  2. Spark : OLAP、Streaming、SQL
  3. Solr:二级索引、全文索引
  4. OpenTSDB:时序
  5. HGraphDB:图
  6. GeoMesa:时空

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

HBase Meetup过往记忆 发表了文章 • 0 个评论 • 714 次浏览 • 2018-10-25 20:13 • 来自相关话题

本文来自于2018年10月20日由中国 HBase 技术社区在武汉举办的中国 HBase Meetup 第六次线下交流会。分享者为过往记忆。
本文ppt下载地址:http://hbase.group/slides/162





 
本次分享的内容主要分为以下五点:
 
HBase基本知识;HBase读写流程;RowKey设计要点;HBase生态介绍;HBase典型案例分析。

首先我们简单介绍一下 HBase 是什么。





HBase 最开始是受 Google 的 BigTable 启发而开发的分布式、多版本、面向列的开源数据库。其主要特点是支持上亿行、百万列,支持强一致性、并且具有高扩展、高可用等特点。

既然 HBase 是一种分布式的数据库,那么其和传统的 RMDB 有什么区别的呢?我们先来看看HBase表核心概念,理解这些基本的核心概念对后面我理解 HBase 的读写以及如何设计 HBase 表有着重要的联系。





HBase 表主要由以下几个元素组成:
 
RowKey:表中每条记录的主键;Column Family:列族,将表进行横向切割,后面简称CF;Column:属于某一个列族,可动态添加列;Version Number:类型为Long,默认值是系统时间戳,可由用户自定义;Value:真实的数据。

大家可以从上面的图看出:一行(Row)数据是可以包含一个或多个 Column Family,但是我们并不推荐一张 HBase 表的 Column Family 超过三个。Column 是属于 Column Family 的,一个 Column Family 包含一个或多个 Column。

在物理层面上,所有的数据其实是存放在 Region 里面的,而 Region 又由 RegionServer 管理,其对于的关系如下:




 
Region:一段数据的集合;RegionServer:用于存放Region的服务。

从上面的图也可以清晰看到,一个 RegionServer 管理多个 Region;而一个 Region 管理一个或多个 Column Family。
到这里我们已经了解了 HBase 表的组成,但是 HBase 表里面的数据到底是怎么存储的呢?





上面是一张从逻辑上看 HBase 表形式,这个和关系型数据库很类似。那么如果我们再深入看,可以看出,这张表的划分可以如下图表示。




从上图大家可以明显看出,这张表有两个 Column Family ,分别为 personal 和 office。而 personal 又有三列name、city 以及 phone;office 有两列 tel 以及 address。由于存储在 HBase 里面的表一般有上亿行,所以 HBase 表会对整个数据按照 RowKey 进行字典排序,然后再对这张表进行横向切割。切割出来的数据是存储在 Region 里面,而不同的 Column Family 虽然属于一行,但是其在底层存储是放在不同的 Region 里。所以这张表我用了六种颜色表示,也就是说,这张表的数据会被放在六个 Region 里面的,这就可以把数据尽可能的分散到整个集群。

在前面我们介绍了 HBase 其实是面向列的数据库,所以说一行 HBase 的数据其实是分了好几行存储,一个列对应一行,HBase 的 KV 结构如下:




为了简便期间,在后面的表示我们删除了类似于 Key Length 的属性,只保留 Row Key、Column Family、Column Qualifier等信息。所以 RowKey 为 Row1 的数据第一列表示为上图最后一行的形式。以此类推,整个表的存储就可以如下表示:




大家可以从上面的 kv 表现形式看出,Row11 的 phone 这列其实是没有数据的,在 HBase 的底层存储里面也就没有存储这列了,这点和我们传统的关系型数据库有很大的区别,有了这个特点, HBase 特别适合存储稀疏表。

我们前面也将了 HBase 其实是多版本的,那如果我们修改了 HBase 表的一列,HBase 又是如何存储的呢?




比如上如中我们将 Row1 的 city 列从北京修改为上海了,如果使用 KV 表示的话,我们可以看出其实底层存储了两条数据,这两条数据的版本是不一样的,最新的一条数据版本比之前的新。总结起来就是:
 
HBase支持数据多版本特性,通过带有不同时间戳的多个KeyValue版本来实现的;每次put,delete都会产生一个新的Cell,都拥有一个版本;默认只存放数据的三个版本,可以配置;查询默认返回最新版本的数据,可以通过制定版本号或版本数获取旧数据。

到这里我们已经了解了 HBase 表及其底层的 KV 存储了,现在让我们来了解一下 HBase 是如何读写数据的。首先我们来看看 HBase 的架构设计,这种图来自于社区:





HBase 的写过程如下:
先将数据写到WAL中;WAL 存放在HDFS之上;每次Put、Delete操作的数据均追加到WAL末端;持久化到WAL之后,再写到MemStore中;两者写完返回ACK到客户端。





MemStore 其实是一种内存结构,一个Column Family 对应一个MemStore,MemStore 里面的数据也是对 Rowkey 进行字典排序的,如下:




既然我们写数都是先写 WAL,再写 MemStore ,而 MemStore 是内存结构,所以 MemStore 总会写满的,将 MemStore 的数据从内存刷写到磁盘的操作成为 flush:




以下几种行为会导致 flush 操作
全局内存控制;MemStore使用达到上限;RegionServer的Hlog数量达到上限;手动触发;关闭RegionServer触发。

每次 flush 操作都是将一个 MemStore 的数据写到一个 HFile 里面的,所以上图中 HDFS 上有许多个 HFile 文件。文件多了会对后面的读操作有影响,所以 HBase 会隔一定的时间将 HFile 进行合并。根据合并的范围不同分为 Minor Compaction 和 Major Compaction:





Minor Compaction: 指选取一些小的、相邻的HFile将他们合并成一个更大的Hfile。
Major Compaction:

将一个column family下所有的 Hfiles 合并成更大的;
删除那些被标记为删除的数据、超过TTL(time-to-live)时限的数据,以及超过了版本数量限制的数据。

HBase 读操作相对于写操作更为复杂,其需要读取 BlockCache、MemStore 以及 HFile。




上图只是简单的表示 HBase 读的操作,实际上读的操作比这个还要复杂,我这里就不深入介绍了。

到这里,有些人可能就想到了,前面我们说 HBase 表按照 Rowkey 分布到集群的不同机器上,那么我们如何去确定我们该读写哪些 RegionServer 呢?这就是 HBase Region 查找的问题,




客户端按照上面的流程查找需要读写的 RegionServer 。这个过程一般是第一次读写的时候进行的,在第一次读取到元数据之后客户端一般会把这些信息缓存到自己内存中,后面操作直接从内存拿就行。当然,后面元数据信息可能还会变动,这时候客户端会再次按照上面流程获取元数据。

到这里整个读写流程得基本知识就讲完了。现在我们来看看 HBase RowKey 的设计要点。我们一般都会说,看 HBase 设计的好不好,就看其 RowKey 设计的好不好,所以RowKey 的设计在后面的写操作至关重要。我们先来看看 Rowkey 的作用




HBase 中的 Rowkey 主要有以下的作用:

读写数据时通过Row Key找到对应的Region
MemStore 中的数据按RowKey字典顺序排序
HFile中的数据按RowKey字典顺序排序

从下图可以看到,底层的 HFile 最终是按照 Rowkey 进行切分的,所以我们的设计原则是结合业务的特点,并考虑高频查询,尽可能的将数据打散到整个集群。




一定要充分分析清楚后面我们的表需要怎么查询。下面我们来看看三种比较场景的 Rowkey 设计方案。














这三种 Rowkey 的设计非常常见,具体的内容图片上也有了,我就不打文字了。

数据如果只是存储在哪里其实并没有什么用,我们还需要有办法能够使用到里面的数据。幸好的是,当前 HBase 有许多的组件可以满足我们各种需求。如下图是 HBase 比较常用的组件:




HBase 的生态主要有:
Phoenix:主要提供使用 SQL 的方式来查询 HBase 里面的数据。一般能够在毫秒级别返回,比较适合 OLTP 场景。Spark:我们可以使用 Spark 进行 OLAP 分析;也可以使用 Spark SQL 来满足比较复杂的 SQL 查询场景;使用 Spark Streaming 来进行实时流分析。Solr:原生的 HBase 只提供了 Rowkey 单主键,如果我们需要对 Rowkey 之外的列进行查找,这时候就会有问题。幸好我们可以使用 Solr 来建立二级索引/全文索引充分满足我们的查询需求。HGraphDB:HGraphDB是分布式图数据库。依托图关联技术,帮助金融机构有效识别隐藏在网络中的黑色信息,在团伙欺诈、黑中介识别等。GeoMesa:目前基于NoSQL数据库的时空数据引擎中功能最丰富、社区贡献人数最多的开源系统。OpenTSDB:基于HBase的分布式的,可伸缩的时间序列数据库。适合做监控系统;譬如收集大规模集群(包括网络设备、操作系统、应用程序)的监控数据并进行存储,查询。

下面简单介绍一下这些组件。






























有了这么多组件,我们都可以干什么呢?来看看 HBase 的典型案例。




HBase 在风控场景、车联网/物联网、广告推荐、电子商务等行业有这广泛的使用。下面是四个典型案例的架构,由于图片里有详细的文字,我就不再打出来了。



















  查看全部
本文来自于2018年10月20日由中国 HBase 技术社区在武汉举办的中国 HBase Meetup 第六次线下交流会。分享者为过往记忆。
本文ppt下载地址:http://hbase.group/slides/162

HBase2-iteblog.jpg

 
本次分享的内容主要分为以下五点:
 
  • HBase基本知识;
  • HBase读写流程;
  • RowKey设计要点;
  • HBase生态介绍;
  • HBase典型案例分析。


首先我们简单介绍一下 HBase 是什么。

HBase4-iteblog.jpg

HBase 最开始是受 Google 的 BigTable 启发而开发的分布式、多版本、面向列的开源数据库。其主要特点是支持上亿行、百万列,支持强一致性、并且具有高扩展、高可用等特点。

既然 HBase 是一种分布式的数据库,那么其和传统的 RMDB 有什么区别的呢?我们先来看看HBase表核心概念,理解这些基本的核心概念对后面我理解 HBase 的读写以及如何设计 HBase 表有着重要的联系。

HBase5-iteblog.jpg

HBase 表主要由以下几个元素组成:
 
  • RowKey:表中每条记录的主键;
  • Column Family:列族,将表进行横向切割,后面简称CF;
  • Column:属于某一个列族,可动态添加列;
  • Version Number:类型为Long,默认值是系统时间戳,可由用户自定义;
  • Value:真实的数据。


大家可以从上面的图看出:一行(Row)数据是可以包含一个或多个 Column Family,但是我们并不推荐一张 HBase 表的 Column Family 超过三个。Column 是属于 Column Family 的,一个 Column Family 包含一个或多个 Column。

在物理层面上,所有的数据其实是存放在 Region 里面的,而 Region 又由 RegionServer 管理,其对于的关系如下:
HBase6-iteblog.jpg

 
  • Region:一段数据的集合;
  • RegionServer:用于存放Region的服务。


从上面的图也可以清晰看到,一个 RegionServer 管理多个 Region;而一个 Region 管理一个或多个 Column Family。
到这里我们已经了解了 HBase 表的组成,但是 HBase 表里面的数据到底是怎么存储的呢?

HBase7-iteblog.jpg

上面是一张从逻辑上看 HBase 表形式,这个和关系型数据库很类似。那么如果我们再深入看,可以看出,这张表的划分可以如下图表示。
HBase8-iteblog.jpg

从上图大家可以明显看出,这张表有两个 Column Family ,分别为 personal 和 office。而 personal 又有三列name、city 以及 phone;office 有两列 tel 以及 address。由于存储在 HBase 里面的表一般有上亿行,所以 HBase 表会对整个数据按照 RowKey 进行字典排序,然后再对这张表进行横向切割。切割出来的数据是存储在 Region 里面,而不同的 Column Family 虽然属于一行,但是其在底层存储是放在不同的 Region 里。所以这张表我用了六种颜色表示,也就是说,这张表的数据会被放在六个 Region 里面的,这就可以把数据尽可能的分散到整个集群。

在前面我们介绍了 HBase 其实是面向列的数据库,所以说一行 HBase 的数据其实是分了好几行存储,一个列对应一行,HBase 的 KV 结构如下:
HBase9-iteblog.jpg

为了简便期间,在后面的表示我们删除了类似于 Key Length 的属性,只保留 Row Key、Column Family、Column Qualifier等信息。所以 RowKey 为 Row1 的数据第一列表示为上图最后一行的形式。以此类推,整个表的存储就可以如下表示:
HBase10-iteblog.jpg

大家可以从上面的 kv 表现形式看出,Row11 的 phone 这列其实是没有数据的,在 HBase 的底层存储里面也就没有存储这列了,这点和我们传统的关系型数据库有很大的区别,有了这个特点, HBase 特别适合存储稀疏表。

我们前面也将了 HBase 其实是多版本的,那如果我们修改了 HBase 表的一列,HBase 又是如何存储的呢?
HBase11-iteblog.jpg

比如上如中我们将 Row1 的 city 列从北京修改为上海了,如果使用 KV 表示的话,我们可以看出其实底层存储了两条数据,这两条数据的版本是不一样的,最新的一条数据版本比之前的新。总结起来就是:
 
  • HBase支持数据多版本特性,通过带有不同时间戳的多个KeyValue版本来实现的;
  • 每次put,delete都会产生一个新的Cell,都拥有一个版本;
  • 默认只存放数据的三个版本,可以配置;
  • 查询默认返回最新版本的数据,可以通过制定版本号或版本数获取旧数据。


到这里我们已经了解了 HBase 表及其底层的 KV 存储了,现在让我们来了解一下 HBase 是如何读写数据的。首先我们来看看 HBase 的架构设计,这种图来自于社区:

HBase13-iteblog.jpg

HBase 的写过程如下:
  • 先将数据写到WAL中;
  • WAL 存放在HDFS之上;
  • 每次Put、Delete操作的数据均追加到WAL末端;
  • 持久化到WAL之后,再写到MemStore中;
  • 两者写完返回ACK到客户端。


HBase14-iteblog.jpg

MemStore 其实是一种内存结构,一个Column Family 对应一个MemStore,MemStore 里面的数据也是对 Rowkey 进行字典排序的,如下:
HBase15-iteblog.jpg

既然我们写数都是先写 WAL,再写 MemStore ,而 MemStore 是内存结构,所以 MemStore 总会写满的,将 MemStore 的数据从内存刷写到磁盘的操作成为 flush:
HBase16-iteblog.jpg

以下几种行为会导致 flush 操作
  • 全局内存控制;
  • MemStore使用达到上限;
  • RegionServer的Hlog数量达到上限;
  • 手动触发;
  • 关闭RegionServer触发。


每次 flush 操作都是将一个 MemStore 的数据写到一个 HFile 里面的,所以上图中 HDFS 上有许多个 HFile 文件。文件多了会对后面的读操作有影响,所以 HBase 会隔一定的时间将 HFile 进行合并。根据合并的范围不同分为 Minor Compaction 和 Major Compaction:

HBase17-iteblog.jpg

Minor Compaction: 指选取一些小的、相邻的HFile将他们合并成一个更大的Hfile。
Major Compaction:

将一个column family下所有的 Hfiles 合并成更大的;
删除那些被标记为删除的数据、超过TTL(time-to-live)时限的数据,以及超过了版本数量限制的数据。

HBase 读操作相对于写操作更为复杂,其需要读取 BlockCache、MemStore 以及 HFile。
HBase18-iteblog.jpg

上图只是简单的表示 HBase 读的操作,实际上读的操作比这个还要复杂,我这里就不深入介绍了。

到这里,有些人可能就想到了,前面我们说 HBase 表按照 Rowkey 分布到集群的不同机器上,那么我们如何去确定我们该读写哪些 RegionServer 呢?这就是 HBase Region 查找的问题,
HBase19-iteblog.jpg

客户端按照上面的流程查找需要读写的 RegionServer 。这个过程一般是第一次读写的时候进行的,在第一次读取到元数据之后客户端一般会把这些信息缓存到自己内存中,后面操作直接从内存拿就行。当然,后面元数据信息可能还会变动,这时候客户端会再次按照上面流程获取元数据。

到这里整个读写流程得基本知识就讲完了。现在我们来看看 HBase RowKey 的设计要点。我们一般都会说,看 HBase 设计的好不好,就看其 RowKey 设计的好不好,所以RowKey 的设计在后面的写操作至关重要。我们先来看看 Rowkey 的作用
HBase21-iteblog.jpg

HBase 中的 Rowkey 主要有以下的作用:

读写数据时通过Row Key找到对应的Region
MemStore 中的数据按RowKey字典顺序排序
HFile中的数据按RowKey字典顺序排序

从下图可以看到,底层的 HFile 最终是按照 Rowkey 进行切分的,所以我们的设计原则是结合业务的特点,并考虑高频查询,尽可能的将数据打散到整个集群。
HBase22-iteblog.jpg

一定要充分分析清楚后面我们的表需要怎么查询。下面我们来看看三种比较场景的 Rowkey 设计方案。

HBase23-iteblog.jpg

HBase24-iteblog.jpg


HBase25-iteblog.jpg

这三种 Rowkey 的设计非常常见,具体的内容图片上也有了,我就不打文字了。

数据如果只是存储在哪里其实并没有什么用,我们还需要有办法能够使用到里面的数据。幸好的是,当前 HBase 有许多的组件可以满足我们各种需求。如下图是 HBase 比较常用的组件:
HBase27-iteblog.jpg

HBase 的生态主要有:
  • Phoenix:主要提供使用 SQL 的方式来查询 HBase 里面的数据。一般能够在毫秒级别返回,比较适合 OLTP 场景。
  • Spark:我们可以使用 Spark 进行 OLAP 分析;也可以使用 Spark SQL 来满足比较复杂的 SQL 查询场景;使用 Spark Streaming 来进行实时流分析。
  • Solr:原生的 HBase 只提供了 Rowkey 单主键,如果我们需要对 Rowkey 之外的列进行查找,这时候就会有问题。幸好我们可以使用 Solr 来建立二级索引/全文索引充分满足我们的查询需求。
  • HGraphDB:HGraphDB是分布式图数据库。依托图关联技术,帮助金融机构有效识别隐藏在网络中的黑色信息,在团伙欺诈、黑中介识别等。
  • GeoMesa:目前基于NoSQL数据库的时空数据引擎中功能最丰富、社区贡献人数最多的开源系统。
  • OpenTSDB:基于HBase的分布式的,可伸缩的时间序列数据库。适合做监控系统;譬如收集大规模集群(包括网络设备、操作系统、应用程序)的监控数据并进行存储,查询。


下面简单介绍一下这些组件。

HBase28-iteblog.jpg


HBase29-iteblog.jpg


HBase30-iteblog.jpg


HBase31-iteblog.jpg


HBase32-iteblog.jpg


HBase33-iteblog.jpg

有了这么多组件,我们都可以干什么呢?来看看 HBase 的典型案例。

HBase35-iteblog.jpg
HBase 在风控场景、车联网/物联网、广告推荐、电子商务等行业有这广泛的使用。下面是四个典型案例的架构,由于图片里有详细的文字,我就不再打出来了。
HBase36-iteblog.jpg


HBase37-iteblog.jpg


HBase38-iteblog.jpg


HBase39-iteblog.jpg

 

Apache HBase Operator Tools

hbasehbasegroup 发表了文章 • 0 个评论 • 944 次浏览 • 2018-10-25 10:07 • 来自相关话题

推荐Apache HBase Operator Tools
Apache HBase HBCK2 Tool

HBCK2 is the successor to hbck, the hbase-1.x fixup tool (A.K.A hbck1). Use it in place of hbck1 making repairs against hbase-2.x installs.

hbck1

The hbck tool that ships with hbase-1.x (A.K.A hbck1) should not be run against an hbase-2.x cluster. It may do damage. While hbck1 is still bundled inside hbase-2.x -- to minimize surprise (it has a fat pointer to HBCK2 at the head of its help output) -- it's write-facility (-fix) has been removed. It can report on the state of an hbase-2.x cluster but its assessments are likely inaccurate since it does not understand the internal workings of an hbase-2.x.

HBCK2 does much less than hbck1 because many of the class of problems hbck1 addressed are either no longer issues in hbase-2.x, or we've made (or will make) a dedicated tool to do what hbck1 used incorporate. HBCK2 also works in a manner that differs from how hbck1 operated, asking the HBase Master to do its bidding, rather than replicate functionality outside of the Master inside the hbck1 tool.

Building HBCK2

Run:mvn installThe built HBCK2 fat jar will be in the target sub-directory.

Running HBCK2

org.apache.hbase.HBCK2 is the name of the HBCK2 main class. After building HBCK2 to generate the HBCK2 jar file, running the below will dump out the HBCK2 usage:$ HBASE_CLASSPATH_PREFIX=./hbase-hbck2-1.0.0-SNAPSHOT.jar ./bin/hbase org.apache.hbase.HBCK2
usage: HBCK2 [OPTIONS] COMMAND <ARGS> Options: -d,--debug run with debug output -h,--help output this help message -p,--hbase.zookeeper.property.clientPort port of target hbase ensemble -q,--hbase.zookeeper.quorum <arg> ensemble of target hbase -v,--version this hbck2 version -z,--zookeeper.znode.parent parent znode of target hbase Commands: assigns [OPTIONS] <ENCODED_REGIONNAME>... Options: -o,--override override ownership by another procedure A 'raw' assign that can be used even during Master initialization. Skirts Coprocessors. Pass one or more encoded RegionNames. 1588230740 is the hard-coded name for the hbase:meta region and de00010733901a05f5a2a3a382e27dd4 is an example of what a user-space encoded Region name looks like. For example: $ HBCK2 assign 1588230740 de00010733901a05f5a2a3a382e27dd4 Returns the pid(s) of the created AssignProcedure(s) or -1 if none. bypass [OPTIONS] <PID>... Options: -o,--override override if procedure is running/stuck -r,--recursive bypass parent and its children. SLOW! EXPENSIVE! -w,--lockWait milliseconds to wait on lock before giving up; default=1 Pass one (or more) procedure 'pid's to skip to procedure finish. Parent of bypassed procedure will also be skipped to the finish. Entities will be left in an inconsistent state and will require manual fixup. May need Master restart to clear locks still held. Bypass fails if procedure has children. Add 'recursive' if all you have is a parent pid to finish parent and children. This is SLOW, and dangerous so use selectively. Does not always work. unassigns <ENCODED_REGIONNAME>... Options: -o,--override override ownership by another procedure A 'raw' unassign that can be used even during Master initialization. Skirts Coprocessors. Pass one or more encoded RegionNames: 1588230740 is the hard-coded name for the hbase:meta region and de00010733901a05f5a2a3a382e27dd4 is an example of what a user-space encoded Region name looks like. For example: $ HBCK2 unassign 1588230740 de00010733901a05f5a2a3a382e27dd4 Returns the pid(s) of the created UnassignProcedure(s) or -1 if none. setTableState <TABLENAME> <STATE> Possible table states: ENABLED, DISABLED, DISABLING, ENABLING To read current table state, in the hbase shell run: hbase> get 'hbase:meta', '<TABLENAME>', 'table:state' A value of \x08\x00 == ENABLED, \x08\x01 == DISABLED, etc. An example making table name 'user' ENABLED: $ HBCK2 setTableState users ENABLED Returns whatever the previous table state was.HBCK2 Overview

HBCK2 is currently a simple tool that does one thing at a time only.

HBCK2 does not do diagnosis, leaving that function to other tooling, described below.

In hbase-2.x, the Master is the final arbiter of all state, so a general principal of HBCK2 is that it asks the Master to effect all repair. This means a Master must be up before you can run an HBCK2 command.

HBCK2 works by making use of an intentionally obscured HbckService hosted on the Master. The Service publishes a few methods for the HBCK2 tool to pull on. The first thing HBCK2 does is poke the cluster to ensure the service is available. It will fail if it is not or if the HbckService is lacking a wanted facility. HBCK2 versions should be able to work across multiple hbase-2 releases. It will fail with a complaint if it is unable to run. There is no HbckService in versions of hbase before 2.0.3 and 2.1.1. HBCK2 will not work against these versions.

Finding Problems

While hbck1 performed analysis reporting your cluster GOOD or BAD, HBCK2 is less presumptious. In hbase-2.x, the operator figures what needs fixing and then uses tooling including HBCK2 to do fixup.

To figure issues in assignment, make use of the following utilities.

Diagnosis Tooling
Master Logs

The Master runs all assignments, server crash handling, cluster start and stop, etc. In hbase-2.x, all that the Master does has been cast as Procedures run on a state machine engine. See Procedure Framework and Assignment Manager for detail on how this new infrastructure works. Each Procedure has a unique Procedure id, its pid, that it lists on each logging. Following the pid, you can trace the lifecycle of a Procedure in the Master logs as Procedures transition from start, through each of the Procedure's various stages to finish. Some Procedures spawn sub-procedures, wait on their Children, and then themselves finish. Each child logs its pid but also its ppid; its parent's pid.

Generally all runs problem free but if some unforeseen circumstance arises, the assignment framework may sustain damage requiring operator intervention. Below we will discuss some such scenarios but they can manifest in the Master log as a Region being STUCK or a Procedure transitioning an entity -- a Region or a Table -- may be blocked because another Procedure holds the exclusive lock and is not letting go.

STUCK Procedures look like this:2018-09-12 15:29:06,558 WARN org.apache.hadoop.hbase.master.assignment.AssignmentManager: STUCK Region-In-Transition rit=OPENING, location=va1001.example.org,22101,1536173230599, table=IntegrationTestBigLinkedList_20180626110336, region=dbdb56242f17610c46ea044f7a42895b/master-status#tables

This section about midway down in Master UI home-page shows a list of tables with columns for whether the table is ENABLED, ENABLING, DISABLING, or DISABLED among other attributes. Also listed are columns with counts of Regions in their various transition states: OPEN, CLOSED, etc. A read of this table is good for figuring if the Regions of this table have a proper disposition. For example if a table is ENABLED and there are Regions that are not in the OPEN state and the Master Log is silent about any ongoing assigns, then something is amiss.

Procedures & Locks

This page off the Master UI home page under the Procedures & Locks menu item in the page heading lists all ongoing Procedures and Locks as well as the current set of Master Procedure WALs (named pv2-0000000000000000###.log under the MasterProcWALs directory in your hbase install). On startup, on a large cluster when furious assigning is afoot, this page is filled with lists of Procedures and Locks. The count of MasterProcWALs will bloat too. If after the cluster settles, there is a stuck Lock or Procedure or the count of WALs doesn't ever come down but only grows, then operator intervention is needed to alieve the blockage.

Lists of locks and procedures can also be obtained via the hbase shell:$ echo "list_locks"| hbase shell &> /tmp/locks.txt $ echo "list_procedures"| hbase shell &> /tmp/procedures.txtThe HBase Canary Tool

The Canary tool is useful verifying the state of assign. It can be run with a table focus or against the whole cluster.

For example, to check cluster assigns:$ hbase canary -f false -t 6000000 &>/tmp/canary.logThe -f false tells the Canary to keep going across failed Region fetches and the -t 6000000 tells the Canary run for ~two hours maximum. When done, check out /tmp/canary.log. Grep for ERROR lines to find problematic Region assigns.

You can do a probe like the Canary's in the hbase shell. For example, given a Region that has a start row of d1dddd0cbelonging to the table testtable, do as follows:hbase> scan 'testtable', {STARTROW => 'd1dddd0c', LIMIT => 10}For an overview on parsing a Region name into its constituent parts, see RegionInfo API.

Other Tools

To figure the list of Regions that are not OPEN on an ENABLED or ENABLING table, read the hbase:meta table info:state column. For example, to find the state of all Regions in the table IntegrationTestBigLinkedList_20180626064758, do the following:$ echo " scan 'hbase:meta', {ROWPREFIXFILTER => 'IntegrationTestBigLinkedList_20180626064758,', COLUMN => 'info:state'}"| hbase shell > /tmp/t.txt...then grep for OPENING or CLOSING Regions.

To move an OPENING issue to OPEN so it agrees with a table's ENABLED state, use the assign command in the hbase shell to queue a new Assign Procedure (watch the Master logs to see the Assign run). If many Regions to assign, use the HBCK2 tool. It can do bulk assigning.

Fixing

General principals include a Region can not be assigned if it is in CLOSING state (or the inverse, unassigned if in OPENINGstate) without first transitioning via CLOSED: Regions must always move from CLOSED, to OPENING, to OPEN, and then to CLOSING, CLOSED.

When making repair, do fixup a table at a time.

Also, if a table is DISABLED, you cannot assign a Region. In the Master logs, you will see that the Master will report that the assign has been skipped because the table is DISABLED. You may want to assign a Region because it is currently in the OPENING state and you want it in the CLOSED state so it agrees with the table's DISABLED state. In this situation, you may have to temporarily set the table status to ENABLED, just so you can do the assign, and then set it back again after the unassign.HBCK2 has facility to allow you do this. See the HBCK2 usage output.

Start-over

At an extreme, if the Master is distraught and all attempts at fixup only turn up undoable locks or Procedures that won't finish, and/or the set of MasterProcWALs is growing without bound, it is possible to wipe the Master state clean. Just move aside the/hbase/MasterProcWALs/ directory under your hbase install and restart the Master process. It will come back as a tabula rasawithout memory of the bad times past.

If at the time of the erasure, all Regions were happily assigned or offlined, then on Master restart, the Master should pick up and continue as though nothing happened. But if there were Regions-In-Transition at the time, then the operator may have to intervene to bring outstanding assigns/unassigns to their terminal point. Read the hbase:meta info:state columns as described above to figure what needs assigning/unassigning. Having erased all history moving aside the MasterProcWALs, none of the entities should be locked so you are free to bulk assign/unassign.

Assigning/Unassigning

Generally, on assign, the Master will persist until successful. An assign takes an exclusive lock on the Region. This precludes a concurrent assign or unassign from running. An assign against a locked Region will wait until the lock is released before making progress. See the [Procedures & Locks] section above for current list of outstanding Locks.

Master startup cannot progress, in holding-pattern until region onlined

This should never happen. If it does, here is what it looks like:2018-10-01 22:07:42,792 WARN org.apache.hadoop.hbase.master.HMaster: hbase:meta,,1.1588230740 is NOT online; state={1588230740 state=CLOSING, ts=1538456302300, server=ve1017.example.org,22101,1538449648131}; ServerCrashProcedures=true. Master startup cannot progress, in holding-pattern until region onlined.The Master is unable to continue startup because there is no Procedure to assign hbase:meta (or hbase:namespace). To inject one, use the HBCK2 tool:HBASE_CLASSPATH_PREFIX=./hbase-hbck2-1.0.0-SNAPSHOT.jar hbase org.apache.hbase.HBCK2 assigns 1588230740...where 1588230740 is the encoded name of the hbase:meta Region.

The same may happen to the hbase:namespace system table. Look for the encoded Region name of the hbase:namespaceRegion and do similar to what we did for hbase:meta.
  查看全部
推荐Apache HBase Operator Tools
Apache HBase HBCK2 Tool

HBCK2 is the successor to hbck, the hbase-1.x fixup tool (A.K.A hbck1). Use it in place of hbck1 making repairs against hbase-2.x installs.

hbck1

The hbck tool that ships with hbase-1.x (A.K.A hbck1) should not be run against an hbase-2.x cluster. It may do damage. While hbck1 is still bundled inside hbase-2.x -- to minimize surprise (it has a fat pointer to HBCK2 at the head of its help output) -- it's write-facility (-fix) has been removed. It can report on the state of an hbase-2.x cluster but its assessments are likely inaccurate since it does not understand the internal workings of an hbase-2.x.

HBCK2 does much less than hbck1 because many of the class of problems hbck1 addressed are either no longer issues in hbase-2.x, or we've made (or will make) a dedicated tool to do what hbck1 used incorporate. HBCK2 also works in a manner that differs from how hbck1 operated, asking the HBase Master to do its bidding, rather than replicate functionality outside of the Master inside the hbck1 tool.

Building HBCK2

Run:mvn installThe built HBCK2 fat jar will be in the target sub-directory.

Running HBCK2

org.apache.hbase.HBCK2 is the name of the HBCK2 main class. After building HBCK2 to generate the HBCK2 jar file, running the below will dump out the HBCK2 usage:$ HBASE_CLASSPATH_PREFIX=./hbase-hbck2-1.0.0-SNAPSHOT.jar ./bin/hbase org.apache.hbase.HBCK2
usage: HBCK2 [OPTIONS] COMMAND <ARGS> Options: -d,--debug run with debug output -h,--help output this help message -p,--hbase.zookeeper.property.clientPort port of target hbase ensemble -q,--hbase.zookeeper.quorum <arg> ensemble of target hbase -v,--version this hbck2 version -z,--zookeeper.znode.parent parent znode of target hbase Commands: assigns [OPTIONS] <ENCODED_REGIONNAME>... Options: -o,--override override ownership by another procedure A 'raw' assign that can be used even during Master initialization. Skirts Coprocessors. Pass one or more encoded RegionNames. 1588230740 is the hard-coded name for the hbase:meta region and de00010733901a05f5a2a3a382e27dd4 is an example of what a user-space encoded Region name looks like. For example: $ HBCK2 assign 1588230740 de00010733901a05f5a2a3a382e27dd4 Returns the pid(s) of the created AssignProcedure(s) or -1 if none. bypass [OPTIONS] <PID>... Options: -o,--override override if procedure is running/stuck -r,--recursive bypass parent and its children. SLOW! EXPENSIVE! -w,--lockWait milliseconds to wait on lock before giving up; default=1 Pass one (or more) procedure 'pid's to skip to procedure finish. Parent of bypassed procedure will also be skipped to the finish. Entities will be left in an inconsistent state and will require manual fixup. May need Master restart to clear locks still held. Bypass fails if procedure has children. Add 'recursive' if all you have is a parent pid to finish parent and children. This is SLOW, and dangerous so use selectively. Does not always work. unassigns <ENCODED_REGIONNAME>... Options: -o,--override override ownership by another procedure A 'raw' unassign that can be used even during Master initialization. Skirts Coprocessors. Pass one or more encoded RegionNames: 1588230740 is the hard-coded name for the hbase:meta region and de00010733901a05f5a2a3a382e27dd4 is an example of what a user-space encoded Region name looks like. For example: $ HBCK2 unassign 1588230740 de00010733901a05f5a2a3a382e27dd4 Returns the pid(s) of the created UnassignProcedure(s) or -1 if none. setTableState <TABLENAME> <STATE> Possible table states: ENABLED, DISABLED, DISABLING, ENABLING To read current table state, in the hbase shell run: hbase> get 'hbase:meta', '<TABLENAME>', 'table:state' A value of \x08\x00 == ENABLED, \x08\x01 == DISABLED, etc. An example making table name 'user' ENABLED: $ HBCK2 setTableState users ENABLED Returns whatever the previous table state was.HBCK2 Overview

HBCK2 is currently a simple tool that does one thing at a time only.

HBCK2 does not do diagnosis, leaving that function to other tooling, described below.

In hbase-2.x, the Master is the final arbiter of all state, so a general principal of HBCK2 is that it asks the Master to effect all repair. This means a Master must be up before you can run an HBCK2 command.

HBCK2 works by making use of an intentionally obscured HbckService hosted on the Master. The Service publishes a few methods for the HBCK2 tool to pull on. The first thing HBCK2 does is poke the cluster to ensure the service is available. It will fail if it is not or if the HbckService is lacking a wanted facility. HBCK2 versions should be able to work across multiple hbase-2 releases. It will fail with a complaint if it is unable to run. There is no HbckService in versions of hbase before 2.0.3 and 2.1.1. HBCK2 will not work against these versions.

Finding Problems

While hbck1 performed analysis reporting your cluster GOOD or BAD, HBCK2 is less presumptious. In hbase-2.x, the operator figures what needs fixing and then uses tooling including HBCK2 to do fixup.

To figure issues in assignment, make use of the following utilities.

Diagnosis Tooling
Master Logs

The Master runs all assignments, server crash handling, cluster start and stop, etc. In hbase-2.x, all that the Master does has been cast as Procedures run on a state machine engine. See Procedure Framework and Assignment Manager for detail on how this new infrastructure works. Each Procedure has a unique Procedure id, its pid, that it lists on each logging. Following the pid, you can trace the lifecycle of a Procedure in the Master logs as Procedures transition from start, through each of the Procedure's various stages to finish. Some Procedures spawn sub-procedures, wait on their Children, and then themselves finish. Each child logs its pid but also its ppid; its parent's pid.

Generally all runs problem free but if some unforeseen circumstance arises, the assignment framework may sustain damage requiring operator intervention. Below we will discuss some such scenarios but they can manifest in the Master log as a Region being STUCK or a Procedure transitioning an entity -- a Region or a Table -- may be blocked because another Procedure holds the exclusive lock and is not letting go.

STUCK Procedures look like this:2018-09-12 15:29:06,558 WARN org.apache.hadoop.hbase.master.assignment.AssignmentManager: STUCK Region-In-Transition rit=OPENING, location=va1001.example.org,22101,1536173230599, table=IntegrationTestBigLinkedList_20180626110336, region=dbdb56242f17610c46ea044f7a42895b/master-status#tables

This section about midway down in Master UI home-page shows a list of tables with columns for whether the table is ENABLED, ENABLING, DISABLING, or DISABLED among other attributes. Also listed are columns with counts of Regions in their various transition states: OPEN, CLOSED, etc. A read of this table is good for figuring if the Regions of this table have a proper disposition. For example if a table is ENABLED and there are Regions that are not in the OPEN state and the Master Log is silent about any ongoing assigns, then something is amiss.

Procedures & Locks

This page off the Master UI home page under the Procedures & Locks menu item in the page heading lists all ongoing Procedures and Locks as well as the current set of Master Procedure WALs (named pv2-0000000000000000###.log under the MasterProcWALs directory in your hbase install). On startup, on a large cluster when furious assigning is afoot, this page is filled with lists of Procedures and Locks. The count of MasterProcWALs will bloat too. If after the cluster settles, there is a stuck Lock or Procedure or the count of WALs doesn't ever come down but only grows, then operator intervention is needed to alieve the blockage.

Lists of locks and procedures can also be obtained via the hbase shell:$ echo "list_locks"| hbase shell &> /tmp/locks.txt $ echo "list_procedures"| hbase shell &> /tmp/procedures.txtThe HBase Canary Tool

The Canary tool is useful verifying the state of assign. It can be run with a table focus or against the whole cluster.

For example, to check cluster assigns:$ hbase canary -f false -t 6000000 &>/tmp/canary.logThe -f false tells the Canary to keep going across failed Region fetches and the -t 6000000 tells the Canary run for ~two hours maximum. When done, check out /tmp/canary.log. Grep for ERROR lines to find problematic Region assigns.

You can do a probe like the Canary's in the hbase shell. For example, given a Region that has a start row of d1dddd0cbelonging to the table testtable, do as follows:hbase> scan 'testtable', {STARTROW => 'd1dddd0c', LIMIT => 10}For an overview on parsing a Region name into its constituent parts, see RegionInfo API.

Other Tools

To figure the list of Regions that are not OPEN on an ENABLED or ENABLING table, read the hbase:meta table info:state column. For example, to find the state of all Regions in the table IntegrationTestBigLinkedList_20180626064758, do the following:$ echo " scan 'hbase:meta', {ROWPREFIXFILTER => 'IntegrationTestBigLinkedList_20180626064758,', COLUMN => 'info:state'}"| hbase shell > /tmp/t.txt...then grep for OPENING or CLOSING Regions.

To move an OPENING issue to OPEN so it agrees with a table's ENABLED state, use the assign command in the hbase shell to queue a new Assign Procedure (watch the Master logs to see the Assign run). If many Regions to assign, use the HBCK2 tool. It can do bulk assigning.

Fixing

General principals include a Region can not be assigned if it is in CLOSING state (or the inverse, unassigned if in OPENINGstate) without first transitioning via CLOSED: Regions must always move from CLOSED, to OPENING, to OPEN, and then to CLOSING, CLOSED.

When making repair, do fixup a table at a time.

Also, if a table is DISABLED, you cannot assign a Region. In the Master logs, you will see that the Master will report that the assign has been skipped because the table is DISABLED. You may want to assign a Region because it is currently in the OPENING state and you want it in the CLOSED state so it agrees with the table's DISABLED state. In this situation, you may have to temporarily set the table status to ENABLED, just so you can do the assign, and then set it back again after the unassign.HBCK2 has facility to allow you do this. See the HBCK2 usage output.

Start-over

At an extreme, if the Master is distraught and all attempts at fixup only turn up undoable locks or Procedures that won't finish, and/or the set of MasterProcWALs is growing without bound, it is possible to wipe the Master state clean. Just move aside the/hbase/MasterProcWALs/ directory under your hbase install and restart the Master process. It will come back as a tabula rasawithout memory of the bad times past.

If at the time of the erasure, all Regions were happily assigned or offlined, then on Master restart, the Master should pick up and continue as though nothing happened. But if there were Regions-In-Transition at the time, then the operator may have to intervene to bring outstanding assigns/unassigns to their terminal point. Read the hbase:meta info:state columns as described above to figure what needs assigning/unassigning. Having erased all history moving aside the MasterProcWALs, none of the entities should be locked so you are free to bulk assign/unassign.

Assigning/Unassigning

Generally, on assign, the Master will persist until successful. An assign takes an exclusive lock on the Region. This precludes a concurrent assign or unassign from running. An assign against a locked Region will wait until the lock is released before making progress. See the [Procedures & Locks] section above for current list of outstanding Locks.

Master startup cannot progress, in holding-pattern until region onlined

This should never happen. If it does, here is what it looks like:2018-10-01 22:07:42,792 WARN org.apache.hadoop.hbase.master.HMaster: hbase:meta,,1.1588230740 is NOT online; state={1588230740 state=CLOSING, ts=1538456302300, server=ve1017.example.org,22101,1538449648131}; ServerCrashProcedures=true. Master startup cannot progress, in holding-pattern until region onlined.The Master is unable to continue startup because there is no Procedure to assign hbase:meta (or hbase:namespace). To inject one, use the HBCK2 tool:HBASE_CLASSPATH_PREFIX=./hbase-hbck2-1.0.0-SNAPSHOT.jar hbase org.apache.hbase.HBCK2 assigns 1588230740...where 1588230740 is the encoded name of the hbase:meta Region.

The same may happen to the hbase:namespace system table. Look for the encoded Region name of the hbase:namespaceRegion and do similar to what we did for hbase:meta.
 

HBase在滴滴出行的应用场景和最佳实践

hbase过往记忆 发表了文章 • 1 个评论 • 574 次浏览 • 2018-10-23 10:27 • 来自相关话题

作者简介:李扬,滴滴出行资深软件开发工程师。2015年加入滴滴出行基础平台部,主要负责HBase和Phoenix以及相关分布式存储技术。在滴滴之前,曾在新浪担任数据工程师,专注于分布式计算和存储。 
责编:郭芮(guorui@csdn.net),关注大数据领域。 
 背景
对接业务类型

HBase是建立在Hadoop生态之上的Database,源生对离线任务支持友好,又因为LSM树是一个优秀的高吞吐数据库结构,所以同时也对接了很多线上业务。在线业务对访问延迟敏感,并且访问趋向于随机,如订单、客服轨迹查询。离线业务通常是数仓的定时大批量处理任务,对一段时间内的数据进行处理并产出结果,对任务完成的时间要求不是非常敏感,并且处理逻辑复杂,如天级别报表、安全和用户行为分析、模型训练等。

多语言支持

HBase提供了多语言解决方案,并且由于滴滴各业务线RD所使用的开发语言各有偏好,所以多语言支持对于HBase在滴滴内部的发展是至关重要的一部分。我们对用户提供了多种语言的访问方式:HBase Java native API、Thrift Server(主要应用于C++、PHP、Python)、JAVA JDBC(Phoenix JDBC)、Phoenix QueryServer(Phoenix对外提供的多语言解决方案)、MapReduce Job(Htable/Hfile Input)、Spark Job、Streaming等。

数据类型

HBase在滴滴主要存放了以下四种数据类型:
 
统计结果、报表类数据:主要是运营、运力情况、收入等结果,通常需要配合Phoenix进行SQL查询。数据量较小,对查询的灵活性要求高,延迟要求一般。原始事实类数据:如订单、司机乘客的GPS轨迹、日志等,主要用作在线和离线的数据供给。数据量大,对一致性和可用性要求高,延迟敏感,实时写入,单点或批量查询。中间结果数据:指模型训练所需要的数据等。数据量大,可用性和一致性要求一般,对批量查询时的吞吐量要求高。线上系统的备份数据:用户把原始数据存在了其他关系数据库或文件服务,把HBase作为一个异地容灾的方案。
 使用场景介绍
场景一:订单事件

这份数据使用过滴滴产品的用户应该都接触过,就是App上的历史订单。近期订单的查询会落在Redis,超过一定时间范围,或者当Redis不可用时,查询会落在HBase上。业务方的需求如下:
 
在线查询订单生命周期的各个状态,包括status、event_type、order_detail等信息。主要的查询来自于客服系统。在线历史订单详情查询。上层会有Redis来存储近期的订单,当Redis不可用或者查询范围超出Redis,查询会直接落到HBase。离线对订单的状态进行分析。写入满足每秒10K的事件,读取满足每秒1K的事件,数据要求在5s内可用。





按照这些要求,我们对Rowkey做出了下面的设计,都是很典型的scan场景。

订单状态表

Rowkey:reverse(order_id) + (MAX_LONG - TS) 
Columns:该订单各种状态

订单历史表

Rowkey:reverse(passenger_id | driver_id) + (MAX_LONG - TS) 
Columns:用户在时间范围内的订单及其他信息

场景二:司机乘客轨迹

这也是一份滴滴用户关系密切的数据,线上用户、滴滴的各个业务线和分析人员都会使用。举几个使用场景上的例子:用户查看历史订单时,地图上显示所经过的路线;发生司乘纠纷,客服调用订单轨迹复现场景;地图部门用户分析道路拥堵情况。





用户们提出的需求:
 
满足App用户或者后端分析人员的实时或准实时轨迹坐标查询;满足离线大规模的轨迹分析;满足给出一个指定的地理范围,取出范围内所有用户的轨迹或范围内出现过的用户。
 其中,关于第三个需求,地理位置查询,我们知道MongoDB对于这种地理索引有源生的支持,但是在滴滴这种量级的情况下可能会发生存储瓶颈,HBase存储和扩展性上没有压力但是没有内置类似MongoDB地理位置索引的功能,没有就需要我们自己实现。通过调研,了解到关于地理索引有一套比较通用的GeohHash算法 。

GeoHash是将二维的经纬度转换成字符串,每一个字符串代表了某一矩形区域。也就是说,这个矩形区域内所有的点(经纬度坐标)都共享相同的GeoHash字符串,比如说我在悠唐酒店,我的一个朋友在旁边的悠唐购物广场,我们的经纬度点会得到相同的GeoHash串。这样既可以保护隐私(只表示大概区域位置而不是具体的点),又比较容易做缓存。





但是我们要查询的范围和GeohHash块可能不会完全重合。以圆形为例,查询时会出现如图4所示的一半在GeoHash块内,一半在外面的情况(如A、B、C、D、E、F、G等点)。这种情况就需要对GeoHash块内每个真实的GPS点进行第二次的过滤,通过原始的GPS点和圆心之间的距离,过滤掉不符合查询条件的数据。





最后依据这个原理,把GeoHash和其他一些需要被索引的维度拼装成Rowkey,真实的GPS点为Value,在这个基础上封装成客户端,并且在客户端内部对查询逻辑和查询策略做出速度上的大幅优化,这样就把HBase变成了一个MongoDB一样支持地理位置索引的数据库。如果查询范围非常大(比如进行省级别的分析),还额外提供了MR的获取数据的入口。

两种查询场景的Rowkey设计如下:
 
单个用户按订单或时间段查询: reverse(user_id) + (Integer.MAX_LONG-TS/1000)给定范围内的轨迹查询:reverse(geohash) + ts/1000 + user_id
 场景三:ETA

ETA是指每次选好起始和目的地后,提示出的预估时间和价格。提示的预估到达时间和价格,最初版本是离线方式运行,后来改版通过HBase实现实时效果,把HBase当成一个KeyValue缓存,带来了减少训练时间、可多城市并行、减少人工干预的好处。 
整个ETA的过程如下:
 
模型训练通过Spark Job,每30分钟对各个城市训练一次;模型训练第一阶段,在5分钟内,按照设定条件从HBase读取所有城市数据;模型训练第二阶段在25分钟内完成ETA的计算;HBase中的数据每隔一段时间会持久化至HDFS中,供新模型测试和新的特征提取。
 Rowkey:salting+cited+type0+type1+type2+TS 
Column:order, feature





场景四:监控工具DCM

用于监控Hadoop集群的资源使用(Namenode,Yarn container使用等),关系数据库在时间维度过程以后会产生各种性能问题,同时我们又希望可以通过SQL做一些分析查询,所以使用Phoenix,使用采集程序定时录入数据,生产成报表,存入HBase,可以在秒级别返回查询结果,最后在前端做展示。





图7、图8、图9是几张监控工具的用户UI,数字相关的部分做了模糊处理。















滴滴在HBase对多租户的管理
我们认为单集群多租户是最高效和节省精力的方案,但是由于HBase对多租户基本没有管理,使用上会遇到很多问题:在用户方面比如对资源使用情况不做分析、存储总量发生变化后不做调整和通知、项目上线下线没有计划、想要最多的资源和权限等;我们平台管理者也会遇到比如线上沟通难以理解用户的业务、对每个接入HBase的项目状态不清楚、不能判断出用户的需求是否合理、多租户在集群上发生资源竞争、问题定位和排查时间长等。

针对这些问题,我们开发了DHS系统(Didi HBase Service)进行项目管理,并且在HBase上通过Namespace、RS Group等技术来分割用户的资源、数据和权限。通过计算开销并计费的方法来管控资源分配。





DHS主要有下面几个模块和功能:
 
项目生命周期管理:包括立项、资源预估和申请、项目需求调整、需求讨论;用户管理:权限管理,项目审批;集群资源管理;表级别的使用情况监控:主要是读写监控、memstore、blockcache、locality。
 当用户有使用HBase存储的需求,我们会让用户在DHS上注册项目。介绍业务的场景和产品相关的细节,以及是否有高SLA要求。

之后是新建表以及对表性能需求预估,我们要求用户对自己要使用的资源有一个准确的预估。如果用户难以估计,我们会以线上或者线下讨论的方式与用户讨论帮助确定这些信息。 
然后会生成项目概览页面,方便管理员和用户进行项目进展的跟踪。

HBase自带的jxm信息会汇总到Region和RegionServer级别的数据,管理员会经常用到,但是用户却很少关注这个级别。根据这种情况我们开发了HBase表级别的监控,并且会有权限控制,让业务RD只能看到和自己相关的表,清楚自己项目表的吞吐及存储占用情况。

通过DHS让用户明确自己使用资源情况的基础之上,我们使用了RS Group技术,把一个集群分成多个逻辑子集群,可以让用户选择独占或者共享资源。共享和独占各有自己的优缺点,如表1。





根据以上的情况,我们在资源分配上会根据业务的特性来选择不同方案:
 
对于访问延迟要求低、访问量小、可用性要求低、备份或者测试阶段的数据:使用共享资源池;对于延迟敏感、吞吐要求高、高峰时段访问量大、可用性要求高、在线业务:让其独占一定机器数量构成的RegionServer Group资源,并且按用户预估的资源量,额外给出20%~30%的余量。
 最后我们会根据用户对资源的使用,定期计算开销并向用户发出账单。

RS Group
RegionServer Group,实现细节可以参照HBase HBASE-6721这个Patch。滴滴在这个基础上作了一些分配策略上的优化,以便适合滴滴业务场景的修改。RS Group简单概括是指通过分配一批指定的RegionServer列表,成为一个RS Group,每个Group可以按需挂载不同的表,并且当Group内的表发生异常后,Region不会迁移到其他的Group。这样,每个Group就相当于一个逻辑上的子集群,通过这种方式达到资源隔离的效果,降低管理成本,不必为每个高SLA的业务线单独搭集群。





总结
在滴滴推广和实践HBase的工作中,我们认为至关重要的两点是帮助用户做出良好的表结构设计和资源的控制。有了这两个前提之后,后续出现问题的概率会大大降低。良好的表结构设计需要用户对HBase的实现有一个清晰的认识,大多数业务用户把更多精力放在了业务逻辑上,对架构实现知之甚少,这就需要平台管理者去不断帮助和引导,有了好的开端和成功案例后,通过这些用户再去向其他的业务方推广。资源隔离控制则帮助我们有效减少集群的数量,降低运维成本,让平台管理者从多集群无止尽的管理工作中解放出来,将更多精力投入到组件社区跟进和平台管理系统的研发工作中,使业务和平台都进入一个良性循环,提升用户的使用体验,更好地支持公司业务的发展。
  查看全部
作者简介:李扬,滴滴出行资深软件开发工程师。2015年加入滴滴出行基础平台部,主要负责HBase和Phoenix以及相关分布式存储技术。在滴滴之前,曾在新浪担任数据工程师,专注于分布式计算和存储。 
责编:郭芮(guorui@csdn.net),关注大数据领域。 
 背景
对接业务类型

HBase是建立在Hadoop生态之上的Database,源生对离线任务支持友好,又因为LSM树是一个优秀的高吞吐数据库结构,所以同时也对接了很多线上业务。在线业务对访问延迟敏感,并且访问趋向于随机,如订单、客服轨迹查询。离线业务通常是数仓的定时大批量处理任务,对一段时间内的数据进行处理并产出结果,对任务完成的时间要求不是非常敏感,并且处理逻辑复杂,如天级别报表、安全和用户行为分析、模型训练等。

多语言支持

HBase提供了多语言解决方案,并且由于滴滴各业务线RD所使用的开发语言各有偏好,所以多语言支持对于HBase在滴滴内部的发展是至关重要的一部分。我们对用户提供了多种语言的访问方式:HBase Java native API、Thrift Server(主要应用于C++、PHP、Python)、JAVA JDBC(Phoenix JDBC)、Phoenix QueryServer(Phoenix对外提供的多语言解决方案)、MapReduce Job(Htable/Hfile Input)、Spark Job、Streaming等。

数据类型

HBase在滴滴主要存放了以下四种数据类型:
 
  • 统计结果、报表类数据:主要是运营、运力情况、收入等结果,通常需要配合Phoenix进行SQL查询。数据量较小,对查询的灵活性要求高,延迟要求一般。
  • 原始事实类数据:如订单、司机乘客的GPS轨迹、日志等,主要用作在线和离线的数据供给。数据量大,对一致性和可用性要求高,延迟敏感,实时写入,单点或批量查询。
  • 中间结果数据:指模型训练所需要的数据等。数据量大,可用性和一致性要求一般,对批量查询时的吞吐量要求高。
  • 线上系统的备份数据:用户把原始数据存在了其他关系数据库或文件服务,把HBase作为一个异地容灾的方案。

 使用场景介绍
场景一:订单事件

这份数据使用过滴滴产品的用户应该都接触过,就是App上的历史订单。近期订单的查询会落在Redis,超过一定时间范围,或者当Redis不可用时,查询会落在HBase上。业务方的需求如下:
 
  • 在线查询订单生命周期的各个状态,包括status、event_type、order_detail等信息。主要的查询来自于客服系统。
  • 在线历史订单详情查询。上层会有Redis来存储近期的订单,当Redis不可用或者查询范围超出Redis,查询会直接落到HBase。
  • 离线对订单的状态进行分析。
  • 写入满足每秒10K的事件,读取满足每秒1K的事件,数据要求在5s内可用。


592fd58ff297e.png

按照这些要求,我们对Rowkey做出了下面的设计,都是很典型的scan场景。

订单状态表

Rowkey:reverse(order_id) + (MAX_LONG - TS) 
Columns:该订单各种状态

订单历史表

Rowkey:reverse(passenger_id | driver_id) + (MAX_LONG - TS) 
Columns:用户在时间范围内的订单及其他信息

场景二:司机乘客轨迹

这也是一份滴滴用户关系密切的数据,线上用户、滴滴的各个业务线和分析人员都会使用。举几个使用场景上的例子:用户查看历史订单时,地图上显示所经过的路线;发生司乘纠纷,客服调用订单轨迹复现场景;地图部门用户分析道路拥堵情况。

592fd62ec3aa9.png

用户们提出的需求:
 
  • 满足App用户或者后端分析人员的实时或准实时轨迹坐标查询;
  • 满足离线大规模的轨迹分析;
  • 满足给出一个指定的地理范围,取出范围内所有用户的轨迹或范围内出现过的用户。

 其中,关于第三个需求,地理位置查询,我们知道MongoDB对于这种地理索引有源生的支持,但是在滴滴这种量级的情况下可能会发生存储瓶颈,HBase存储和扩展性上没有压力但是没有内置类似MongoDB地理位置索引的功能,没有就需要我们自己实现。通过调研,了解到关于地理索引有一套比较通用的GeohHash算法 。

GeoHash是将二维的经纬度转换成字符串,每一个字符串代表了某一矩形区域。也就是说,这个矩形区域内所有的点(经纬度坐标)都共享相同的GeoHash字符串,比如说我在悠唐酒店,我的一个朋友在旁边的悠唐购物广场,我们的经纬度点会得到相同的GeoHash串。这样既可以保护隐私(只表示大概区域位置而不是具体的点),又比较容易做缓存。

592fd6715b13f.png

但是我们要查询的范围和GeohHash块可能不会完全重合。以圆形为例,查询时会出现如图4所示的一半在GeoHash块内,一半在外面的情况(如A、B、C、D、E、F、G等点)。这种情况就需要对GeoHash块内每个真实的GPS点进行第二次的过滤,通过原始的GPS点和圆心之间的距离,过滤掉不符合查询条件的数据。

592fd6ba65517.png

最后依据这个原理,把GeoHash和其他一些需要被索引的维度拼装成Rowkey,真实的GPS点为Value,在这个基础上封装成客户端,并且在客户端内部对查询逻辑和查询策略做出速度上的大幅优化,这样就把HBase变成了一个MongoDB一样支持地理位置索引的数据库。如果查询范围非常大(比如进行省级别的分析),还额外提供了MR的获取数据的入口。

两种查询场景的Rowkey设计如下:
 
  • 单个用户按订单或时间段查询: reverse(user_id) + (Integer.MAX_LONG-TS/1000)
  • 给定范围内的轨迹查询:reverse(geohash) + ts/1000 + user_id

 场景三:ETA

ETA是指每次选好起始和目的地后,提示出的预估时间和价格。提示的预估到达时间和价格,最初版本是离线方式运行,后来改版通过HBase实现实时效果,把HBase当成一个KeyValue缓存,带来了减少训练时间、可多城市并行、减少人工干预的好处。 
整个ETA的过程如下:
 
  • 模型训练通过Spark Job,每30分钟对各个城市训练一次;
  • 模型训练第一阶段,在5分钟内,按照设定条件从HBase读取所有城市数据;
  • 模型训练第二阶段在25分钟内完成ETA的计算;
  • HBase中的数据每隔一段时间会持久化至HDFS中,供新模型测试和新的特征提取。

 Rowkey:salting+cited+type0+type1+type2+TS 
Column:order, feature

592fd708c4246.png

场景四:监控工具DCM

用于监控Hadoop集群的资源使用(Namenode,Yarn container使用等),关系数据库在时间维度过程以后会产生各种性能问题,同时我们又希望可以通过SQL做一些分析查询,所以使用Phoenix,使用采集程序定时录入数据,生产成报表,存入HBase,可以在秒级别返回查询结果,最后在前端做展示。

592fd7375c265.png

图7、图8、图9是几张监控工具的用户UI,数字相关的部分做了模糊处理。

592fd7610405d.png


592fd77d8ebbe.png


592fd7997641f.png

滴滴在HBase对多租户的管理
我们认为单集群多租户是最高效和节省精力的方案,但是由于HBase对多租户基本没有管理,使用上会遇到很多问题:在用户方面比如对资源使用情况不做分析、存储总量发生变化后不做调整和通知、项目上线下线没有计划、想要最多的资源和权限等;我们平台管理者也会遇到比如线上沟通难以理解用户的业务、对每个接入HBase的项目状态不清楚、不能判断出用户的需求是否合理、多租户在集群上发生资源竞争、问题定位和排查时间长等。

针对这些问题,我们开发了DHS系统(Didi HBase Service)进行项目管理,并且在HBase上通过Namespace、RS Group等技术来分割用户的资源、数据和权限。通过计算开销并计费的方法来管控资源分配。

592fd7f8d4da8.png

DHS主要有下面几个模块和功能:
 
  • 项目生命周期管理:包括立项、资源预估和申请、项目需求调整、需求讨论;
  • 用户管理:权限管理,项目审批;
  • 集群资源管理;
  • 表级别的使用情况监控:主要是读写监控、memstore、blockcache、locality。

 当用户有使用HBase存储的需求,我们会让用户在DHS上注册项目。介绍业务的场景和产品相关的细节,以及是否有高SLA要求。

之后是新建表以及对表性能需求预估,我们要求用户对自己要使用的资源有一个准确的预估。如果用户难以估计,我们会以线上或者线下讨论的方式与用户讨论帮助确定这些信息。 
然后会生成项目概览页面,方便管理员和用户进行项目进展的跟踪。

HBase自带的jxm信息会汇总到Region和RegionServer级别的数据,管理员会经常用到,但是用户却很少关注这个级别。根据这种情况我们开发了HBase表级别的监控,并且会有权限控制,让业务RD只能看到和自己相关的表,清楚自己项目表的吞吐及存储占用情况。

通过DHS让用户明确自己使用资源情况的基础之上,我们使用了RS Group技术,把一个集群分成多个逻辑子集群,可以让用户选择独占或者共享资源。共享和独占各有自己的优缺点,如表1。

592fd853ebaac.png

根据以上的情况,我们在资源分配上会根据业务的特性来选择不同方案:
 
  • 对于访问延迟要求低、访问量小、可用性要求低、备份或者测试阶段的数据:使用共享资源池;
  • 对于延迟敏感、吞吐要求高、高峰时段访问量大、可用性要求高、在线业务:让其独占一定机器数量构成的RegionServer Group资源,并且按用户预估的资源量,额外给出20%~30%的余量。

 最后我们会根据用户对资源的使用,定期计算开销并向用户发出账单。

RS Group
RegionServer Group,实现细节可以参照HBase HBASE-6721这个Patch。滴滴在这个基础上作了一些分配策略上的优化,以便适合滴滴业务场景的修改。RS Group简单概括是指通过分配一批指定的RegionServer列表,成为一个RS Group,每个Group可以按需挂载不同的表,并且当Group内的表发生异常后,Region不会迁移到其他的Group。这样,每个Group就相当于一个逻辑上的子集群,通过这种方式达到资源隔离的效果,降低管理成本,不必为每个高SLA的业务线单独搭集群。

592fd8a900ab5.png

总结
在滴滴推广和实践HBase的工作中,我们认为至关重要的两点是帮助用户做出良好的表结构设计和资源的控制。有了这两个前提之后,后续出现问题的概率会大大降低。良好的表结构设计需要用户对HBase的实现有一个清晰的认识,大多数业务用户把更多精力放在了业务逻辑上,对架构实现知之甚少,这就需要平台管理者去不断帮助和引导,有了好的开端和成功案例后,通过这些用户再去向其他的业务方推广。资源隔离控制则帮助我们有效减少集群的数量,降低运维成本,让平台管理者从多集群无止尽的管理工作中解放出来,将更多精力投入到组件社区跟进和平台管理系统的研发工作中,使业务和平台都进入一个良性循环,提升用户的使用体验,更好地支持公司业务的发展。
 

连接HBase的正确姿势

hbasehbasegroup 发表了文章 • 1 个评论 • 1767 次浏览 • 2018-10-01 17:42 • 来自相关话题

在云HBase值班的时候,经常会遇见有用户咨询诸如“HBase是否支持连接池?”这样的问题,也有用户因为应用中创建的Connection对象过多,触发Zookeeper的连接数限制,导致客户端连不上的。究其原因,都是因为对HBase客户端的原理不了解造成的。本文介绍HBase客户端的Connection对象与Socket连接的关系并且给出Connection的正确用法。
Connection是什么?
在云HBase用户中,常见的使用Connection的错误方法有:
自己实现一个Connection对象的资源池,每次使用都从资源池中取出一个Connection对象;每个线程一个Connection对象。每次访问HBase的时候临时创建一个Connection对象,使用完之后调用close关闭连接。
从这些做法来看,这些用户显然是把Connection对象当成了单机数据库里面的连接对象来用了。然而作为分布式数据库,HBase客户端需要和多个服务器中的不同服务角色建立连接,所以HBase客户端中的Connection对象并不是简单对应一个socket连接。HBase的API文档当中对Connection的定义是:
A cluster connection encapsulating lower level individual connections to actual servers and a connection to zookeeper.
我们知道,HBase客户端要连接三个不同的服务角色:
Zookeeper:主要用于获得meta-region位置,集群Id、master等信息。HBase Master:主要用于执行HBaseAdmin接口的一些操作,例如建表等。HBase RegionServer:用于读、写数据。
下图简单示意了客户端与服务器交互的步骤:



HBase客户端的Connection包含了对以上三种Socket连接的封装。Connection对象和实际的Socket连接之间的对应关系如下图:



HBase客户端代码真正对应Socket连接的是RpcConnection对象。HBase使用PoolMap这种数据结构来存储客户端到HBase服务器之间的连接。PoolMap封装ConcurrentHashMap的结构,key是ConnectionId(封装服务器地址和用户ticket),value是一个RpcConnection对象的资源池。当HBase需要连接一个服务器时,首先会根据ConnectionId找到对应的连接池,然后从连接池中取出一个连接对象。
HBase提供三种资源池的实现,分别是Reusable,RoundRobin和ThreadLocal。具体实现可以通过hbase.client.ipc.pool.type配置项指定,默认为Reusable。连接池的大小也可以通过hbase.client.ipc.pool.size配置项指定,默认为1。
连接HBase的正确姿势
从以上分析不难得出,在HBase中Connection类已经实现对连接的管理功能,所以不需要在Connection之上再做额外的管理。另外,Connection是线程安全的,然而Table和Admin则不是线程安全的,因此正确的做法是一个进程共用一个Connection对象,而在不同的线程中使用单独的Table和Admin对象。//所有进程共用一个Connection对象
connection=ConnectionFactory.createConnection(config);
...
//每个线程使用单独的Table对象
Table table = connection.getTable(TableName.valueOf("test"));
try {
...
} finally {
table.close();
}HBase客户端默认的是连接池大小是1,也就是每个RegionServer 1个连接。如果应用需要使用更大的连接池或指定其他的资源池类型,也可以通过修改配置实现: 
Connection源码解析
Connection创建RpcClient的核心入口:/**
* constructor
* @param conf Configuration object
*/
ConnectionImplementation(Configuration conf,
ExecutorService pool, User user) throws IOException {
...
try {
...
this.rpcClient = RpcClientFactory.createClient(this.conf, this.clusterId, this.metrics);
...
} catch (Throwable e) {
// avoid leaks: registry, rpcClient, ...
LOG.debug("connection construction failed", e);
close();
throw e;
}
}RpcClient使用PoolMap数据结构存储客户端到HBase服务器之间的连接映射,PoolMap封装ConcurrentHashMap结构,其中key是ConnectionId[new ConnectionId(ticket, md.getService().getName(), addr)],value是RpcConnection对象的资源池。protected final PoolMap<ConnectionId, T> connections;

/**
* Construct an IPC client for the cluster <code>clusterId</code>
* @param conf configuration
* @param clusterId the cluster id
* @param localAddr client socket bind address.
* @param metrics the connection metrics
*/
public AbstractRpcClient(Configuration conf, String clusterId, SocketAddress localAddr,
MetricsConnection metrics) {
...
this.connections = new PoolMap<>(getPoolType(conf), getPoolSize(conf));
...
}当HBase需要连接一个服务器时,首先会根据ConnectionId找到对应的连接池,然后从连接池中取出一个连接对象,获取连接的核心实现:/**
* Get a connection from the pool, or create a new one and add it to the pool. Connections to a
* given host/port are reused.
*/
private T getConnection(ConnectionId remoteId) throws IOException {
if (failedServers.isFailedServer(remoteId.getAddress())) {
if (LOG.isDebugEnabled()) {
LOG.debug("Not trying to connect to " + remoteId.address
+ " this server is in the failed servers list");
}
throw new FailedServerException(
"This server is in the failed servers list: " + remoteId.address);
}
T conn;
synchronized (connections) {
if (!running) {
throw new StoppedRpcClientException();
}
conn = connections.get(remoteId);
if (conn == null) {
conn = createConnection(remoteId);
connections.put(remoteId, conn);
}
conn.setLastTouched(EnvironmentEdgeManager.currentTime());
}
return conn;
}连接池根据ConnectionId获取不到连接则创建RpcConnection的具体实现:protected NettyRpcConnection createConnection(ConnectionId remoteId) throws IOException {
return new NettyRpcConnection(this, remoteId);
}

NettyRpcConnection(NettyRpcClient rpcClient, ConnectionId remoteId) throws IOException {
super(rpcClient.conf, AbstractRpcClient.WHEEL_TIMER, remoteId, rpcClient.clusterId,
rpcClient.userProvider.isHBaseSecurityEnabled(), rpcClient.codec, rpcClient.compressor);
this.rpcClient = rpcClient;
byte connectionHeaderPreamble = getConnectionHeaderPreamble();
this.connectionHeaderPreamble =
Unpooled.directBuffer(connectionHeaderPreamble.length).writeBytes(connectionHeaderPreamble);
ConnectionHeader header = getConnectionHeader();
this.connectionHeaderWithLength = Unpooled.directBuffer(4 + header.getSerializedSize());
this.connectionHeaderWithLength.writeInt(header.getSerializedSize());
header.writeTo(new ByteBufOutputStream(this.connectionHeaderWithLength));
}

protected RpcConnection(Configuration conf, HashedWheelTimer timeoutTimer, ConnectionId remoteId,
String clusterId, boolean isSecurityEnabled, Codec codec, CompressionCodec compressor)
throws IOException {
if (remoteId.getAddress().isUnresolved()) {
throw new UnknownHostException("unknown host: " + remoteId.getAddress().getHostName());
}
this.timeoutTimer = timeoutTimer;
this.codec = codec;
this.compressor = compressor;
this.conf = conf;

UserGroupInformation ticket = remoteId.getTicket().getUGI();
SecurityInfo securityInfo = SecurityInfo.getInfo(remoteId.getServiceName());
this.useSasl = isSecurityEnabled;
Token<? extends TokenIdentifier> token = null;
String serverPrincipal = null;
if (useSasl && securityInfo != null) {
AuthenticationProtos.TokenIdentifier.Kind tokenKind = securityInfo.getTokenKind();
if (tokenKind != null) {
TokenSelector<? extends TokenIdentifier> tokenSelector = AbstractRpcClient.TOKEN_HANDLERS
.get(tokenKind);
if (tokenSelector != null) {
token = tokenSelector.selectToken(new Text(clusterId), ticket.getTokens());
} else if (LOG.isDebugEnabled()) {
LOG.debug("No token selector found for type " + tokenKind);
}
}
String serverKey = securityInfo.getServerPrincipal();
if (serverKey == null) {
throw new IOException("Can't obtain server Kerberos config key from SecurityInfo");
}
serverPrincipal = SecurityUtil.getServerPrincipal(conf.get(serverKey),
remoteId.address.getAddress().getCanonicalHostName().toLowerCase());
if (LOG.isDebugEnabled()) {
LOG.debug("RPC Server Kerberos principal name for service=" + remoteId.getServiceName()
+ " is " + serverPrincipal);
}
}
this.token = token;
this.serverPrincipal = serverPrincipal;
if (!useSasl) {
authMethod = AuthMethod.SIMPLE;
} else if (token != null) {
authMethod = AuthMethod.DIGEST;
} else {
authMethod = AuthMethod.KERBEROS;
}

// Log if debug AND non-default auth, else if trace enabled.
// No point logging obvious.
if ((LOG.isDebugEnabled() && !authMethod.equals(AuthMethod.SIMPLE)) ||
LOG.isTraceEnabled()) {
// Only log if not default auth.
LOG.debug("Use " + authMethod + " authentication for service " + remoteId.serviceName
+ ", sasl=" + useSasl);
}
reloginMaxBackoff = conf.getInt("hbase.security.relogin.maxbackoff", 5000);
this.remoteId = remoteId;
} 查看全部
在云HBase值班的时候,经常会遇见有用户咨询诸如“HBase是否支持连接池?”这样的问题,也有用户因为应用中创建的Connection对象过多,触发Zookeeper的连接数限制,导致客户端连不上的。究其原因,都是因为对HBase客户端的原理不了解造成的。本文介绍HBase客户端的Connection对象与Socket连接的关系并且给出Connection的正确用法。
Connection是什么?
在云HBase用户中,常见的使用Connection的错误方法有:
  1. 自己实现一个Connection对象的资源池,每次使用都从资源池中取出一个Connection对象;
  2. 每个线程一个Connection对象。
  3. 每次访问HBase的时候临时创建一个Connection对象,使用完之后调用close关闭连接。

从这些做法来看,这些用户显然是把Connection对象当成了单机数据库里面的连接对象来用了。然而作为分布式数据库,HBase客户端需要和多个服务器中的不同服务角色建立连接,所以HBase客户端中的Connection对象并不是简单对应一个socket连接。HBase的API文档当中对Connection的定义是:
A cluster connection encapsulating lower level individual connections to actual servers and a connection to zookeeper.
我们知道,HBase客户端要连接三个不同的服务角色:
  1. Zookeeper:主要用于获得meta-region位置,集群Id、master等信息。
  2. HBase Master:主要用于执行HBaseAdmin接口的一些操作,例如建表等。
  3. HBase RegionServer:用于读、写数据。

下图简单示意了客户端与服务器交互的步骤:
客户端与服务器交互的步骤.png
HBase客户端的Connection包含了对以上三种Socket连接的封装。Connection对象和实际的Socket连接之间的对应关系如下图:
Connection对象与Socket连接之间的对应关系.png
HBase客户端代码真正对应Socket连接的是RpcConnection对象。HBase使用PoolMap这种数据结构来存储客户端到HBase服务器之间的连接。PoolMap封装ConcurrentHashMap的结构,key是ConnectionId(封装服务器地址和用户ticket),value是一个RpcConnection对象的资源池。当HBase需要连接一个服务器时,首先会根据ConnectionId找到对应的连接池,然后从连接池中取出一个连接对象。
HBase提供三种资源池的实现,分别是Reusable,RoundRobin和ThreadLocal。具体实现可以通过hbase.client.ipc.pool.type配置项指定,默认为Reusable。连接池的大小也可以通过hbase.client.ipc.pool.size配置项指定,默认为1。
连接HBase的正确姿势
从以上分析不难得出,在HBase中Connection类已经实现对连接的管理功能,所以不需要在Connection之上再做额外的管理。另外,Connection是线程安全的,然而Table和Admin则不是线程安全的,因此正确的做法是一个进程共用一个Connection对象,而在不同的线程中使用单独的Table和Admin对象。
//所有进程共用一个Connection对象
connection=ConnectionFactory.createConnection(config);
...
//每个线程使用单独的Table对象
Table table = connection.getTable(TableName.valueOf("test"));
try {
...
} finally {
table.close();
}
HBase客户端默认的是连接池大小是1,也就是每个RegionServer 1个连接。如果应用需要使用更大的连接池或指定其他的资源池类型,也可以通过修改配置实现: 
Connection源码解析
Connection创建RpcClient的核心入口:
/**
* constructor
* @param conf Configuration object
*/
ConnectionImplementation(Configuration conf,
ExecutorService pool, User user) throws IOException {
...
try {
...
this.rpcClient = RpcClientFactory.createClient(this.conf, this.clusterId, this.metrics);
...
} catch (Throwable e) {
// avoid leaks: registry, rpcClient, ...
LOG.debug("connection construction failed", e);
close();
throw e;
}
}
RpcClient使用PoolMap数据结构存储客户端到HBase服务器之间的连接映射,PoolMap封装ConcurrentHashMap结构,其中key是ConnectionId[new ConnectionId(ticket, md.getService().getName(), addr)],value是RpcConnection对象的资源池。
protected final PoolMap<ConnectionId, T> connections;

/**
* Construct an IPC client for the cluster <code>clusterId</code>
* @param conf configuration
* @param clusterId the cluster id
* @param localAddr client socket bind address.
* @param metrics the connection metrics
*/
public AbstractRpcClient(Configuration conf, String clusterId, SocketAddress localAddr,
MetricsConnection metrics) {
...
this.connections = new PoolMap<>(getPoolType(conf), getPoolSize(conf));
...
}
当HBase需要连接一个服务器时,首先会根据ConnectionId找到对应的连接池,然后从连接池中取出一个连接对象,获取连接的核心实现:
/**
* Get a connection from the pool, or create a new one and add it to the pool. Connections to a
* given host/port are reused.
*/
private T getConnection(ConnectionId remoteId) throws IOException {
if (failedServers.isFailedServer(remoteId.getAddress())) {
if (LOG.isDebugEnabled()) {
LOG.debug("Not trying to connect to " + remoteId.address
+ " this server is in the failed servers list");
}
throw new FailedServerException(
"This server is in the failed servers list: " + remoteId.address);
}
T conn;
synchronized (connections) {
if (!running) {
throw new StoppedRpcClientException();
}
conn = connections.get(remoteId);
if (conn == null) {
conn = createConnection(remoteId);
connections.put(remoteId, conn);
}
conn.setLastTouched(EnvironmentEdgeManager.currentTime());
}
return conn;
}
连接池根据ConnectionId获取不到连接则创建RpcConnection的具体实现:
protected NettyRpcConnection createConnection(ConnectionId remoteId) throws IOException {
return new NettyRpcConnection(this, remoteId);
}

NettyRpcConnection(NettyRpcClient rpcClient, ConnectionId remoteId) throws IOException {
super(rpcClient.conf, AbstractRpcClient.WHEEL_TIMER, remoteId, rpcClient.clusterId,
rpcClient.userProvider.isHBaseSecurityEnabled(), rpcClient.codec, rpcClient.compressor);
this.rpcClient = rpcClient;
byte connectionHeaderPreamble = getConnectionHeaderPreamble();
this.connectionHeaderPreamble =
Unpooled.directBuffer(connectionHeaderPreamble.length).writeBytes(connectionHeaderPreamble);
ConnectionHeader header = getConnectionHeader();
this.connectionHeaderWithLength = Unpooled.directBuffer(4 + header.getSerializedSize());
this.connectionHeaderWithLength.writeInt(header.getSerializedSize());
header.writeTo(new ByteBufOutputStream(this.connectionHeaderWithLength));
}

protected RpcConnection(Configuration conf, HashedWheelTimer timeoutTimer, ConnectionId remoteId,
String clusterId, boolean isSecurityEnabled, Codec codec, CompressionCodec compressor)
throws IOException {
if (remoteId.getAddress().isUnresolved()) {
throw new UnknownHostException("unknown host: " + remoteId.getAddress().getHostName());
}
this.timeoutTimer = timeoutTimer;
this.codec = codec;
this.compressor = compressor;
this.conf = conf;

UserGroupInformation ticket = remoteId.getTicket().getUGI();
SecurityInfo securityInfo = SecurityInfo.getInfo(remoteId.getServiceName());
this.useSasl = isSecurityEnabled;
Token<? extends TokenIdentifier> token = null;
String serverPrincipal = null;
if (useSasl && securityInfo != null) {
AuthenticationProtos.TokenIdentifier.Kind tokenKind = securityInfo.getTokenKind();
if (tokenKind != null) {
TokenSelector<? extends TokenIdentifier> tokenSelector = AbstractRpcClient.TOKEN_HANDLERS
.get(tokenKind);
if (tokenSelector != null) {
token = tokenSelector.selectToken(new Text(clusterId), ticket.getTokens());
} else if (LOG.isDebugEnabled()) {
LOG.debug("No token selector found for type " + tokenKind);
}
}
String serverKey = securityInfo.getServerPrincipal();
if (serverKey == null) {
throw new IOException("Can't obtain server Kerberos config key from SecurityInfo");
}
serverPrincipal = SecurityUtil.getServerPrincipal(conf.get(serverKey),
remoteId.address.getAddress().getCanonicalHostName().toLowerCase());
if (LOG.isDebugEnabled()) {
LOG.debug("RPC Server Kerberos principal name for service=" + remoteId.getServiceName()
+ " is " + serverPrincipal);
}
}
this.token = token;
this.serverPrincipal = serverPrincipal;
if (!useSasl) {
authMethod = AuthMethod.SIMPLE;
} else if (token != null) {
authMethod = AuthMethod.DIGEST;
} else {
authMethod = AuthMethod.KERBEROS;
}

// Log if debug AND non-default auth, else if trace enabled.
// No point logging obvious.
if ((LOG.isDebugEnabled() && !authMethod.equals(AuthMethod.SIMPLE)) ||
LOG.isTraceEnabled()) {
// Only log if not default auth.
LOG.debug("Use " + authMethod + " authentication for service " + remoteId.serviceName
+ ", sasl=" + useSasl);
}
reloginMaxBackoff = conf.getInt("hbase.security.relogin.maxbackoff", 5000);
this.remoteId = remoteId;
}

hbase和hadoop版本兼容

hbasebeyond 发表了文章 • 0 个评论 • 951 次浏览 • 2018-09-18 10:20 • 来自相关话题

这里是Hadoop和hbase版本兼容的问题,另外社区有同学经常问到是否可以直接升级到最新版本的问题,目前还不建议大家在生产环境升级,可以在开发环境进行测试升级
123.jpeg

这里是Hadoop和hbase版本兼容的问题,另外社区有同学经常问到是否可以直接升级到最新版本的问题,目前还不建议大家在生产环境升级,可以在开发环境进行测试升级

互联网公司大数据面试题参考指南

sparkhbasegroup 发表了文章 • 0 个评论 • 1449 次浏览 • 2018-09-17 09:59 • 来自相关话题

以下面试题都是群里小伙伴提供的,现场真题(包含校招题)
1.网易大数据面试题
说说项目Spark哪部分用得好,如何调优Java哪部分了解比较好聊聊并发,并发实现方法,volatile关键字说说HashMap的底层原理为什么要重写hashcode和equals说说jvm各个垃圾收集器运用在什么情形jvm调优说说io为什么考虑转行呢?是因为原专业不好就业吗?

2.蚂蚁金服面试题
小文件的合并MR与Spark的区别关注哪些名人的博客对大数据领域有什么自己的见解平常怎么学习大数据的StringBuilder与StringBuffer的区别HashMap与Hashtable的区别谈谈你对树的理解数据库索引的实现jvm的内存模型jvm的垃圾收集器jvm的垃圾收集算法HDFS架构HDFS读写流程Hadoop3.0做了哪些改进谈谈YARN为什么项目选择使用Spark,你觉得Spark的优点在哪里了解Flink与Storm嘛,他们与Spark Streaming的区别在哪里1TB文件,取重复的词,top5指定的资源的场景下,如何快速统计出来
 
3.美图面试题
为什么选择美图,你知道美图地点在哪里嘛介绍下你做的项目吧数据统一管理平台,我挺感兴趣的,你说说吧我大概知道是怎么回事了,java web这块你参与开发了吗你刚刚项目提到了元数据,你能说说hive的元数据管理嘛,对它了解嘛还是hive,你对hive有哪些原理性了解呢知道AST、operator tree这些长什么样吗那你的hive转mr过程是怎么了解的呢?除了谓词下推,还能说说其它的优化嘛?别说数据倾斜的调优jvm了解不,说下垃圾收集算法平常用java和scala语言哪个多点如果我现在要使用map集合,你觉得哪种适合多线程情况下进行访问如何去监控线程Spark 出现OOM,你觉得该怎么进行调优呢?不去动jvm的参数你觉得join该怎么优化你对未来的规划是什么?(五年内)你也就是走技术路线咯

4.​美图二面
ThriftServer的HA如何去实现,能说下实现的思路嘛说下Zookeeper的watch机制是如何实现的嘛?场景题:
     现在有1个client,2个server,当我动态加入一台机器,或者删除一台机器,或者某台机器宕机了,client该如何去感知到,说下实现思路(不使用Zookeeper),如何通信,说说具体实现?
 
5.七牛云面试题
快排hive和hdfs之间的联系inode和文件描述符linux指令如何创建文件http中header中放入key value 有什么变化系统调用和库函数区别http缓冲实现机智session cookie  区别进程间通信方式jsp本质http请求状状态get post put remove数据库join 数据库引擎hibernate和mybiters区别jvm垃圾回收hive和关系型数据库区别hive实现原理spark与mr的区别 查看全部
以下面试题都是群里小伙伴提供的,现场真题(包含校招题)
1.网易大数据面试题
  • 说说项目
  • Spark哪部分用得好,如何调优
  • Java哪部分了解比较好
  • 聊聊并发,并发实现方法,volatile关键字说说
  • HashMap的底层原理
  • 为什么要重写hashcode和equals
  • 说说jvm
  • 各个垃圾收集器运用在什么情形
  • jvm调优
  • 说说io
  • 为什么考虑转行呢?是因为原专业不好就业吗?


2.蚂蚁金服面试题
  • 小文件的合并
  • MR与Spark的区别
  • 关注哪些名人的博客
  • 对大数据领域有什么自己的见解
  • 平常怎么学习大数据的
  • StringBuilder与StringBuffer的区别
  • HashMap与Hashtable的区别
  • 谈谈你对树的理解
  • 数据库索引的实现
  • jvm的内存模型
  • jvm的垃圾收集器
  • jvm的垃圾收集算法
  • HDFS架构
  • HDFS读写流程
  • Hadoop3.0做了哪些改进
  • 谈谈YARN
  • 为什么项目选择使用Spark,你觉得Spark的优点在哪里
  • 了解Flink与Storm嘛,他们与Spark Streaming的区别在哪里
  • 1TB文件,取重复的词,top5指定的资源的场景下,如何快速统计出来

 
3.美图面试题
  • 为什么选择美图,你知道美图地点在哪里嘛
  • 介绍下你做的项目吧
  • 数据统一管理平台,我挺感兴趣的,你说说吧
  • 我大概知道是怎么回事了,java web这块你参与开发了吗
  • 你刚刚项目提到了元数据,你能说说hive的元数据管理嘛,对它了解嘛
  • 还是hive,你对hive有哪些原理性了解呢
  • 知道AST、operator tree这些长什么样吗
  • 那你的hive转mr过程是怎么了解的呢?
  • 除了谓词下推,还能说说其它的优化嘛?别说数据倾斜的调优
  • jvm了解不,说下垃圾收集算法
  • 平常用java和scala语言哪个多点
  • 如果我现在要使用map集合,你觉得哪种适合多线程情况下进行访问
  • 如何去监控线程
  • Spark 出现OOM,你觉得该怎么进行调优呢?不去动jvm的参数
  • 你觉得join该怎么优化
  • 你对未来的规划是什么?(五年内)
  • 你也就是走技术路线咯


4.​美图二面
  • ThriftServer的HA如何去实现,能说下实现的思路嘛
  • 说下Zookeeper的watch机制是如何实现的嘛?
  • 场景题:

     现在有1个client,2个server,当我动态加入一台机器,或者删除一台机器,或者某台机器宕机了,client该如何去感知到,说下实现思路(不使用Zookeeper),如何通信,说说具体实现?
 
5.七牛云面试题
  • 快排
  • hive和hdfs之间的联系
  • inode和文件描述符
  • linux指令如何创建文件
  • http中header中放入key value 有什么变化
  • 系统调用和库函数区别
  • http缓冲实现机智
  • session cookie  区别
  • 进程间通信方式
  • jsp本质
  • http请求状状态
  • get post put remove
  • 数据库join 
  • 数据库引擎
  • hibernate和mybiters区别
  • jvm垃圾回收
  • hive和关系型数据库区别
  • hive实现原理
  • spark与mr的区别

数据批量导入,导致Hbase region 100G,未进行分裂是什么原因

回复

hbasezsh 回复了问题 • 3 人关注 • 1 个回复 • 815 次浏览 • 2018-09-14 10:48 • 来自相关话题

瑾谦之Phoenix系列专辑

Phoenixhbasegroup 发表了文章 • 2 个评论 • 815 次浏览 • 2018-09-12 11:28 • 来自相关话题

概述#这测试
###测试
Phoenix是一个开源的HBASE SQL层。它不仅可以使用标准的JDBC API替代HBASE client API创建表,插入和查询HBASE,也支持二级索引、事物以及多种SQL层优化。
此系列文章将会从Phoenix的语法和功能特性、相关工具、实践经验以及应用案例多方面从浅入深的阐述。希望对Phoenix入门、在做架构设计和技术选型的同学能有一些帮助。

大纲
[Phoenix] 一、快速入门
[Phoenix] 二、数据类型
[Phoenix] 三、DML语法
[Phoenix] 四、加盐表
[Phoenix] 五、二级索引
[Phoenix] 六、MR在Ali-Phoenix上的使用
[Phoenix] 七、如何使用自增ID
[Phoenix] 八、动态列
[Phoenix] 九、分页查询
[Phoenix] 十、全局索引设计实践
[Phoenix] 十一、如何分析查询计划(待完成)
[Phoenix] 十二、Phoenix表数据导入(待完成)
[Phoenix] 十三、从传统关系型数据库到phoenix(待完成)
[Phoenix] 十四、案例1(待完成)
[Phoenix] 十五、案例2(待完成)

其它
Phoenix索引生命周期
 
链接
https://yq.aliyun.com/album/82
https://yq.aliyun.com/articles/574090?spm=a2c4e.11155435.0.0.21ff4db4EO8sz6
  查看全部
概述#这测试
###测试
Phoenix是一个开源的HBASE SQL层。它不仅可以使用标准的JDBC API替代HBASE client API创建表,插入和查询HBASE,也支持二级索引、事物以及多种SQL层优化。
此系列文章将会从Phoenix的语法和功能特性、相关工具、实践经验以及应用案例多方面从浅入深的阐述。希望对Phoenix入门、在做架构设计和技术选型的同学能有一些帮助。

大纲
[Phoenix] 一、快速入门
[Phoenix] 二、数据类型
[Phoenix] 三、DML语法
[Phoenix] 四、加盐表
[Phoenix] 五、二级索引
[Phoenix] 六、MR在Ali-Phoenix上的使用
[Phoenix] 七、如何使用自增ID
[Phoenix] 八、动态列
[Phoenix] 九、分页查询
[Phoenix] 十、全局索引设计实践
[Phoenix] 十一、如何分析查询计划(待完成)
[Phoenix] 十二、Phoenix表数据导入(待完成)
[Phoenix] 十三、从传统关系型数据库到phoenix(待完成)
[Phoenix] 十四、案例1(待完成)
[Phoenix] 十五、案例2(待完成)

其它
Phoenix索引生命周期
 
链接
https://yq.aliyun.com/album/82
https://yq.aliyun.com/articles/574090?spm=a2c4e.11155435.0.0.21ff4db4EO8sz6
 

HBase 优化实战

hbase过往记忆 发表了文章 • 0 个评论 • 665 次浏览 • 2018-08-10 12:44 • 来自相关话题

背景

Datastream一直以来在使用HBase分流日志,每天的数据量很大,日均大概在80亿条,10TB的数据。对于像Datastream这种数据量巨大、对写入要求非常高,并且没有复杂查询需求的日志系统来说,选用HBase作为其数据存储平台,无疑是一个非常不错的选择。

HBase是一个相对较复杂的分布式系统,并发写入的性能非常高。然而,分布式系统从结构上来讲,也相对较复杂,模块繁多,各个模块之间也很容易出现一些问题,所以对像HBase这样的大型分布式系统来说,优化系统运行,及时解决系统运行过程中出现的问题也变得至关重要。正所谓:“你”若安好,便是晴天;“你”若有恙,我便没有星期天。

历史现状

HBase交接到我们团队手上时,已经在线上运行有一大段时间了,期间也偶尔听到过系统不稳定的、时常会出现一些问题的言论,但我们认为:一个能被大型互联网公司广泛采用的系统(包括Facebook,twitter,淘宝,小米等),其在性能和可用性上是毋庸置疑的,何况像Facebook这种公司,是在经过严格选型后,放弃了自己开发的Cassandra系统,用HBase取而代之。既然这样,那么,HBase的不稳定、经常出问题一定有些其他的原因,我们所要做的,就是找出这些HBase的不稳定因素,还HBase一个“清白”。“查案”之前,先来简单回顾一下我们接手HBase时的现状(我们运维着好几个HBase集群,这里主要介绍问题最多那个集群的调优):





应用反应经常会过段时间出现数据写入缓慢,导致应用端数据堆积现象,是否可以通过增加机器数量来解决?

  其实,那个时候,我们本身对HBase也不是很熟悉,对HBase的了解,也仅仅在做过一些测试,了解一些性能,对内部结构,实现原理之类的基本上都不怎么清楚。于是刚开始几天,各种问题,每天晚上拉着一男一起摸索,顺利的时候,晚上8,9点就可以暂时搞定线上问题,更多的时候基本要到22点甚至更晚(可能那个时候流量也下去了),通过不断的摸索,慢慢了解HBase在使用上的一些限制,也就能逐渐解决这一系列过程中发现的问题。后面挑几个相对比较重要,效果较为明显的改进点,做下简单介绍。

调优

首先根据目前17台机器,50000+的QPS,并且观察磁盘的I/O利用率和CPU利用率都相当低来判断:当前的请求数量根本没有达到系统的性能瓶颈,不需要新增机器来提高性能。如果不是硬件资源问题,那么性能的瓶颈究竟是什么?

Rowkey设计问题

现象

打开HBase的Web端,发现HBase下面各个RegionServer的请求数量非常不均匀,第一个想到的就是HBase的热点问题,具体到某个具体表上的请求分布如下:




HBase表请求分布

上面是HBase下某张表的region请求分布情况,从中我们明显可以看到,部分region的请求数量为0,而部分的请求数量可以上百万,这是一个典型的热点问题。

原因

HBase出现热点问题的主要原因无非就是rowkey设计的合理性,像上面这种问题,如果rowkey设计得不好,很容易出现,比如:用时间戳生成rowkey,由于时间戳在一段时间内都是连续的,导致在不同的时间段,访问都集中在几个RegionServer上,从而造成热点问题。

解决

知道了问题的原因,对症下药即可,联系应用修改rowkey规则,使rowkey数据随机均匀分布,效果如下:




Rowkey重定义后请求分布

建议

  对于HBase来说,rowkey的范围划定了RegionServer,每一段rowkey区间对应一个RegionServer,我们要保证每段时间内的rowkey访问都是均匀的,所以我们在设计的时候,尽量要以hash或者md5等开头来组织rowkey。

Region重分布

现象

HBase的集群是在不断扩展的,分布式系统的最大好处除了性能外,不停服横向扩展也是其中之一,扩展过程中有一个问题:每次扩展的机器的配置是不一样的,一般,后面新加入的机器性能会比老的机器好,但是后面加入的机器经常被分配很少的region,这样就造成了资源分布不均匀,随之而来的就是性能上的损失,如下:




HBase各个RegionServer请求

上图中我们可以看到,每台RegionServer上的请求极为不均匀,多的好几千,少的只有几十

原因

资源分配不均匀,造成部分机器压力较大,部分机器负载较低,并且部分Region过大过热,导致请求相对较集中。

解决

迁移部分老的RegionServer上的region到新加入的机器上,使每个RegionServer的负载均匀。通过split切分部分较大region,均匀分布热点region到各个RegionServer上。




HBase region请求分布

对比前后两张截图我们可以看到,Region总数量从1336增加到了1426,而增加的这90个region就是通过split切分大的region得到的。而对region重新分布后,整个HBase的性能有了大幅度提高。

建议

Region迁移的时候不能简单开启自动balance,因为balance主要的问题是不会根据表来进行balance,HBase的自动balance只会根据每个RegionServer上的Region数量来进行balance,所以自动balance可能会造成同张表的region会被集中迁移到同一个台RegionServer上,这样就达不到分布式的效果。

基本上,新增RegionServer后的region调整,可以手工进行,尽量使表的Region都平均分配到各个RegionServer上,另外一点,新增的RegionServer机器,配置最好与前面的一致,否则资源无法更好利用。

对于过大,过热的region,可以通过切分的方法生成多个小region后均匀分布(注意:region切分会触发major compact操作,会带来较大的I/O请求,请务必在业务低峰期进行)

HDFS写入超时

现象

HBase写入缓慢,查看HBase日志,经常有慢日志如下:

WARN org.apache.hadoop.ipc.HBaseServer- (responseTooSlow): {"processingtimems":36096, "call":"multi(org.apache.hadoop.hbase.client.MultiAction@7884377e), rpc version=1, client version=29, methodsFingerPrint=1891768260", "client":"xxxx.xxx.xxx.xxxx:44367", "starttimems":1440239670790, "queuetimems":42081, "class":"HRegionServer", "responsesize":0, "method":"multi"}

并且伴有HDFS创建block异常如下:

INFO  org.apache.hadoop.hdfs.DFSClient - Exception in createBlockOutputStream

org.apache.hadoop.hdfs.protocol.HdfsProtoUtil.vintPrefixed(HdfsProtoUtil.java:171)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.createBlockOutputStream(DFSOutputStream.java:1105)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.nextBlockOutputStream(DFSOutputStream.java:1039)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:487)

一般地,HBase客户端的写入到RegionServer下某个region的memstore后就返回,除了网络外,其他都是内存操作,应该不会有长达30多秒的延迟,外加HDFS层抛出的异常,我们怀疑很可能跟底层数据存储有关。

原因

定位到可能是HDFS层出现了问题,那就先从底层开始排查,发现该台机器上10块盘的空间利用率都已经达到100%。按理说,作为一个成熟的分布式文件系统,对于部分数据盘满的情况,应该有其应对措施。的确,HDFS本身可以设置数据盘预留空间,如果部分数据盘的预留空间小于该值时,HDFS会自动把数据写入到另外的空盘上面,那么我们这个又是什么情况?

  最终通过多方面的沟通确认,发现了主要原因:我们这批机器,在上线前SA已经经过处理,每块盘默认预留100G空间,所以当通过df命令查看盘使用率为100%时,其实盘还有100G的预留空间,而HDFS层面我们配置的预留空间是50G,那么问题就来了:HDFS认为盘还有100G空间,并且多于50G的预留,所以数据可以写入本地盘,但是系统层面却禁止了该写入操作,从而导致数据写入异常。

解决

解决的方法可以让SA释放些空间出来便于数据写入。当然,最直接有效的就是把HDFS的预留空间调整至100G以上,我们也正是这样做的,通过调整后,异常不再出现,HBase层面的slow log也没有再出现。同时我们也开启了HDFS层面的balance,使数据自动在各个服务器之间保持平衡。

建议

磁盘满了导致的问题很难预料,HDFS可能会导致部分数据写入异常,MySQL可能会出现直接宕机等等,所以最好的办法就是:不要使盘的利用率达到100%。

网络拓扑

现象

通过rowkey调整,HDFS数据balance等操作后,HBase的确稳定了许多,在很长一段时间都没有出现写入缓慢问题,整体的性能也上涨了很多。但时常会隔一段时间出现些slow log,虽然对整体的性能影响不大,但性能上的抖动还是很明显。

原因

由于该问题不经常出现,对系统的诊断带来不小的麻烦,排查了HBase层和HDFS层,几乎一无所获,因为在大多数情况下,系统的吞吐量都是正常的。通过脚本收集RegionServer所在服务器的系统资源信息,也看不出问题所在,最后怀疑到系统的物理拓扑上,HBase集群的最大特点是数据量巨大,在做一些操作时,很容易把物理机的千兆网卡都吃满,这样如果网络拓扑结构存在问题,HBase的所有机器没有部署在同一个交换机上,上层交换机的进出口流量也有可能存在瓶颈。网络测试还是挺简单的,直接ping就可以,我们得到以下结果:共17台机器,只有其中一台的延迟存在问题,如下:




网络延迟测试:Ping结果

同一个局域网内的机器,延迟达到了毫秒级别,这个延迟是比较致命的,因为分布式存储系统HDFS本身对网络有要求,HDFS默认3副本存在不同的机器上,如果其中某台机器的网络存在问题,这样就会影响到该机器上保存副本的写入,拖慢整个HDFS的写入速度。

解决

  网络问题,联系机房解决,机房的反馈也验证了我们的想法:由于HBase的机器后面进行了扩展,后面加入的机器有一台跟其他机器不在同一个交换机下,而这台机器正是我们找出的有较大ping延时这台,整个HBase物理结构如下:




HBase物理拓扑结构

跟机房协调,调整机器位置,使所有的HBase机器都位于同一个交换机下,问题迎刃而解。

建议

  对于分布式大流量的系统,除了系统本身,物理机的部署和流量规划也相当重要,尽量使集群中所有的机器位于相同的交换机下(有容灾需求的应用除外),集群较大,需要跨交换机部署时,也要充分考虑交换机的出口流量是否够用,网络硬件上的瓶颈诊断起来相对更为困难。

JVM参数调整

解决了网络上面的不稳定因素,HBase的性能又得到进一步的提高,随之也带来了另外的问题。

现象

  根据应用反应,HBase会阶段性出现性能下降,导致应用数据写入缓慢,造成应用端的数据堆积,这又是怎么回事?经过一系列改善后HBase的系统较之以前有了大幅度增长,怎么还会出现数据堆积的问题?为什么会阶段性出现?




从上图看,HBase平均流量QPS基本能达到12w,但是每过一段时间,流量就会下降到接近零点,同时这段时间,应用会反应数据堆积。

原因

  这个问题定位相对还是比较简单,结合HBase的日志,很容易找到问题所在:

org.apache.hadoop.hbase.util.Sleeper - We slept 41662ms instead of 3000ms, this is likely due to a long garbage collecting pause and it's usually bad

通过上述日志,基本上可以判定是HBase的某台RegionServer出现GC问题,导致了服务在很长一段时间内禁止访问。

  HBase通过一系列的调整后,整个系统的吞吐量增加了好几倍,然而JVM的堆大小没有进行相应的调整,整个系统的内存需求变大,而虚拟机又来不及回收,最终导致出现Full GC

解决

   GC问题导致HBase整个系统的请求下降,通过适当调整JVM参数的方式,解决HBase RegionServer的GC问题。

建议

  对于HBase来说,本身不存在单点故障,即使宕掉1,2台RegionServer,也只是使剩下几台的压力有所增加,不会导致整个集群服务能力下降很多。但是,如果其中某台RegionServer出现Full GC问题,那么这台机器上所有的访问都会被挂起,客户端请求一般都是batch发送的,rowkey的随机分布导致部分请求会落到该台RegionServer上,这样该客户端的请求就会被阻塞,导致客户端无法正常写数据到HBase。所以,对于HBase来说,宕机并不可怕,但长时间的Full GC是比较致命的,配置JVM参数的时候,尽量要考虑避免Full GC的出现。

后记

  经过前面一系列的优化,目前Datastream的这套HBase线上环境已经相当稳定,连续运行几个月都没有任何HBase层面由于系统性能不稳定导致的报警,平均性能在各个时间段都比较稳定,没有出现过大幅度的波动或者服务不可用等现象。

本文来自网易实践者社区 查看全部
背景

Datastream一直以来在使用HBase分流日志,每天的数据量很大,日均大概在80亿条,10TB的数据。对于像Datastream这种数据量巨大、对写入要求非常高,并且没有复杂查询需求的日志系统来说,选用HBase作为其数据存储平台,无疑是一个非常不错的选择。

HBase是一个相对较复杂的分布式系统,并发写入的性能非常高。然而,分布式系统从结构上来讲,也相对较复杂,模块繁多,各个模块之间也很容易出现一些问题,所以对像HBase这样的大型分布式系统来说,优化系统运行,及时解决系统运行过程中出现的问题也变得至关重要。正所谓:“你”若安好,便是晴天;“你”若有恙,我便没有星期天。

历史现状

HBase交接到我们团队手上时,已经在线上运行有一大段时间了,期间也偶尔听到过系统不稳定的、时常会出现一些问题的言论,但我们认为:一个能被大型互联网公司广泛采用的系统(包括Facebook,twitter,淘宝,小米等),其在性能和可用性上是毋庸置疑的,何况像Facebook这种公司,是在经过严格选型后,放弃了自己开发的Cassandra系统,用HBase取而代之。既然这样,那么,HBase的不稳定、经常出问题一定有些其他的原因,我们所要做的,就是找出这些HBase的不稳定因素,还HBase一个“清白”。“查案”之前,先来简单回顾一下我们接手HBase时的现状(我们运维着好几个HBase集群,这里主要介绍问题最多那个集群的调优):

menu.saveimg_.savepath20180810124058_.jpg

应用反应经常会过段时间出现数据写入缓慢,导致应用端数据堆积现象,是否可以通过增加机器数量来解决?

  其实,那个时候,我们本身对HBase也不是很熟悉,对HBase的了解,也仅仅在做过一些测试,了解一些性能,对内部结构,实现原理之类的基本上都不怎么清楚。于是刚开始几天,各种问题,每天晚上拉着一男一起摸索,顺利的时候,晚上8,9点就可以暂时搞定线上问题,更多的时候基本要到22点甚至更晚(可能那个时候流量也下去了),通过不断的摸索,慢慢了解HBase在使用上的一些限制,也就能逐渐解决这一系列过程中发现的问题。后面挑几个相对比较重要,效果较为明显的改进点,做下简单介绍。

调优

首先根据目前17台机器,50000+的QPS,并且观察磁盘的I/O利用率和CPU利用率都相当低来判断:当前的请求数量根本没有达到系统的性能瓶颈,不需要新增机器来提高性能。如果不是硬件资源问题,那么性能的瓶颈究竟是什么?

Rowkey设计问题

现象

打开HBase的Web端,发现HBase下面各个RegionServer的请求数量非常不均匀,第一个想到的就是HBase的热点问题,具体到某个具体表上的请求分布如下:
201806281646495b8a5674-3a09-4c12-8f81-5ff43ebc0fa9.jpg

HBase表请求分布

上面是HBase下某张表的region请求分布情况,从中我们明显可以看到,部分region的请求数量为0,而部分的请求数量可以上百万,这是一个典型的热点问题。

原因

HBase出现热点问题的主要原因无非就是rowkey设计的合理性,像上面这种问题,如果rowkey设计得不好,很容易出现,比如:用时间戳生成rowkey,由于时间戳在一段时间内都是连续的,导致在不同的时间段,访问都集中在几个RegionServer上,从而造成热点问题。

解决

知道了问题的原因,对症下药即可,联系应用修改rowkey规则,使rowkey数据随机均匀分布,效果如下:
20180628164659ff3e72b7-9be5-4671-ba17-6a37aafd0cdd.jpg

Rowkey重定义后请求分布

建议

  对于HBase来说,rowkey的范围划定了RegionServer,每一段rowkey区间对应一个RegionServer,我们要保证每段时间内的rowkey访问都是均匀的,所以我们在设计的时候,尽量要以hash或者md5等开头来组织rowkey。

Region重分布

现象

HBase的集群是在不断扩展的,分布式系统的最大好处除了性能外,不停服横向扩展也是其中之一,扩展过程中有一个问题:每次扩展的机器的配置是不一样的,一般,后面新加入的机器性能会比老的机器好,但是后面加入的机器经常被分配很少的region,这样就造成了资源分布不均匀,随之而来的就是性能上的损失,如下:
201806281647298f5c069c-bc91-4706-bb78-ad825e4a15d4.jpg

HBase各个RegionServer请求

上图中我们可以看到,每台RegionServer上的请求极为不均匀,多的好几千,少的只有几十

原因

资源分配不均匀,造成部分机器压力较大,部分机器负载较低,并且部分Region过大过热,导致请求相对较集中。

解决

迁移部分老的RegionServer上的region到新加入的机器上,使每个RegionServer的负载均匀。通过split切分部分较大region,均匀分布热点region到各个RegionServer上。
20180628164752abef950e-8d45-42ca-a5fe-0b1396a96b5f.jpg

HBase region请求分布

对比前后两张截图我们可以看到,Region总数量从1336增加到了1426,而增加的这90个region就是通过split切分大的region得到的。而对region重新分布后,整个HBase的性能有了大幅度提高。

建议

Region迁移的时候不能简单开启自动balance,因为balance主要的问题是不会根据表来进行balance,HBase的自动balance只会根据每个RegionServer上的Region数量来进行balance,所以自动balance可能会造成同张表的region会被集中迁移到同一个台RegionServer上,这样就达不到分布式的效果。

基本上,新增RegionServer后的region调整,可以手工进行,尽量使表的Region都平均分配到各个RegionServer上,另外一点,新增的RegionServer机器,配置最好与前面的一致,否则资源无法更好利用。

对于过大,过热的region,可以通过切分的方法生成多个小region后均匀分布(注意:region切分会触发major compact操作,会带来较大的I/O请求,请务必在业务低峰期进行)

HDFS写入超时

现象

HBase写入缓慢,查看HBase日志,经常有慢日志如下:

WARN org.apache.hadoop.ipc.HBaseServer- (responseTooSlow): {"processingtimems":36096, "call":"multi(org.apache.hadoop.hbase.client.MultiAction@7884377e), rpc version=1, client version=29, methodsFingerPrint=1891768260", "client":"xxxx.xxx.xxx.xxxx:44367", "starttimems":1440239670790, "queuetimems":42081, "class":"HRegionServer", "responsesize":0, "method":"multi"}

并且伴有HDFS创建block异常如下:

INFO  org.apache.hadoop.hdfs.DFSClient - Exception in createBlockOutputStream

org.apache.hadoop.hdfs.protocol.HdfsProtoUtil.vintPrefixed(HdfsProtoUtil.java:171)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.createBlockOutputStream(DFSOutputStream.java:1105)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.nextBlockOutputStream(DFSOutputStream.java:1039)

org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer.run(DFSOutputStream.java:487)

一般地,HBase客户端的写入到RegionServer下某个region的memstore后就返回,除了网络外,其他都是内存操作,应该不会有长达30多秒的延迟,外加HDFS层抛出的异常,我们怀疑很可能跟底层数据存储有关。

原因

定位到可能是HDFS层出现了问题,那就先从底层开始排查,发现该台机器上10块盘的空间利用率都已经达到100%。按理说,作为一个成熟的分布式文件系统,对于部分数据盘满的情况,应该有其应对措施。的确,HDFS本身可以设置数据盘预留空间,如果部分数据盘的预留空间小于该值时,HDFS会自动把数据写入到另外的空盘上面,那么我们这个又是什么情况?

  最终通过多方面的沟通确认,发现了主要原因:我们这批机器,在上线前SA已经经过处理,每块盘默认预留100G空间,所以当通过df命令查看盘使用率为100%时,其实盘还有100G的预留空间,而HDFS层面我们配置的预留空间是50G,那么问题就来了:HDFS认为盘还有100G空间,并且多于50G的预留,所以数据可以写入本地盘,但是系统层面却禁止了该写入操作,从而导致数据写入异常。

解决

解决的方法可以让SA释放些空间出来便于数据写入。当然,最直接有效的就是把HDFS的预留空间调整至100G以上,我们也正是这样做的,通过调整后,异常不再出现,HBase层面的slow log也没有再出现。同时我们也开启了HDFS层面的balance,使数据自动在各个服务器之间保持平衡。

建议

磁盘满了导致的问题很难预料,HDFS可能会导致部分数据写入异常,MySQL可能会出现直接宕机等等,所以最好的办法就是:不要使盘的利用率达到100%。

网络拓扑

现象

通过rowkey调整,HDFS数据balance等操作后,HBase的确稳定了许多,在很长一段时间都没有出现写入缓慢问题,整体的性能也上涨了很多。但时常会隔一段时间出现些slow log,虽然对整体的性能影响不大,但性能上的抖动还是很明显。

原因

由于该问题不经常出现,对系统的诊断带来不小的麻烦,排查了HBase层和HDFS层,几乎一无所获,因为在大多数情况下,系统的吞吐量都是正常的。通过脚本收集RegionServer所在服务器的系统资源信息,也看不出问题所在,最后怀疑到系统的物理拓扑上,HBase集群的最大特点是数据量巨大,在做一些操作时,很容易把物理机的千兆网卡都吃满,这样如果网络拓扑结构存在问题,HBase的所有机器没有部署在同一个交换机上,上层交换机的进出口流量也有可能存在瓶颈。网络测试还是挺简单的,直接ping就可以,我们得到以下结果:共17台机器,只有其中一台的延迟存在问题,如下:
20180628164811ef38f3c7-dcc1-492f-83f8-fe7bb8ba94fa.jpg

网络延迟测试:Ping结果

同一个局域网内的机器,延迟达到了毫秒级别,这个延迟是比较致命的,因为分布式存储系统HDFS本身对网络有要求,HDFS默认3副本存在不同的机器上,如果其中某台机器的网络存在问题,这样就会影响到该机器上保存副本的写入,拖慢整个HDFS的写入速度。

解决

  网络问题,联系机房解决,机房的反馈也验证了我们的想法:由于HBase的机器后面进行了扩展,后面加入的机器有一台跟其他机器不在同一个交换机下,而这台机器正是我们找出的有较大ping延时这台,整个HBase物理结构如下:
20180628164824ce396dad-ae37-49b5-8fa7-de67ad187b5c.jpg

HBase物理拓扑结构

跟机房协调,调整机器位置,使所有的HBase机器都位于同一个交换机下,问题迎刃而解。

建议

  对于分布式大流量的系统,除了系统本身,物理机的部署和流量规划也相当重要,尽量使集群中所有的机器位于相同的交换机下(有容灾需求的应用除外),集群较大,需要跨交换机部署时,也要充分考虑交换机的出口流量是否够用,网络硬件上的瓶颈诊断起来相对更为困难。

JVM参数调整

解决了网络上面的不稳定因素,HBase的性能又得到进一步的提高,随之也带来了另外的问题。

现象

  根据应用反应,HBase会阶段性出现性能下降,导致应用数据写入缓慢,造成应用端的数据堆积,这又是怎么回事?经过一系列改善后HBase的系统较之以前有了大幅度增长,怎么还会出现数据堆积的问题?为什么会阶段性出现?
201806281648395c7bae34-f068-41f5-9c8d-a865f5f8f70c.jpg

从上图看,HBase平均流量QPS基本能达到12w,但是每过一段时间,流量就会下降到接近零点,同时这段时间,应用会反应数据堆积。

原因

  这个问题定位相对还是比较简单,结合HBase的日志,很容易找到问题所在:

org.apache.hadoop.hbase.util.Sleeper - We slept 41662ms instead of 3000ms, this is likely due to a long garbage collecting pause and it's usually bad

通过上述日志,基本上可以判定是HBase的某台RegionServer出现GC问题,导致了服务在很长一段时间内禁止访问。

  HBase通过一系列的调整后,整个系统的吞吐量增加了好几倍,然而JVM的堆大小没有进行相应的调整,整个系统的内存需求变大,而虚拟机又来不及回收,最终导致出现Full GC

解决

   GC问题导致HBase整个系统的请求下降,通过适当调整JVM参数的方式,解决HBase RegionServer的GC问题。

建议

  对于HBase来说,本身不存在单点故障,即使宕掉1,2台RegionServer,也只是使剩下几台的压力有所增加,不会导致整个集群服务能力下降很多。但是,如果其中某台RegionServer出现Full GC问题,那么这台机器上所有的访问都会被挂起,客户端请求一般都是batch发送的,rowkey的随机分布导致部分请求会落到该台RegionServer上,这样该客户端的请求就会被阻塞,导致客户端无法正常写数据到HBase。所以,对于HBase来说,宕机并不可怕,但长时间的Full GC是比较致命的,配置JVM参数的时候,尽量要考虑避免Full GC的出现。

后记

  经过前面一系列的优化,目前Datastream的这套HBase线上环境已经相当稳定,连续运行几个月都没有任何HBase层面由于系统性能不稳定导致的报警,平均性能在各个时间段都比较稳定,没有出现过大幅度的波动或者服务不可用等现象。

本文来自网易实践者社区

中国HBase技术社区微信公众号:
hbasegroup

欢迎加入HBase生态+Spark社区钉钉大群