网易视频云是网易倾力打造的一款基于云计算的分布式多媒体处理集群和专业音视频技术,为客户提供稳定流畅、低时延、高并发的视频直播、录制、存储、转码及点播等音视频的 PaaS 服务。在线教育、远程医疗、娱乐秀场、在线金融等各行业及企业用户只需经过简单的开发即可打造在线音视频平台。现在,网易视频云与大家分享 HBase 客户端实践–重试机制。
在运维 HBase 的这段时间里,发现业务用户一方面比较关注 HBase 本身服务的读写性能:吞吐量以及读写延迟,另一方面也会比较关注 HBase 客户端使用上的问题,主要集中在两个方面:是否提供了重试机制来保证系统操作的容错性?是否有必要的超时机制保证系统能够 fastfail ,保证系统的低延迟特性?
这个系列我们集中介绍 HBase 客户端使用上的这两大问题,本文通过分析之前一个真实的案例来介绍 HBase 客户端提供的重试机制,并通过配置合理的参数使得客户端在保证一定容错性的同时还能够保证系统的低延迟特性。
案发现场
最近某业务在使用 HBase 客户端读取数据时出现了大量线程 block 的情况,业务方保留了当时的线程堆栈信息,如下图所示:
看到这样的问题,首先从日志和监控排查了业务表和 region server ,确认了在很长时间内确实没有请求进来,除此之外并没有其他有用的信息,同时也没有接到该集群上其他用户的异常反馈,从现象看,这次异常是在特定环境下才会触发的。
案件分析过程
1.根据上图图 1 所示,所有的请求都 block 在<0x0000000782a936f0>这把全局锁上,这里需要关注两个问题:
哪个线程持有了这把全局锁<0x0000000782a936f0>?
这是一把什么样的全局锁(对于问题本身并不重要,有兴趣可以参考步骤 3 )?
2.哪个线程持有了这把锁?
2.1 很容易在 jstack 日志中通过搜索找到全局锁<0x0000000782a936f0>被如下线程持有:
定睛一看,该线程持有了这把全局锁,而且处于 TIMED_WAITING 状态,因此这把锁可能长时间不释放,导致所有需要这把全局锁的线程都阻塞等待。好了,那问题就转化成了:为什么这个线程会处于 TIME_WAITING 状态?
2.2 根据上图提示,查看源码中 RpcRetryingCall.java 的 115 行代码,可以确定该线程处于 TIME_WAITING 状态是因为自己休眠导致,如下图所示:
RpcRetryingCall 函数是 Rpc 请求重试机制的实现,所以可以有两点推断:
HBase 客户端请求在那个时间段网络有异常导致 rpc 请求失败,进入重试逻辑 根据 HBase 的重试机制(退避机制),每两次重试机制之间会休眠一段时间,即上图 115 行代码,这个休眠时间太长导致这个线程一直处于 TIME_WAITING 状态。
休眠时间由上图中 expectedSleep = callable.sleep(pause,tries + 1)决定,根据 hbase 算法(见第三部分),默认最大的 expectedSleep 为 20s ,整个重试时间会持续 8min ,这也就是说全局锁会被持有 8min ,可这并不能解释持续将近几个小时的阻塞无请求。除非有两种情况:
配置有问题:需要客户端检查 hbase.client.pause 和 hbase.client.retries.number 两个参数配置出现异常,比如 hbase.client.pause 参数如果手抖配成了 10000 ,就有可能出现几个小时阻塞的情况
网络持续有问题:如果线程 1 持有全局锁重试失败之后退出,线程 2 竞争到这把锁,此时网络依然有问题,线程 2 会再次进入重试,重试 8min 之后失败退出,循环下去,也有可能出现几个小时阻塞的情况
和业务方确认配置,所有参数基本属于默认配置,因此猜测一不成立,那最有可能的情况就是猜测二。经过确认,在事发当时(凌晨 0 点~早上 6 点)确实存在很多服务因为云网络升级异常发生抖动的情况出现。然而因为没有具体的日志信息,所以并不能完全确认猜测是否正确。但是,通过问题的分析可以进一步明白 HBase 重试机制以及部分客户端参数优化策略,这也是写这篇文章的初衷之一。
3. 再来看看这把全局锁到底是什么锁,查看源码可知这把锁是下图中红框中的 regionLockObject 对象:
参考源码注释可知,这把锁是为了防止同时多线程并发加载 meta 分区。全局锁代码块首先会从缓存中查找 meta 分区,如果不存在会执行 prefetchRegionCache 方法远程查找并写入缓存,因此如果第一个线程成功加载 meta 分区数据并写入缓存,后来线程可以直接使用。
正常情况下, prefetchRegionCache 方法只有在缓存不存在的情况下会执行,如果此时网络不存在问题,远程查找 meta 分区信息会很快完成,持锁时间也会很短。一旦网络出现长时间抖动,就有可能出现这把锁一直被持有,阻塞其他线程。
HBase Rpc 重试机制
通过上文分析可知, HBase 的重试机制是这次异常发生的关键点,有必要对其进行一次解析。 HBase 执行 rpc 失败之后会执行重试操作,
重试的最大次数可以通过配置文件配置,对应的参数为 hbase.client.retries.number,0.98 版本中该参数的默认值为 31 。
同时每两次重试之间会 sleep 一段时间,即上文提到的 expectedSleep 变量,该变量实现具体算法如下:
其中 RETRY_BACKOFF 是一个重试系数表,由小到大递增表示重试时间会随着重试次数逐渐递增。 pause 变量可以通过配置文件配置,对应的参数为 hbase.client.pause,0.98 版本中该参数的默认值为 100 。
暂时忽略 jitter 这个小随机变量,默认情况下最大的重试间隔休眠时间 expectedSleep = 100 * 200 = 20s 。默认重试次数为 31 ,则每次连接集群重试之间的暂停时间将依次为:
[ 100 , 200 , 300 , 500 , 1000 , 2000 , 4000 , 10000 , 10000 , 10000 , 10000 , 20000 , 20000 ,…,20000 ]
这意味着客户端将在 448s 内重试 30 次,然后放弃连接到集群.
客户端参数优化实践
很显然,根据上面第二部分和第三部分的介绍,一旦在网络出现抖动的异常情况下,默认最差情况下一个线程会存在 8min 左右的重试时间,从而会导致其他线程都阻塞在 regionLockObject 这把全局锁上。为了构建一个更稳定、低延迟的 HBase 系统,除过需要对服务器端参数做各种调整外,客户端参数也需要做相应的调整:
1. hbase.client.pause:默认为 100 ,可以减少为 50
2. hbase.client.retries.number:默认为 31 ,可以减少为 21
修改后,通过上面算法可以计算出每次连接集群重试之间的暂停时间将依次为:
[ 50 , 100 , 150 , 250 , 500 , 1000 , 2000 , 5000 , 5000 , 5000 , 5000 , 10000 , 10000 ,…, 10000 ]
客户端将会在 2min 内重试 20 次,然后放弃连接到集群,进而会再将全局锁交给其他线程,执行其他请求。
总结
这篇文章从一个客户端异常入手,通过堆栈分析、源码追踪、合理推断,一方面整理分享程序异常的定位方法,一方面介绍 HBase Rpc 的重试机制以及客户端参数优化。
更多技术分享,请关注网易视频云官方网站( http://vcloud.163.com/) 或者网易视频云官方微信( vcloud163 )进行交流与咨询