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

  

   

posted @ 2014-08-15 14:35 吴镝 阅读(...) 评论(...) 编辑 收藏