HBase Snapshot原理和实现
HBase 从0.95开始引入了Snapshot,可以对table进行Snapshot,也可以Restore到Snapshot。Snapshot可以在线做,也可以离线做。Snapshot的实现不涉及到table实际数据的拷贝,仅仅拷贝一些元数据,比如组成table的region info,表的descriptor,还有表对应的HFile的文件的引用。本文基于0.98.4
Snapshot命令如下所示:
hbase> snapshot 'sync_stage:Photo', 'PhotoSnapshot' //对sync_stage这个namespace下的Photo表做一次snapshot(表只有一个column family,叫做PHOTO),snapshot名字叫做PhotoSnapshot
这个Snapshot执行后,所有相关的元数据都会被保存在(假设hbase.rootdir设置为/sync/hbase) hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot目录中。如下所示:
$ bin/hadoop fs -ls -R hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot/ -rw-r--r-- 3 work supergroup 44 2014-08-15 10:32 hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot/.snapshotinfo //Snapshot的一些描述信息 drwxr-xr-x - work supergroup 0 2014-08-15 10:32 hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot/.tabledesc -rw-r--r-- 3 work supergroup 543 2014-08-15 10:32 hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot/.tabledesc/.tableinfo.0000000001 //Photo表的HTableDescriptor的序列化 drwxr-xr-x - work supergroup 0 2014-08-15 10:32 hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot/.tmp drwxr-xr-x - work supergroup 0 2014-08-15 10:32 hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot/0595cf25f61de0f1c3ddf38e50a59b07 //Photo表有三个region,这里显示region encode name -rw-r--r-- 3 work supergroup 58 2014-08-15 10:32 hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot/0595cf25f61de0f1c3ddf38e50a59b07/.regioninfo // region的HRegionInfo序列化 drwxr-xr-x - work supergroup 0 2014-08-15 10:32 hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot/0595cf25f61de0f1c3ddf38e50a59b07/PHOTO -rw-r--r-- 3 work supergroup 0 2014-08-15 10:32 hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot/0595cf25f61de0f1c3ddf38e50a59b07/PHOTO/7cfdcf5ef122422499e4bffa71485ee1 //这里的PHOTO是column family,从下面可以看出,这个column family下一共有3个HFile文件,这里,HFile文件名为7cfdcf5ef122422499e4bffa71485ee1,这个文件是空文件,代表对实际存有数据的同名HFile的一个引用,下个图可以看到 drwxr-xr-x - work supergroup 0 2014-08-15 10:32 hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot/395b0d05df155fddbb03b1da908dae3d -rw-r--r-- 3 work supergroup 72 2014-08-15 10:32 hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot/395b0d05df155fddbb03b1da908dae3d/.regioninfo drwxr-xr-x - work supergroup 0 2014-08-15 10:32 hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot/395b0d05df155fddbb03b1da908dae3d/PHOTO -rw-r--r-- 3 work supergroup 0 2014-08-15 10:32 hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot/395b0d05df155fddbb03b1da908dae3d/PHOTO/74e4639c360c4cc59806ba48d13ba230 drwxr-xr-x - work supergroup 0 2014-08-15 10:32 hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot/87a9a6202d9f68d9ccbd184b19cb8933 -rw-r--r-- 3 work supergroup 55 2014-08-15 10:32 hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot/87a9a6202d9f68d9ccbd184b19cb8933/.regioninfo drwxr-xr-x - work supergroup 0 2014-08-15 10:32 hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot/87a9a6202d9f68d9ccbd184b19cb8933/PHOTO -rw-r--r-- 3 work supergroup 0 2014-08-15 10:32 hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot/87a9a6202d9f68d9ccbd184b19cb8933/PHOTO/902eafefe50f4ba2ba01bd80d0846cf8
看看Photo表PHOTO column family下的HFile文件:
bin/hadoop fs -ls -R hdfs://sync/hbase/data/sync_stage/Photo/ drwxr-xr-x - work supergroup 0 2014-07-31 16:49 hdfs://sync/hbase/data/sync_stage/Photo/.tabledesc -rw-r--r-- 3 work supergroup 543 2014-07-31 16:49 hdfs://sync/hbase/data/sync_stage/Photo/.tabledesc/.tableinfo.0000000002 drwxr-xr-x - work supergroup 0 2014-07-31 16:49 hdfs://sync/hbase/data/sync_stage/Photo/.tmp drwxr-xr-x - work supergroup 0 2014-08-01 17:09 hdfs://sync/hbase/data/sync_stage/Photo/0595cf25f61de0f1c3ddf38e50a59b07 -rw-r--r-- 3 work supergroup 58 2014-08-01 16:31 hdfs://sync/hbase/data/sync_stage/Photo/0595cf25f61de0f1c3ddf38e50a59b07/.regioninfo drwxr-xr-x - work supergroup 0 2014-08-01 16:31 hdfs://sync/hbase/data/sync_stage/Photo/0595cf25f61de0f1c3ddf38e50a59b07/PHOTO -rw-r--r-- 3 work supergroup 67780675 2014-08-01 16:31 hdfs://sync/hbase/data/sync_stage/Photo/0595cf25f61de0f1c3ddf38e50a59b07/PHOTO/7cfdcf5ef122422499e4bffa71485ee1 //这个HFile存有实际的数据,并且HFile文件名相同 drwxr-xr-x - work supergroup 0 2014-08-02 12:51 hdfs://sync/hbase/data/sync_stage/Photo/395b0d05df155fddbb03b1da908dae3d -rw-r--r-- 3 work supergroup 72 2014-08-01 17:33 hdfs://sync/hbase/data/sync_stage/Photo/395b0d05df155fddbb03b1da908dae3d/.regioninfo drwxr-xr-x - work supergroup 0 2014-08-01 17:33 hdfs://sync/hbase/data/sync_stage/Photo/395b0d05df155fddbb03b1da908dae3d/PHOTO -rw-r--r-- 3 work supergroup 101932288 2014-08-01 17:33 hdfs://sync/hbase/data/sync_stage/Photo/395b0d05df155fddbb03b1da908dae3d/PHOTO/74e4639c360c4cc59806ba48d13ba230 drwxr-xr-x - work supergroup 0 2014-08-02 12:51 hdfs://sync/hbase/data/sync_stage/Photo/87a9a6202d9f68d9ccbd184b19cb8933 -rw-r--r-- 3 work supergroup 55 2014-08-01 17:33 hdfs://sync/hbase/data/sync_stage/Photo/87a9a6202d9f68d9ccbd184b19cb8933/.regioninfo drwxr-xr-x - work supergroup 0 2014-08-01 17:33 hdfs://sync/hbase/data/sync_stage/Photo/87a9a6202d9f68d9ccbd184b19cb8933/PHOTO -rw-r--r-- 3 work supergroup 222931250 2014-08-01 17:33 hdfs://sync/hbase/data/sync_stage/Photo/87a9a6202d9f68d9ccbd184b19cb8933/PHOTO/902eafefe50f4ba2ba01bd80d0846cf8
下面看看Snapshot的原理。
Snapshot的过程类似于两阶段提交,大体过程是,HMaster收到snapshot命令后,作为coordinator,然后从meta region中取出Photo表的region和对应的region server的信息,这些region server就作为两阶段提交的participant,prepare阶段就相当于对region server本地的Photo表的region做快照存入HDFS的临时目录,commit阶段其实就是HMaster把临时目录改成正确的目录。期间,HMaster和region server的数据共享通过ZK来完成。
下面看Snapshot的具体实现。
在HMaster端,由SnapshotManager类的对象来负责和Snapshot相关的事务,内部有一个类型为ProcedureCoordinator的对象,名为coordinator,从名字可以看出它就是协调者。HMaster收到Snapshot命令,执行public SnapshotResponse snapshot(RpcController controller, SnapshotRequest request)函数,函数内部从request中解析出SnapshotDescription对象,它就是对这次Snapshot的描述,其中就包括Snapshot的名字PhotoSnapshot,和Snapshot的表Photo等。然后调用SnapshotManager的takeSnapshot()方法,方法内部首先会检查Photo表是不是正在做Snapshot,或者名为PhotoSnapshot的snapshot已经做完了等前置检查,如果没有,由于这里做的是online snapshot,即表仍然可以读写处于enable状态,在这里,会调用snapshotEnabledTable(),进而提交一个EnabledTableSnapshotHandler任务给内部线程池处理,在提交之前,也会做一些检查,并且准备好snapshot用的临时目录,在这个例子中,临时目录为hdfs://sync/hbase/.hbase-snapshot/.tmp/PhotoSnapshot,前置检查环境准备等由函数prepareToTakeSnapshot(snapshot)负责。重点看EnabledTableSnapshotHandler,它继承于TakeSnapshotHandler,任务入口函数在TakeSnapshotHandler的process()方法。下面重点看这个方法。
process方法主要干几件事:
1. 将SnapshotDescription对象序列化写入到hdfs://sync/hbase/.hbase-snapshot/.tmp/PhotoSnapshot目录的.snapshotinfo文件中。
2. 调用TableInfoCopyTask任务将Photo表的最新的.tableinfo拷到hdfs://sync/hbase/.hbase-snapshot/.tmp/PhotoSnapshot/.tabledesc/ 目录下,名字为.tableinfo.0000000001。
3. 从meta region中独处Photo表的region和所在region server信息,传给snapshotRegions()函数,该函数被EnabledTableSnapshotHandler覆盖,流程进入EnabledTableSnapshotHandler的 snapshotRegions()。
4. proc.waitForCompleted(); 等待snapshot完成,其实就是等待completedLatch变成0
5. 做一些postcheck ,然后调用completeSnapshot(this.snapshotDir, this.workingDir, this.fs) 将working dir改成正确的目录位置hdfs://sync/hbase/.hbase-snapshot/PhotoSnapshot
下面重点看第三步.EnabledTableSnapshotHandler的snapshotRegions(),这个函数首先调用
Procedure proc = coordinator.startProcedure(this.monitor, this.snapshot.getName(), this.snapshot.toByteArray(), Lists.newArrayList(regionServers));
启动一个Procedure,在HMaster端,这个Snapshot由一个Procedure来表示,在RegionServer端,有SubProcedure表示,后续会看到。实际上,这里提供了一套框架,以后如果有其他的需要两阶段提交的任务也可以放进来做。
Procedure同样提交给内部线程池处理,Procedure是一个callable,入口函数在call()。call内主要是执行如下几个函数:
sendGlobalBarrierStart(); // 发布Snapshot任务 waitForLatch(acquiredBarrierLatch, monitor, wakeFrequency, "acquired");//等所有的相关的region server都acquire这个任务 sendGlobalBarrierReached(); //建立reached节点 waitForLatch(releasedBarrierLatch, monitor, wakeFrequency, "released"); //等待所有的相关的region server完成本地snapshot
sendGlobalBarrierComplete(); //将zk上相关节点删除
completedLatch.countDown();// proc结束
Procedure有几个关键的成员变量,acquiringMembers 初始化为Photo表的regions所在的serverName,意思是说这个任务需要这些serverName作为参与者,HMaster在ZK上发布Snapshot任务,需要这些参与者都去acquire这个任务后,大家才可以进入下一个阶段。在当前例子,HMaster调用sendGlobalBarrierStart()方法发布任务,方法内部实际上调用coordinator(ProcedureCoordinator类)对象的ZKProcedureCoordinatorRpcs类型成员的sendGlobalBarrierAcquire()方法去ZK上发布Snapshot任务,实际上就是在zk上创建/hbase/online-snapshot/acquired/PhotoSnapshot 路径,并且PhotoSnapshot是一个目录,目录的data为SnapshotDescription的序列化。RegionServer启动的时候会监控/hbase/online-snapshot/acquired目录的改动,当region server在目录下发现一个新的节点后,就会在/hbase/online-snapshot/acquired目录下建立一个代表自己的znode,名字为region server的server name,代表当前region server已经检测到这个任务了。一旦HMaster检测到一个新的znode,会触发coordinator的ZKProcedureUtil类型的名为zkProc的成员变量的nodeCreated()方法,从而调用coordinator的memberAcquiredBarrier()方法,检测,如果新加的节点确实在acquiringMembers内,则将acquiredBarrierLatch这个CountDownLatch减1。这里需要检查新加的节点不在acquiringMembers内的原因在于,实际上,不相关的region server也会acquire这个任务,只是当它发现自己没有相关的region后,直接就执行完成了。所有的acquire成功的server name都会从acquiringMembers移除然后加入到inBarrierMembers中,随后,调用sendGlobalBarrierReached()在zk上创建节点 /hbase/online-snapshot/reached/PhotoSnapshot,并且监控目录下的节点变化,本地snapshot完成的region server会在这个目录下建立一个代表自己的节点,与前面类似,通过releasedBarrierLatch这个CountDownLatch来控制。
下面看看RegionServer检测到/hbase/online-snapshot/acquired下面的snapshot任务后如何做。
RegionServer使用RegionServerSnapshotManager来管理Snapshot相关的事务,主要工作由内部类型为ZKProcedureMemberRpcs
的成员变量memberRpcs来完成,region server初始化时,就会调用ZKProcedureMemberRpcs的waitForNewProcedures()方法来监控zk上/hbase/online-snapshot/acquired下面节点的变化。当检测节点增加后,会调用ProcedureMember的
public Subprocedure createSubprocedure(String opName, byte[] data) { return builder.buildSubprocedure(opName, data); }
方法来创建SubProcedure,这里的builder是SnapshotSubprocedureBuilder,它的buildSubprocedure()会创建FlushSnapshotSubprocedure类型的subprocedure,FlushSnapshotSubprocedure有一个名为regions的成员变量,这里会进行初始化,从region server的online regions列表中检查是否有被snapshot表的region,如果有,则初始化regions,否则regions为空。同样,这个subprocedure会提交给内部的线程池处理.FlushSnapshotSubprocedure继承于Subprocedure,它是一个callable,入口函数是call。这个call实际上执行如下几个函数:
acquireBarrier();// 对于FlushSnapshotSubprocedure来说,do nothing rpcs.sendMemberAcquired(this); //在acquired下建立znode代表自己 waitForReachedGlobalBarrier(); //等在inGlobalBarrier这个CountDownLatch上,初始化为1,只有reached下面相应的snapshot节点建立后(这说明所有相关的re//gion server都已经acquire 任务了)才继续往下走 insideBarrier(); //调用子类FlushSnapshotSubprocedure的insideBarrier rpcs.sendMemberCompleted(this); //本地snapshot完成后,在reached下建立一个znode代表自己 releasedLocalBarrier.countDown(); executionTimeoutTimer.complete();
可以看出,只有reached相应节点建立,region server才可以往下走进行实际的snapshot操作,而reached节点的建立只有HMaster看到所有的相关的region server都已经acquire了任务后才会去建立,这就达到了同步的目的。
下面看FlushSnapshotSubprocedure的insideBarrier().
对于regions(创建FlushSnapshotSubprocedure的时候进行了初始化,这些regions就是本region server所包含的被snapshot表的region)里的每个region提交一个RegionSnapshotTask类型的任务,然后等待所有的这些task完成。
每个RegionSnapshotTask的任务就是真正的这个region的数据进行snapshot,下面重点看。
1. 调region.flushcache(),转而调internalFlushcache(status)=>internalFlushcache(this.log, -1, status),主要逻辑在internalFlushcache(this.log, -1, status)中。看下面一段:
this.updatesLock.writeLock().lock();//加写锁,以便冻结region内所有的memstore long totalFlushableSize = 0; status.setStatus("Preparing to flush by snapshotting stores"); List<StoreFlushContext> storeFlushCtxs = new ArrayList<StoreFlushContext>(stores.size()); long flushSeqId = -1L; try { // Record the mvcc for all transactions in progress.
// 目的是为了后续调用mvcc.waitForRead(w),使得w之前的所有的写事务结束并且可见,以便flush时不会把没有commit的事务flush到HFile中。
w = mvcc.beginMemstoreInsert(); mvcc.advanceMemstore(w); // check if it is not closing. if (wal != null) { if (!wal.startCacheFlush(this.getRegionInfo().getEncodedNameAsBytes())) { String msg = "Flush will not be started for [" + this.getRegionInfo().getEncodedName() + "] - because the WAL is closing."; status.setStatus(msg); return new FlushResult(FlushResult.Result.CANNOT_FLUSH, msg); }
// flush 操作对应的日志的sequence id flushSeqId = this.sequenceId.incrementAndGet(); } else { // use the provided sequence Id as WAL is not being used for this flush.
flushSeqId = myseqid; } for (Store s : stores.values()) { totalFlushableSize += s.getFlushableSize(); storeFlushCtxs.add(s.createFlushContext(flushSeqId)); } // prepare flush (take a snapshot) for (StoreFlushContext flush : storeFlushCtxs) { flush.prepare(); // 冻结memstore,后续进行flush到HFile } } finally { this.updatesLock.writeLock().unlock(); //解写锁,可以继续接受写入了 }
然后调用mvcc.waitForRead(w),该函数返回后,那么w之前的所有的写事务都已经结束并且对外可见,后续即可flush。接着,进行实际的flush操作,调用每个
StoreFlushContext的flushCache(),进而会调到HStore的flushCache():
// 对于不同的storeEngine返回的Flusher不一样,默认是DefaultStoreEngine,还可以是StripeStoreEngine,它来源于Compression策略参看(HBASE-7667)
StoreFlusher flusher = storeEngine.getStoreFlusher(); IOException lastException = null; for (int i = 0; i < flushRetriesNumber; i++) { try {
//对memstore进行flush,返回的文件名通过 fs.createTempName()得到,generateUniqueName(null)得到文件名(不包括目录)
//对于DefaultStoreEngine来说,一个memstore会产生一个HFile,StripeStoreEngine会产生几个(HBASE-7667) List<Path> pathNames = flusher.flushSnapshot( snapshot, logCacheFlushId, snapshotTimeRangeTracker, flushedSize, status); Path lastPathName = null; try { for (Path pathName : pathNames) { lastPathName = pathName; validateStoreFile(pathName); } return pathNames;
2. 调region.addRegionToSnapshot(),它主要是将region info写入到snapshot到临时目录中的文件.regioninfo中,然后在临时目录的各个column family文件夹中,创建和存有数据的HFile文件名相同的空文件,代表对实际HFile的引用。
至此,Snapshot结束.
参考资料:
hbase-server-0.98.4-hadoop2.jar
https://issues.apache.org/jira/browse/HBASE-7667
https://issues.apache.org/jira/browse/HBASE-6055