前言
HBase是一个基于RowKey进行检索的分布式数据库。它按照行的方向将表中的数据切分成一个个Region,而每个Region都会存在一个起始行StartKey和一个终止行EndKey。Region会最终选择一个RegionSever上线,并依靠RegionSever对外提供数据存取服务。那么,HBase是如何实现数据的检索,也就是它如何将需要读写的行Row准确的定位到其所在Region和RegionServer上的呢?此处,我们就将研究下HRegion的定位。
之前我们已经研究过HBase读取数据的应用–Scan,在Scan的过程中,它每次通过RPC与服务端通信,都是针对特定的Region及其所在RegionServer进行数据读取请求,将数据缓存至客户端。在它迭代获取数据的Scanner的next()中,会检查缓存中是否存在数据,若无,则加载缓存,然后直接从缓存中拉取数据,代码如下:
@Override
public Result next() throws IOException {
try {
lock.lock();
while (cache.isEmpty()) {
handleException();
if (this.closed) {
return null;
}
try {
notEmpty.await();
} catch (InterruptedException e) {
throw new InterruptedIOException("Interrupted when wait to load cache");
}
}
Result result = pollCache();
if (prefetchCondition()) {
notFull.signalAll();
}
return result;
} finally {
lock.unlock();
handleException();
}
}
而这个加载缓存的loadCache()方法,则会调用call()方法,发送RPC请求给对应的RegionServer上的Region,那么它是如何定位Region的呢?我们先看下这个call()方法,代码如下:
Result[] call(Scan scan, ScannerCallableWithReplicas callable,
RpcRetryingCaller<Result[]> caller, int scannerTimeout)
throws IOException, RuntimeException {
if (Thread.interrupted()) {
throw new InterruptedIOException();
}
// callWithoutRetries is at this layer. Within the ScannerCallableWithReplicas,
// we do a callWithRetries
// caller为RpcRetryingCaller类型
// callable为ScannerCallableWithReplicas类型
return caller.callWithoutRetries(callable, scannerTimeout);
}
实际上caller为RpcRetryingCaller类型,而callable为ScannerCallableWithReplicas类型,我们看下RpcRetryingCaller的callWithoutRetries()方法,关键代码如下:
// 先调用prepare()方法,再调用call()方法,超时时间为callTimeout
callable.prepare(false);
return callable.call(callTimeout);
实际上最终调用的是callable的call()方法,也就是ScannerCallableWithReplicas的call()方法,我们跟进下关键代码:
代码如下(示例):
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
Region的定位是通过调用RpcRetryingCallerWithReadReplicas的getRegionLocations()方法进行的,它需要是否使用缓存标识位useCache、主从复制replicaId、ClusterConnection集群连接器cConnection,表名tableName、所在行Row等关键参数,并返回RegionLocations,用于表示Region的位置信息。而RegionLocations中存在一个数组locations,它的定义如下:
// locations array contains the HRL objects for known region replicas indexes by the replicaId.
// elements can be null if the region replica is not known at all. A null value indicates
// that there is a region replica with the index as replicaId, but the location is not known
// in the cache.
private final HRegionLocation[] locations; // replicaId -> HRegionLocation.
它是一个HRegionLocation类型的数组,实际上存储的是replicaId到HRegionLocation的映射,replicaId就是数组的下标。而上面调用getRegionLocations()方法时,传入的replicaId为RegionReplicaUtil.DEFAULT_REPLICA_ID,也就是0。那么HRegionLocation是什么呢?看下它的两个关键成员变量就知道了:
private final HRegionInfo regionInfo;
private final ServerName serverName;
从RpcRetryingCallerWithReadReplicas的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);
}
} catch (DoNotRetryIOException | InterruptedIOException | RetriesExhaustedException e) {
throw e;
} catch (IOException e) {
throw new RetriesExhaustedException("Cannot get the location for replica" + replicaId
+ " of region for " + Bytes.toStringBinary(row) + " in " + tableName, e);
}
if (rl == null) {
throw new RetriesExhaustedException("Cannot get the location for replica" + replicaId
+ " of region for " + Bytes.toStringBinary(row) + " in " + tableName);
}
return rl;
}
}
其实逻辑很简单,就分两种情况,使用缓存和不使用缓存。而且,我们也应该能猜出来,即便是使用缓存,如果缓存中没有的话,它还是会走一遍不使用缓存的流程,将获取到的Region位置信息加载到缓存中,然后再返回给外部调用者,最终我们需要共同研究的仅仅是不使用缓存的情况下如何定位Region而已。
首先,我们来看下不使用缓存的情况下,是如何进行Region定位的。它调用的是ClusterConnection的relocateRegion()方法,而这个ClusterConnection是一个接口,它的实例化,是在HTable中进行,然后一层层传递过来的。我们先看下它的实例化,在HTable的构造方法中,代码如下:
this.connection = ConnectionManager.getConnectionInternal(conf);
通过ConnectionManager的静态方法getConnectionInternal(),从配置信息conf中加载而来。继续看下它的代码:
static ClusterConnection getConnectionInternal(final Configuration conf)
throws IOException {
// 根据配置信息conf构造HConnectionKey
HConnectionKey connectionKey = new HConnectionKey(conf);
synchronized (CONNECTION_INSTANCES) {
// 先从CONNECTION_INSTANCES中根据HConnectionKey获取连接HConnectionImplementation类型的connection,
// CONNECTION_INSTANCES为HConnectionKey到HConnectionImplementation的映射集合
HConnectionImplementation connection = CONNECTION_INSTANCES.get(connectionKey);
if (connection == null) {// 如果CONNECTION_INSTANCES中不存在
// 调用createConnection()方法创建一个HConnectionImplementation
connection = (HConnectionImplementation)createConnection(conf, true);
// 将新创建的HConnectionImplementation与HConnectionKey的对应关系存入CONNECTION_INSTANCES
CONNECTION_INSTANCES.put(connectionKey, connection);
} else if (connection.isClosed()) {// 如果CONNECTION_INSTANCES中存在,且已关闭的话
// 调用ConnectionManager的deleteConnection()方法,删除connectionKey对应的记录:
// 1、调用decCount()方法减少计数;
// 2、从CONNECTION_INSTANCES类表中移除connectionKey对应记录;
// 3、调用HConnectionImplementation的internalClose()方法处理关闭连接事宜
ConnectionManager.deleteConnection(connectionKey, true);
// 调用createConnection()方法创建一个HConnectionImplementation
connection = (HConnectionImplementation)createConnection(conf, true);
// 将新创建的HConnectionImplementation与HConnectionKey的对应关系存入CONNECTION_INSTANCES
CONNECTION_INSTANCES.put(connectionKey, connection);
}
// 连接计数器增1
connection.incCount();
// 返回连接
return connection;
}
}
这个HConnectionKey实际上是连接的一个Key类,包含了连接对应的hbase.zookeeper.quorum、hbase.zookeeper.property.clientPort等重要信息,而获取连接的方法也很简单,如果之前创建过key相同的连接,直接从CONNECTION_INSTANCES集合中根据HConnectionKey获取,并将连接计数器增1,直接返回连接,获取不到的话,根据HConnectionKey创建一个新的,并加入CONNECTION_INSTANCES集合,而且,如果获取到的连接是Closed的话,调用ConnectionManager的deleteConnection()方法,删除connectionKey对应的记录,创建一个新的连接创建一个HConnectionImplementation,并加入到CONNECTION_INSTANCES集合。
述ClusterConnection的实现类就是HConnectionImplementation,那么我们回到正轨上,继续研究Region的定位,先看下不使用缓存的情况的情况下是如何处理的。好,我们进入HConnectionImplementation的relocateRegion()方法,代码如下:
@Override
public RegionLocations relocateRegion(final TableName tableName,
final byte [] row, int replicaId) throws IOException{
// Since this is an explicit request not to use any caching, finding
// disabled tables should not be desirable. This will ensure that an exception is thrown when
// the first time a disabled table is interacted with.
if (!tableName.equals(TableName.META_TABLE_NAME) && isTableDisabled(tableName)) {
throw new TableNotEnabledException(tableName.getNameAsString() + " is disabled.");
}
return locateRegion(tableName, row, false, true, replicaId);
}
先做一个必要的检查,如果表不是META表,并且表被禁用的话,直接抛出TableNotEnabledException,然后调用locateRegion()方法进行Region的定位,传入的useCache为false,retry为true,即不使用缓存,并且进行重试。
总结
以上就是今天要讲的内容, 过程如上所述,要点下篇继续叙述。