韦德娱乐城HBase写入和读取优化

2019-11-15 21:13 来源:未知

首先我们简单回顾下整个写入流程

client api ==> RPC ==>  server IPC ==> RPC queue ==> RPC handler ==> write WAL ==> write memstore ==> flush to  filesystem

整个写入流程从客户端调用API开始,数据会通过protobuf编码成一个请求,通过scoket实现的IPC模块被送达server的RPC队列中。最后由负责处理RPC的handler取出请求完成写入操作。写入会先写WAL文件,然后再写一份到内存中,也就是memstore模块,当满足条件时,memstore才会被flush到底层文件系统,形成HFile。


       HBase中的写入方法有主要分为实时的put以及批量导入bulkload,这里主要介绍一下实时写入put以及一些HBase里面与MVCC相关的东西,版本依旧是社区版1.0.0。

在开发中,突然遇到了发现Hbase的读取和写入变的非常慢,然后再次检查了代码,有两个地方更改后性能有大幅的提升,说明如下。
网上优化可能很多,这个只是我简单的实践记录。

测试环境

当写入过快时会遇见什么问题?

写入过快时,memstore的水位会马上被推高。
你可能会看到以下类似日志:

RegionTooBusyException: Above memstore limit, regionName=xxxxx ...

这个是Region的memstore占用内存大小超过正常的4倍,这时候会抛异常,写入请求会被拒绝,客户端开始重试请求。当达到128M的时候会触发flush memstore,当达到128M * 4还没法触发flush时候会抛异常来拒绝写入。两个相关参数的默认值如下:

hbase.hregion.memstore.flush.size=128M
hbase.hregion.memstore.block.multiplier=4

或者这样的日志:

regionserver.MemStoreFlusher: Blocking updates on hbase.example.host.com,16020,1522286703886: the global memstore size 1.3 G is >= than blocking 1.3 G size
regionserver.MemStoreFlusher: Memstore is above high water mark and block 528ms

这是所有region的memstore内存总和开销超过配置上限,默认是配置heap的40%,这会导致写入被阻塞。目的是等待flush的线程把内存里的数据flush下去,否则继续允许写入memestore会把内存写爆

hbase.regionserver.global.memstore.upperLimit=0.4  # 较旧版本,新版本兼容
hbase.regionserver.global.memstore.size=0.4 # 新版本

当写入被阻塞,队列会开始积压,如果运气不好最后会导致OOM,你可能会发现JVM由于OOM crash或者看到如下类似日志:

ipc.RpcServer: /192.168.x.x:16020 is unable to read call parameter from client 10.47.x.x
java.lang.OutOfMemoryError: Java heap space

韦德娱乐城,HBase这里我认为有个很不好的设计,捕获了OOM异常却没有终止进程。这时候进程可能已经没法正常运行下去了,你还会在日志里发现很多其它线程也抛OOM异常。比如stop可能根本stop不了,RS可能会处于一种僵死状态。


       在regionserver服务端,与put相关的操作差不多最后都会调用到HRegion的doMiniBatchMutation(BatchOperationInProgress)方法,以下截图是从RSRpcServices的mutate方法一直到该方法的调用栈(从下往上看)。

1 读取优化

Scan操作时候设置缓存

    result.setCaching(10000)
    result.setBatch(5000)

测试硬件:4核i5处理器,8G内存,1T硬盘,千兆网络

如何避免RS OOM?

一种是加快flush速度:

hbase.hstore.blockingWaitTime = 90000 ms
hbase.hstore.flusher.count = 2
hbase.hstore.blockingStoreFiles = 10

当达到hbase.hstore.blockingStoreFiles配置上限时,会导致flush阻塞等到compaction工作完成。阻塞时间是hbase.hstore.blockingWaitTime,可以改小这个时间。hbase.hstore.flusher.count可以根据机器型号去配置,可惜这个数量不会根据写压力去动态调整,配多了,非导入数据多场景也没用,改配置还得重启。

同样的道理,如果flush加快,意味这compaction也要跟上,不然文件会越来越多,这样scan性能会下降,开销也会增大。

hbase.regionserver.thread.compaction.small = 1
hbase.regionserver.thread.compaction.large = 1

增加compaction线程会增加CPU和带宽开销,可能会影响正常的请求。如果不是导入数据,一般而言是够了。好在这个配置在云HBase内是可以动态调整的,不需要重启。

韦德娱乐城 1

2 写入优化

写入慢最后发现是没有控制一次发HBase的量,直接进行大量的数据一次性写入,造成HBase卡死的一样,更改如下:

           val realSize = batchData.size()
            if (realSize <= 5000) {
              table.put(batchData)
            } else {
             val part = realSize / 5000
             for (index <- 0 until part) {
                table.put(batchData.subList(index * 5000, (index   1) * 5000))
              }
             table.put(batchData.subList(part * 5000, realSize))
             table.put(batchData.subList(0, 5000))
           }
           table.flushCommits()

测试软件:Ubuntu 12.10 64位,Hadoop版本:0.20.205,hbase版本:0.90.5

上述配置都需要人工干预,如果干预不及时server可能已经OOM了,这时候有没有更好的控制方法?
hbase.ipc.server.max.callqueue.size = 1024 * 1024 * 1024 # 1G

直接限制队列堆积的大小。当堆积到一定程度后,事实上后面的请求等不到server端处理完,可能客户端先超时了。并且一直堆积下去会导致OOM,1G的默认配置需要相对大内存的型号。当达到queue上限,客户端会收到CallQueueTooBigException 然后自动重试。通过这个可以防止写入过快时候把server端写爆,有一定反压作用。线上使用这个在一些小型号稳定性控制上效果不错。

阅读原文

put(或者叫mutate)在regionserver中的方法调用栈

测试设置:一个master(namenode)和三台resigonServer(datanode),向HBase集群写入1千万个数据(一个数据15K左右)

       下面主要看doMiniBatchMutation方法,这个方法主要处理mutate(例如put、delete)以及replay过程,关于replay的过程会选择性的跳过。

测试结果

       函数体中有很详细的注释,主要把一个批量mutate过程分为了以下几个步骤:

韦德娱乐城 2

1.获取相关的锁,由于HBase要确保行一级的原子性,所以获取锁的时候获取的是整个rowkey的锁而不是单个cell的锁;也只有当至少获取一个锁的时候,这个方法才会继续,否则直接返回。

上图第一列和最后一列分别是插入相同数据再HBase中和HDFS中,可以看见差距很大,HBase上数据的插入时间是HDFS的10倍左右

2.更新cell中的时间戳(timestamp)以及获取mvcc相关参数,其中timestamp(也可以叫做version)可以在客户端自己手动指定,所以在一致性上不能用来做参考,也许正是因此才会引入一个叫做sequenceId的概念(当然更多的用途是为了保证修改操作在HLog里面的顺序)来完成mvcc,最后会介绍一下mvcc以及在这里HBase是如何处理mvcc的。

向HBase中插入数据比HDFS性能差这么多,笔者就研究一下是什么原因让HBase写性能这么不好。向HBase中插入数据的过程大致是这样:client插入数据时先向master请求,master回复哪个resigionserver的哪个region可以给插入数据,然后client直接和resigionserver通信插入数据,resigionserver判断该数据插入到哪个datablock里(resigion是由datablock组成的),然后以HFile的形式存储在HDFS中(数据不一定在resigionserver本地)。

3.将这些put操作写入memstore,虽然数据库系统中写日志永远比写数据重要,但是这里可以认为当前“事务”尚未提交,即使现在挂了没有日志恢复也不要紧,因为这个“事务”是没有提交的。

影响HBase写入性能的一个因素就是用put类插入数据的缓存区问题。用put类插入数据时,默认的情况是写入一次数据由clinet和resigionserver进行一次RPC来插入数据。由于是1千万个数据,多次进行进程间通信势必会影响时间。HBase给客户端提供了写缓冲区,当缓冲区填满之后才执行写入操作,这样就减少了写入的测次数。

4.构建walEdit,这一步主要是为了构建WALEdit类型的walEdit变量,这个变量主要是以list的形式聚合了很多HBase里面cell的概念,以后会写入到HLog中。

首先取消自动写入,setAutoFlush(false)

5.追加刚才构建好的walEdit:首先构造一个walKey,注意这里的walKey的sequenceId为默认值-1,到后面才会修改为跟region挂钩的唯一递增id;接着调用wal的append方法并返回一个递增数值(txid),用来表示这个追加到wal内存中日志条目的编号,在第七步中这个数值将会作为参数传入,确保该数值之前的日志信息都被写入到HLog日志文件中,而且在append方法中会保证walKey的sequenceId变成了region的sequenceId(也是一个递增序列)。

然后设置写缓冲区大小(默认是2MB)setWriteBufferSize()或者更改hbase-site.xml的hbase.client.write.buffer的属性

6.释放获取的锁。

上面列表可以看出把缓冲区设为20M还是对写入时间有改进,但是改成200M写入时间更长(为什么?)

7.将wal写入磁盘,正如第五步所说,这里保证txid以及之前的日志条目都被写入到日志文件中了,一旦写完便可以认为这个“事务”成功了,这里跟MySQL里面的auto commit很像。

另一个因素就是WAL(write ahead log),因为每一个resigion都有一个memstore用内存来暂时存放数据,进行排序,最后再吸入HFile里面去,这样做为了减少磁盘寻道而节省时间,但是为了灾难恢复,所以会把内存中的数据进行记录。所以笔者把WAL关闭之后,又测了下性能,还是有一点帮助的,但是帮助不是太大,可见WAL不是写入的瓶颈。(setWriteToWal(false))

8.提交本次操作,让put操作对读可见,核心步骤就是增加对应memstore的readpoint,使得以前讲的MemStoreScanner可以看见put过来的数据,这根后面讲的mvcc有关。

因为HBase对查询方便,能够快速的读取数据,写入时必然会采取一些措施进行排序,这就是HBase的合并和分裂机制。HBase官方为了提升写入性能,给出一种方案就是预分配resigion,也就是池的概念,你先分配一些resigion,用的时候直接用就行了。本来这1千万个数据要存储900个resigion,所以笔者预先分配了150个resigion(分配900个resigion,建表时间太长,出现异常,还没有解决),结果写入时间提升了很多,基本是原来的一半,如果能预先分配900个resigion,应该更能节省时间。

关于HBase里面的MVCC

韦德娱乐城 3

       mvcc即多版本并发控制,针对的问题就就是在数据库系统中,什么样的数据是应该被看到的,什么样的数据即使有也不应该被读取,典型的情况就是uncommitted的数据。现在的数据库系统即使不是通过天然具有递增属性的时间也是通过类似的递增数列完成mvcc的,给每一条插入的数据按照时间打个标签T,读取事务开始的时候也获取当前时间t,这个读取事务只能读取T<t的数据。

      假设现在线程A执行完上面的第三步,将C这个cell插入到了memstore中,正在此时线程B要读取对应rowkey的所有数据,那么C该不该被读取到呢?HBase里面的事物隔离级别默认情况下可以说是“read committed”所以C明显不该被读取,要如何避免呢?timestamp字段默认情况下是服务端的系统时间,但是用户可以在客户端随意修改覆盖,不能用作mvcc。但是刚好put(或者说是mutate操作)在wal中是有序的,还是根据region提供的sequenceId生成的唯一递增序列,可以用这种唯一递增序列来做饭多版本并发控制的效果。接下来只需要保证每次到了第八步的时候保证memstore的readpoint大于已经提交的最大sequenceId,就可以正确读取了。生成这个与mvcc相关的sequenceId是在上面的第二个步骤中进行的。

       但是有个问题没有解决,很有可能先开始的事务A(假设sequenceId为1)比后开始的事务B(假设sequenceId为2)晚完成。B完成之后将memstore的readpoint设置为2了,这样后面的读取不就可以通过memstore暴露的api读取到A尚未提交的数据了吗?在HBase中与mvcc相关的类里面尚未提交的put操作对应的sequenceId都增加了10亿,以保证在没有提交之前这些数据是不能读取到的,这样一来,A在没有提交之前对应的sequenceId实际是1000000001,根据目前的sequenceId(2)是看不到的。

       以上是关于HBase里面写入的理解,但是有个问题就是找了好久都没发现第八步完成后是怎样从第三步的memstore里面的减去这多加的10亿。

版权声明:本文由19463331韦德国际发布于韦德国际1946手机版,转载请注明出处:韦德娱乐城HBase写入和读取优化