1. 程式人生 > >HBase基礎框架級特性Procedure解讀

HBase基礎框架級特性Procedure解讀

標題中提及”事務”可能會給大家帶來誤解,這篇文章不是在討論HBase如何支援分散式事務能力的,而是介紹HBase用來處理內部事務操作的特性,這個特性被稱之為Procedure V2,也是2.0版本的主打特性之一。

這篇文章的內容組織結構為:

  1. Procedure特性的設計初衷。
  2. Procedure的完整生命週期。
  3. Procedure框架關鍵角色。
  4. Procedure重點模組實現細節。

我們先在看一下這個特性存在的必要性。

建表為例: 建表過程,包含如下幾個關鍵的操作:

  1. 初始化所有的RegionInfos。
  2. 建表前的必要檢查。
  3. 建立表的檔案目錄佈局。
  4. 將Region寫到Meta表中。
  5. ……

從使用者看來,一個建表操作要麼是成功的,要麼是失敗的,並不存在一種中間狀態,因此,用”事務”來描述這種操作可以更容易理解。如果曾使用過1.0或更早的版本,相信一定曾被這個問題”折磨”過:“一個表明明沒有建立成功,當使用者嘗試再次重建的時候,卻被告知該表已存在”,究其根因,在於建表遇到異常後,沒有進行合理的Rollback操作,導致叢集處於一種不一致的狀態。

類似的需求,在HBase內部司空見慣:Disable/Enable表,修改表,RegionServer Failover處理,Snapshot, Assign Region, Flush Table…這些操作,都具備兩個特點:多步操作,有限狀態機(或簡單或複雜)。因此,可以實現一個公共能力,這樣可避免每個特性各自為營,導致大量難以維護的冗餘程式碼。

最初版本的Procedure,由Online Snapshot特性引入(HBASE-7290),主要用來協調分散式請求。而最新版本的Procedure(稱之為Procedure V2),受Accumulo的Fault-Tolerant Executor (FATE)的啟發而重新設計,該特性已經在HBase內部得到了廣泛的應用,因此,可以將其稱之為一個基礎框架級特性瞭解該特性的設計原理是學習2.x原始碼的基礎。本文範疇內所探討的Procedure,均指Procedure V2。

Procedure的生命週期

一個Procedure由一個或一系列操作構成。一個Procedure的執行結果,要麼是成功,要麼是失敗,失敗後不會讓叢集處於一種不一致的狀態。

接下來,我們將以Create Table操作為例,來介紹一個Procedure的完整的生命週期。

建立

Master收到一個Create Table的請求後會建立一個CreateTableProcedure例項。

CreateTableProcedure涉及到一系列的操作,而每一個操作都關聯了一個操作狀態。在CreateTableState中,定義了與之相關的所有操作狀態:

CreateTableProcedure內部定義了自身的初始狀態為CREATE_TABLE_PRE_OPERATION,而且定義了每一種狀態時對應的處理操作以及當前這個狀態完成後應該要切換至哪個狀態。如下表列出了當前狀態與成功後的退出狀態:

Procedure本身還有一個執行狀態,這套執行狀態的定義如下:

正是為了有所區分,本文將CreateTableProcedure內部定義的私有狀態,稱之為操作狀態,而執行狀態則是所有的Procedure都擁有的狀態資訊,從每一個狀態的定義很容易看出它的作用。

提交

Master將創建出來的CreateTableProcedure例項,提交給ProcedureExecutor

ProcedureExecutor關於一個新提交的Procedure,做如下幾步處理:

  1. 必要檢查(初始狀態,確保無Parent Procedure,確保有Owner資訊)。
  2. 設定Nonce Key(如果存在)以及Procedure ID。
  3. 將新的Procedure寫入到ProcedureStore中,持久化。
  4. 將新的Procedure新增到ProcedureScheduler的排程佇列的尾部。

執行

ProcedureExecutor初始化階段,啟動了若干個WorkerThread,具體數量可配置。

WorkerThread不斷從ProcedureScheduler中poll出新的待執行的Procedure,而後:

  1. 獲取IdLock: 獲取Procedure關聯的IdLock,避免同一個Procedure在多個執行緒中同時處理。
  2. 獲取資源鎖: 呼叫Procedure內部定義的Acquire Lock請求,獲取Procedure自身所需的資源鎖。
    IdLock是為了確保一個Procedure只被一個執行緒呼叫,而這裡的Lock是為了確保這一個Table只能被一個Procedure處理,這裡需要獲取Namespace的共享鎖,以及當前這個Table的互斥鎖,這裡其實是一個分散式鎖的需求,容易想到用ZooKeeper實現,事實上,這個Lock也可以用Procedure來實現,在後續章節中將會講到這一點。
  3. 處理當前操作狀態:執行Procedure初始狀態所定義的處理邏輯,處理完後會返回當前這個Procedure物件。每一步執行完,都將最新的狀態持久化到ProcedureStore中。
  4. 處理下一操作狀態:如果返回物件依然是原來的Procedure,而且未失敗,則意味著需要繼續下一步處理。
  5. 迴圈處理:迴圈3,4步即可處理完所有CreateTableProcedure內部定義的所有處理。
    釋放鎖資源。

完成

處理每一個操作狀態時,都產生一個狀態返回值(Flow),如果還有下一個待處理的狀態,則返回Flow.HAS_MORE_STATE,如果全部執行完成,則返回Flow.NO_MORE_STATE,藉此可以判斷一個Procedure是否執行完成。一個執行成功後的Procedure,執行狀態被設定為SUCCESS。

完成後的Procedure,需要在ProcedureStore中進行標記刪除。

Procedure框架關鍵角色

通過上一章節的內容,我們已經知道了,在一個Procedure執行過程中,涉及到如下幾個關鍵角色:

ProcedureExecutor

負責提交執行Procedure。Procedure的執行操作,主要由其負責的多個WorkerThread來完成。

Procedure的持久化,由ProcedureExecutor提交給ProcedureStore。後續的每一次狀態更新,也由ProcedureExecutor向ProcedureStore發起Update請求。

新的Procedure也會提交給ProcedureScheduler,由ProcedureScheduler完成排程。ProcedureExecutor中的WorkerThread從ProcedureScheduler中獲取待執行的Procedure。

ProcedureStore

用來持久化新提交的Procedure以及後續的每一次狀態更新值。

ProcedureStore的預設實現類為WALProcedureStore,基於日誌檔案來持久化Procedure資訊,雖然稱之為WAL,但與HBase自身的WAL日誌檔案的實現完全不同,類似點在於:

1.當超過一定大小後或者超過一定的時間週期後,需要roll一個新的WAL檔案出來,避免一個WAL檔案過大。
2.需要實現一套關於無用WAL日誌檔案的跟蹤清理機制,避免WAL檔案佔用過大的儲存空間。
3.實現了一套類似於RingBuffer的機制,通過打包sync併發的寫入請求,來提升寫入吞吐量。

ProcedureScheduler

負責排程一個叢集內的各種型別的Procedure請求,支援按優先順序排程,相同優先順序的Procedure則支援公平排程

我們先來看看Procedure的幾大類型:

  • Meta Procedure:唯一的一種型別為RecoverMetaProcedure,該型別已被廢棄。
  • Server Procedure:目前也只有一種型別:ServerCrashProcedure,用來負責RegionServer程序故障後的處理。
  • Peer Procedure:與Replication相關,如AddPeerProcedure, RemovePeerProcedure等等。
  • Table Procedure: Table Procedure的型別最為豐富,如CreateTableProcedure, DisableTableProcedure, EnableTableProcedure, AssignProcedure, SplitTableRegionProcedure,…..涵蓋了表級別、Region級別的各類操作。

在ProcedureScheduler中,需要同時排程這幾種型別的Procedure,排程的優先順序順序(由高到低)為:

    Meta -> Server -> Peer -> Table。

在每一種型別內部,又有內部的優先順序定義。以Table Procedure為例,Meta Table的優先順序最高,System Table(如acl表)其次,普通使用者表的優先順序最低。

技術細節

看到這裡,你也許會認為,”Procedure特性原來如此簡單!”,但如果仔細閱讀這部分程式碼,就會深刻體會到它在實現上的複雜度,導致這種複雜度的客觀原因總結起來有如下幾點:

  • 要實現一個統一的狀態機管理框架本身就比較複雜,可以說將Assign Region/Create Table/Split Region等流程的複雜度轉嫁了過來。
  • 需要支援優先順序排程與公平排程
  • WALProcedureStore無用WAL檔案的跟蹤與清理,重啟後的回放,均需要嚴謹的處理。
  • 涉及複雜的拓撲結構:一個Procedure中間執行過程可能會產生多個Sub-Procedures,這過程需要協調。
  • 不依賴於ZooKeeper的分散式鎖機制。
  • 跨節點Rpc請求協調。

在實際實現中,還內部維護了幾個私有的資料結構,如Avl-Tree, FairQueue以及Bitmap,這也是導致實現複雜度過高的一大原因。接下來選擇了三點內容來展開講解,這三點內容也算是Procedure框架裡的難點部分,分別為:WAL清理機制,Procedure排程策略以及分散式鎖與事件通知機制。

WAL清理機制

WALProcedureStore也存在WAL日誌檔案的roll機制,這樣就會產生多個WAL檔案。對於一箇舊的WAL檔案,如何認定它可以被安全清理了?這就需要在WALProcedureStore中設計一個合理的關於Procedure狀態的更新機制。

WALProcedureStore使用ProcedureStoreTracker物件來跟蹤Procedure的寫入/更新與刪除操作,這個物件被稱之為storeTracker

在ProcedureStoreTracker中,只需要用ProcID(long型別)來表示一個Procedure。即使只記錄大量的ProcIDs,也會佔用大量的記憶體空間,因此,在ProcedureStoreTracker內部實現了一個簡單的Bitmap,用一個BIT來表示一個ProcID,這個Bitmap採用了分割槽彈性擴充套件的設計:每一個分割槽稱之為一個BitsetNode,每一個BitsetNode有一個起始值(Start),使用一個long陣列來表示這個分割槽對應的Bitmap,每一個long數值包含64位,因此可以用來表示64個ProcIDs。一個BitsetNode應該可以包含X個long數值,這樣就可以表示從Start值開始的X * 64個ProcIDs,但可惜,現在的程式碼實現還是存在問題的(應該是BUG),導致一個BitsetNode只能包含1個long值。

如果瞭解過Java Bitset的原理,或者是RoaringBitmap,就會發現這個實現並無任何新穎之處。

在一個BitsetNode內部,其實包含兩個Bitmap: 一個Bitmap(modified)用來記錄Insert/Update的ProcIDs,另一個Bitmap(Deleted)用來記錄已經被Delete的ProcIDs。例如,如果Proc Y在Bitmap(modified)所對應的BIT為1,在Bitmap(Deleted)中所對應的BIT為0,則意味著這個Procedure仍然存活(或許剛剛被寫入,或許剛剛被更新)。如果在Bitmap(Modified)中對應的BIT為1,但在Bitmap(Deleted)中所對應的BIT為1,則意味著這個Procedure已被刪除了。

如果一箇舊的WAL檔案所關聯的所有的Procedures,都已經被更新過(每一次更新,意味著Procedure的狀態已經發生變化,則舊日誌記錄則已失去意義),或者都已經被刪除,則這個WAL檔案就可以被刪除了。在實現上,同樣可以用另外一個ProcedureStoreTracker物件(稱之為holdingCleanupTracker)來跟蹤最老的WAL中的Procedure的狀態,每當有新的Procedure發生更新或者被刪除,都同步刪除holdingCleanupTracker中對應的ProcID即可。當然,還得考慮另外一種情形,如果有個別Procedure遲遲未更新如何處理? 這時,只要強制觸發這些Procedures的更新操作即可。

這樣描述起來似乎很簡單,但這裡卻容易出錯,而且一旦出錯,可能會導致WAL日誌被誤刪,或者堆積大量的日誌檔案無法被清理,出現這樣的問題都是致命的。

Procedure排程策略

關於排程策略的基礎需求,可以簡單被表述為:

  1. 不同型別的Procedure優先順序不同,如Server Procedure要優先於Table Procedure被排程。
  2. 即使同為Table Procedure型別,也需要按照Table的型別進行優先順序排程,對於相同的優先順序型別,則採用公平排程策略。

MasterProcedureScheduler是預設的ProcedureScheduler實現,接下來,我們看一下它的內部實現。

MasterProcedureScheduler將同一型別的Procedure,放在一個被稱之為FairQueue的佇列中,這樣,共有四種類型的FairQueue佇列,這四個佇列分別被稱之為MetaRunQueue, ServerRunQueue, PeerRunQueue, TableRunQueue),在排程時,按照上述羅列的順序進行排程,這樣,就確保了不同型別間的整體排程順序。

簡單起見,我們假設這四個佇列中,僅有TableRunQueue有資料,其它皆空,這樣確保會排程到TableRunQueue中的Procedure。在TableRunQueue中,本身會涉及多個Table,而每一個Table也可能會涉及多個Procedures:

    TableA -> {ProcA1, ProcA2, ProcA3}
    TableB -> {ProcB1, ProcB2, ProcB3, ProcB4, ProcB5}
    TableC -> {ProcC1, ProcC2, ProcC3, ProcC4}

每一個Table以及所涉及到的Procedure列表,被封裝成了一個TableQueue物件,在TableQueue中,使用了一個雙向佇列(Deque)來儲存Procedures列表,Deque的特點是既可以在佇列兩端進行插入。在這個Deque中的順序,直接決定了同一個Table的Procedures之間的排程順序

當需要為TableB寫入一個新的Procedure時,需要首先快速獲取TableB所關聯的TableQueue物件,常見的思路是將所有的TableQueue儲存在一個ConcurrentHashMap中,以TableName為Key,然而,這裡卻沒有采用ConcurrentHashMap,而是實現了一個Avl-Tree(自動平衡二叉樹),這樣設計的考慮點為:ConcurrentHashMap中的寫入會建立額外的Tree Node物件,當物件的寫入與刪除非常頻繁時對於GC的壓力較大(請參考AvlUtil.java)。Avl-Tree利於快速獲取檢索,但寫入效能卻慢於紅黑樹,因為涉及到過多的翻轉操作。這樣,基於TableName,可以快速從這個Avl-Tree中獲取對應的TableQueue物件,而後就可以將這個新的Procedure寫入到這個TableQueue的Deque中,寫入時還可以指定寫入到頭部還是尾端。

現在我們已經瞭解了TableQueue物件,而且知道了多個TableQueue被儲存在了一個類似於Map的資料結構中,還有一個關鍵問題沒有解決:如何實現不同Table間的排程?

所有的TableQueue,都存放在TableRunQueue(再強調一下,這是一個FairQueue物件)中,而且按Table的優先順序順序組織。每當有一個新的TableQueue物件產生時,都會按照該TableQueue所關聯的Table的優先順序,插入到TableRunQueue中的合適位置。

當從這個TableRunQueue中poll一個新的TableQueue時,高優先順序的TableQueue先被poll出來。如果被poll出來的TableQueue為普通優先順序(priority值為1),為了維持公平排程的原則,在TabelRunQueue中將這個TableQueue從頭部移到尾部,這樣下一次將會排程到其它的TableQueue。

再簡單總結一下:TableQueue物件用來描述一個Table所關聯的Procedures佇列,TableQueue物件存在於兩個資料結構中,一個為Avl-Tree,這樣可以基於TableName快速獲取對應的TableQueue,以便快速寫入;另一個數據結構為FairQueue,這是為了實現多Table間的排程。

分散式鎖與事件通知機制

同樣圍繞Create Table的例子,說明一下關於分散式鎖的需求:

  • 兩個相同表的CreateTableProcedure不應該被同時執行
  • 同一個Namespace下的多個不同表的CreateTableProcedure允許被同時執行
  • 只要存在未完成的CreateTableProcedure,所關聯的Namespace不允許被刪除

實現上述需求,Procedure框架採用了共享鎖/互斥鎖方案:

  • 當CreateTableProcedure執行時,需要獲取對應Namespace的共享鎖,以及所要建立的Table的互斥鎖。
  • 刪除一個Namespace則需要獲取這個Namespace的互斥鎖,這意味著只要有一個Procedure持有該Namespace的共享鎖,則無法被刪除。

當一個Procedure X試圖去獲取一個Table的互斥鎖時,碰巧該Table的互斥鎖被其它Procedure持有,此時,Procedure X需要加到這個Table的鎖的等待佇列中,一旦該鎖被釋放,Procedure X需要被喚醒。

回顧一下Procedure的執行過程:

  1. Acquire Lock
  2. Execute

獲取鎖資源的操作,只需要在”Acquire Lock”步驟完成即可。

MasterProcedureScheduler中,使用一個SchemaLocking的物件來維護所有的鎖資源,如Server Lock, Namespace Lock,Table Lock等等。以Table Lock資源為例:一個Table的鎖資源,使用一個LockAndQueue物件進行抽象,顧名思義,在這個物件中,既有Lock,又有關於這個鎖資源的Procedure等待佇列;多個Table的LockAndQueue物件被組織在一個Map中,以TableName為Key。

同時,還可以將獲取鎖資源的操作封裝成一個Procedure,稱之為LockProcedure,以供Procedure框架之外的特性使用,如TakeSnapshotHandler,可以利用該機制來獲取Table的互斥鎖。

Procedure框架就是這樣沒有依賴於ZooKeeper,實現了自身的分散式鎖與訊息通知機制。

總結

本文先從Procedure的設計初衷著手,而後以Create Table為例介紹了一個Procedure的生命週期,通過這個過程,可以簡單瞭解整個框架所涉及到的幾個角色,因此,在接下來的章節中,進一步細化了Procedur框架中的幾個角色。最後一部分,選擇了整個框架中比較複雜的幾個模組,展開了實現細節。受限於篇幅,有幾部分內容未涉及到:

  1. WAL資料格式與WAL回放機制
  2. Notification-Bus(當然這部分也只實現了一小部分)
  3. 複雜Procedure拓撲結構情形
  4. Procedure超時處理

作為應用Procedure框架的最典型流程Region Assignment,在此文範疇內幾乎未涉及。因為關於Region Assignment的故事太精彩,又太揪心,所以會放在一篇獨立的文章中專門講解。

參考資訊

  • http://hbase.apache.org/book.html#pv2
  • http://hbase.apache.org/book.html#amv2
  • HBASE-13439: Procedure Framework(Pv2)
  • HBASE-13203 Procedure v2 – master create/delete table
  • HBASE-14837: Procedure V2 – Procedure Queue Improvement
  • HBASE-20828: Finish-up AMv2 Design/List of Tenets/Specification of operation
  • HBASE-20338: WALProcedureStore#recoverLease() should have fixed sleeps for retrying rollWriter()
  • HBASE-21354: Procedure may be deleted improperly during master restarts resulting in “corrupt”
  • HBASE-20973: ArrayIndexOutOfBoundsException when rolling back procedure…
  • HBASE-19756: Master NPE during completed failed eviction
  • HBASE-19953: Avoid calling post* hook when procedure fails
  • HBASE-19996: Some nonce procs might not be cleaned up(follow up HBASE-19756)