HBase的写事务,MVCC及新的写线程模型

   MVCC是实现高性能数据库的关键技术,主要为了读不影响写。几乎所有数据库系统都用这技术,比如Spanner,看这里。Percolator,看这里。当然还有mysql。本文说HBase的MVCC和0.98引入的新写线程模型。

   HBase region server的存储模型类LSM,将随机写转换为顺序写,写操作直接写内存,然后写操作日志来持久化修改避免宕机丢数据。通常,为了提高性能,采用group commit技术,及多次修改一起写,一起写操作日志,充分利用磁盘的顺序IO。对于HBase来说,group commit在HRegion类doMiniBatchMutation(BatchOperationInProgress<?> batchOp)函数中,这里面实现了HBase的MVCC,本文主要分析该函数。

   MVCC多版本控制协议,显然,数据(KeyValue)上需要被打上版本号,这样读的时候,就可以根据版本号过滤掉一些不可见的数据。HBase中有一个类MultiVersionConsistencyControl用来保存系统范围内的一些版本信息,比如写事务开始时会从MultiVersionConsistencyControl中拿memstoreWrite加1作为

本次写事务的版本号,随后这个事务写入的所有数据,以KeyValue的形式,都被打上了这个版本号。读事务开始时也会从MultiVersionConsistencyControl中拿

memstoreRead作为读事务的版本号,那么该读事务只能读取版本号小于等于这个版本号的数据(KeyValue)。组织KeyValue的核心数据结构是KeyValueSkipListSet,内部是JDK提供的ConcurrentSkipListMap,一个并发跳表实现。

    HBase的事务实现简单来说,并发控制采用两阶段锁实现。这里省略一些细节,比如修正KeyValue的timestamp,数据的check等。

    首先对于所有需要修改的行,一次性拿住所有行锁,然后调用mvcc对象的beginMemstoreInsert方法,获得一个WriteEntry对象,包含这次写事务的写版本号,通过mvcc.memstoreWrite加1获得,记作writeNumber,然后将WriteEntry放入队列writeQueue中,队列操作被锁保护。这个队列用来保存多个并发写事务的WriteEntry,方便后续推进mvcc.memstoreRead,memstoreRead作为读事务的事务版本号使用,这样当memstoreRead被推进,读事务可以读的数据就越来越新。然后,将batch里的数据都add到各个HStore的memstore中,每个数据KeyValue都被打上writeNumber,这没有问题,因为memstoreRead没有向前推进,故后续的读事务读不到这次数据。接着根据batch中的数据构建WALEdit,WALEdit相当于HLog中具体一条一条日志Entry的内容,Entry的头部是HLogKey结构,包含这条log entry对应的table name,region name,以及log entry的sequence number,region级别的,根据WALEdit和HLogKey组装成一个Entry后,然后将这个Entry 加到内存中的buffer pendingWrites中(还没开始写hdfs,只是写入内存中),然后为append的这条日志产生一个HLog范围内的id,记作txid(名字不是很恰当),txid不实际的存储在Entry中,只是用于标识这次写事务写入的日志,只有这些日志被实际的持久化到hdfs中后,请求才可以返回。

写入buffer后,即释放所有的行锁,两阶段锁的过程结束。最后,就是调用void syncer(long txid) 函数等待这次事务相应的日志被持久化到hdfs中(实际的写hdfs和sync是其他线程做的,牵扯到写线程模型,后续描述),一旦持久化完成,就标记一下WriteEntry,代表本次写事务对应的日志已持久化完成。然后就可以尝试去推进mvcc的memstoreRead。推进的过程实际上就是去writeQueue里从头到尾去看,找连续的已经完成的WriteEntry,最后一个WriteEntry的writeNumber即是最新的点,可以赋值给mvcc.memstoreRead,后续读事务一开始就去拿mvcc.memstoreRead,从而能都到最新的数据。这里需要一个队列的原因在于,写事务是并发的,有多个写线程同时都在执行写操作,先拿到memstoreWrite进队列的线程不一定先往pendingWrites中append,从而导致memstoreWrite更大的写事务的日志可能先被持久化到hdfs中。这里,writeQueue就是为了处理这种乱序的情况。最后,一个写事务什么时候可以返回给客户端?对于客户端来说,客户端希望后续可以看到自己之前成功commit的事务的数据,所以,只需要mvcc.memstoreRead 大于等于事务对应的WriteEntry的writeNumber即可。

   现在说0.98引入的大幅提高吞吐量的写线程模型(HBASE-8755)。

   和一个写事务有关的线程除了执行事务操作的工作线程外,还有如下几种:

    1. 一个将内存中的pendingWrites写入HDFS(不sync)的线程,对应类AsyncWriter

    2. 一个sync hdfs的线程,对应类AsyncSyncer

    3. 一个sync完成后唤醒工作线程的线程,对应类AsyncNotifier

 从工作线程开始,多个工作线程写内存中的pendingWrites,通过pendingWritesLock保护,写完后,得到txid,通过执行this.asyncWriter.setPendingTxid(txid) 去告诉AsyncWriter线程内存中有数据了,你可以往hdfs中写了,AsyncWriter加锁pendingWritesLock,将pendingWrites拿出来,解锁,然后将pendingWrites写入hdfs,接着找一个空闲的AsyncSyncer,通asyncSyncers[i].setWrittenTxid(this.lastWrittenTxid)

告诉它有新的数据需要sync了,AsyncSyncer调用AsyncWriter的sync操作, sync完成后,将最后sync的txid记录在变量AsyncSyncer中,然后调用asyncNotifier.setFlushedTxid(this.lastSyncedTxid) 通知AsyncNotifier 又sync完了一批,可以去唤醒工作线程,让他们自己看看是否自己当前执行事务的日志已经持久化。AsyncNotifier和工作线程通过syncedTillHere这个AtomicLong进行同步,AsyncNotifier会将最后一个sync成功的txid记录在syncedTillHere中,

工作线程会等在syncedTillHere上,每次被叫醒后,看看自己的txid是否小于等于syncedTillHere,条件满足则工作线程继续往下走,做推进mvcc点相关的工作。

 

 

posted @ 2014-08-08 11:48  吴镝  阅读(3175)  评论(0编辑  收藏  举报