HBase Snapshot原理和实现

jopen 9年前

  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