2021SC@SDUSC hbase源码分析(二)写入数据(1)

2021SC@SDUSC hbase源码分析(二)写入数据(1)

2021SC@SDUSC 2021SC@SDUSC 2021SC@SDUSC 2021SC@SDUSC

Hbase写入总览

Hbase采用LSM树架构,这种架构更适合于写多读少的场景。

 需要说明的是,Hbase服务端并没有提供update、delete接口,Hbase中对数据的更新、删除操作在服务器端也被认为为写入操作,其中更新操作会写入一个最新版本数据删除操作会写入一条标记为deleted的KV数据

综上,Hbase中的更新、删除操作的流程与写入流程完全一致。

虽然Hbase在不断更新,但其中的逻辑差别不大。

大体写入流程

从整体架构的视角来看,写入流程可以概括为三个阶段:

  1. 客户端处理阶段
  2. Region写入阶段
  3. MemStore Flush阶段

本篇博客首先对客户端处理阶段进行源码分析。

写入时组件交互

在这里插入图片描述

客户端处理阶段

在 HBase中,大部分的操作都是在RegionServer完成的,Client端想要插入、删除、查询数据都需要先找到相应的 RegionServer。

hbase客户端处理写入请求的核心流程可以分为三步:

1.用户提交put请求

用户提交put请求后,Hbase客户端会将写入的数据添加到本地缓冲区中,符合一定条件就会通过AsyncProcess异步批量提交。

get、put、delete等客户可用的函数都在src/main/java/org/apache/hadoop/hbase/client目录下的HTable.java文件中,其中有两个变量:

  //AsuncProcess 对象在后续batch方法中调用了其中的submit方法进行了提交
  @VisibleForTesting
  AsyncProcess multiAp;
  private final RpcRetryingCallerFactory rpcCallerFactory;
  private final RpcControllerFactory rpcControllerFactory;

分别定义了rpc调用的工厂和一个异步处理的进程。

客户端的put方法

@Override
public void put(final Put put) throws IOException {
    //校验put不能超过10M
  validatePut(put);
    
        //获取一个callable对象,并且实现了一个匿名对象
  ClientServiceCallable<Void> callable =
      new ClientServiceCallable<Void>(this.connection, getName(), put.getRow(),
          this.rpcControllerFactory.newController(), put.getPriority()) {
    @Override
    protected Void rpcCall() throws Exception {
      MutateRequest request =
          RequestConverter.buildMutateRequest(getLocation().getRegionInfo().getRegionName(), put);
      doMutate(request);
      return null;
    }
  };
    
      //最后调用了一个callWithRetries 一个可重试的调用
  rpcCallerFactory.<Void> newCaller(this.writeRpcTimeoutMs).callWithRetries(callable,
      this.operationTimeoutMs);

}
@Override
public void put(final List<Put> puts) throws IOException {
  for (Put put : puts) {
    validatePut(put);
  }
  Object[] results = new Object[puts.size()];
  try {
    batch(puts, results, writeRpcTimeoutMs);
  } catch (InterruptedException e) {
    throw (InterruptedIOException) new InterruptedIOException().initCause(e);
  }
}

从上面代码可以看出:Hbase客户端在进行写入初期,既可以一次put一行记录,也可以一次put多行记录

两个方法内部都会调用validatePut方法,其主要作用是来验证Put是否有效,主要是判断kv的长度。我们追踪validatePut到其本类。代码如下:

// validate for well-formedness
static void validatePut(Put put, int maxKeyValueSize) throws IllegalArgumentException {
  if (put.isEmpty()) {
    throw new IllegalArgumentException("No columns to insert");
  }
  if (maxKeyValueSize > 0) {
    for (List<Cell> list : put.getFamilyCellMap().values()) {
      for (Cell cell : list) {
        if (cell.getSerializedSize() > maxKeyValueSize) {
          throw new IllegalArgumentException("KeyValue size too large");
        }
      }
    }
  }
}

可见其功能是验证put对象是否合法。

单次put请求

首先我们先看单次put方法中的代码:
我们可以发现它总共做了三件事:

  1. 校验put不能超过10M
  2. 获取一个callable对象,并且实现了一个匿名对象
  3. 最后调用了一个callWithRetries 一个可重试的调用

接下来,我们去看callWithRetries这个方法:

@Override
public T callWithRetries(RetryingCallable<T> callable, int callTimeout)
throws IOException, RuntimeException {
  List<RetriesExhaustedException.ThrowableWithExtraContext> exceptions = new ArrayList<>();
  tracker.start();
  context.clear();
  for (int tries = 0;; tries++) {
    long expectedSleep;
    try {

       //prepare很明显就是该方法的核心内容 
      callable.prepare(tries != 0);
        
      interceptor.intercept(context.prepare(callable, tries));
      return callable.call(getTimeout(callTimeout));
    } 
      ...
  }
}

然后我们发现prepare这个方法,进入查看:

public void prepare(final boolean reload) throws IOException {
  if (getRpcController().isCanceled()) return;
  if (Thread.interrupted()) {
    throw new InterruptedIOException();
  }
  if (reload || location == null) {
      //后续获取相关locations的方法
    RegionLocations rl = getRegionLocations(false, id, cConnection, tableName, get.getRow());
    location = id < rl.size() ? rl.getRegionLocation(id) : null;
  }

  if (location == null || location.getServerName() == null) {
    throw new HBaseIOException("There is no location for replica id #" + id);
  }

  setStubByServiceName(this.location.getServerName());
}

根据其中的判断语句,我们发现其中的getRegionLocations方法,我们追根溯源,找到其核心代码:

static RegionLocations getRegionLocations(boolean useCache, int replicaId,
               ClusterConnection cConnection, TableName tableName, byte[] row)
    throws RetriesExhaustedException, DoNotRetryIOException, InterruptedIOException {

  RegionLocations rl;
  try {
    if (useCache) {
      rl = cConnection.locateRegion(tableName, row, true, true, replicaId);
    } else {
      rl = cConnection.relocateRegion(tableName, row, replicaId);
    }
  } 
    ...

  return rl;
}

其中最主要的是try-catch语句中的这个if-else语句,我们直接进入if成立的语句中的方法进行查看(其中else语句最终也会回到if中的locateRegion方法,毫无疑问,locateRegion是很重要方法):

public RegionLocations locateRegion(final TableName tableName, final byte[] row, boolean useCache,
    boolean retry, int replicaId) throws IOException {
  checkClosed();
  if (tableName == null || tableName.getName().length == 0) {
    throw new IllegalArgumentException("table name cannot be null or zero length");
  }
  if (tableName.equals(TableName.META_TABLE_NAME)) {
    return locateMeta(tableName, useCache, replicaId);
  } else {
    // Region not in the cache - have to go to the meta RS
    return locateRegionInMeta(tableName, row, useCache, retry, replicaId);
  }
}

根据locateRegion方法的返回值,我们可以发现:上来就去缓存中去拿元数据,但是我们缓冲区并没有数据,这个时候,他会进行重试,这个时候 relocateMeta 就为true了

进入locateMeta方法后,我们最终发现这个时候就把我们的表元数据进行缓存了

private RegionLocations locateMeta(final TableName tableName,
    boolean useCache, int replicaId) throws IOException {
  // HBASE-10785: We cache the location of the META itself, so that we are not overloading
  // zookeeper with one request for every region lookup. We cache the META with empty row
  // key in MetaCache.
  byte[] metaCacheKey = HConstants.EMPTY_START_ROW; // use byte[0] as the row for meta
  RegionLocations locations = null;
  if (useCache) {
    locations = getCachedLocation(tableName, metaCacheKey);
    if (locations != null && locations.getRegionLocation(replicaId) != null) {
      return locations;
    }
  }

然后把locations返回了。

流程:

  1. 获取meta表
  2. 根据meta表获取对应的偏移量所在的region
  3. 然后根据region拿到servername去访问

这样就可以写数据了

多次put请求

 多次put请求及后续的RS寻址构造RPCRegion写入等阶段放在下一篇博客来分析。

如有不足或错误,欢迎指正
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值