HBase限流机制源码分析

master启动的时候会初始化MasterQuotaManager,并启动该manager;

MasterQuotaManager实现了RegionStateListener接口,可以监听region的状态变化,RegionStateListener接口中定义了三个事件,分别是onRegionSplit,onRegionSplitReverted,onRegionMerged。

MasterQuotaManager中包括了三把锁,分别是namespaceLocks、tableLocks和userLocks。

MasterQuotaManager中包括一个namespaceQuotaManger,namespaceQuotaManger用于在namespace下的table以及table region发生变化时,能够保持住namespace的quota不变。

用户的setQuota请求是路由到master上去执行的。

MasterQuotaManager负责quota表的创建管理,并根据用户命令在quota表中新增、修改或者删除数据,主要的方法就是setQuota(final SetQuotaRequest req),方法中由于使用NamedLock,因此对同一namespace、table和user,同一时刻只有一个修改可被执行。

Quota是被缓存在cache(一个ConcurrentHashMap的数据结构)中的,分别是namespaceQuotaCache、tableQuotaCache和userQuotaCache,由QuotaCache类管理,这个类的成员变量QuotaRefresherChore继承了ScheduledChore,会定期调度,默认的调度周期是5分钟(REFRESH_DEFAULT_PERIOD),此外还有个淘汰因子(EVICT_PERIOD_FACTOR,默认是5)。

QuotaCache只要用于缓存在RegionServer中,RegionServerQuotaManager每从cache中取出一条数据,该条数据的lastquery就会更新为当时的取出时间,如果一条数据长期没有被query,当时间超过了evictPeriod(淘汰因子*调度周期,25分钟),那么这条数据就会被置换出内存。

cache中的每条记录都记录了lastUpdate,这个字段的值是该记录从quota table中读出时的当时时间,如果一条的lastUpdate超过了refreshPeriod(5分钟),那么这条缓存会失效并从table重新读取更新;

Regionserver启动之后会新创建RegionServerQuotaManager并启动它,RegionServerQuotaManager的主要作用就是维护了一份Quota缓存(也就是上面说到的QuotaCache),并定期更新缓存中的值,RegionServer对外提供了getQuota和checkQuota两个方法,其中:

RSRpcServices中的get/mutate/scan三个方法执行前会调用RegionServerQuotaManager的checkQuota,checkQuota从用户请求中解析出用户名,用户组,用户请求的Table等信息,首先根据这些信息去获取当前可用的配额信息(getQuota方法)。

这块有个非常复杂的设计,我们前面讲过,配额是缓存在QuotaCahe的三个Map中,这些map的value是QuotaState类型的对象,当缓存更新也就是调用相应的fetchXXXQuotaState方法时,会从Table中逐一读出用户设置的quota值,并对每一条读出的结果通过setQuotas,注入到QuotaState的globalLimiter中,返回的globalLimiter是TimeBasedLimiter(实现了QuotaLimiter接口)类型的对象。

TimeBasedLimiter这个对象中管理了所有的限额数据,包括reqsLimiter/reqSizeLimiter/writeSizeLimiter/writeSizeLimiter/readReqsLimiter/readSizeLimiter。

再说回到checkQuota,checkQuota拿到了限额信息就是前面说到globalLimiter,也就是QuotaLimiter类型的对象。

用户A访问namespace B下的Table C,这种场景下,getQuota方法会同时拿到A、B、C三者的QuotaLimiter,将三者包装成一个DefaultOperationQuota返回,然后调用DefaultOperationQuota的checkQuota以检查请求是否超过了限额。checkQuota的方法主体如下图:

HBase限流机制源码分析

方法会将用户请求折算成writeSize或者readSize,然后与已有配额作对比(也就是canExecute方法的内容),如果write&readSize超过了配额大小,那么canExecute会返回false。那么此时服务端会抛出ThrottlingException异常,这个异常会一直向上抛回给客户端。

注意限额是会定期refill的,refill的逻辑在RateLimiter的refill方法中,此方法的意义如下面这段注释中所说:

/**

* This limiter will refill resources at every TimeUnit/resources interval. For example: For a

* limiter configured with 10resources/second, then 1 resource will be refilled after every 100ms

* (1sec/10resources)

*/

如果请求的size小于限额,那么canExecute返回true,并调用limiter的grabQuota方法更新各个limiter已消耗的配额。checkQuota方法返回,用户请求继续执行,注意的checkQuota方法在用户请求路径上的第一步,也就是服务端ipc收到用户请求后首先检查配额是否满足,如果不满,就抛异常拒绝掉请求,此时请求未对服务端的内存,磁盘io,网络io等造成消耗。

总结起来,HBase服务端的限流设计大体上如下图中所示:

HBase限流机制源码分析

主要要点有以下几点:

1、请求限额既可以限制单位时间的请求数,也可以限制请求量,如读次数/s,可读MB/s等等;

2、服务端接收到用户请求后第一步就是查看配额是否足够,如果充足,请求继续执行,否则抛回给客户端ThrottlingException异常;

3、每接收一个请求,配额会做相应的扣减,同时配额会定期refill,refill逻辑取了配额的倒数,既10次/s的配额,那么每100ms会恢复一个配额;

4、RegionServerQuotaManager的缓存更新逻辑包含两层,QuotaCache中的数据每5分钟更新一次,每25分钟将QuotaCache这段时间内未touch的数据删除掉;

HBase自带的限流规则对请求类型的区分比较粗,只区分了Read&Write,对Read中进一步的Scan&Get则未区别对待,在实际使用中,我们发现scan请求由于服务端处理时间较长,因此占用handler的时间也就长,导致其他请求因为获取不到handler而被阻塞。而大量的put可能造成的后果是memstore频繁地被写满,导致RegionServer的flush频繁发生,这一方面增多了因flush导致的内存碎片,另一方面在hdfs中积累了大量的小文件,既增大了regionserver管理这些hfile文件的内存压力,也增大了发生major compact的可能。

因此使用建议如下:

  • 通过quota的方式对写入频繁的表做限流控制;
  • 通过设置hbase.ipc.server.callqueue.read.ratio和hbase.ipc.server.callqueue.scan.ratio对hbase的scan占用的handler进行限制(取0到1之间的值,默认是0);
  • get请求不用做限制,get请求往往时效性较高,但对服务端资源占用较小;
  • 引导用户设置合理的hbase.client.retries.number,至少应大于1;