hbase源码系列(十一)Put、Delete在服务端是如何处理? 

 

在讲完之后HFile和HLog之后,今天我想分享是Put在Region Server经历些了什么?相信前面看了《HTable探秘》的朋友都会有印象,没看过的建议回去先看看,Put是通过MultiServerCallable来提交的多个Put,好,我们就先去这个类吧,在call方法里面,我们找到了这句。

  1. responseProto = getStub().multi(controller, requestProto);

它调用了Region Server的multi方法。好,我们立即杀到HRegionServer去,搜索找到multi这个方法。

  1. public MultiResponse multi(final RpcController rpcc, final MultiRequest request)
  2. throws ServiceException {
  3. // RpcController是属于后门的,这样返回的数据就不用序列化了
  4. PayloadCarryingRpcController controller = (PayloadCarryingRpcController)rpcc;
  5. CellScanner cellScanner = controller != null? controller.cellScanner(): null;
  6. if (controller != null) controller.setCellScanner(null);
  7. List<CellScannable> cellsToReturn = null;
  8. MultiResponse.Builder responseBuilder = MultiResponse.newBuilder();
  9. //取出来所有的Actionfor (RegionAction regionAction : request.getRegionActionList()) {
  10. this.requestCount.add(regionAction.getActionCount());
  11. RegionActionResult.Builder regionActionResultBuilder = RegionActionResult.newBuilder();
  12. HRegion region;
  13. try {
  14. //获取对应的HRegion
  15. region = getRegion(regionAction.getRegion());
  16. } catch (IOException e) {
  17. responseBuilder.addRegionActionResult(regionActionResultBuilder.build());
  18. continue; // 报告这个action有错 }
  19.  
  20. if (regionAction.hasAtomic() && regionAction.getAtomic()) {
  21. try {
  22. //如果是原子操作,就走原子操作的通道 mutateRows(region, regionAction.getActionList(), cellScanner);
  23. } catch (IOException e) {
  24. regionActionResultBuilder.setException(ResponseConverter.buildException(e));
  25. }
  26. } else {
  27. // 非原子性提交,把错误内部处理了
  28. cellsToReturn = doNonAtomicRegionMutation(region, regionAction, cellScanner,
  29. regionActionResultBuilder, cellsToReturn);
  30. }
  31. responseBuilder.addRegionActionResult(regionActionResultBuilder.build());
  32. }
  33. // 如果需要返回数据的话,就new一个createCellScanner扔回去if (cellsToReturn != null && !cellsToReturn.isEmpty() && controller != null) {
  34. controller.setCellScanner(CellUtil.createCellScanner(cellsToReturn));
  35. }
  36. return responseBuilder.build();
  37. }

这个方法里面还包括了PayloadCarryingRpcController和CellScanner可以看得出来它不只是被Put来用的,但是这些我们不管我们只看Put如何处理就行了。

1、取出来所有的action(Put),这里主要是put,因为我们调用客户端就是这么调用的,其实别的类型也可以支持,获取他们对应的region。

2、根据action的原子性来判断走哪个方法,原子性操作走mutateRows,非原子性操作走doNonAtomicRegionMutation方法,我查了一下这个Atomic到底是怎么回事,我搜索了一下代码,发现在调用HTable的mutateRow方法的时候,它设置了Atomic为true,这个是应该是支持一行数据的原子性的,有这个需求的童鞋可以尝试用这个方法,也是可以提交多个,包括Put、Delete操作。

  1. regionMutationBuilder.setAtomic(true);
  2. getStub().multi(null, request);

我们先看doNonAtomicRegionMutation,这是我们常用的方式。

  1.    List<ClientProtos.Action> mutations = null;
  2. for (ClientProtos.Action action: actions.getActionList()) {
  3. ClientProtos.ResultOrException.Builder resultOrExceptionBuilder = null;
  4. try {
  5. Result r = null;
  6. if (action.hasGet()) {
  7. Get get = ProtobufUtil.toGet(action.getGet());
  8. r = region.get(get);
  9. } else if (action.hasMutation()) {
  10. MutationType type = action.getMutation().getMutateType();
  11. if (type != MutationType.PUT && type != MutationType.DELETE && mutations != null &&
  12. !mutations.isEmpty()) {
  13. // 如果这个操作不是Put或者Delete的话,就一下子把前面的活都先干了? doBatchOp(builder, region, mutations, cellScanner);
  14. mutations.clear();
  15. }
  16. switch (type) {
  17. case APPEND:
  18. r = append(region, action.getMutation(), cellScanner);
  19. break;
  20. case INCREMENT:
  21. r = increment(region, action.getMutation(), cellScanner);
  22. break;
  23. case PUT:
  24. case DELETE:
  25. // 前面的那些,我们都用得少,或者是不用,不用管它们,看这里就行if (mutations == null) {
  26. mutations = new ArrayList<ClientProtos.Action>(actions.getActionCount());
  27. }
  28. mutations.add(action);
  29. break;
  30. default:
  31. throw new DoNotRetryIOException("Unsupported mutate type: " + type.name());
  32. }
  33. } else {
  34. throw new HBaseIOException("Unexpected Action type");
  35. }
  36. if (r != null) {
  37. ClientProtos.Result pbResult = null;
  38. if (isClientCellBlockSupport()) {
  39. pbResult = ProtobufUtil.toResultNoData(r);
  40. //if (cellsToReturn == null) cellsToReturn = new ArrayList<CellScannable>();
  41. cellsToReturn.add(r);
  42. } else {
  43. pbResult = ProtobufUtil.toResult(r);
  44. }
  45. //把result编译成Protobuf码,返回
  46. resultOrExceptionBuilder =
  47. ClientProtos.ResultOrException.newBuilder().setResult(pbResult);
  48. }
  49. } catch (IOException ie) {
  50. resultOrExceptionBuilder = ResultOrException.newBuilder().
  51. setException(ResponseConverter.buildException(ie));
  52. }
  53. if (resultOrExceptionBuilder != null) {
  54. // Propagate index. resultOrExceptionBuilder.setIndex(action.getIndex());
  55. builder.addResultOrException(resultOrExceptionBuilder.build());
  56. }
  57. }
  58. //进行批量操作 if (mutations != null && !mutations.isEmpty()) {
  59. doBatchOp(builder, region, mutations, cellScanner);
  60. }
  61. return cellsToReturn;

这里面代码很多,也适配了很多种类型,是个大而全的方法,但是我们这里用到的只是把Put、Delete等的类型转换添加到mutations的列表里,然后走下面这个批量操作。

此外get的批量操作也是走的这个方法,里面它走的是HRegion.get的方法返回一个Result。

  1. doBatchOp(builder, region, mutations, cellScanner);

doBatchOp里面的代码我就补贴了,老帖代码就没意思了。

1、还是得把Put、Delete给转换类型,这里的批量操作只支持全是Delete或者全是Put。

2、用HRegion.batchMutate方法来执行操作,返回OperationStatus数组,记录每个action的状态,是成功,还是失败,或者是别的状态。

在batchMutate里面首先就是检查是否是只读状态,然后检查是否是Meta Region的,是不执行MemStore检查了,因为MemStore的堆内存超过了阻塞队列的MemStore大小,就会报错误,太恶劣了。。。没catch的哦。

  1. long addedSize = doMiniBatchMutation(batchOp, isReplay);
  2. //MemStore的大小到了阀值,就要flush到文件了if (isFlushSize(newSize)) {
  3. requestFlush();
  4. }

doMiniBatchMutation就是我们的终极boss了,是个很长很臭的类,贴代码都不能一下子全贴。

1、实例化几个重要的类,后面具体会用到

  1. //日志,isInReplay是否支持重做,这里是false
  2. WALEdit walEdit = new WALEdit(isInReplay);
    //控制多版本的MemStore flush的结果,每次flush的w都是一样的,就好像同一批号的食品
  3. MultiVersionConsistencyControl.WriteEntry w = null;
    long txid = 0;
  4. //日志同步是否成功boolean walSyncSuccessful = false;
  5. boolean locked = false;

2、检查Put和Delete里面的列族是否和Region持有的列族的定义相同,有时候我们在Delete的时候是不填列族的,这里它给这个缺的列族来一个KeyValue.Type.DeleteFamily,删除列族的类型。

3、给Row加锁,先计算hash值做key,如果该key没上过锁,就上一把锁,然后计算出来要写的action有多少个,记录到numReadyToWrite。

4、更新时间戳,把该action里面的所有的kv的时间戳更新为最新的时间戳,它这里也会把之前的没运行的也一起更新。

5、给该region加锁,这个时间点之后,就不允许读了,等待时间需要根据numReadyToWrite的数量来计算。

  1. //加锁,现在要上锁了,这段时间内不允许读
    lock(this.updatesLock.readLock(), numReadyToWrite);
  2. locked = true;
  3.  
  4. //等待时间final long waitTime = Math.min(maxBusyWaitDuration,
  5. busyWaitDuration * Math.min(numReadyToWrite, maxBusyWaitMultiplier));
  6. if (!lock.tryLock(waitTime, TimeUnit.MILLISECONDS)) {
  7. throw new RegionTooBusyException(
  8. "failed to get a lock in " + waitTime + "ms");
  9. }

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

  1. //为这次添加进MemStore的数据添加一个批次号
  2. w = mvcc.beginMemstoreInsert();
  3.  
  4. //这是批次号的计算方式,nextWriteNumber就等于memstore的写的次数+1
  5. public WriteEntry beginMemstoreInsert() {
  6. synchronized (writeQueue) {
  7. long nextWriteNumber = ++memstoreWrite;
  8. WriteEntry e = new WriteEntry(nextWriteNumber);
  9. writeQueue.add(e);
  10. return e;
  11. }
  12. }

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

  1. //把kv们写入memstorelong addedSize = 0;
  2. for (int i = firstIndex; i < lastIndexExclusive; i++) {
  3. if (batchOp.retCodeDetails[i].getOperationStatusCode() != OperationStatusCode.NOT_RUN) {
  4. continue;
  5. }
  6. addedSize += applyFamilyMapToMemstore(familyMaps[i], w);
  7. }

这个添加到MemStore里面也没啥神秘的,因为MemStore里面有两个kv的集合,它只是把kv添加到集合里面去,看下面的代码就知道了。

  1. private long applyFamilyMapToMemstore(Map<byte[], List<Cell>> familyMap,
  2. MultiVersionConsistencyControl.WriteEntry localizedWriteEntry) {
  3. long size = 0;try {for (Map.Entry<byte[], List<Cell>> e : familyMap.entrySet()) {
  4. byte[] family = e.getKey();
  5. List<Cell> cells = e.getValue();
  6. //把kv添加到memstore当中
  7. Store store = getStore(family);
  8. for (Cell cell: cells) {
  9. KeyValue kv = KeyValueUtil.ensureKeyValue(cell);
  10. kv.setMvccVersion(localizedWriteEntry.getWriteNumber());
  11. size += store.add(kv);
  12. }
  13. }
  14. }
    return size;
  15. }

注意这一句话kv.setMvccVersion(localizedWriteEntry.getWriteNumber());  后面会用到的。

8、把kv添加到日志当中,标志状态为成功,如果是用户设置了不写入日志的,它就不写入日志了。

  1. Durability durability = Durability.USE_DEFAULT;
  2. for (int i = firstIndex; i < lastIndexExclusive; i++) {
  3. // 跳过状态不对的if (batchOp.retCodeDetails[i].getOperationStatusCode()
  4. != OperationStatusCode.NOT_RUN) {
  5. continue;
  6. }
  7. //标志状态为成功
  8. batchOp.retCodeDetails[i] = OperationStatus.SUCCESS;
  9.  
  10. Mutation m = batchOp.operations[i];
  11. //获取自定义的日志同步方式
  12. Durability tmpDur = getEffectiveDurability(m.getDurability());
  13. if (tmpDur.ordinal() > durability.ordinal()) {
  14. durability = tmpDur;
  15. }
  16. if (tmpDur == Durability.SKIP_WAL) {
  17. //记录日志的kv的大小,但是不写入到日志当中 recordMutationWithoutWal(m.getFamilyCellMap());
  18. continue;
  19. }
  20. //把列族里面的kv全部添加到walEdit当中 addFamilyMapToWALEdit(familyMaps[i], walEdit);
  21. }

9、先异步添加日志,这里为什么是异步的,因为之前给上锁了,暂时不能读了,如果这里调用的是同步的方法,后果自己想象下。

  1. Mutation mutation = batchOp.operations[firstIndex];
  2. if (walEdit.size() > 0) {
  3. //异步添加日志
  4. txid = this.log.appendNoSync(this.getRegionInfo(), this.htableDescriptor.getTableName(),
  5. walEdit, mutation.getClusterIds(), now, this.htableDescriptor);
  6. }

10、释放之前创建的锁。

  1. //释放相关的锁if (locked) {
  2. this.updatesLock.readLock().unlock();
  3. locked = false;
  4. }
  5. releaseRowLocks(acquiredRowLocks);

11、同步日志。

  1. if (walEdit.size() > 0) {
  2. syncOrDefer(txid, durability);
  3. }
  4. walSyncSuccessful = true;

12、结束该批次的操作。

  1. if (w != null) {
  2. mvcc.completeMemstoreInsert(w);
  3. w = null;
  4. }

到这里其实就是结束了。但是如果添加到了MemStore里面了,但是日志没有同步成功呢?

  1. finally {
  2. if (!walSyncSuccessful) {
  3. //如果日志没有成功, rollbackMemstore(batchOp, familyMaps, firstIndex, lastIndexExclusive);
  4. }
  5. ......
  6. }

一路跟踪代码下去,跟踪到代码在MemStore的rollback方法里面。

  1. KeyValue found = this.snapshot.get(kv);
  2. if (found != null && found.getMvccVersion() == kv.getMvccVersion()) {
  3.  this.snapshot.remove(kv);
  4. }
  5. // 比较一下mvcc,相同就删除.
  6. found = this.kvset.get(kv);
  7. if (found != null && found.getMvccVersion() == kv.getMvccVersion()) {
  8. removeFromKVSet(kv);
  9. long s = heapSizeChange(kv, true);
  10. this.size.addAndGet(-s);
  11. }

比较了MvccVersion,发现是同一批次的,就干掉了。

过程写得比较凌乱了,把之前的总结一下吧:

1、做准备工作,实例化变量

2、检查Put和Delete里面的列族是否和Region持有的列族的定义相同。

3、给Row加锁,先计算hash值做key,如果该key没上过锁,就上一把锁,然后计算出来要写的action有多少个,记录到numReadyToWrite。

4、更新时间戳,把该action里面的所有的kv的时间戳更新为最新的时间戳,它这里也会把之前的没运行的也一起更新。

5、给该region加锁,这个时间点之后,就不允许读了,等待时间需要根据numReadyToWrite的数量来计算。

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

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

8、把kv添加到日志当中,标志状态为成功,如果是用户设置了不写入日志的,它就不写入日志了。

9、先异步添加日志。

10、释放之前创建的锁。

11、同步日志。

12、结束该批次的操作。

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

上面的过程适用于Put和Delete的批量操作,但是这里总感觉很好奇,就这样结束了,Put和Delete操作就没区别吗,那它怎么删除数据的?

返回在第4步更新时间戳的时候,发现了一些猫腻,Delete的情况执行了prepareDeleteTimestamps方法,看看吧。

  1. void prepareDeleteTimestamps(Map<byte[], List<Cell>> familyMap, byte[] byteNow)
  2. throws IOException {
  3. for (Map.Entry<byte[], List<Cell>> e : familyMap.entrySet()) {
  4. byte[] family = e.getKey();
  5. List<Cell> cells = e.getValue();
  6. //列和count的映射
  7. Map<byte[], Integer> kvCount = new TreeMap<byte[], Integer>(Bytes.BYTES_COMPARATOR);
  8.  
  9. for (Cell cell: cells) {
  10. KeyValue kv = KeyValueUtil.ensureKeyValue(cell);
  11. // 如果是时间戳是最新的话就执行下面这些操作
    if (kv.isLatestTimestamp() && kv.isDeleteType()) {
  12. //new一个Get从Store里面去搜索
    } else {
  13. kv.updateLatestStamp(byteNow);
  14. }
  15. }
  16. }
  17. }

看来一下代码,这里是上来先判断是否是最新的时间戳,我就回去看来一下Delete的构造函数,尼玛。。。

  1. public Delete(byte [] row) {
  2. this(row, HConstants.LATEST_TIMESTAMP);
  3. }
  4.  
  5. public Delete(byte [] row, long timestamp) {
  6. this(row, 0, row.length, timestamp);
  7. }

只传了rowkey进去的,它就是最新的。。然后看了一下注释,凡是在这个时间点之前的所有版本的所有列,我们都要删除。

好吧,我们很无奈的宣布,我们只能走kv.isLatestTimestamp() && kv.isDeleteType(),下面是没放出来的代码。

  1. byte[] qual = kv.getQualifier();
  2. if (qual == null) qual = HConstants.EMPTY_BYTE_ARRAY;
  3. //想到相同列的每次+1
  4. Integer count = kvCount.get(qual);
  5. if (count == null) {
  6. kvCount.put(qual, 1);
  7. } else {
  8. kvCount.put(qual, count + 1);
  9. }
  10. //更新之后把最新的count数量
  11. count = kvCount.get(qual);
  12.  
  13. Get get = new Get(kv.getRow());
  14. get.setMaxVersions(count);
  15. get.addColumn(family, qual);
  16. //从store当中取出相应的result来
  17. List<Cell> result = get(get, false);
  18.  
  19. if (result.size() < count) {
  20. // Nothing to delete 数量不够。。 更新最新的时间戳为现在的时间 kv.updateLatestStamp(byteNow);
  21. continue;
  22. }
  23. //数量超过了也不行if (result.size() > count) {
  24. throw new RuntimeException("Unexpected size: " + result.size());
  25. }
  26. //取最后一个的时间戳
  27. KeyValue getkv = KeyValueUtil.ensureKeyValue(result.get(count - 1));
  28. //更新kv的时间戳为getkv的时间戳 Bytes.putBytes(kv.getBuffer(), kv.getTimestampOffset(),
  29. getkv.getBuffer(), getkv.getTimestampOffset(), Bytes.SIZEOF_LONG);

这里又干了一个Get操作,把列族的多个版本的内容取出来,如果数量不符合预期也会有问题,但是这后面操作的中心思想就是:

(a)按照预期来说,取出来的少了,就设置删除的时间戳为现在;

(b)取出来的多了,就报错;

(c)刚好的,就把Delete的时间戳设置为最大的那个的时间戳,但即便是这样也没有删除数据。

回到这里我又想起来,只有在Compaction之后,hbase的文件才会变小,难道是在那个时候删除的?那在删除之前,我们进行Get或者Scan操作的时候,会不会读到这些没有被删除的数据呢?

好,让我们拭目以待。

11 hbase源码系列(十一)Put、Delete在服务端是如何处理的更多相关文章

  1. HBase源码系列之HFile

    本文讨论0.98版本的hbase里v2版本.其实对于HFile能有一个大体的较深入理解是在我去查看"到底是不是一条记录不能垮block"的时候突然意识到的. 首先说一个对HFile ...

  2. hbase源码系列(十二)Get、Scan在服务端是如何处理

    hbase源码系列(十二)Get.Scan在服务端是如何处理?   继上一篇讲了Put和Delete之后,这一篇我们讲Get和Scan, 因为我发现这两个操作几乎是一样的过程,就像之前的Put和Del ...

  3. 9 hbase源码系列(九)StoreFile存储格式

    hbase源码系列(九)StoreFile存储格式    从这一章开始要讲Region Server这块的了,但是在讲Region Server这块之前得讲一下StoreFile,否则后面的不好讲下去 ...

  4. 10 hbase源码系列(十)HLog与日志恢复

    hbase源码系列(十)HLog与日志恢复   HLog概述 hbase在写入数据之前会先写入MemStore,成功了再写入HLog,当MemStore的数据丢失的时候,还可以用HLog的数据来进行恢 ...

  5. hbase源码系列(十五)终结篇&Scan续集-->如何查询出来下一个KeyValue

    这是这个系列的最后一篇了,实在没精力写了,本来还想写一下hbck的,这个东西很常用,当hbase的Meta表出现错误的时候,它能够帮助我们进行修复,无奈看到3000多行的代码时,退却了,原谅我这点自私 ...

  6. hbase源码系列(十二)Get、Scan在服务端是如何处理?

    继上一篇讲了Put和Delete之后,这一篇我们讲Get和Scan, 因为我发现这两个操作几乎是一样的过程,就像之前的Put和Delete一样,上一篇我本来只打算写Put的,结果发现Delete也可以 ...

  7. hbase源码系列(十四)Compact和Split

    先上一张图讲一下Compaction和Split的关系,这样会比较直观一些. Compaction把多个MemStore flush出来的StoreFile合并成一个文件,而Split则是把过大的文件 ...

  8. hbase源码系列(二)HTable 探秘

    hbase的源码终于搞一个段落了,在接下来的一个月,着重于把看过的源码提炼一下,对一些有意思的主题进行分享一下.继上一篇讲了负载均衡之后,这一篇我们从client开始讲吧,从client到master ...

  9. hbase源码系列(一)Balancer 负载均衡

    看源码很久了,终于开始动手写博客了,为什么是先写负载均衡呢,因为一个室友入职新公司了,然后他们遇到这方面的问题,某些机器的硬盘使用明显比别的机器要多,每次用hadoop做完负载均衡,很快又变回来了. ...

随机推荐

  1. HDU 1754 I Hate It【线段树 单点更新】

    题意:给出n个数,每次操作修改它的第s个数,询问给定区间的数的最大值 把前面两道题结合起来就可以了 自己还是敲不出来------------- #include<iostream> #in ...

  2. 前端学习之路——scss篇

    一.什么是SASS SASS是一种CSS的开发工具,提供了许多便利的写法,大大节省了设计者的时间,使得CSS的开发,变得简单和可维护. 二.安装和使用 Sass依赖于ruby环境,所以装sass之前先 ...

  3. SpringCloud学习笔记(10)----Spring Cloud Netflix之声明式 REST客户端 -Feign的高级特性

    1. Feign的默认配置 Feign 的默认配置 Spring Cloud Netflix 提供的默认实现类:FeignClientsConfiguration 解码器:Decoder feignD ...

  4. VB学习笔记(一)VB操作字符串

    在vb中 dim a# 定义a变量为双精度型变量~ #是类型符 % 整型 & 长整型 !单精度 $ 字符型 VB中strconv 的作用 StrConv("要转换的字符串" ...

  5. [ZJOI2007]捉迷藏 (点分树+堆*3)

    点分树一点都不会啊(还是太菜了) 点分树就是我们点分治构成的新树.满足深度很小. 然后我们就可以在上面瞎维护东西了. 三个大根堆: \(C[u]\)里装的是点分树中u的子树所有点到点分树中u的父亲的距 ...

  6. PL SQL Developer使用总结

    如果OS为windows 7 64位系统,Oracle版本为 Oracle 11g 64 安装PL SQL Developer 请参考    http://myskynet.blog.51cto.co ...

  7. oracle数据库回滚

    线下测试数据误操作,回滚攻略--把数据捞出来,这个时间自己设置--表名一定要是:xx_tbd日期 CREATE TABLE user_tbd0718ASselect * from user as of ...

  8. 题解 洛谷 P4047 【[JSOI2010]部落划分】

    我觉得几乎就是一道最小生成树模板啊... 题解里许多大佬都说选第n-k+1条边,可我觉得要这么讲比较容易理解 (虚边为能选的边,实边为最小生成树) 令n=5,k=2,(1,3)<(1,2)< ...

  9. android -- 小问题 关于ListView设置了OnScrollListener之后onScrollStateChanged()和onScroll方法监听不到的问题

    关于ListView设置了OnScrollListener之后onScrollStateChanged()和onScroll方法监听不到的问题: 原因: 首先OnScrollListener是焦点滚动 ...

  10. UVA 11825 - Hackers&#39; Crackdown 状态压缩 dp 枚举子集

    UVA 11825 - Hackers' Crackdown 状态压缩 dp 枚举子集 ACM 题目地址:option=com_onlinejudge&Itemid=8&page=sh ...