1. 背景
在用arthas神器来诊断hbase异常进程这篇文章中,我详细地记录了一起生产环境中使用HBase的事故,事故发生的大致起因是,一个异常scan导致CPU使用率飙升至百分之百,且巨高不下,从而导致整个集群宕机。(用arthas神器来诊断HBase异常进程)
虽然,借助于arthas这个神器,我们很轻易地就定位到了是scan的问题。而且事后,我们在业务层面上也采取了很多的优化手段。但是对于这个罪魁祸首,却一直没有找到一个完美的解决方案,总不能让业务那边一用scan就战战兢兢,如履薄冰吧。
在上篇文章的最后,靠着匮乏的多线程功底,对于scan这个问题,我写了写自己的看法。
![7c83e538b096528af147676d0572f710.png](https://i-blog.csdnimg.cn/blog_migrate/0b8986186c64741e8890e7fe9982caad.png)
事故发生的最本质的原因是scan的线程持续地占用着CPU的资源,且不会被释放,我们在业务日志中甚至能看到,某些scan能持续几个小时,这就太令人匪夷所思了。
原因渐渐明晰,就可以对症下药了。HBase提供了几个请求队列相关的核心参数,主要用于设置读写线程配比和进行读写队列隔离,以及get和scan的队列隔离。初看这些个参数的解释,还真搞不明白其中,队列、队列隔离的概念是什么。
在没有看源码之前,我简单地以为HBase服务端处理客户端的读写请求的时候,就是两个线程池,分别处理读和写的请求。甚至,如果你不做读写隔离的话,那就是一个线程池,一个线程是处理所有的读写请求;如果设置了读写隔离,就是两个线程池分别工作;如果进一步设置了get和scan的隔离,那么,就是三个线程池。这三个线程池,分别处理写请求,get请求,scan请求。每种请求所占的线程池资源肯定是不一样的,例如:scan比较耗时,那么,就应该控制其核心线程数的大小,如果此类值设置的过大,耗时的scan请求肯定会占用大量CPU,尤其是发生全表扫描的情况。
在看了源码之后,虽然HBase server端用的不是线程池,但也差不多,反正用的是线程,?。其中队列什么的,应该就是类似于线程池中的队列概念,简单点,就是一个阻塞队列,线程池容纳不了了,就丢到不同的队列中等着呗。所谓队列要分离,那更好理解了,假如现在有三个窗口,卖菜,卖馒头,卖包子的。如果不隔离,仨窗口合一个,那队伍得多长。如果隔离开,我想买包子,就去卖包子的窗口等着。
下面我从源码的角度,具体分析下请求队列的相关参数,是怎么来控制HBase服务端线程池、队列等的初始化的。自己的一些粗浅的理解,有错误下方留言我及时更正?。
2. 在IDEA中搭建HBase1.2.0的源码阅读环境
虽然我们线上的HBase版本是2.1.0,但是这一块的代码,跟HBase1.2.0相比应该没有太大的改变,后续测试的时候也能证实。我是在IDEA中DEBUG HMaster
进程,在hbase-default.xml
配置HBase请求队列的相关参数,来观察线程的初始化情况。有关HBase1.2.0怎么在IDEA中debug,公众号的历史文章中有涉及,感兴趣的朋友可以参考。
HBASE源码导入IDEA并开启DEBUG调试
3. HBase请求队列的相关参数
我在hbase-default.xml配置的参数是:
<property>
<name>hbase.regionserver.handler.countname>
<value>100value>
<description>默认为30,服务器端用来处理用户请求的线程数。生产线上通常需要将该值调到100~200。description>
property>
<property>
<name>hbase.ipc.server.callqueue.handler.factorname>
<value>0.1value>
<description>为0则共享全部队列,服务器端设置队列个数,假如该值为0.1,
那么服务器就会设置handler.count * 0.1 = 30 * 0.1 = 3个队列description>
property>
<property>
<name>hbase.ipc.server.callqueue.read.rationame>
<value>0.5value>
<description>默认为0,服务器端设置读写业务分别占用的队列百分比以及handler百分比。
假如该值为0.5,表示读写各占一半队列,同时各占一半handler
description>
property>
<property>
<name>hbase.ipc.server.callqueue.scan.rationame>
<value>0.2value>
property>
上述参数的配置值以及说明,参考的是范老师的HBase原理与实践——第12章。按照参数的说明,最终我们可以计算出以下值:
handler.count 是100
队列个数是100 x 0.1 = 10
写队列 10 x 0.5 = 5
读队列总值 10 x 0.5 = 5
get 队列 5 - 5 x 0.2 = 4
scan队列 5 x 0.2 = 1
参数设置完毕,我们就可以在源码层面观察这些值的变化和线程以及队列是如何初始化的啦。
4. debug源码观察队列参数是如何作用的
4.1SimpleRpcScheduler
这几个参数是在哪个类中被初始化的呢?我也不知道,只能在IDEA中Find In Path,搜一下hbase.ipc.server.callqueue.handler.factor
,找到的类是,SimpleRpcScheduler。其父类是RpcScheduler。RpcScheduler应该就是处理RPC相关的类了,该类的相关UML图如下。