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