了解HBase架构的用户应该知道,HBase是一种基于LSM模型的分布式数据库。LSM的全称是Log-Structured Merge-Trees。即日志-结构化合并-树。
相比于Oracle普通索引所採用的B+树,LSM模型的最大特点就是,在读写之间採取一种平衡,牺牲部分读数据的性能,来大幅度的提升写数据的性能。通俗的讲,HBase写数据如此快,正是因为基于LSM模型,将数据写入内存和日志文件后即马上返回。
可是,数据始终在内存和日志中是不妥当的,首先内存毕竟是有限的稀缺资源。持续的写入会造成内存的溢出,而日志的写入仅是因为内存数据系统宕机或进程退出后立马消失而採取的一种保护性措施,而不是作为终于的数据持久化。
日志文件不能用来做终于持久化的另外一个原因,就是写入日志时不过简单的追加(append)。读数据时效率会很很的低。
MemStore的flush就是为了解决上述问题而採取的一种有效措施。关于B+树、LSM模型,读者可自行补脑。本文只阐述HRegion上MemStore的flsuh流程,而关于何时发生flush等其它内容将在其它的博文中进行分析。
说了这么多,以下,就開始奇妙的源代码分析之旅吧~
先从宏观上对HRegion上fMemStore的flush流程有一个总体的把握。
HRegion上flush的入口方法为flushCache(),其处理总体流程如图所看到的:
以下,我们看下HRegion上flushCache()方法,代码例如以下:
/**
* Flush the cache.
*
* When this method is called the cache will be flushed unless:
* <ol>
* <li>the cache is empty</li>
* <li>the region is closed.</li>
* <li>a flush is already in progress</li>
* <li>writes are disabled</li>
* </ol>
*
* <p>This method may block for some time, so it should not be called from a
* time-sensitive thread.
* 这种方法可能会堵塞一段时间,所以对时间敏感的线程不应该调用该方法。
*
* @return true if the region needs compacting
*
* @throws IOException general io exceptions
* @throws DroppedSnapshotException Thrown when replay of wal is required
* because a Snapshot was not properly persisted. The region is put in closing mode, and the
* caller MUST abort after this.
*/
public FlushResult flushcache() throws IOException {
// fail-fast instead of waiting on the lock
// 高速失败,而不是等待锁
// 假设Region正处于关闭状态。记录日志,并返回CANNOT_FLUSH的刷新结果
if (this.closing.get()) {
String msg = "Skipping flush on " + this + " because closing";
LOG.debug(msg);
return new FlushResult(FlushResult.Result.CANNOT_FLUSH, msg);
}
// 获取任务追踪器。并创建初始状态
MonitoredTask status = TaskMonitor.get().createStatus("Flushing " + this);
// 设置任务追踪器的状态:请求Region读锁
status.setStatus("Acquiring readlock on region");
// block waiting for the lock for flushing cache
// 获取Region的读锁,堵塞等待刷新缓存的锁释放
/**
* 我的理解,这个lock锁好像是Region行为上的一个读写锁,加上这个锁,控制Region的总体行为,比方flush、compact、close等。
* flush和compact使用的是读锁,是一个共享锁,意味着flush和compact能够同步进行。可是不能运行close。由于close是写锁。
* 它是一个独占锁。一旦它占用锁。其它线程就不能发起flush、compact等操作,当然,close线程本身除外,由于Region在下线前要保证
* MemStore内的数据被flush到文件。
*/
lock.readLock().lock();
try {
// 假设Region已经下线。记录日志并返回CANNOT_FLUSH的结果
if (this.closed.get()) {
String msg = "Skipping flush on " + this + " because closed";
LOG.debug(msg);
status.abort(msg);
return new FlushResult(FlushResult.Result.CANNOT_FLUSH, msg);
}
// 假设协处理器不为空
if (coprocessorHost != null) {
// 设置任务追踪器的状态:运行协处理器预刷写钩子preFlush()方法
status.setStatus("Running coprocessor pre-flush hooks");
// 运行协处理器预刷写钩子preFlush()方法
coprocessorHost.preFlush();
}
if (numMutationsWithoutWAL.get() > 0) {
numMutationsWithoutWAL.set(0);
dataInMemoryWithoutWAL.set(0);
}
synchronized (writestate) {
if (!writestate.flushing && writestate.writesEnabled) {
// 假设writestate不是flushing,且writestate的能够读取启用,将状态中的flushing设置为true。表示正在刷新
this.writestate.flushing = true;
} else {
// 否则记录日志。并返回CANNOT_FLUSH的结果
if (LOG.isDebugEnabled()) {
LOG.debug("NOT flushing memstore for region " + this
+ ", flushing=" + writestate.flushing + ", writesEnabled="
+ writestate.writesEnabled);
}
String msg = "Not flushing since "
+ (writestate.flushing ? "already flushing"
: "writes not enabled");
status.abort(msg);
return new FlushResult(FlushResult.Result.CANNOT_FLUSH, msg);
}
}
try {
// 运行真正的flush
FlushResult fs = internalFlushcache(status);
// 刷新结束后。假设协处理器不为空,运行协处理器的钩子方法postFlush()
if (coprocessorHost != null) {
status.setStatus("Running post-flush coprocessor hooks");
coprocessorHost.postFlush();
}
// 状态追踪器标记完毕状态
status.markComplete("Flush successful");
// 返回刷新结果
return fs;
} finally {
synchronized (writestate) {
// 将writestate中的flushing、flushRequested均设置为false
writestate.flushing = false;
this.writestate.flushRequested = false;
writestate.notifyAll();
}
}
} finally {
// 释放读锁
lock.readLock().unlock();
// 清空状态
status.cleanup();
}
}
通过代码我们能够知道。其处理逻辑例如以下:
1、首先须要推断下HRegion的状态,假设Region正处于关闭状态,记录日志,并返回CANNOT_FLUSH的刷新结果。
ps:这是大数据诸多框架,比方HDFS、HBase、Spark等绝大多数内部处理流程採取的一种通用的模式。推断涉及到的实体,比方HRegion、DataNode、DataXceiveServer等的状态,比方正在关闭closing、已经关闭closed等,目的是协调各实体协同工作,保障本处理流程是真实有效的。
2、获取任务追踪器,并创建初始状态:Flushing ****HRegion,初始化后的状态对象为MonitoredTask类型的status;
3、设置任务追踪器的状态:请求Region读锁:Acquiring readlock on region;
4、获取Region的读锁,堵塞等待刷新缓存的锁释放;
5、再次推断HRegion的状态。假设Region已经下线,记录日志并返回CANNOT_FLUSH的结果。
6、假设协处理器不为空:
6.1、设置任务追踪器的状态:运行协处理器预刷写钩子preFlush()方法:Running coprocessor pre-flush hooks;
6.2、运行协处理器预刷写钩子preFlush()方法;
7、假设writestate不是flushing。且writestate的能够读取启用,将状态中的flushing设置为true,表示正在刷新,否则记录日志,并返回CANNOT_FLUSH的结果。
8、调用internalFlushcache()方法。运行真正的flush。
9、刷新结束后,假设协处理器不为空,设置状态,即Running post-flush coprocessor hooks。并运行协处理器的钩子方法postFlush();
10、状态追踪器标记完毕状态:Flush successful。
11、将writestate中的flushing、flushRequested均设置为false;
12、释放读锁。并清空状态追踪器的状态。
13、返回刷新结果。
至此,HRegion上MemStore的flush流程所有完成,当中internalFlushcache()是其真正运行flush的核心方法。关于这部分我们将在下一篇文章中解说。在此,仅仅是概括下外围的总体流程。
关于上述流程,有下面几点须要单独说明下:
1、关于closing和closed标志位状态的推断
HRegion中,有两个关于关闭的状态标志位成员变量,分别定义例如以下:
final AtomicBoolean closed = new AtomicBoolean(false);
final AtomicBoolean closing = new AtomicBoolean(false);
为什么须要两个状态标志位呢?我么知道,Region下线关闭时,须要处理一些诸如flush等的操作,所以一般比較耗时。那么在其下线关闭期间,我们不希望该Region再运行flsuh、compact等请求,所以,我们就须要两个标志位,一个表示正在关闭过程的closing。另外一个是已经关闭的closed。
所以。flush、compact等流程的运行,都会去推断这两个状态位,确保flush和compact同意被运行。
2、关于读写锁的使用
HRegion中,保持了两把锁,分别定义例如以下:
// Used to guard closes 用于保护关闭
final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// Stop updates lock 停止更新锁
private final ReentrantReadWriteLock updatesLock = new ReentrantReadWriteLock();
这两个锁均为ReentrantReadWriteLock类型的读写锁,当中,lock用于Region的close、compact、flush等的并发控制,它控制的是Region的总体行为,更详细的,compact()和flushCache()方法中,用的是lock的读锁--共享锁,而doClose()方法中,用的是lock的写锁--独占锁,这也就意味着,在Region下线,运行doClose()方法时。它必须等待compact()和flushCache()方法调用完,且一旦它获得了lock的写锁,兴许Region将不会再运行Region的compact和flush,当然,doClose()内部仍然会在下线前flush掉它的memstore,同一时候共享锁业也实现了Region的flush和compact在理论上能够同一时候进行。而updatesLock则用于Region数据更新方面,在flush的核心方法internalFlushcache()中,则是使用的updatesLock的写锁。
3、关于写状态WriteState的使用
再来说下HRegion的另外一个成员变量writestate,它是HRegion的内部类WriteState类型的,这个类是一种协调刷新、合并与关闭操作的很使用的数据结构。它的关键成员变量定义例如以下:
// Set while a memstore flush is happening.
// 当一个memstore刷新发生时设置
volatile boolean flushing = false;
// Set when a flush has been requested.
// 当一个刷新请求发生时设置
volatile boolean flushRequested = false;
// Number of compactions running.
// 合并进行的数目
volatile int compacting = 0;
// Gets set in close. If set, cannot compact or flush again.
// 假设被设置,将不再支持合并与刷新
volatile boolean writesEnabled = true;
// Set if region is read-only
// 假设Region仅仅读时设置
volatile boolean readOnly = false;
// whether the reads are enabled. This is different than readOnly, because readOnly is
// static in the lifetime of the region, while readsEnabled is dynamic
// 读取是否启用。这是不同于仅仅读的,由于仅仅读是一生静态的,而readsEnabled是动态的
volatile boolean readsEnabled = true;
当中。当一个flush发生或者正在进行时。flushing会被设置为true。而当一个flush请求发生时,flushRequested被设置为true。
另外,还包括了合并进行的数目compatcing、可写状态writesEnabled、可读状态readsEnabled和仅仅读状态readOnly等。
为什么要用这么一个数据结构来表示Region的状态呢?我们知道,HRegion代表了HBase表中按行切分的区域。在HRegion上,可能存在flush、compact等多种操作。使用单一的操作并不能非常好的表达出HRegion的状态。所以作者构思出这么一个数据结构,协调fulsh、compact及其HRegion读写等状态。
好了,期待下一篇关于flush核心流程的介绍吧!