设为首页 加入收藏

TOP

HBase写请求分析(二)
2015-11-21 01:52:08 来源: 作者: 【 】 浏览:2
Tags:HBase 请求 分析
/O的步骤,这个会大大影响写请求的性能。当然,如果业务场景对数据稳定性要求不高,关键是写入请求,那么可以调用Put.setDurability(Durability.SKIP_WAL),这样就可以跳过这个步骤。

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

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

当大量的写请求数据添加到

首页 上一页 1 2 3 下一页 尾页 2/3/3
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇xtrabackup自动还原脚本v2 下一篇MongoDB数据模型和索引学习总结

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: