hbase源码系列(二)HTable 探秘
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 探秘的更多相关文章
- hbase源码系列(十二)Get、Scan在服务端是如何处理
hbase源码系列(十二)Get.Scan在服务端是如何处理? 继上一篇讲了Put和Delete之后,这一篇我们讲Get和Scan, 因为我发现这两个操作几乎是一样的过程,就像之前的Put和Del ...
- 11 hbase源码系列(十一)Put、Delete在服务端是如何处理
hbase源码系列(十一)Put.Delete在服务端是如何处理? 在讲完之后HFile和HLog之后,今天我想分享是Put在Region Server经历些了什么?相信前面看了<HTab ...
- 9 hbase源码系列(九)StoreFile存储格式
hbase源码系列(九)StoreFile存储格式 从这一章开始要讲Region Server这块的了,但是在讲Region Server这块之前得讲一下StoreFile,否则后面的不好讲下去 ...
- 10 hbase源码系列(十)HLog与日志恢复
hbase源码系列(十)HLog与日志恢复 HLog概述 hbase在写入数据之前会先写入MemStore,成功了再写入HLog,当MemStore的数据丢失的时候,还可以用HLog的数据来进行恢 ...
- HBase源码系列之HFile
本文讨论0.98版本的hbase里v2版本.其实对于HFile能有一个大体的较深入理解是在我去查看"到底是不是一条记录不能垮block"的时候突然意识到的. 首先说一个对HFile ...
- 手牵手,从零学习Vue源码 系列二(变化侦测篇)
系列文章: 手牵手,从零学习Vue源码 系列一(前言-目录篇) 手牵手,从零学习Vue源码 系列二(变化侦测篇) 陆续更新中... 预计八月中旬更新完毕. 1 概述 Vue最大的特点之一就是数据驱动视 ...
- hbase源码系列(十二)Get、Scan在服务端是如何处理?
继上一篇讲了Put和Delete之后,这一篇我们讲Get和Scan, 因为我发现这两个操作几乎是一样的过程,就像之前的Put和Delete一样,上一篇我本来只打算写Put的,结果发现Delete也可以 ...
- Spring源码系列(二)--bean组件的源码分析
简介 spring-bean 组件是 Spring IoC 的核心,我们可以使用它的 beanFactory 来获取所需的对象,对象的实例化.属性装配和初始化等都可以交给 spring 来管理. 本文 ...
- HBase源码分析:HTable put过程
HBase版本:0.94.15-cdh4.7.0 在 HBase中,大部分的操作都是在RegionServer完成的,Client端想要插入.删除.查询数据都需要先找到相应的 RegionServer ...
随机推荐
- flock防止crontab脚本周期内未执行完重复执行(转)
如果某脚本要运行30分钟,可以在Crontab里把脚本间隔设为至少一小时来避免冲突.而比较糟的情况是可能该脚本在执行周期内没有完成,接着第二个脚本又开始运行了.如何确保只有一个脚本实例运行呢?一个好用 ...
- mysql-8.0.11 比较坑的地方dba门要淡定
[事件描述] 突然之间大量的连接进入数据库.并放开手干,这个使得mysql使用了大量的内存,触发了linux的oom机制.然后mysql就这样 被linux给干掉了.没错MySQL宕机了,要相信我说的 ...
- PHP实现无符号右移(js中的 >>>)
移位包括有符号左移(<<).有符号右移(>>).无符号右移(>>>),其中 js 支持三种移位,PHP只支持前两种移位(没查到第三种),恰好需要PHP进行无符 ...
- 如何安全的下载Devcon.exe文件
devcon.exe是windows设备管理器的命令行版本,可以让你在cmd中修改设备,但是微软没有提供单独的下载,只能下载一个2G多的wdk包(windows drive kits)才行.私下使用别 ...
- UIScrollView中图片放大后不居中,或居中后移动有偏差解决办法
转自:http://blog.sina.com.cn/s/blog_6ae8b50d0100yret.html 问题的描述如下:要显示一张图片,也只需要显示一张图片,该图片可能很大,也可能很小,但是要 ...
- 根据第三方提供的webservice地址获取文件信息
import org.apache.axis.client.Call; import org.apache.axis.client.Service; import org.apache.axis.en ...
- ES6,新增数据结构WeakSet的用法
WeakSet和Set类似,同样是元素不重复的集合,它们的区别是WeakSet内的元素必须是对象,不能是其它类型. 特性: 1.元素必须是对象. 添加一个number类型的元素. const ws = ...
- 每日英语:KFC's Crisis in China Tests Ingenuity of Man Who Built Brand
Sam Su for years ran one of the highest-flying foreign business operations in China. These days, he' ...
- linux命令(50):comm命令的用法,求交集
Linux comm命令 使用局限比较大,适用于特殊场合: Linux comm命令用于比较两个已排过序的文件. 排序:sort -u file 这项指令会一列列地比较两个已排序文件的差异,并将其结果 ...
- 【设计模式】适配器模式与Reader、InputStream之间的适配
简述 适配器模式,目的是将A对象通过适配.转换的方式转换成B对象来使用,为什么转换为B对象?因为用户通过依赖B对象来使用. 适配器的实现有两种方式. 继承实现 组合实现 基于Java单继承的机制,组合 ...