hbase的源码终于搞一个段落了,在接下来的一个月,着重于把看过的源码提炼一下,对一些有意思的主题进行分享一下。继上一篇讲了负载均衡之后,这一篇我们从client开始讲吧,从client到master再到region server,按照这个顺序来开展,网友也可以对自己感兴趣的部分给我留言或者直接联系我的QQ。

  现在我们讲一下HTable吧,为什么讲HTable,因为这是我们最常见的一个类,这是我们对hbase中数据的操作的入口。

  

1.Put操作

  下面是一个很简单往hbase插入一条记录的例子。

HBaseConfiguration conf =  (HBaseConfiguration) HBaseConfiguration.create();
byte[] rowkey = Bytes.toBytes("cenyuhai");
byte[] family = Bytes.toBytes("f");
byte[] qualifier = Bytes.toBytes("name");
byte[] value = Bytes.toBytes("岑玉海");

HTable table = new HTable(conf, "test");
Put put = new Put(rowkey);
put.add(family,qualifier,value);

table.put(put);

  我们平常就是采用这种方式提交的数据,为了提高重用性采用HTablePool,最新的API推荐使用HConnection.getTable("test")来获得HTable,旧的HTablePool已经被抛弃了。好,我们下面开始看看HTable内部是如何实现的吧,首先我们看看它内部有什么属性。

  /** 实际提交数据所用的类 */  protected HConnection connection;/** 需要提交的数据的列表 */
  protected List<Row> writeAsyncBuffer = new LinkedList<Row>();  /** flush的size */
  private long writeBufferSize;
  /** 是否自动flush */
  private boolean autoFlush;
  /** 当前的数据的size,达到指定的size就要提交 */
  protected long currentWriteBufferSize;
  protected int scannerCaching;
  private int maxKeyValueSize;
  private ExecutorService pool;  // For Multi
  /** 异步提交 */
  protected AsyncProcess<Object> ap;  ** rpc工厂 */
  private RpcRetryingCallerFactory rpcCallerFactory;

  主要是靠上面的这些家伙来干活的,这里面的connection、ap、rpcCallerFactory是用来和后台通信的,HTable只是做一个操作,数据进来之后,添加到writeAsyncBuffer,满足条件就flush。

  下面看看table.put是怎么执行的:

    doPut(put);
    if (autoFlush) {
      flushCommits();
    }

  执行put操作,如果是autoFush,就提交,先看doPut的过程,如果之前的ap异步提交到有问题,就先进行后台提交,不过这次是同步的,如果没有错误,就把put添加到队列当中,然后检查一下当前的 buffer的大小,超过我们设置的内容的时候,就flush掉。

if (ap.hasError()){
      backgroundFlushCommits(true);
}
currentWriteBufferSize += put.heapSize();
writeAsyncBuffer.add(put);
while (currentWriteBufferSize > writeBufferSize) {
    backgroundFlushCommits(false);
}

  写下来,让我们看看backgroundFlushCommits这个方法吧,它的核心就这么一句ap.submit(writeAsyncBuffer, true) ,如果出错了的话,就报错了。所以网上所有关于客户端调优的方法里面无非就这么几种:

1)关闭autoFlush

2)关闭wal日志

3)把writeBufferSize设大一点,一般说是设置成5MB

  经过实践,就第二条关闭日志的效果比较明显,其它的效果都不明显,因为提交的过程是异步的,所以提交的时候占用的时间并不多,提交到server端后,server还有一个写入的队列,(⊙o⊙)… 让人想起小米手机那恶心的排队了。。。所以大规模写入数据,别指望着用put来解决。。。mapreduce生成hfile,然后用bulk load的方式比较好。

  不废话了,我们继续追踪ap.submit方法吧,F3进去。

  

      int posInList = -1;
      Iterator<? extends Row> it = rows.iterator();
      while (it.hasNext()) {
        Row r = it.next();
        //为row定位
        HRegionLocation loc = findDestLocation(r, 1, posInList);

        if (loc != null && canTakeOperation(loc, regionIncluded, serverIncluded)) {
          // loc is null if there is an error such as meta not available.
          Action<Row> action = new Action<Row>(r, ++posInList);
          retainedActions.add(action);
          addAction(loc, action, actionsByServer);
          it.remove();
        }
      }

  循环遍历r,为每个r找到它的位置loc,loc是HRegionLocation,里面记录着这行记录所在的目标region所在的位置,loc怎么获得呢,走进findDestLocation方法里面,看到了这么一句。

  

loc = hConnection.locateRegion(this.tableName, row.getRow());

  通过表名和rowkey,使用HConnection就可以定位到它的位置,这里就先不讲定位了,稍后放一节出来讲,请看这一篇《Client如何找到正确的Region Server》,否则篇幅太长了,这里我们只需要记住,提交操作,是要知道它对应的region在哪里的。

  定位到它的位置之后,它把loc添加到了actionsByServer,一个region server对应一组操作。(插句题外话为什么这里叫action呢,其实我们熟知的Put、Delete,以及不常用的Append、Increment都是继承自Row的,在接口传递时候,其实都是视为一种操作,到了后台之后,才做区分)。

  接下来,就是多线程的rpc提交了。

MultiServerCallable<Row> callable = createCallable(loc, multiAction);
......
res = createCaller(callable).callWithoutRetries(callable);

  再深挖一点,把它们的实现都扒出来吧。

  protected MultiServerCallable<Row> createCallable(final HRegionLocation location,
      final MultiAction<Row> multi) {
    return new MultiServerCallable<Row>(hConnection, tableName, location, multi);
  }

  protected RpcRetryingCaller<MultiResponse> createCaller(MultiServerCallable<Row> callable) {
    return rpcCallerFactory.<MultiResponse> newCaller();
  }

  ok,看到了,先构造一个MultiServerCallable,然后再通过rpcCallerFactory做最后的call操作。

  好了,到这里再总结一下put操作吧,前面写得有点儿凌乱了。

  (1)把put操作添加到writeAsyncBuffer队列里面,符合条件(自动flush或者超过了阀值writeBufferSize)就通过AsyncProcess异步批量提交。

  (2)在提交之前,我们要根据每个rowkey找到它们归属的region server,这个定位的过程是通过HConnection的locateRegion方法获得的,然后再把这些rowkey按照HRegionLocation分组。

  (3)通过多线程,一个HRegionLocation构造MultiServerCallable<Row>,然后通过rpcCallerFactory.<MultiResponse> newCaller()执行调用,忽略掉失败重新提交和错误处理,客户端的提交操作到此结束。

  

2.Delete操作

  对于Delete,我们也可以通过以下代码执行一个delete操作

Delete del = new Delete(rowkey);
table.delete(del);

  这个操作比较干脆,new一个RegionServerCallable<Boolean>,直接走rpc了,爽快啊。

RegionServerCallable<Boolean> callable = new RegionServerCallable<Boolean>(connection,
        tableName, delete.getRow()) {
      public Boolean call() throws IOException {
        try {
          MutateRequest request = RequestConverter.buildMutateRequest(
            getLocation().getRegionInfo().getRegionName(), delete);
          MutateResponse response = getStub().mutate(null, request);
          return Boolean.valueOf(response.getProcessed());
        } catch (ServiceException se) {
          throw ProtobufUtil.getRemoteException(se);
        }
      }
    };
rpcCallerFactory.<Boolean> newCaller().callWithRetries(callable, this.operationTimeout);

  这里面注意一下这行MutateResponse response = getStub().mutate(null, request);

  getStub()返回的是一个ClientService.BlockingInterface接口,实现这个接口的类是HRegionServer,这样子我们就知道它在服务端执行了HRegionServer里面的mutate方法。

3.Get操作

  get操作也和delete一样简单

  

Get get = new Get(rowkey);
Result row = table.get(get);

  get操作也没几行代码,还是直接走的rpc

public Result get(final Get get) throws IOException {
    RegionServerCallable<Result> callable = new RegionServerCallable<Result>(this.connection,
        getName(), get.getRow()) {
      public Result call() throws IOException {
        return ProtobufUtil.get(getStub(), getLocation().getRegionInfo().getRegionName(), get);
      }
    };
    return rpcCallerFactory.<Result> newCaller().callWithRetries(callable, this.operationTimeout);
}

  注意里面的ProtobufUtil.get操作,它其实是构建了一个GetRequest,需要的参数是regionName和get,然后走HRegionServer的get方法,返回一个GetResponse

public static Result get(final ClientService.BlockingInterface client,
      final byte[] regionName, final Get get) throws IOException {
    GetRequest request =
      RequestConverter.buildGetRequest(regionName, get);
    try {
      GetResponse response = client.get(null, request);
      if (response == null) return null;
      return toResult(response.getResult());
    } catch (ServiceException se) {
      throw getRemoteException(se);
    }
}

4.批量操作

  

  针对put、delete、get都有相应的操作的方式:

  1.Put(list)操作,很多童鞋以为这个可以提高写入速度,其实无效。。。为啥?因为你构造了一个list进去,它再遍历一下list,执行doPut操作。。。。反而还慢点。

  2.delete和get的批量操作走的都是connection.processBatchCallback(actions, tableName, pool, results, callback),具体的实现在HConnectionManager的静态类HConnectionImplementation里面,结果我们惊人的发现:

AsyncProcess<?> asyncProcess = createAsyncProcess(tableName, pool, cb, conf);
asyncProcess.submitAll(list);
asyncProcess.waitUntilDone();

  它走的还是put一样的操作,既然是一样的,何苦代码写得那么绕呢?

5.查询操作

  现在讲一下scan吧,这个操作相对复杂点。还是老规矩,先上一下代码吧。

        Scan scan = new Scan();
        //scan.setTimeRange(new Date("20140101").getTime(), new Date("20140429").getTime());
        scan.setBatch(10);
        scan.setCaching(10);
        scan.setStartRow(Bytes.toBytes("cenyuhai-00000-20140101"));
        scan.setStopRow(Bytes.toBytes("cenyuhai-zzzzz-201400429"));
        //如果设置为READ_COMMITTED,它会取当前的时间作为读的检查点,在这个时间点之后的就排除掉了
        scan.setIsolationLevel(IsolationLevel.READ_COMMITTED);
        RowFilter rowFilter = new RowFilter(CompareOp.EQUAL, new RegexStringComparator("pattern"));
        ResultScanner resultScanner = table.getScanner(scan);
        Result result = null;
        while ((result = resultScanner.next()) != null) {
            //自己处理去吧...
        }

  这个是带正则表达式的模糊查询的scan查询,Scan这个类是包括我们查询所有需要的参数,batch和caching的设置,在我的另外一篇文章里面有写《hbase客户端设置缓存优化查询》

Scan查询的时候,设置StartRow和StopRow可是重头戏,假设我这里要查我01月01日到04月29日总共发了多少业务,中间是业务类型,但是我可能是所有的都查,或者只查一部分,在所有都查的情况下,我就不能设置了,那但是StartRow和StopRow我不能空着啊,所以这里可以填00000-zzzzz,只要保证它在这个区间就可以了,然后我们加了一个RowFilter,然后引入了正则表达式,之前好多人一直在问啊问的,不过我这个例子,其实不要也可以,因为是查所有业务的,在StartRow和StopRow之间的都可以要。

  好的,我们接着看,F3进入getScanner方法

if (scan.isSmall()) {
      return new ClientSmallScanner(getConfiguration(), scan, getName(), this.connection);
}
return new ClientScanner(getConfiguration(), scan, getName(), this.connection);

  这个scan还分大小, 没关系,我们进入ClientScanner看一下吧, 在ClientScanner的构造方法里面发现它会去调用nextScanner去初始化一个ScannerCallable。好的,我们接着来到ScannerCallable里面,这里需要注意的是它的两个方法,prepare和call方法。在prepare里面它主要干了两个事情,获得region的HRegionLocation和ClientService.BlockingInterface接口的实例,之前说过这个继承这个接口的只有Region Server的实现类。

  public void prepare(final boolean reload) throws IOException {
    this.location = connection.getRegionLocation(tableName, row, reload);    //HConnection.getClient()这个方法简直就是神器啊
    setStub(getConnection().getClient(getLocation().getServerName()));
  }

  ok,我们下面看看call方法吧

  public Result [] call() throws IOException {
     // 第一次走的地方,开启scanner
      if (scannerId == -1L) {
        this.scannerId = openScanner();
      } else {
        Result [] rrs = null;
        ScanRequest request = null;
        try {
          request = RequestConverter.buildScanRequest(scannerId, caching, false, nextCallSeq);
          ScanResponse response = null;             // 准备用controller去携带返回的数据,这样的话就不用进行protobuf的序列化了                 PayloadCarryingRpcController controller = new PayloadCarryingRpcController();                controller.setPriority(getTableName());
          response = getStub().scan(controller, request);
          nextCallSeq++;
          long timestamp = System.currentTimeMillis();
          // Results are returned via controller
          CellScanner cellScanner = controller.cellScanner();
          rrs = ResponseConverter.getResults(cellScanner, response);
      } catch (IOException e) {                      }         }     return rrs;

    }
    return null;
  }

  在call方法里面,我们可以看得出来,实例化ScanRequest,然后调用scan方法的时候把PayloadCarryingRpcController传过去,这里跟踪了一下,如果设置了codec的就从PayloadCarryingRpcController里面返回结果,否则从response里面返回。

  好的,下面看next方法吧。

    @Override
    public Result next() throws IOException { if (cache.size() == 0) {
        Result [] values = null;
        long remainingResultSize = maxScannerResultSize;
        int countdown = this.caching;           // 设置获取数据的条数              callable.setCaching(this.caching);
        boolean skipFirst = false;
        boolean retryAfterOutOfOrderException  = true;
        do {
        if (skipFirst) {
         // 上次读的最后一个,这次就不读了,直接跳过就是了
              callable.setCaching(1);
              values = this.caller.callWithRetries(callable);
              callable.setCaching(this.caching);
              skipFirst = false;
            }       values = this.caller.callWithRetries(callable);
          if (values != null && values.length > 0) {
            for (Result rs : values) {          //缓存起来               cache.add(rs);
              for (Cell kv : rs.rawCells()) {//计算出keyvalue的大小,然后减去
                remainingResultSize -= KeyValueUtil.ensureKeyValue(kv).heapSize();
              }
              countdown--;
              this.lastResult = rs;
            }
           }
          // Values == null means server-side filter has determined we must STOP
        } while (remainingResultSize > 0 && countdown > 0 && nextScanner(countdown, values == null));
            //缓存里面有就从缓存里面取            if (cache.size() > 0) {
          return cache.poll();
        }
     return null;
    }

  从next方法里面可以看出来,它是一次取caching条数据,然后下一次获取的时候,先把上次获取的最后一个给排除掉,再获取下来保存在cache当中,只要缓存不空,就一直在缓存里面取。

  好了,至此Scan到此结束。

hbase源码系列(二)HTable 探秘的更多相关文章

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

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

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

    hbase源码系列(十一)Put.Delete在服务端是如何处理?    在讲完之后HFile和HLog之后,今天我想分享是Put在Region Server经历些了什么?相信前面看了<HTab ...

  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源码系列之HFile

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

  6. 手牵手,从零学习Vue源码 系列二(变化侦测篇)

    系列文章: 手牵手,从零学习Vue源码 系列一(前言-目录篇) 手牵手,从零学习Vue源码 系列二(变化侦测篇) 陆续更新中... 预计八月中旬更新完毕. 1 概述 Vue最大的特点之一就是数据驱动视 ...

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

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

  8. Spring源码系列(二)--bean组件的源码分析

    简介 spring-bean 组件是 Spring IoC 的核心,我们可以使用它的 beanFactory 来获取所需的对象,对象的实例化.属性装配和初始化等都可以交给 spring 来管理. 本文 ...

  9. HBase源码分析:HTable put过程

    HBase版本:0.94.15-cdh4.7.0 在 HBase中,大部分的操作都是在RegionServer完成的,Client端想要插入.删除.查询数据都需要先找到相应的 RegionServer ...

随机推荐

  1. 启动和停止kafka 及kafka manager

    启动kafka: sh /app/pet_kafka_xxxx_cluster/bin/kafka-server-start.sh -daemon /app/pet_kafka_xxxx_cluste ...

  2. Latex中如何设置字体颜色(3种方式)

    Latex中如何设置字体颜色(三种方式)   1.直接使用定义好的颜色 \usepackage{color} \textcolor{red/blue/green/black/white/cyan/ma ...

  3. SQL Server中判断字符串出现的位置及字符串截取

    首先建一张测试表: )); insert into teststring values ('张三,李四,王五,马六,萧十一,皇宫'); 1.判断字符串中某字符(字符串)出现的次数,第一次出现的位置最后 ...

  4. MySql(十六):MySql架构设计——MySQL Cluster

    前言: MySQL Cluster 是一个基于 NDB Cluster 存储引擎的完整的分布式数据库系统.不仅仅具有高可用性,而且可以自动切分数据,冗余数据等高级功能.和 Oracle Real Cl ...

  5. 复习:JSP基本的语法(JSP凝视 + JSP指令 + JSP脚本元素 + JSP动作元素)

    JSP原理: 1.    对于每个请求.jsp容器都会创建一个新的线程来处理它: 2.    Servlet容器载入jsp后转换成的servlet(.class文件)是常驻内存的,所以对应速度一般比較 ...

  6. The superclass "javax.servlet.http.HttpServlet" was not found 问题解决

    项目中报" The superclass "javax.servlet.http.HttpServlet" was not found "这个错误,是因为缺少t ...

  7. xcode6 怎样下载ios7模拟器

    1. 怎样下载ios7模拟器 点击xcode.选择"Preferences".选择"downloads",就能够看见IOS 7.1,只是下载有点慢. 2. 怎样 ...

  8. DDR3控制

    很简单的,app_en和app_rdy一握手,代表MIG接受了一个写数据请求或者读数据请求,只要保证app_en和app_rdy握手,根本就不关心写数据rdy,这是MIG的一个bug,你看它源码就知道 ...

  9. Java方法内部需要重新请求的一种机制

    有这样一个需求,当调用某个方法抛出异常,比如通过 HttpClient 调用远程接口时由于网络原因报 TimeOut 异常:或者所请求的接口返回类似于“处理中”这样的信息,需要重复去查结果时,我们希望 ...

  10. password学4——Java 加密解密之消息摘要算法(MD5 SHA MAC)

    Java 加密解密之消息摘要算法(MD5 SHA MAC) 消息摘要 消息摘要(Message Digest)又称为数字摘要(Digital Digest). 它是一个唯一相应一个消息或文本的固定长度 ...