HBase中rowkey是索引,任何对全表的扫描或是统计都需要用到scan接口。
本文主要探讨的是客户端是如何通过scan来扫描hbase的table的。
主要关注几个问题:
1.每一个Table可能不止一个region,分布在不同的regionserver上。客户端需要找到每个region的位置并与之通信;
2.Hbase是以append形式把数据写进去的,无论是写还是删除只是在keyvalue上打个标签,然后put进内存而已。等到region的内存满64m以后,会flush成一个新的文件写入磁盘。真正的数据删除操作会等到执行compact时候进行;那么在compact之前,在内存或者是storfile里面就很夹杂着很多delete或put的数据,甚至会有相同的数据。如何正确地识别它们,并且高效率的读取所想要的数据,这就是一个很大的问题;
3.客户端的scan可以配置几个参数maxversion,batch,caching。第一个是功能性需求,第二和第三个则对于client的性能有提高,这几个参数是如何作用的;
4.客户端起多个线程进行scan的时候如何做到每个线程不相互影响或者说数据不会紊乱
5.如何在保证正确性的前提下高效地scan是最重要的
首先来看看一个简单的scan的客户端程序
public static void getAllData(HTable table) throws IOException {
Scan s = new Scan();
s.setMaxVersions();
s.setBatch(1000);
ResultScanner rs = table.getScanner(s);
for (Result r : rs) {
for (KeyValue kv : r.raw()) {
System.out.print("rowkey : " + new String(kv.getRow()) + " ");
System.out.println(new String(kv.getFamily()) + ":"
+ new String(kv.getQualifier()) + " timestamp " + kv.getTimestamp()
+ new String(kv.getValue()));
}
}
}
上面的函数即是一个简单的scan table的示例
调用的实现函数table.getScanner(s)
public ResultScanner getScanner(final Scan scan) throws IOException {
ClientScanner s = new ClientScanner(scan);//new 一个ClientScanner对象
s.initialize();// 初始化
return s;
}
ClientScanner包含一个scan,以及caching。
客户端与regionserver通信一次,会找到regionserver的region,并扫描region返回一定数据。
这个数据量是由scan的Batch指定的。
而caching的作用就是通信一次找到region,调用扫描caching次,也就是说用这两个参数的话,一次通信可以返回的数据为caching*batch条。显然这会减少客户端和rs的通信量。
返回的数据时先放到客户端进行缓存的(这样也是为了提高性能)如果一次返回的数据过多的话,很有可能造成客户端的内存oom,通过batch的控制可以很好的解决这个问题。
clientScanner的初始化过程主要如下
private boolean nextScanner(int nbRows, final boolean done)
throws IOException {
// Close the previous scanner if it's open
if (this.callable != null) {
this.callable.setClose();
getConnection().getRegionServerWithRetries(callable);
this.callable = null;
}
// Where to start the next scanner
byte [] localStartKey;
// if we're at end of table, close and return false to stop iterating
if (this.currentRegion != null) {
byte [] endKey = this.currentRegion.getEndKey();
if (endKey == null ||
Bytes.equals(endKey, HConstants.EMPTY_BYTE_ARRAY) ||
checkScanStopRow(endKey) ||
done) {
close();
return false;
}
localStartKey = endKey;
} else {
localStartKey = this.scan.getStartRow();
}
try {
callable = getScannerCallable(localStartKey, nbRows);
// Open a scanner on the region server starting at the
// beginning of the region
getConnection().getRegionServerWithRetries(callable);
this.currentRegion = callable.getHRegionInfo();
} catch (IOException e) {
close();
throw e;
}
return true;
}
第一步是关闭callable,关闭的原因,我们下面会分析到
第二步确定localstartkey,当只有一个region的时候localstartkey是不用变化的,因为client是通过startkey来确定region的位置的。但是当有多个region的时候startkey就会变化。startkey是scan中的statrow,我们前面说过hbase是以rowkey作为索引的,也就是此处的startkey,通过tablename以及startrow我们可以知道第一个region的位置。
第三步就是getConnection().getRegionServerWithRetries(callable);
这个过程包含两步:
1.找到regionserver,并与之建立通信
2 .如果是第一次扫描一个region的话会rpc调用regionserver的openscanner方法,构造一个Regionscanner,以后的数据查找都是使用该regionscanner来实现的,其内部结构比较复杂具体可见;然后会随机生成一个scannerId,与regionscanner一起放入map中,并把id返回给客户端。客户端拿到这个id以后,会与rs进行通信,rpc调用regionserver的next()方法,第一步就是会从map中取出其对应的regionscanner,类似一个tcp的三次握手,具体的取数据过程就是由regionscanner来负责的。
clientscanner重写了迭代器:
主要的一些功能是:当扫描的数据为空了,注意这里是指数据为空,并非是null,当初看代码的时候在此处纠结了很长时间。数据为空的时候,此时就认为该region已经扫描结束了,需要找第二个region并构造其regionscanner(region是有序的,以其在meta表中的顺序依次扫描)也就是调用nextscanner()方法。
如果这个时候callable没有关闭的话,那么regionscanner就不会清除。regionscanner中维护的是一系列storeScanner,每个storescanner下面又有很多storefilescanner,这些会消耗很多内存。
此外构建新的nextscanner的时候会重置startkey,以寻找下一个region。
依次循环知道到达endkey位置 。。
本文主要分析的是客户端的机制,涉及到服务器端的都是一笔带过。