背景介绍
在HBase1.1.0发布之前,HBase同一集群上的用户、表都是平等的,没有优劣之分。这种’大同’社会看起来完美,实际上有很多问题。最棘手的主要有这么两个,其一是某些业务较其他业务重要,需要在资源有限的情况下优先保证核心重要业务的正常运行,其二是有些业务在某些场景下会时常’抽风’,QPS常常居高不下,严重消耗系统资源,导致其他业务无法正常运转。
这实际上是典型的多租户问题,社区针对这个问题提出了相应的应对措施,主要有如下三点:
(1)资源限制,主要针对用户、namespace以及表的QPS和请求大小进行限制,详见HBase-11598
(2)资源调度,主要针对任务进行优先级调度,通常会优先调度实时交互而且小的任务,而批量操作任务或者长时间操作任务(大scan)优先级相对较低,详见HBase-10993
(3)资源隔离,将不同表通过物理隔离的方式分布到不同的RegionServer上,详见HBase-6721
本文将会重点介绍HBase中的资源限制方案 – Quotas,主要对其使用方式、实现原理进行介绍,并对其实际效果通过实践进行验证。另外,本文还会对HBase的资源调度原理进行简单介绍,并对主要配置进行讲解。
资源限制-Quotas
Quotas使用条件
(1)HBase版本在1.1.0以上,或者低版本HBase应用了对应的Patch(HBase-11598)
(2)Quotas功能默认是关闭的,需要在配置文件hbase-site.xml中通过设置hbase.quota.enabled为true打开。设置完成之后,需要重启HMaster才能生效。
Quotas语句详解
hbase> set_quota TYPE => THROTTLE, THROTTLE_TYPE => READ, USER => 'u1', TABLE => 't2', LIMIT => '10req/sec'
(1)Quotas分别支持表级别以及用户级别资源限制,或者同时支持表级别和用户级别,如示例所示
(2)THROTTLE_TYPE可以取值READ / WRITE,分别对随机读和随机写进行限制
(3)LIMIT 可以从两个维度对资源进行限制,分别为req/time 和 size/time,前者限制单位时间内的请求数,后者限制单位时间内的请求数据量。需要指明的是time的单位可以是sec | min | hour | day,size的单位可以是B(bytes) | K | M | G | T | P,因此LIMIT可以表示为’1000req/min’或者’100G/day’,分别表示’限制1分钟的请求数在1000次以内’,’限制每天的数据量为100G’
常用Quotas语句
hbase> set_quota TYPE => THROTTLE, TABLE => 't1', LIMIT => '1000req/sec'
hbase> set_quota TYPE => THROTTLE, THROTTLE_TYPE => WRITE, USER => 'u1', LIMIT => '10M/sec'
(1)set_quota命令执行的限制都是针对单个RegionServer来说的,并不是针对整个集群
(2)set_quota命令默认执行后并不会立刻生效,需要等待一段时间才会生效,等待时间默认为5min。可以通过参数 hbase.quota.refresh.period 进行设置,比如可以通过设置
hbase.quota.refresh.period = 60000将生效时间缩短为1min
(3)可以通过命令list_quotas查看当前所有执行的set_quota命令
Quotas – 实现原理
原理很简单,如果请求数超过设置的Quota数,就抛出异常!有同学会说也没在日志中看到任何异常嘛,这是因为这类异常日志级别是debug,而默认的日志输出级别为info,可以通过调整log4j来查看。但是这类异常实在太多,没有必要输出。
Quotas – 实践效果
了解了Quotas的使用方法以及基本原理,是不是很想试一试它的功效,笔者在测试环境做了如下的测试:
集群规模 |
RS JVM内存配置
|
硬盘
|
HBase版本 |
YCSB版本
|
4台RegionServer
|
72G |
12 * 3.6T
|
1.1.2
|
0.8.0
|
2. 测试环境新建两张表,分别称为A和B。两张表的数据构成都相同,10亿条数据,每条数据500Bytes,总大小500G左右。
3. 分别使用两个YCSB客户端分别对这两张表执行读写混合操作(读写比为1:1),再然后对B表不断执行set_quota操作,对该表QPS进行限制。再分别观察A表和B表的QPS以及读延迟变化情况。
4. 为了方便理解,下面测试结果中A表称为Unthrottle_Table,B表称为Throttle_Table。测试结果如下:
通过测试基本可以看出,随着B表执行的QPS限制越来越严格,上图中Throttle_Table表对应的吞吐量(红色柱状图)越来越小,相应Unthrottle_Table表(紫色柱状图)对应的吞吐量却越来越大,这是因为B表执行QPS限制之后各种硬件资源就会更多地分配给A表。
总体来说,Quotas功能总体看来基本完成了资源限制的职能,达到了资源限制的目的。同时支持用户级别和表级别,另外同时支持请求大小和请求数量两个维度,基本涵盖了常见的资源限制维度;另外,易用性也是一大亮点,比较人性化,只需要在Shell界面上敲一行命令就可以搞定。
资源调度
在 0.99版本之前,HBase只提供了一种请求队列类型:FIFO队列,意为先到的请求会优先被处理,后到的请求需要等待之前的请求被处理完。这样的设计有一个致命的缺陷,就是在线交互式查询有可能会被离线大scan长时间阻塞,而从优先级的角度讲在线交互式查询无疑更加重要。
0.99版本之后,HBase将默认请求队列由FIFO类型改为了Deadline类型,用来解决上述缺陷。提起DeadLine队列,很多对Linux IO调度算法比较了解的同学并不陌生,Linux IO常用调度算法主要有Noop、CFQ(Completely Fair Queuing)以及Deadline,其中Noop调度算法基本可以认为就是FIFO算法,因此同样存在上述弊端;而CFQ算法会按照IO请求的地址进行排序,这样处理的目的在于尽量少地减少磁盘移动,实际效果来看确实极大的提升了IO的吞吐率,但是相比Noop,部分IO请求有可能会一直排到队尾,存在饿死的情况。Deadline算法首先将读写IO队列进行了分离,而且读IO优先级要高于写IO优先级;除此之外,它还会为每一个IO请求设置一个时间戳,用以判断请求是否长时间没有得到处理,进而需要优先处理。需要知道的是,对于常见数据库环境来说(Oracle,MySQL等),Deadline算法总是最佳选择。
那HBase新增的Deadline算法和Linux IO中Deadline算法是否一样呢?答案是肯定的,至少两者实现思路基本是一致的。接下来主要结合HBase请求调度源码对Deadline算法进行深入分析。
Deadline算法基于Deadline类型队列实现,Deadline类型队列和FIFO类型队列不同,属于优先级队列,里面的元素会按照优先级进行排序,优先级高的排在队首,优先级低的排在队尾。很显然,Deadline算法目标是使得在线交互式查询请求优先级更高,而离线长scan请求优先级更低。除此之外还有一个通常不会被注意的目标:不能出现任何请求被饿死!在弄懂具体的实现机制前,需要首先搞清楚一个问题:如何量化一个scan的请求长短?
如何量化一个scan的请求长短:这个问题的理解需要对scan的流程有一个大体认识,一次scan请求并不会将所有数据查询返回,这一方面是因为在数据量大的场景下诸如带宽之类的系统资源会被严重消耗,另一方面也有可能会因为数据量大导致客户端OOM。因此HBase实际上将一次scan请求分为多次连续的next小请求执行,每次查询纪录数用户可以配置,默认为100条。这样假如一次scan查询总纪录数为1000,每次查询返回100条,就需要10次客户端到服务器端的next请求。看到这里,很多童鞋已经明白,可以通过当前RPC请求次数(即next RPC调用次数)粗略地衡量scan的长短,比如当前scanA的RPC请求次数为10,scanB的RPC请求次数为5,就可以认为scanA长于scanB,那理论上scanA的这次请求优先级就会低于scanB的这次请求。
HBase在具体实现中会为每一个请求设置一个deadline(时间期限),代表这个请求的处理期限,deadline越小,请求优先级越高。
这个deadline参数是理解HBase资源调度的关键,它由两部分构成:后半部分的核心在于vtime,代表当前scan的next请求次数,可见vtime越大(scan越长),对应的deadline越大,优先级越低;因为设定get操作的vtime为0,因此同等条件下get操作优先级最高;可见,通过vtime就可以实现请求优先级功能。那对于长scan,会不会出现因为优先级太低长时间得不到处理饿死的情况呢?这就需要看看前半部分,timestamp表示请求点的绝对时间戳,设置绝对时间戳是为了保证该请求的deadline肯定早于5s(等式后面部分最大就是5s)之后所有请求的deadline,从而能够保证不会被饿死;
好吧,上面不是说Linux IO调度系统中Deadline算法还实现了读IO和写IO的分离,那HBase实现了么?当然,用户只需要通过简单的配置就不仅可以实现读请求和写请求的分离,还可以实现了scan请求的分离。
默认场景下,HBase只提供一个队列,所有请求都会进入该队列进行优先级排序。用户可以通过设置参数hbase.ipc.server.callqueue.handler.factor来设置多个队列,队列个数等于该参数 * handlercount,比如该参数设置为0.1,总的handlercount胃150,则会产生15个独立队列。
独立队列产生之后,可以通过参数 hbase.ipc.server.callqueue.read.ratio 来设置读写队列比例,比如设置0.6,则表示会有9个队列用于接收读请求,6个用于接收写请求;另外,可以通过参数 hbase.ipc.server.callqueue.scan.ratio 设置get和scan的队列比例,比如设置为0.1,表示1个队列用于scan请求,另外8个用于get请求;
总结
本文主要介绍了HBase中多租户实现中的两个重要手段:资源限制以及资源调度,对其工作原理以及使用方法进行了解析。后续再针对资源隔离这个重头戏进行深入解析~
本文转载自:http://hbasefly.com