一、背景
一直听说hbase scanner的逆向查询比正向查询要慢很多,而且多消耗了很多资源。原因在哪里,并没有找到明确的答案。分为客户端和服务端两篇,正向和逆向的原理以及其中的异同。本篇讨论客户端。参考的jar包版本是hbase-client-1.1.13。
二、代码分析
1. 创建Scanner
HTable.getScanner(Scan)
org.apache.hadoop.hbase.client.Scan负责提供产生Scanner需要的配置信息,包括startRow、stopRow、maxResultSize、caching等。下面是maxResultSize和caching对应的官网解释:
hbase.client.scanner.caching
Number of rows that we try to fetch when calling next on a scanner if it is not served from (local, client) memory. This configuration works together with hbase.client.scanner.max.result.size to try and use the network efficiently. The default value is Integer.MAX_VALUE by default so that the network will fill the chunk size defined by hbase.client.scanner.max.result.size rather than be limited by a particular number of rows since the size of rows varies table to table. If you know ahead of time that you will not require more than a certain number of rows from a scan, this configuration should be set to that row limit via Scan#setCaching. Higher caching values will enable faster scanners but will eat up more memory and some calls of next may take longer and longer times when the cache is empty. Do not set this value such that the time between invocations is greater than the scanner timeout; i.e. hbase.client.scanner.timeout.period
====>以下是上面的重点翻译:
hbase.client.scanner.caching配置的是一个int类型的值,表示一次RPC请求从Region Server端获取的数据行数。为什么叫做caching呢?由于每次next()是获取一行数据,但是一次RPC调用是多行,实际是先缓存在内存中,当某次next()发现缓存中没有数据时才会发起RPC调用。这个参数和hbase.client.scanner.max.result.size配合使用,可以使网络使用更有效率。hbase.client.scanner.caching默认值是Integer.MAX_VALUE,即231-1,这个时候就以hbase.client.scanner.max.result.size为准。
hbase.client.scanner.max.result.size
Maximum number of bytes returned when calling a scanner’s next method. Note that when a single row is larger than this limit the row is still returned completely. The default value is 2MB, which is good for 1ge networks. With faster and/or high latency networks this value should be increased.
====>以下是上面的重点翻译:
一次RPC调用返回的最大字节数。注意当表的一行数据大小大于这个限制,仍然完整返回完整的数据行。默认配置是2MB。对于更快或者高延迟的网络,这个值应该增加。
根据配置不同,可以产生4种不同的ClientScanner,分别是ClientScanner、ReversedClientScanner、ClientSmallScanner、ClientSmallReversedScanner。以上本文只讨论ClientScanner与ReversedClientScanner。见图1。
正向查询走的ClientScanner,其中ReversedClientScanner是ClientScanner的子类,大部分操作是复用的ClientScanner。
2.通过Scanner获取数据
ClientScanner通过执行next()每次得到一条行记录,遍历得到查询结果。一次查询可以分别跨多个Region,在ClientScanner实例内部维护着多个ScannerCallable实例(并不是同时实例多个对象,注意,这里方便理解他们之间的关系),每个对应一个Region来获取数据,关系对应如图2。
逆向查询是下面的关系
下面对此过程进行详细说明。
2.1 获取region所在region server
ScannerCallable对象有两个比较重要的方法:prepare和call。其中prepare是每次向服务器发起RPC调用(就是call方法)获取数据前做的准备工作,call是从服务器获取数据。
prepare方法中根据用户表名和startRowKey在meta表中查询需要查询的region所在region server的连接信息。具体实现在:
org.apache.hadoop.hbase.client.ConnectionManager.locateRegionInMeta中。
2.2 读取数据
调用这个方法获取一个org.apache.hadoop.hbase.client.Result实例,表示一行数据,可以是多个Cell、一个Cell的多个版本。如果cache中有数据则从缓存中获取返回,如果缓存中没有数据则调用loadCache()方法,从服务器端获取多行数据,条数根据org.apache.hadoop.hbase.client.Scan中的caching配置,同时受到maxResultSize参数影响。然后再从缓存中返回一行数据。此处缓存通过LinkedList实现。
ClientScanner.next()是对外提供给用户遍历Scan命中的数据用的,内部隐藏了如何切换Region的逻辑,通过调用方法
protected boolean nextScanner(int nbRows, final boolean done)
切换当前使用的ScannerCallable对象。例如:一共{1,2,3,4,5,6} 6条数据,其中{1,2,3}在Region A上,{4,5,6}在Region B上。用户代码中只需要调用ClientScanner.next()循环获取数据即可,一开始实例化ScannerCallable(A)用于查询Region A的数据,假设caching是1,每次读取1条数据。当读取到3之后,到达当前Region的最后一条数据,下一次next()会新实例化ScannerCallable(B)并开始读取4以及后面的数据。
3.正向Scan和逆向Scan在有哪些不同
3.1 ReversedClientScanner vs ClientScanner
ReversedClientScanner是ClientScanner的子类,重写了nextScanner和checkScanStopRow方法,前者用于切换下一个Region,后者判断本次查询是否读取完毕。
逆向
正向
3.2 ReversedScannerCallable vs ScannerCallable
区别在于获取region server的方式上。ScannerCallable中的时间复杂度是O(1),而ReversedScannerCallabe中的时间复杂度是O(n),但事实上这个n并不大,可以近似于O(1)了。最大的区别在于ReversedScannerCallabe中获取region server地址的locateRegionsInRange方法。以下为源码:
/**
* Get the corresponding regions for an arbitrary range of keys.
* @param startKey Starting row in range, inclusive
* @param endKey Ending row in range, exclusive
* @param reload force reload of server location
* @return A list of HRegionLocation corresponding to the regions that contain
* the specified range
* @throws IOException
*/
private List<HRegionLocation> locateRegionsInRange(byte[] startKey,
byte[] endKey, boolean reload) throws IOException {
final boolean endKeyIsEndOfTable = Bytes.equals(endKey,
HConstants.EMPTY_END_ROW);
if ((Bytes.compareTo(startKey, endKey) > 0) && !endKeyIsEndOfTable) {
throw new IllegalArgumentException("Invalid range: "
+ Bytes.toStringBinary(startKey) + " > "
+ Bytes.toStringBinary(endKey));
}
List<HRegionLocation> regionList = new ArrayList<HRegionLocation>();
byte[] currentKey = startKey;
do {
RegionLocations rl = RpcRetryingCallerWithReadReplicas.getRegionLocations(!reload, id,
getConnection(), getTableName(), currentKey);
HRegionLocation regionLocation = id < rl.size() ? rl.getRegionLocation(id) : null;
if (regionLocation != null && regionLocation.getRegionInfo().containsRow(currentKey)) {
regionList.add(regionLocation);
} else {
throw new DoNotRetryIOException("Does hbase:meta exist hole? Locating row "
+ Bytes.toStringBinary(currentKey) + " returns incorrect region "
+ regionLocation.getRegionInfo());
}
currentKey = regionLocation.getRegionInfo().getEndKey();
} while (!Bytes.equals(currentKey, HConstants.EMPTY_END_ROW)
&& (endKeyIsEndOfTable || Bytes.compareTo(currentKey, endKey) < 0));
return regionList;
}
ScannerCallable的O(1)代码为:
RegionLocations rl = RpcRetryingCallerWithReadReplicas.getRegionLocations(!reload,
id, getConnection(), getTableName(), getRow());
location = id < rl.size() ? rl.getRegionLocation(id) : null;
三、结论
导致逆向查询慢于正向查询和客户端的关系不大。问题应该出在服务端上,下一篇进行服务端的分析。