HBase Snapshot原理和實現

jopen 10年前發布 | 32K 次閱讀 HBase NoSQL數據庫

  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

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!