HBase的寫事務,MVCC及新的寫線程模型


   MVCC是實現高性能數據庫的關鍵技術,主要為了讀不影響寫。幾乎所有數據庫系統都用這技術,比如Spanner,看這里。Percolator,看這里。當然還有mysql。本文說HBase的MVCC和0.98引入的新寫線程模型。

   HBase region server的存儲模型類LSM,將隨機寫轉換為順序寫,寫操作直接寫內存,然后寫操作日志來持久化修改避免宕機丟數據。通常,為了提高性能,采用group commit技術,及多次修改一起寫,一起寫操作日志,充分利用磁盤的順序IO。對於HBase來說,group commit在HRegion類doMiniBatchMutation(BatchOperationInProgress<?> batchOp)函數中,這里面實現了HBase的MVCC,本文主要分析該函數。

   MVCC多版本控制協議,顯然,數據(KeyValue)上需要被打上版本號,這樣讀的時候,就可以根據版本號過濾掉一些不可見的數據。HBase中有一個類MultiVersionConsistencyControl用來保存系統范圍內的一些版本信息,比如寫事務開始時會從MultiVersionConsistencyControl中拿memstoreWrite加1作為

本次寫事務的版本號,隨后這個事務寫入的所有數據,以KeyValue的形式,都被打上了這個版本號。讀事務開始時也會從MultiVersionConsistencyControl中拿

memstoreRead作為讀事務的版本號,那么該讀事務只能讀取版本號小於等於這個版本號的數據(KeyValue)。組織KeyValue的核心數據結構是KeyValueSkipListSet,內部是JDK提供的ConcurrentSkipListMap,一個並發跳表實現。

    HBase的事務實現簡單來說,並發控制采用兩階段鎖實現。這里省略一些細節,比如修正KeyValue的timestamp,數據的check等。

    首先對於所有需要修改的行,一次性拿住所有行鎖,然后調用mvcc對象的beginMemstoreInsert方法,獲得一個WriteEntry對象,包含這次寫事務的寫版本號,通過mvcc.memstoreWrite加1獲得,記作writeNumber,然后將WriteEntry放入隊列writeQueue中,隊列操作被鎖保護。這個隊列用來保存多個並發寫事務的WriteEntry,方便后續推進mvcc.memstoreRead,memstoreRead作為讀事務的事務版本號使用,這樣當memstoreRead被推進,讀事務可以讀的數據就越來越新。然后,將batch里的數據都add到各個HStore的memstore中,每個數據KeyValue都被打上writeNumber,這沒有問題,因為memstoreRead沒有向前推進,故后續的讀事務讀不到這次數據。接着根據batch中的數據構建WALEdit,WALEdit相當於HLog中具體一條一條日志Entry的內容,Entry的頭部是HLogKey結構,包含這條log entry對應的table name,region name,以及log entry的sequence number,region級別的,根據WALEdit和HLogKey組裝成一個Entry后,然后將這個Entry 加到內存中的buffer pendingWrites中(還沒開始寫hdfs,只是寫入內存中),然后為append的這條日志產生一個HLog范圍內的id,記作txid(名字不是很恰當),txid不實際的存儲在Entry中,只是用於標識這次寫事務寫入的日志,只有這些日志被實際的持久化到hdfs中后,請求才可以返回。

寫入buffer后,即釋放所有的行鎖,兩階段鎖的過程結束。最后,就是調用void syncer(long txid) 函數等待這次事務相應的日志被持久化到hdfs中(實際的寫hdfs和sync是其他線程做的,牽扯到寫線程模型,后續描述),一旦持久化完成,就標記一下WriteEntry,代表本次寫事務對應的日志已持久化完成。然后就可以嘗試去推進mvcc的memstoreRead。推進的過程實際上就是去writeQueue里從頭到尾去看,找連續的已經完成的WriteEntry,最后一個WriteEntry的writeNumber即是最新的點,可以賦值給mvcc.memstoreRead,后續讀事務一開始就去拿mvcc.memstoreRead,從而能都到最新的數據。這里需要一個隊列的原因在於,寫事務是並發的,有多個寫線程同時都在執行寫操作,先拿到memstoreWrite進隊列的線程不一定先往pendingWrites中append,從而導致memstoreWrite更大的寫事務的日志可能先被持久化到hdfs中。這里,writeQueue就是為了處理這種亂序的情況。最后,一個寫事務什么時候可以返回給客戶端?對於客戶端來說,客戶端希望后續可以看到自己之前成功commit的事務的數據,所以,只需要mvcc.memstoreRead 大於等於事務對應的WriteEntry的writeNumber即可。

   現在說0.98引入的大幅提高吞吐量的寫線程模型(HBASE-8755)。

   和一個寫事務有關的線程除了執行事務操作的工作線程外,還有如下幾種:

    1. 一個將內存中的pendingWrites寫入HDFS(不sync)的線程,對應類AsyncWriter

    2. 一個sync hdfs的線程,對應類AsyncSyncer

    3. 一個sync完成后喚醒工作線程的線程,對應類AsyncNotifier

 從工作線程開始,多個工作線程寫內存中的pendingWrites,通過pendingWritesLock保護,寫完后,得到txid,通過執行this.asyncWriter.setPendingTxid(txid) 去告訴AsyncWriter線程內存中有數據了,你可以往hdfs中寫了,AsyncWriter加鎖pendingWritesLock,將pendingWrites拿出來,解鎖,然后將pendingWrites寫入hdfs,接着找一個空閑的AsyncSyncer,通asyncSyncers[i].setWrittenTxid(this.lastWrittenTxid)

告訴它有新的數據需要sync了,AsyncSyncer調用AsyncWriter的sync操作, sync完成后,將最后sync的txid記錄在變量AsyncSyncer中,然后調用asyncNotifier.setFlushedTxid(this.lastSyncedTxid) 通知AsyncNotifier 又sync完了一批,可以去喚醒工作線程,讓他們自己看看是否自己當前執行事務的日志已經持久化。AsyncNotifier和工作線程通過syncedTillHere這個AtomicLong進行同步,AsyncNotifier會將最后一個sync成功的txid記錄在syncedTillHere中,

工作線程會等在syncedTillHere上,每次被叫醒后,看看自己的txid是否小於等於syncedTillHere,條件滿足則工作線程繼續往下走,做推進mvcc點相關的工作。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM