HBase写请求分析

HBase作为分布式NoSQL数据库系统,不单支持宽列表,并且对于随机读写来说也具有较高的性能。在高性能的随机读写事务的同时,HBase也能保持事务的一致性。目前HBase只支持行级别的事务一致性。本文主要探讨一下HBase的写请求流程,主要基于0.98.8版本的实现。

客户端写请求

   HBase提供的Java client API是以HTable为主要接口,对应其中的HBase表。写请求API主要为HTable.put(write和update)、HTable.delete等。以HTable.put为例子,首先来看看客户端是怎么把请求发送到HRegionServer的。

每个put请求表示一个KeyValue数据,考虑到客户端有大量的数据需要写入到HBase表,HTable.put默认是会把每个put请求都放到本地缓存中去,当本地缓存大小超过阀值(默认为2MB)的时候,就要请求刷新,即把这些put请求发送到指定的HRegionServer中去,这里是利用线程池并发发送多个put请求到不同的HRegionServer。但如果多个请求都是同一个HRegionServer,甚至是同一个HRegion,则可能造成对服务端造成压力,为了避免发生这种情况,客户端API会对写请求做了并发数限制,主要是针对put请求需要发送到的HRegionServer和HRegion来进行限制,具体实现在AsyncProcess中。主要参数设定为:

  • hbase.client.max.total.tasks              客户端最大并发写请求数,默认为100
  • hbase.client.max.perserver.tasks      客户端每个HRegionServer的最大并发写请求数,默认为2
  • hbase.client.max.perregion.tasks      客户端每个HRegion最大并发写请求数,默认为1

为了提高I/O效率,AsyncProcess会合并同一个HRegion对应的put请求,然后再一次把这些相同HRegion的put请求发送到指定HRegionServer上去。另外AsyncProcess也提供了各种同步的方法,如waitUntilDone等,方便某些场景下必须对请求进行同步处理。每个put和读请求一样,都是要通过访问hbase:meta表来查找指定的HRegionServer和HRegion,这个流程和读请求一致,可以参考文章的描述。

服务端写请求

    当客户端把写请求发送到服务端时,服务端就要开始执行写请求操作。HRegionServer把写请求转发到指定的HRegion执行,HRegion每次操作都是以批量写请求为单位进行处理的。主要流程实现在HRegion.doMiniBatchMutation,大致如下:

  1. 获取写请求里指定行的行锁。由于这些批量写请求之间是不保证一致性(只保证行一致性),因此每次只会尝试阻塞获取至少一个写请求的行锁,其它已被获取的行锁则跳过这次更新,等待下次迭代的继续尝试获取
  2. 更新已经获得行锁的写请求的时间戳为当前时间
  3. 获取HRegion的updatesLock的读锁。
  4. 获取MVCC(Multi-Version Concurrency Control)的最新写序号,和写请求KeyValue数据一起写入到MemStore。
  5. 构造WAL(Write-Ahead Logging) edit对象
  6. 把WAL edit对象异步添加到HLog中,获取txid号
  7. 释放第3步中的updatesLock的读锁以及第1步中获得的行锁
  8. 按照第6步中txid号同步HLog
  9. 提交事务,把MVCC的读序号前移到第4步中获取到的写序号
  10. 如果以上步骤出现失败,则回滚已经写入MemStore的数据
  11. 如果MemStore缓存的大小超过阀值,则请求当前HRegion的MemStore刷新操作。

经过以上步骤后,写请求就属于被提交的事务,后面的读请求就能读取到写请求的数据。这些步骤里面都包含了HBase的各种特性,主要是为了保证可观的写请求的性能的同时,也确保行级别的事务ACID特性。接下来就具体分析一下一些主要步骤的具体情况。

HRegion的updatesLock

步骤3中获取HRegion的updatesLock,是为了防止MemStore在flush过程中和写请求事务发生线程冲突。

首先要知道MemStore在写请求的作用。HBase为了提高读性能,因此保证存储在HDFS上的数据必须是有序的,这样就能使用各种特性,如二分查找,提升读性能。但由于HDFS不支持修改,因此必须采用一种措施把随机写变为顺序写。MemStore就是为了解决这个问题。随机写的数据写如MemStore中就能够在内存中进行排序,当MemStore大小超过阀值就需要flush到HDFS上,以HFile格式进行存储,显然这个HFile的数据就是有序的,这样就把随机写变为顺序写。另外,MemStore也是HBase的LSM树(Log-Structured
Merge Tree)的实现部分之一。

在MemStore进行flush的时候,为了避免对读请求的影响,MemStore会对当前内存数据kvset创建snapshot,并清空kvset的内容,读请求在查询KeyValue的时候也会同时查询snapshot,这样就不会受到太大影响。但是要注意,写请求是把数据写入到kvset里面,因此必须加锁避免线程访问发生冲突。由于可能有多个写请求同时存在,因此写请求获取的是updatesLock的readLock,而snapshot同一时间只有一个,因此获取的是updatesLock的writeLock。

获取MVCC写序号

MVCC是HBase为了保证行级别的事务一致性的同时,提升读请求的一种并发事务控制的机制。MVCC的机制不难理解,可以参考这里

MVCC的最大优势在于,读请求和写请求之间不会互相阻塞冲突,因此读请求一般不需要加锁(只有两个写同一行数据的写请求需要加锁),只有当写请求被提交了后,读请求才能看到写请求的数据,这样就可以避免发生“脏读”,保证了事务一致性。具体MVCC实现可以参考HBase的一位PMC Member的这篇文章

WAL(Write-Ahead Logging) 与HLog

    WAL是HBase为了避免遇到节点故障无法服务的情况下,能让其它节点进行数据恢复的机制。HBase进行写请求操作的时候,默认都会把KeyValue数据写入封装成WALEdit对象,然后序列化到HLog中,在0.98.8版本里采用ProtoBuf格式进行序列化WAL。HLog是记录HBase修改的日志文件,和数据文件HFile一样,也是存储于HDFS上,因此保证了HLog文件的可靠性。这样如果机器发生宕机,存储在MemStore的KeyValue数据就会丢失,HBase就可以利用HLog里面记录的修改日志进行数据恢复。

每个HRegionServer只有一个HLog对象,因此当前HRegionServer上所有的HRegion的修改都会记录到同一个日志文件中,在需要数据恢复的时候再慢慢按照HRegion分割HLog里的修改日志(Log Splitting)。

整个写请求里,WALEdit对象序列化写入到HLog是唯一会发生I/O的步骤,这个会大大影响写请求的性能。当然,如果业务场景对数据稳定性要求不高,关键是写入请求,那么可以调用Put.setDurability(Durability.SKIP_WAL),这样就可以跳过这个步骤。

HBase为了减轻写入HLog产生I/O的影响,采用了较为粒度较细的多线程并发模式(详细可参考HBASE-8755)。HLog的实现为FSHLog,主要过程涉及三个对象:AsyncWriter、AsyncSyncer和AsyncNotifier。整个写入过程涉及步骤5-8。

  1. HRegion调用FSHLog.appendNoSync,把修改记录添加到本地buffer中,通知AsyncWriter有记录插入,然后返回一个long型递增的txid作为这条修改记录。注意到这是一个异步调用。
  2. HRegion之后会马上释放updatesLock的读锁以及获得的行锁,然后再调用FSHLog.sync(txid),来等待之前的修改记录写入到HLog中。
  3. AsyncWriter从本地buffer取出修改记录,然后将记录经过压缩以及ProtoBuf序列化写入到FSDataOutputStream的缓存中,然后再通知AsyncSyncer。由于AsyncSyncer的工作量较大,因此总共有5条线程,AsyncWriter会选择其中一条进行唤醒。
  4. AsyncSyncer判断是否有其它AsyncSyncer线程已经完成了同步任务,如果是则继续等待AsyncWriter的同步请求。否则的话就把FSDataOutputStream的缓存写入到HDFS中去,然后唤醒AsyncNotifier
  5. AsyncNotifier的任务较为简单,只是把所有正在等待同步的写请求线程唤醒,不过事实上该过程同样较为耗时,因此另外分出AsyncNotifier线程,而不是在AsyncSyncer完成通知任务。
  6. HRegion被唤醒,发现自己的txid已经得到同步,也就是修改记录写入到HLog中,于是接着其它操作。

在以上的写入过程中,第2步里HRegion先把记录写入HLog的buffer,然后再释放之前获得的锁后才同步等待写入完成,这样可以有效降低锁持有的时间,提高其它写请求的并发。另外,AsyncWriter、AsyncSyncer和AsyncNotifier组成的新的写模型主要负担起HDFS写操作的任务,对比起旧的写模型(需要每个写请求的线程来负责写HDFS,大量的线程导致严重的锁竞争),最主要是大大降低了线程同步过程中的锁竞争,有效地提高了线程的吞吐量。这个写过程对于大批量写请求来说,能够提高吞吐量,但对于写请求并发量较小,线程竞争较低的环境下,由于每个写请求必须等待Async*线程之间的同步,增加了线程上下文切换的开销,会导致性能稍微下降(在0.99版本里采用了LMAX
Disruptor同步模型,并把FSHLog进行了重构,HBASE-10156)。

MVCC读序号前移

    完成HLog的写之后,整个写请求事务就已经完成流程,因此就需要提交事务,让其它读请求可以看到这个写请求的数据。前面已经略微介绍过MVCC的作用,这里关注一下MVCC是如何处理读序号前移。

MVCC在内部维持一个long型写序号memstoreWrite,一个long型读序号memstoreRead,还有一个队列writeQueue。当HRegion调用beginMemStoreInsert要求分配一个写序号的时候,就会把写序号自增1,并返回,并同时把一个写请求添加到writeQueue尾部。代码如下:

public WriteEntry beginMemstoreInsert() {
  synchronized (writeQueue) {
    long nextWriteNumber = ++memstoreWrite;
    WriteEntry e = new WriteEntry(nextWriteNumber);
    writeQueue.add(e);
    return e;
  }
}

HRegion把这个写序号和每个新插入的KeyValue数据进行关联。当写请求完成的时候,HRegion调用completeMemstoreInsert请求读序号前移,MVCC首先把写请求记录为完成,然后查看writeQueue队列,从队列头部开始取出所有已经完成的写请求,最后一个完成的写请求的序号则会赋值给memstoreRead,表示这是当前最大可读的读序号,如果HRegion的写请求的序号比读序号要小,则完成了事务提交,否则HRegion会一直循环等待提交完成。相关代码如下:

public void completeMemstoreInsert(WriteEntry e) {
  advanceMemstore(e);
  waitForRead(e);
}

boolean advanceMemstore(WriteEntry e) {
  synchronized (writeQueue) {
    e.markCompleted();
    long nextReadValue = -1;
    while (!writeQueue.isEmpty()) {
      ranOnce=true;
      WriteEntry queueFirst = writeQueue.getFirst();
      //...
      if (queueFirst.isCompleted()) {
        nextReadValue = queueFirst.getWriteNumber();
        writeQueue.removeFirst();
      } else {
        break;
      }
    }

    if (nextReadValue > 0) {
      synchronized (readWaiters) {
        memstoreRead = nextReadValue;
        readWaiters.notifyAll();
      }
    }
    if (memstoreRead >= e.getWriteNumber()) {
      return true;
    }
    return false;
  }
}

public void waitForRead(WriteEntry e) {
  boolean interrupted = false;
  synchronized (readWaiters) {
    while (memstoreRead < e.getWriteNumber()) {
      try {
        readWaiters.wait(0);
      } catch (InterruptedException ie) {
        //...
      }
    }
  }
}

由此可见,MVCC保证了事务提交的串行顺序性,如果有某个写请求提交成功,则任何写序号小于这个写序号的写请求必然提交成功。因此在读请求的时候,只要获取MVCC的读请求序号则可以读取任何最新提交成功写请求的写数据。另外,MVCC只限制在事务提交的这个过程的串行,在实际的写请求过程中,其它步骤都是允许并发的,因此不会对性能造成太大的影响。

至此,HBase的一个写请求的事务提交过程就完成。在整个写过程里,都采用了大量的方法去避免锁竞争、缩短获取锁的时间以及保证事务一致性等措施。由于MemStore在内存的缓存上始终有大小限制,因此当MemStore超过阀值的时候,HBase就要刷新数据到HDFS上,形成新的HFile。接下来看看这个过程。

MemStore的flush

当大量的写请求数据添加到MemStore上,MemStore超过阀值,HRegion就会请求把MemStore的数据flush到HDFS上。另外要注意到的是,这里flush的单位是单个HRegion,也就是说如果有多个HStore,只要有一个MemStore超过阀值,这个HRegion所属的所有HStore都要执行flush操作。

  • HRegion首先要获取updatesLock的写锁,这样就防止有新的写请求到来
  • 请求获取MVCC的写序号
  • 请求MemStore生成snapshot
  • 释放updatesLock的写锁
  • 提交之前获取的MVCC写序号,等待之前的事务完成,防止回滚事务写入HFile
  • 把snapshot的KeyValue数据写入到HFile里

主要集中来看看把snapshot的KeyValue数据写入HFile部分。先来看看HFile的格式:

之前在读请求文章里已经介绍个HFile的这个格式。HFile要保证每个HBlock大小约为64KB,采用DataBlock多级索引和BloomFilter一级索引的方法来构成HFile结构。整个写过程比较简单,在循环里便利获取MemStore的snapshot的KeyValue数据,然后不断写DataBlock里,如果当前DataBlock的总大小超过64KB,则DataBlock就停止添加数据(设置了压缩会进行压缩处理),同时计算DataBlock的索引,并添加到内存中,另外如果开启了BloomFilter属性也要写入对应的BloomBlock,这个过程中会注意保存未压缩大小等FileInfo数据。

当所有的snapshot数据都写入完DataBlock中后,就要开始写入DataBlock的多级索引了。HBase会根据之前保存的索引计算多级索引的级数,如果索引数量不多,则有可能只有RootIndexBlock一个级别。同时也会根据RookIndexBlock获得MidKey的数据。最后就按照顺序写入FileInfo以及BloomFilter的索引,还有Trailer。

总结

HBase采用了MemStore把随机写变为顺序写,这样有助于提高读请求的效率。另外也为了避免数据丢失使用HLog来记录修改日志。在整个写过程中,使用了多种手段减轻了锁竞争,提高了线程吞吐量,也注意缩短锁获取的时间,尽可能地提高并发。通过利用MVCC也避免了读写请求之间的影响。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-25 06:53:40

HBase写请求分析的相关文章

hbase读写请求详细解释

2019/2/28 星期四 hbase读写请求详细解释hbase的读写过程读请求过程 1.客户端通过 ZooKeeper 以及-ROOT-表和.META.表找到目标数据所在的 RegionServer(就是 数据所在的 Region 的主机地址)2.zk返回结果给客户端3.联系 RegionServer 查询目标数据4.RegionServer 定位到目标数据所在的 Region,发出查询请求5.Region 先在 Memstore 中查找,命中则返回6.如果在 Memstore 中找不到,则在

(转)HBase工程师线上工作经验总结----HBase常见问题及分析

阅读本文可以带着下面问题:1.HBase遇到问题,可以从几方面解决问题?2.HBase个别请求为什么很慢?你认为是什么原因?3.客户端读写请求为什么大量出错?该从哪方面来分析?4.大量服务端exception,一般原因是什么?5.系统越来越慢的原因是什么?6.Hbase数据写进去,为什么会没有了,可能的原因是什么?7. regionserver发生abort,遇到最多是什么情况?8.从哪些方面可以判断HBase集群是否健康?9.为了加强HBase的安全性,你会采取哪些措施?在Tcon分布式系统测

HBase工程师线上工作经验总结----HBase常见问题及分析

阅读本文可以带着下面问题:1.HBase遇到问题,可以从几方面解决问题?2.HBase个别请求为什么很慢?你认为是什么原因?3.客户端读写请求为什么大量出错?该从哪方面来分析?4.大量服务端exception,一般原因是什么?5.系统越来越慢的原因是什么?6.Hbase数据写进去,为什么会没有了,可能的原因是什么?7. regionserver发生abort,遇到最多是什么情况?8.从哪些方面可以判断HBase集群是否健康?9.为了加强HBase的安全性,你会采取哪些措施? 在Tcon分布式系统

分布式存储系统Kudu与HBase的简要分析与对比

本文来自网易云社区 作者:闽涛 背景 Cloudera在2016年发布了新型的分布式存储系统--kudu,kudu目前也是apache下面的开源项目.Hadoop生态圈中的技术繁多,HDFS作为底层数据存储的地位一直很牢固.而HBase作为Google BigTable的开源产品,一直也是Hadoop生态圈中的核心组件,其数据存储的底层采用了HDFS,主要解决的是在超大数据集场景下的随机读写和更新的问题.Kudu的设计有参考HBase的结构,也能够实现HBase擅长的快速的随机读写.更新功能.那

HBase的compact分析

HBase是基于LSM树存储模型的分布式NoSQL数据库.LSM树对比普遍的B+树来说,能够获得较高随机写性能的同时,也能保持可靠的随机读性能(可参考这里).在进行读请求的时候,LSM树要把多个子树(类似B+树结构)进行归并查询,对于HBase来说,这些子树就是HFile(还包括内存上的树结构MemStore).因此归并查询的子树数越少,查询的性能就越高. Compact的作用 在写请求的这篇文章里,已经介绍过对于每个写请求,都必须写入MemStore以及HLog才算完成事务提交.当MemSto

HBase写性能初步测试

背景 刚接触HBase,在本机上对线下HBase集群做了初步的写性能测试,下面对测试内容做详细说明. 说明 HBase环境 0.96版本,8台region server,默认配置 写数据说明 单column family,两个column qualifier的值为字符串+随机8位正整数,Row Key为两个quailifer值相连后串上随机Long 比如:val1 = dd1977285, val2 =cc6549921, rowkey = rondom.nextLong() + val1 +

Hbase写数据,存数据,读数据的详细过程

转自:http://www.aboutyun.com/thread-10886-1-1.html 附HBase 0.94之后Split策略: http://www.aboutyun.com/thread-11211-1-1.html 1.Client写入需要哪些过程?2.Hbase是如何读取数据的? Client写入 -> 存入MemStore,一直到MemStore满 -> Flush成一个StoreFile,直至增长到一定阈值 -> 出发Compact合并操作 -> 多个Sto

Nodejs通过Thrift操作hbase卡住原因分析及与javascript的垃圾回收机制的关系

在最近使用Nodejs通过Thrift操作hbase的时候写了个脚本,不断发送http请求,从而取得hbase下所需的数据,但是在run的过程中for循环并没有执行完全,在执行一部分后会卡住,就再也进不到hbase下取数据,出现socket hang up的错误,查了很多资料也没解决.当时认为是hbase的并发数问题,其并发数的限制导致了资源负载的极限,后来不断测试找到原因所在,其实与hbase处理并发的能力无关,真正的原因是jsvascript的垃圾回收机制使得资源使用达到瓶颈,下面是代码处理

HBase的split分析

HBase在新建一个表的时候,默认会把所有数据都会放在一个HRegion上,主节点HMaster根据一定的策略把HRegion分配到不同的HRegionServer从节点上,客户端在进行读写操作的时候,就会访问对应HRegionServer的HRegion.当HRegion的数据量超过阀值的时候,为了防止单个热点访问带来的压力,HBase就会对HRegion进行split操作,一个父HRegion分为两个子HRegion,后续的数据写入操作就会分配到两个HRegion里,减轻了单个热点的负载.由