在 HBase: The Definitive Guide 中, Lars George 介绍了 HBase 的一个新特性 Counter Increment,即把一个 column 当作 一个 counter,这样便于给某些在线应用提供实时统计功能。(PS:比如帖子的实时浏览量:PV)
传统上,如果没有 counter,当我们要给一个 column 的值 +1 或者其他数值时,就需要先从该 column 读取值,然后在客户端修改值,最后写回给 Region Server,即一个 Read-Modify-Write (RMW) 操作。在这样的过程中,按照 Lars 的描述1,还需要对操作所在的 row 事先加锁,事后解锁。这会引起许多 contention,以及随之而来的很多问题。而 HBase 的 increment 接口就保证在 Region Server 端原子性的完成一个客户端请求。
至于 increment 的性能如何,我们只有做测试才能知道。YCSB 已经提供了 Read-Modify-Write 的测试接口,而 increment 接口需要自己完成2。
然后新建 YCSB 的 workload 配置文件:
recordcount=12000
operationcount=12000000
workload=com.yahoo.ycsb.workloads.CoreWorkload
readallfields=false
incrementproportion=1
readproportion=0
updateproportion=0
scanproportion=0
insertproportion=0
requestdistribution=zipfan
recordcount=12000
operationcount=12000000
workload=com.yahoo.ycsb.workloads.CoreWorkload
readallfields=false
readproportion=0
updateproportion=0
scanproportion=0
insertproportion=0
readmodifywriteproportion=1
requestdistribution=zipfan
测试放在集群上进行:1台 master,6台 regionserver,独立的 zookeeper 集群;底层 HDFS 与 HBase 重叠。当前每个 Region Server 负责 255 左右的 region。YCSB 在另一台机器上,开 120 个线程。测试结果如下。
首先看 throughput:
测试图说明测试的时间基本上已经足够长,性能的波动也有反映出来。RMW 虽然波动较大,但总体性能居然优于 Increment,这是比较意外的事情。而 Increment 的表现非常稳定。从箱线图的角度来看也一致。Latency 的情况类似。
最后对测试结果做下简单的分析。出乎意料,RMW 没有想象的那么差,不过中间波动的情况值得深究一下;Increment 的平均 latency 在 10ms 左右,比 RMW 的 15ms 要好,而且几乎没有性能波动现象。
仔细分析 YCSB 的 RMW 操作的代码,它其实只是简单的将 read() 和 update() 封装起来3:
db.read (table,keyname,fields, new HashMap < String,String > ( ) ) ;
db.update (table,keyname,values ) ;
它并没有对所操作的 row 进行加锁、解锁操作,而是简单的读取改写。这在 counter 的应用场景中是不可接受的。不加锁在大并发情况下,很容易导致 counter 的值与预期不符。
继续修改 YCSB,由发起请求的客户端对相应的 row 加锁4。之后再进行 Increment 与 RMW 之间的性能比较。
可以预期的是新的 RMW 的性能会非常的差。这里给个测试结果的片段:
200 sec: 15818 operations; 91.3 current ops/sec; [RMW AverageLatency(ms)=2903.83]
210 sec: 15818 operations; 0 current ops/sec;
220 sec: 15818 operations; 0 current ops/sec;
230 sec: 16880 operations; 106.2 current ops/sec; [RMW AverageLatency(ms)=3136.13]
240 sec: 16880 operations; 0 current ops/sec;
250 sec: 16880 operations; 0 current ops/sec;
260 sec: 17747 operations; 86.69 current ops/sec; [RMW AverageLatency(ms)=4262.21]
270 sec: 17747 operations; 0 current ops/sec;
280 sec: 17747 operations; 0 current ops/sec;
290 sec: 18412 operations; 66.5 current ops/sec; [RMW AverageLatency(ms)=5508.39]
在这里修改 RMW 中的关键代码在于:
lock = _hTable.lockRow (Bytes.toBytes (key) ) ;
r = _hTable.get (g ) ;
_hTable.put (p ) ;
_hTable.unlockRow (lock ) ;
即在读之前先加锁,写之后放锁。之前的 RMW 的瓶颈在 Read 操作,并且 put 操作可以在 server 端批处理;而这里,锁的引入导致线程间的 contention 陡增。也就是说,线程数量的增加不一定会带来性能的提升,可能反而使性能变差。我们可以以线程数为变量做些测试(以 100s 为限)。
可以看到当线程数超过 16 之后,throughput 反而稍微变差了,更糟糕的是,latency 大幅度的提升。当线程数定在 128 时,情况就变的非常糟糕:latency 基本上是几秒,或者 NA;而 throughput 只能维持在 200 左右。这验证了之前的想法。
所以 HBase 引入 Increment/Counter 是非常重要的,对某些需要原子性更改操作的应用来说则是“致命”的。除了单个 increment 的接口 incrementColumnValue() 外,还有批量 increment 的接口increment(Increment),方便客户端调用。
除此之外,HBase 还在进行 Coprocessor 的开发,使计算直接在 Region Server 上进行,省去了繁琐耗时的数据移动。(PS:这就是所谓的移动计算比移动数据更划算)
Tips:以上测试数据由于 Hbase 版本很老,加上作者硬件、集群规模限制,仅供参考~