消灭毛刺!HBase2.0全链路offheap效果拔群

阿里云HBase2.0版本正式上线

阿里云HBase2.0版本是基于社区2018年发布的HBase2.0.0版本开发的全新版本。在社区HBase2.0.0版本基础上,做了大量的改进和优化,吸收了众多阿里内部成功经验,比社区HBase版本具有更好的稳定性和性能,同时具备了HBase2.0提供的全新能力。HBase2.0提供的新功能介绍可以参照这篇文章。如果想要申请使用全新的HBase2.0版本,可以在此链接申请试用。在HBase2.0提供的众多功能中,最引人注目的就是全链路的offheap能力了。根据HBase社区官方文档的说法,全链路的offheap功能能够显著减少JVM heap里的数据生成和拷贝,减少垃圾的产生,减少GC的停顿时间。


在线业务在使用hbase读写数据时,我们可能会发现,HBase的平均延迟会很低,可能会低于1ms,但P999延迟(99.9%请求返回的最大时间)可能会高达数百ms。这就是所谓的"毛刺",这些毛刺可能会造成我们的在线业务出现部分请求超时,造成服务质量的下降。而对于HBase来说,GC的停顿,很多时候是造成这样的毛刺的“罪非祸首”。那HBase2.0中的全链路offheap对减少GC停顿,降低P999延迟,真的有那么神奇的功效吗?

全链路offheap原理

在HBase的读和写链路中,均会产生大量的内存垃圾和碎片。比如说写请求时需要从Connection的ByteBuffer中拷贝数据到KeyValue结构中,在把这些KeyValue结构写入memstore时,又需要将其拷贝到MSLAB中,WAL Edit的构建,Memstore的flush等等,都会产生大量的临时对象,和生命周期结束的对象。随着写压力的上升,GC的压力也会越大。读链路也同样存在这样的问题,cache的置换,block数据的decoding,写网络中的拷贝等等过程,都会无形中加重GC的负担。而HBase2.0中引入的全链路offheap功能,正是为了解决这些GC问题。大家知道Java的内存分为onheap和offheap,而GC只会整理onheap的堆。全链路Offheap,就意味着HBase在读写过程中,KeyValue的整个生命周期都会在offheap中进行,HBase自行管理offheap的内存,减少GC压力和GC停顿。


写链路的offheap包括以下几个优化:

  1. 在RPC层直接把网络流上的KeyValue读入offheap的bytebuffer中
  2. 使用offheap的MSLAB pool
  3. 使用支持offheap的Protobuf版本(3.0+)

读链路的offheap主要包括以下几个优化:

  1. 对BucketCache引用计数,避免读取时的拷贝
  2. 使用ByteBuffer做为服务端KeyValue的实现,从而使KeyValue可以存储在offheap的内存中
  3. 对BucketCache进行了一系列性能优化

对比测试

全链路offheap效果怎么样,是骡子是马,都要拿出来试试了。测试的准备工作和相关参数如下:

HBase版本

本次测试选用的1.x版本是云HBase1.1版本截止目前为止最新的AliHB-1.4.9版本,2.x版本是云HBase2.0版本截止目前为止最新的AliHB-2.0.1。这里所有的版本号均为阿里内部HBase分支——AliHB的版本号,与社区的版本号无任何关联。

机型

所有的测试都是针对一台8核16G的ECS机器上部署的RegionServer。底层的HDFS共有两个datanode(副本数为2),其中一个与该RegionServer部署在同一台。每个datanode节点挂载了4块150GB的SSD云盘

测试工具

本次测试所用的是hbase自带的pe工具,由于原生的PE工具不支持不支持单行put和指定batch put数量,因此我对PE工具做了一定的改造,并回馈给了社区,具体内容和使用方法参见这篇文章

表属性

测试表的分区为64个,compression算法为SNAPPY,Encoding设置为NONE。所有的region都只在一台RegionServer上。

相关的HBase参数

共同参数

  • HBase的heap大小为9828MB,其中新生代区大小为1719MB
  • 使用的GC算法为CMS GC,当老年代占用大小超过75%时开始CMS GC。
  • hfile.block.cache.size 为0.4, 也就是说默认的lru cache的大小为3931.2MB
  • hbase.regionserver.global.memstore.size 为0.35, 即默认的memstore的大小为3439.8MB
  • 开启了读写分离,在做写相关的测试时,写线程为90个,读线程为10个。在做读相关测试时(包括读写混合),写线程为20个,读线程为80个

HBase2.xoffheap相关参数

在测写场景时,使用了HBase2.x的默认参数,即只开启了RPC链路上的offheap,并没有开始memstore的offheap。因为根据测试,我们发现开启memstore的offheap并没有带来多大改善,究其原因,还是因为Memstore的offheap只是把KeyValue数据offheap,而Memstore本身使用的Java原生的ConcurrentSkipListMap,其索引结构会在JVM的heap中产生大量的内存碎片,因此只把KeyValue offheap的效果并不是很明显。毕竟,在HBase-1.x开始,就有了MSLAB来管理Memstore中的KeyValue对象,内存结构已经比较紧凑。

在测读场景时:

  • hbase-env.sh中设置HBASE_OFFHEAPSIZE=5G (RPC和HDFS 客户端需要部分DirectMemory)
  • hbase.bucketcache.ioengine 调成offheap
  • hbase.bucketcache.size 调成 3911,即使用3911MB的DirectMemory来做L2 的cache来 cache data block(之前的测试发现L1中meta block index block的大小大约为20MB,所以在原来onheap的cache基础上减去了20MB)
  • 由于cahce的一部分放入offheap,heapsize减至6290MB
  • block cache的比例不变,用来做L1 cache来cache META block(可能远远大约meta block的需求,但测试中只需保证meta block 100%命中即可,大了不会影响测试)

注意,本次测试旨在测试HBase2.x与HBase1.x版本在相同压力下延迟和GC的表现情况,并非测试HBase的最大吞吐能力,因此测试所用的客户端线程数也只限制在了60~64个,远没有达到云HBase的最大吞吐能力

单行写场景

单行写测试时使用PE工具开启64个写线程,每个写线程随机往HBase表中写入150000行,共960w行。每行的value size为200bytes。所用的PE命令为

hbase pe --nomapred --oneCon=true --valueSize=200 --compress=SNAPPY --rows=150000 --autoFlush=true --presplit=64 randomWrite 64
版本 TPS AVG RT 95% RT 99% RT 99.9% RT MAX RT
AliHB-1.4.9 38737 1.1ms 2ms 2ms 45ms 140ms
AliHB-2.0.1 40371 0.7ms 1ms 2ms 5ms 140ms
版本 AVG younggc Time younggc GC频率
AliHB-1.4.9 90ms 0.6次/s
AliHB-2.0.1 110ms 0.28次/s

可以看到,使用了HBase-2.x的写链路offheap后,单行写的P999延迟从45ms降低到了5ms,效果非常明显。同时吞吐有5%的提升,带来这种效果的原因就是写链路的offheap使HBase在heap的young区减少了临时对象的产生,younggc发生的频率从0.6次每秒降低到了0.28次每秒。这样受到younggc影响的请求量也会大大减少。因此P999延迟急剧下降.

批量写

在批量写测试中,一次batch的个数是100。使用的命令为:

hbase pe --nomapred --oneCon=true --valueSize=200 --compress=SNAPPY --rows=200000 --autoFlush=true --presplit=64 --multiPut=100 randomWrite 64

测试的场景和参数配置与单行写保持一致

版本 TPS AVG RT 95% RT 99% RT 99.9% RT MAX RT
AliHB-1.4.9 81477 72ms 110ms 220ms 350ms 420ms
AliHB-2.0.1 ​97985 ​67ms 75ms 220ms ​280ms 300ms
版本 AVG younggc Time younggc GC频率
AliHB-1.4.9 120ms 0.6次/s
AliHB-2.0.1 180ms 0.28次/s

可以看到,使用了HBase-2.x的写链路offheap后,从平均延迟到最大延迟,都有不同程度的下降,GC的频率也降到1.x版本的一半以下。因此吞吐也上涨了20%。

100%Cache命中单行Get

在此场景中,先使用以下命令先往表中灌了120w行数据

hbase pe --nomapred --oneCon=true --valueSize=200 --compress=SNAPPY --rows=200000 --autoFlush=true --presplit=64 --multiPut=100 sequentialWrite  60

再保证所有数据刷盘,major compact成一个文件后,先做cache的预热,然后使用如下命令进行单行读取:

 hbase pe --nomapred --oneCon=true --rows=200000 randomRead   60

测试结果如下:

版本 QPS AVG RT 95% RT 99% RT 99.9% RT MAX RT
AliHB-1.4.9​ 53895 ​0.04ms ​1ms ​1ms ​1ms 30ms
AliHB-2.0.1​ 49518 0.05ms ​​0ms ​1ms ​1ms ​14ms

注:百分比的延迟统计最低分辨率是1ms,所以低于1ms时会显示为0

版本 AVG younggc Time younggc GC频率
AliHB-1.4.9​ ​25ms 0.4次/s
AliHB-2.0.1​ 8ms 0.35次/s

可以看到,在100%内存命中场景下,HBase2.x的吞吐性能有了8%的下滑。这是预料之中的,这在HBase的官方文档中也有解释:读取offheap的内存会比读onheap的内存性能会稍稍下滑。另外,由于在100%内存命中的场景下,onheap的cache也不会发生置换,所以产生的gc开销会比较小,所以在这个场景中,HBase1.x版本的P999延迟也已经比较低。但是,在这个GC不会很严重的场景里(没有写,没有开Block-encoding,cache里内容不用decode可以直接使用),HBase2.x版本仍然可以把最大延迟降到1.x版本的一半,非常难能可贵。

部分cache命中单行读

在这个场景中,先使用以下命令往表中灌了3600w行数据,这些数据会超过设置的cache大小,从而会产生一定的cache miss。
灌数据:

hbase pe --nomapred --oneCon=true --valueSize=200 --compress=SNAPPY --rows=600000 --autoFlush=true --presplit=64 --multiPut=100 sequentialWrite  60

再保证所有数据刷盘,major compact成一个文件后,先做cache的预热,然后使用如下命令进行单行读取:

hbase pe --nomapred --oneCon=true --rows=600000 randomRead   60
版本 QPS AVG RT 95% RT 99% RT 99.9% RT MAX RT
AliHB-1.4.9​​ 14944 ​2.5ms ​5ms 80ms 200ms ​300ms
AliHB-2.0.1​ ​15372 ​1.7ms 5ms ​34ms 65ms ​130ms
版本 AVG younggc Time younggc GC频率 CMS GC Remark AVG Time CMS GC 频率
AliHB-1.4.9​​ 80ms 2.4次/s 50ms 0.25次/s
AliHB-2.0.1​ 21ms 2.5次/s 0 0

在部分cache命中的场景中,由于会有一定的cahce miss,在读的过程中,会产生cache内容的置换。如果这些内存的置换发生在heap里,会显著加重GC的负担。因此,在这个GC压力比较大的场景中,HBase2.x的全链路读offheap产生了非常优秀的效果,无论是吞吐,平均延迟还是P999和最大延迟,都全面超越HBase1.x版本。由于cache不会在heap中产生垃圾,因此GC的频率和耗时都显著降低,基本消灭了CMSGC。更加难能可贵的是,使用了offheap的bucketcache由于每个bucket都是固定大小,因此在放入不定大小的data block时不可能完全放满,从而会造成一些空间的浪费。因此虽然我把两者的cache大小调到一样的大小,HBase1.x的测试中,data block的命中率有58%,HBase2.x的测试中命中率只有40%。也就是说,HBase2.x在命中率更低的情况下,取得的吞吐和延迟都更加优秀!但这从另外一个方面说明,同样的内存大小,在使用offheap功能后,cache的命中率会降低,因此使用offheap时最好使用速度更高的介质做存储,比如本次测试中选用的SSD云盘。保证读取速度不会被落盘而拖慢太多。

读写混合测试

读写混合测试是大部分生产环境中面对的真实场景。大批量的写和部分命中的读都会产生GC压力,两者一起发生,GC压力可想而知。
在这个测试中,灌数据和读取和部分cache命中场景中使用的命令一致。只不过在读取的同时,在另外一台客户端上起了一个20个线程的批量写测试,去写另外一个Table

hbase pe --nomapred --oneCon=true --valueSize=200 --table=WriteTable  --compress=SNAPPY --blockEncoding=DIFF  --rows=600000000 --autoFlush=true --presplit=64 --multiPut=100 randomWrite 20
版本 QPS AVG RT 95% RT 99% RT 99.9% RT MAX RT
AliHB-1.4.9​ 3945 ​11ms 5ms 180ms ​8700ms ​9000ms
AliHB-2.0.1​ 12028 ​2ms ​5ms ​45ms ​100ms 250ms

注:表中的QPS指的是读的吞吐

版本 AVG younggc Time younggc GC频率 CMS GC Remark AVG Time CMS GC 频率 Full GC time(concurrent mode failure)
AliHB-1.4.9​ ​ 80ms 0.7次/s ​/(绝大部分退化成full gc) ​0.08次/s 约7~9s
AliHB-2.0.1 ​40ms 2.1次/s ​/ ​几乎为0

在读写混合测试中,在此压力下,CMS GC的速度已经跟不上heap中产生的垃圾的速度。因此在发生CMS时,由于CMS还没完成时old区已经满(concurrent mode failure),因此CMS GC都退化成了Full GC,从而产生了7到9s的‘stop the world’停顿。因此,1.x中P999被这样的Full GC影响,P999已经上升到了8700ms。而由于HBase2.x使用了读链路offheap。在此场景中仍然稳如泰山,CMS GC发生的频率几乎为0。所以在读写混合场景中,HBase2.x的吞吐是HBase1.x的4倍,P999延迟仍然保持在了100ms之内!

总结

通过上面的测试,我们发现HBase2.x的全链路offheap功能确实能够降低GC停顿时间,在各个场景中,都显示出了非常显著的效果。特别是在部分cache命中和读写混合这两个通常在生产环境中遇到的场景,可谓是效果拔群。所以说HBase2.x中的全链路offheap是我们在生产环境中去降低毛刺,增加吞吐的利器。

云端使用

HBase2.0版本目前已经在阿里云提供商业化服务,任何有需求的用户都可以在阿里云端使用深入改进的、一站式的HBase服务。云HBase版本与自建HBase相比在运维、可靠性、性能、稳定性、安全、成本等方面均有很多的改进,欢迎大家通过下面的连接申请使用阿里云HBase2.0版本,使用全链路offheap这个利器去给生产服务带来更好的稳定性和服务质量。
https://www.aliyun.com/product/hbase

上一篇:关于MySQL中的一些极限值的初步验证纠错


下一篇:PostgreSQL sharding : citus 系列6 - count(distinct xx) 加速 (use 估值插件 hll|hyperloglog)