HBase的put流程源码分析

hbase是一个nosql型数据库,本文我们会分析一下客户的数据是通过什么样的路径写入到hbase的。

HBase作为一种列族数据库,其将相关性较高的列聚合成一个列族单元,不同的列族单元物理上存储在不同的文件(HFile)内。一个表的数据会水平切割成不同的region分布在集群中不同的regionserver上。客户端访问集群时会首先得到该表的region在集群中的分布,之后的数据交换由客户端和regionserver间通过rpc通信实现,下面我们从hbase源码里探究客户端put数据的流程。本文参考的源码是1.1.2版本的hbase

1)客户端

put在客户端的操作主要分为三个步骤,下面分别从三个步骤展开解释:

(一)、客户端缓存用户提交的put请求

get/delete/put/append/increment等等等等客户可用的函数都在客户端的HTable.java文件中。 

在HTable.java文件中有如下的两个变量: 
private RpcRetryingCallerFactory rpcCallerFactory; 
private RpcControllerFactory rpcControllerFactory; 
protected AsyncProcess multiAp; 
如上的几个变量分别定义了rpc调用的工厂和一个异步处理的进程 

客户端的put请求调用getBufferedMutator().mutate(put),进入mutate这个函数可以看到它会把用户提交的此次put操作放入到列表writeAsyncBuffer中,当buffer中的数据超过规定值时,由后台进程进行提交。



(二)、将writeBuffer中的put操作根据region的不同进行分组,分别放入不同的Map集合

进程提交由函数backgroudFlushCommits完成,提交动作包含同步提交和异步提交两种情况,由传入的参数boolean控制。进入上述函数分析。



可见当传入backgroudFlushCommits的参数为false时执行的是异步提交,参数为true时执行的是同步提交。

与此同时,可以发现无论异步提交还是同步提交,实际的提交动作是由AsyncProcess ap执行的,调用的语句如下: 
ap.submit(tableName,writeAsyncBuffer,true,null,false) 
需要注意的是多数情况下执行的是异步提交,只有在异步提交出错的情况下执行同步提交。 

进入submit函数,可以看到它循环遍历参数writeAsyncBuffer中的每一行,通过connection.locateRegion函数找到其在集群的位置loc,将该位置与操作action一起绑定在变量actionByServer中。 

这里的region定位是由ClusterConnection类型的变量connection完成的,进入其locateRegion方法可以看出,如果客户端有缓存,则直接从缓存读取,否则从META表中读出了region所处的位置,并缓存此次的读取结果。返回的结果是RegionLocations类型的变量。

actionByServer是一个Map<ServerName,MulteAction<Row>>类型的变量,从该变量的类型定义可以看出,其将用户的一批写请求中,写入regionserver地址相同的动作归类到一起。

(三)、提交服务端RegionServer处理,在回调函数中与服务端交互。

最后调用sumitMultiActions函数将所有请求提交给服务端,它接受了上面的actionByServer作为参数,内部实例化一个AsyncRequestFutureImpl类执行异步的提交动作



从sendMultiAction函数中一步步向里查看代码,其将用户的action请求通过getNewMultiActionRunnable、SingleServerRequestRunnable层层调用最终落到了hbase的RPC框架中,每个用户请求包装成包装MultiServerCallable对象,其是一个Runnable对象,在该对象中用户请求与服务端建立起RPC联系。所有的runnable对象最终交到AsyncProcess对象的内部线程池中处理执行。

2)服务端

客户端MultiServerCallable的call方法中调用了服务端的multi函数执行提交动作,进入服务端。

multi方法内部会根据请求是否是原子请求,执行不同的操作语句,这里我们以非原子性提交为例,其执行了doNonAtomicRegionMutation()函数,这个函数中先进行一些rpc请求的编码,将编码后的action相关信息组织到一个List<ClientProtos.Action>类型的变量mutations中,这里的编码采用的proto buffer的编码方案,然后调用doBatchOp()语句,其接受了mutations作为参数。 

在doBatchOp函数中,可以看到其最终调用的batchMutate执行的批量操作,这里操作的结果会返回到OperationStatus类型的变量codes[]中,包括了以下几种状态:BAD_FAMILY;SANITY_CHECK_FAILURE;SUCCESS等状态。 这些状态记录了每个action的执行结果,包括成功啦、失败啦等等。

就一步地这些请求被包装成一个MutationBatch类型的对象传入batchMutate,batchMutatue首先判断一下资源的状态,然后调用doMiniBatchMutation()执行最终的put操作,该操作返回的是写入数据的大小addedSize,根据addedSize计算此时memstore的size以决定是否flush,如果达到了flush的要求,执行requestFlush()。doMiniBatchMutation接受了MutationBatch类型的对象继续作为其参数。关键代码如下所示:
[java]  view plain  copy
  1. while (!batchOp.isDone()) {   //操作未完成前一直循环  
  2.   if (!batchOp.isInReplay()) {  
  3.       checkReadOnly();              //判断是否是只读状态  
  4.   }  
  5.   checkResources();               //检查相关资源  
  6.   
  7.   if (!initialized) {  
  8.       this.writeRequestsCount.add(batchOp.operations.length);   //更新写请求计数器  
  9.       if (!batchOp.isInReplay()) {  
  10.         doPreMutationHook(batchOp);  
  11.       }  
  12.       initialized = true;  
  13.   }  
  14.   long addedSize = doMiniBatchMutation(batchOp);    //最终的put操作是落在这里的  
  15.   long newSize = this.addAndGetGlobalMemstoreSize(addedSize);     //以原子操作的方式增加Region上的MemStore内存的大小  
  16.   if (isFlushSize(newSize)) {    //判断memstore的大小是否达到阈值,决定是否flush  
  17.       requestFlush();  
  18.   }  
  19. }  
服务端的put主要实现在HRegion.java的doMiniBatchMutation(),该函数主要利用了group commit技术,即多次修改一起写。 

首先对于所有要修改的行,一次性拿住所有行锁,在2944行实现。 
rowLock = getRowLockInternal(mutation.getRow(),shouldBlock) ,注意的是这里的锁是写锁。

put和delete在客户端都是由这个函数实现的,在2960行针对两者的不同第一次出现了不同的处理,分别将put和delete操作归类到putsCfSet和deletesCfSet两个不同的集合中,这两个集合分别代表了put/delete的列族集合,数据类型为Set<byte[]>。 

第二步是修正keyvalue的时间戳,把action里面的所有kv时间戳修正为最新的时间。时间戳修正之后,在3009行 
lock(this.updatesLock.readLock(),numReadyToWrite) 加入了读锁。 

然后获得该批写入memstore数据的批次号mvccNum,mvccNum同时也是此次写事务的版本号,由this.sequenceId加一获得的 

然后通过w=mvcc.beginMemstoreInsertWithSeqNum(mvccNum),进入函数beginMemstoreInsertWithSeqNum,可以看见,该函数通过传入的mvccNum new一个新的WriteEntry对象,然后将WriteEntry放入队列writeQueue中,这一步加队列的操作是被锁保护起来的。 

writeQueue队列用于保存多个并发写事务的WriteEntry。 

然后,就是将batch中的数据写入到各个store的memstore中,并根据batch中的数据构建WAL edit。 

构造WAL edit之后,将该条数据对应的table name、region info、cluster id等等包装成一个HLogKey结构的对象,该对象即为walkey,将walKey和WAL edit共同组装成一个entry之后将之append到内存中的ringbuffer数据结构中。 

注意的是这次的append操作产生一个HLog范围内的id,记作txid。txid用于标识这次写事务写入的HLog日志。 

写入buffer后,即释放所有的行锁,两阶段锁过程结束。然后在3153行 
syncOrDefer(txid,durability) 
将这次事务的日志持久化到hfs中,一旦持久化完成便提交此次事务,代码在3170行,其调用了completeMemstoreInsertWithSeqNum(),走进这个函数会发现其在写入mvccnum之后,调用了waitForPreviousTransactoinsComplete()函数,这个函数实际是推进了mvcc的memstoreRead,推进的思路如下: 

先锁上writeQueue队列,然后一个一个看,找连续的已完成的WriteEntry,最后一个WriteEntry的writeNumber即是最新的点,此时可以赋值给mvcc.memstoreRead,后续读事务一开始就去拿mvcc.memstoreRead,从而能够拿到本次写入的数据。 

这里要补充一句,此时写入的数据存储在memstore中,并没有持久化到hdfs中,内存中的key-value是以skip list的数据结构存储的。

总结上面hbase的写路径可以发现在hbase的写入过程中应用到了如下的一些技术:
首先,客户端的rpc请求传递到服务端时,函数AsyncRequestFutureImpl()是一个Lazy优化,或者说是一个异步的优化,虽然函数声明了一个对服务端的rpc调用,但是它并没有马上呼叫服务端,而是在需要时才真正呼叫服务端。

第二,数据提交时采用了group commit技术,理解group commit可以用挖煤做比喻,是一铲子一铲子挖比较快,还是一次挖出一车比较省力。

第三,MVCC即多版本并发控制

限于篇幅和本人的知识有限,以上所说的只是简单描述了hbase的写事务的主干路径,并简要指出了其中的关键技术点,此外还有幂等控制、回滚操作、错误处理以及写入线程模型等等等等,即便是提到的mvcc、group commit也只是蜻蜓点水,如果展开还有很多很精彩的内容值得大家研究,如果你也对hbase感兴趣,欢迎与我一起讨论,共同提高。

参考资料:
http://www.cnblogs.com/foxmailed/p/3897884.html

  Hbase-0.98.6源码分析--Put写操作Client端流程

客户端程序写数据通过HTable和Put进行操作,我们从客户端代码开始分析写数据的流程:


       可以看到,客户端写数据最终的调用了HTableInterface的put()方法,因为HTableInterface只是一个接口,所以最终调用的是它的子类HTable的put()方法。进入HTable.put():


       从上面代码可以看出:你既可以一次put一行记录也可以一次put多行记录,两个方法内部都会调用doPut方法,最后再来根据autoFlush(默认为true),即自动提交,判断是否需要flushCommits刷写提交,在autoFlush为false的时候,如果当前容量超过了缓冲区大小(默认值为:2097152=2M),也会调用flushCommits方法。也就是说,在自动提交情况下,你可以手动控制通过一次put多条记录,然后将这些记录flush,以提高写操作吞吐量。

       首先看下flushCommits()方法:


       只是简单地调用了backgroundFlushCommits()方法,该方法会在后面讲到。

       进入doPut()方法:

       

       从上面的代码可以看出,backgroundFlushCommits()这个刷新操作可以是制定异步提交还是同步提交,从doPut方法中来看默认是以异步的方式进行,这里的ap是AsyncProcess类的一个实例,该类使用多线程的来实现异步的请求,也就是说,并非每一次put操作都是直接往HBase里面写数据的,而是等到缓存区域内的数据多到一定程度(默认设置是2M),再进行一次写操作。当然这次操作在Server端应当还是要排队执行的,具体执行机制这里不作展开。可以确定的是,HConnection在HTable的put操作中,只是起到一个定位RegionServer的作用,在定位到RegionServer之后,操作都是由cilent端通过rpc调用完成的。这个结论在插入/查询/删除中是一致的。

       writeAsyncBuffer.add(put)就是向一个异步缓冲区添加该操作,然后当一定条件的时候进行flash,当发生flash操作的时候,才会真正的去执行该操作,这主要是提高系统的吞吐率,接下来我们去看看这个flush的操作内部。


     看下waitUntilDone()方法:

    进入waitForMaximumCurrentTasks()方法:


      由这个waitForMaximumCurrentTasks()方法,可以清晰了了解到waitUntilDone()方法的操作流程,具体要等待到什么时候呢?等到tasksSent的值减去tasksDone的值等于0,tasksSent表示提交的任务数,tasksDone表示完成的任务数。

       现在就可以重新总结一下backgroundFlushCommits()方法,在第965行,submit()方法传入的参数是true,表示需要等待rpc调用结束。第980行,如果有部分数据提交失败,同时没有设置清空失败的数据时,把数据重新添加到writeAsyncBuffer列表中。最后在finally块中,清空当前currentWriteBufferSize的大小,如果有数据没有提交成功,
重新把未提交的数据的大小计算起来添加到currentWriteBufferSize中。

       比较doPut()和flushCommits(),如果在doput的过程中,也就是调用htable.put(Put)的时候,如果缓存大小超过了客户端写缓存大小的限制,调用backgroundFlushCommits()方法方法是异步的;而在flushcommit方法中,backgroundFlushCommits()这个方法是同步的。

       接下来就是重要的提交过程,submit()方法:



       进入sendMultiAction()方法,看它是如何发送put请求的:



       从上面的代码可以看出,每个任务都是通过HBase的RPC框架与服务器进行通信,并获取返回的结果。其中最重要的两个组件我用红色方框已经圈出,看下他俩的具体实现:


       先构造一个MultiServerCallable,然后再通过rpcCallerFactory将其封装为RpcRetryingCaller做最后的call操作。

查看MultiServerCallable:


       注释里就说的很明白了,client端通过MultiServerCallable.call()方法调用res的rpc的multi()方法,来实现put提交请求。可以想象,根据讲过的《Hadoop RPC机制-原理篇》,HRegionServer端必定也有一个multi()方法。

       总结put操作:
  (1)把put操作添加到writeAsyncBuffer队列里面,符合条件(自动flush或者超过了阀值writeBufferSize)就通过AsyncProcess异步批量提交。
  (2)在提交之前,我们要根据每个rowkey找到它们归属的region server,这个定位的过程是通过HConnection的locateRegion方法获得的,然后再把这些rowkey按照HRegionLocation分组。在获得具体region位置的时候,会对最近使用的region server做缓存,如果缓存中保存了相应的region server信息,就直接使用这个region信息,连接这个region server,否则会对master进行一次rpc操作,获得region server信息,客户端的操作put、get、delete等操作每次都是封装在一个Action对象中进行提交操作的,都是一系列的的action一起提交,这就是MultiAction。
  (3)通过多线程,一个HRegionLocation构造MultiServerCallable<Row>,然后通过rpcCallerFactory.<MultiResponse> newCaller()执行调用,忽略掉失败重新提交和错误处理,客户端的提交操作到此结束。

      下篇文章将会介绍HRegionServer如何响应客户端发出的Put请求。

  在《Hbase-0.98.6源码分析--Put写操作Client端流程》中,介绍了put操作的流程,最后client端是通过MultiServerCallable.call()调用multi()方法来进行rpc请求的。追踪multi()方法,进入ClientProtos.ClientService.BlockingInterface接口的multi()抽象方法,再次追踪该方法,进入实现该方法的HRegionServer实例,查看multi()方法的具体实现:



       这个方法里面还包括了PayloadCarryingRpcController和CellScanner可以看得出来它不只是被Put来用的,但是这些我们不管我们只看Put如何处理就行了。在该方法的3464行调用了getRegion()方法,来获取对应的HRegion,简单看一下:



       分析下getRegionByEncodedName()方法流程,看它如何从当前regionserver中的onlnieRegions中得到请求的region.:

       1.从onlineRegions中取出HRegion实例
       2.如果onlineRegions列表中不包含此region,从movedRegions列表中拿到region,region的moved超时是2分钟,
如果movedRegions列表中能拿到此region,同时move时间超时,并从movedRegions列表中移出引region返回null,
否则返回正在moved的region,如果movedRegions中返回的region不为null,throwRegionMovedException
       3.从regionsInTransitionInRS中获取此region,如果能拿到,同时拿到的值为true,表示region还在做opening操作,
Throw RegionOpeningException
       4.如果以上得到的值都为null,表示此server中没有此region,throw NotServingRegionException
此时基本上只有一个可能,region在做split.或者move到其它server(刚完成move,client请求时不在此server)

      总结下multi()方法的操作:

       1、取出来所有的action(Put),这里主要是put,因为我们调用客户端就是这么调用的,其实别的类型也可以支持,获取他们对应的region。
       2、根据action的原子性来判断走哪个方法,原子性操作走mutateRows,非原子性操作走doNonAtomicRegionMutation方法,我查了一下这个Atomic到底是怎么回事,我搜索了一下代码,发现在调用HTable的mutateRow方法的时候,它设置了Atomic为true,这个是应该是支持一行数据的原子性的,有这个需求的童鞋可以尝试用这个方法,也是可以提交多个,包括Put、Delete操作。

       接下来看doNonAtomicRegionMutation()方法,用于处理非原子性的put/delete/get操作,这是我们常用的方式:




       这里面代码很多,也适配了很多种类型,是个大而全的方法,但是我们这里用到的只是把Put、Delete等的类型转换添加到mutations的列表里,然后走最后的圈出的doBatchOp()这个批量操作,然而这个代码也比较长,简单说一下该方法的思路:

       1、还是得把Put、Delete给转换类型,这里的批量操作只支持全是Delete或者全是Put。
       2、用HRegion.batchMutate方法来执行操作,返回OperationStatus数组,记录每个action的状态,是成功,还是失败,或者是别的状态。
       在batchMutate()里面首先就是检查是否是只读状态,然后检查是否是Meta Region的,是否执行MemStore检查了。


        终于到了最终的Big Boss类,这个类很长很长很长。。。。。。
        1、重要的成员变量

        2、检查Put和Delete里面的列族是否和Region持有的列族的定义相同,有时候我们在Delete的时候是不填列族的,这里它给这个缺的列族来一个KeyValue.Type.DeleteFamily,删除列族的类型。
        3、给Row加锁,先计算hash值做key,如果该key没上过锁,就上一把锁,然后计算出来要写的action有多少个,记录到numReadyToWrite。
        4、更新时间戳,把该action里面的所有的kv的时间戳更新为最新的时间戳,它这里也会把之前的没运行的也一起更新。
  5、给该region加锁,这个时间点之后,就不允许读了,等待时间需要根据numReadyToWrite的数量来计算。


        6、上锁之后,就是Put、Delete等的重点。给这些写入memstore的数据创建一个批次号。


        7、把kv们写入到memstore当中,然后计算出来一个添加数据之后的新的MemStore的大小addedSize。

        MemStore里面有两个kv的集合,调用applyFamilyMapToMemstore()把kv添加到集合里面去。
        8、把kv添加到日志当中,标志状态为成功,如果是用户设置了不写入日志的,它就不写入日志了。
        9、先异步添加日志,这里为什么是异步的,因为之前给上锁了,暂时不能读了。
       10、释放之前创建的锁。

       11、同步日志。
       12、结束该批次的操作。
       Final、同步日志没成功的,最后根据批次回滚MemStore中的操作。


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值