HBase寫請求分析,hbase寫請求

來源:互聯網
上載者:User

HBase寫請求分析,hbase寫請求

    HBase作為分布式NoSQL資料庫系統,不單支援寬列表,並且對於隨機讀寫來說也具有較高的效能。在高效能的隨機讀寫事務的同時,HBase也能保持事務的一致性。目前HBase只支援行層級的事務一致性。本文主要探討一下HBase的寫請求流程,主要基於0.98.8版本的實現。

用戶端寫請求

   HBase提供的Java client API是以HTable為主要介面,對應其中的HBase表。寫請求API主要為HTable.put(write和update)、HTable.delete等。以HTable.put為例子,首先來看看用戶端是怎麼把請求發送到HRegionServer的。

   每個put請求表示一個KeyValue資料,考慮到用戶端有大量的資料需要寫入到HBase表,HTable.put預設是會把每個put請求都放到本機快取中去,當本機快取大小超過閥值(預設為2MB)的時候,就要請求重新整理,即把這些put請求發送到指定的HRegionServer中去,這裡是利用線程池並發發送多個put請求到不同的HRegionServer。但如果多個請求都是同一個HRegionServer,甚至是同一個HRegion,則可能造成對服務端造成壓力,為了避免發生這種情況,用戶端API會對寫請求做了並發數限制,主要是針對put請求需要發送到的HRegionServer和HRegion來進行限制,具體實現在AsyncProcess中。主要參數設定為:

  • hbase.client.max.total.tasks              用戶端最大並發寫請求數,預設為100
  • hbase.client.max.perserver.tasks      用戶端每個HRegionServer的最大並發寫請求數,預設為2
  • hbase.client.max.perregion.tasks      用戶端每個HRegion最大並發寫請求數,預設為1

    為了提高I/O效率,AsyncProcess會合并同一個HRegion對應的put請求,然後再一次把這些相同HRegion的put請求發送到指定HRegionServer上去。另外AsyncProcess也提供了各種同步的方法,如waitUntilDone等,方便某些情境下必須對請求進行同步處理。每個put和讀請求一樣,都是要通過訪問hbase:meta表來尋找指定的HRegionServer和HRegion,這個流程和讀請求一致,可以參考文章的描述。

服務端寫請求

    當用戶端把寫請求發送到服務端時,服務端就要開始執行寫請求操作。HRegionServer把寫請求轉寄到指定的HRegion執行,HRegion每次操作都是以批量寫請求為單位進行處理的。主要流程實現在HRegion.doMiniBatchMutation,大致如下:

  1. 擷取寫請求裡指定行的行鎖。由於這些批量寫請求之間是不保證一致性(只保證行一致性),因此每次只會嘗試阻塞擷取至少一個寫請求的行鎖,其它已被擷取的行鎖則跳過這次更新,等待下次迭代的繼續嘗試擷取
  2. 更新已經獲得行鎖的寫請求的時間戳記為目前時間
  3. 擷取HRegion的updatesLock的讀鎖。
  4. 擷取MVCC(Multi-Version Concurrency Control)的最新寫序號,和寫請求KeyValue資料一起寫入到MemStore。
  5. 構造WAL(Write-Ahead Logging) edit對象
  6. 把WAL edit對象非同步添加到HLog中,擷取txid號
  7. 釋放第3步中的updatesLock的讀鎖以及第1步中獲得的行鎖
  8. 按照第6步中txid號同步HLog
  9. 提交事務,把MVCC的讀序號前移到第4步中擷取到的寫序號
  10. 如果以上步驟出現失敗,則復原已經寫入MemStore的資料
  11. 如果MemStore緩衝的大小超過閥值,則請求當前HRegion的MemStore重新整理操作。

    經過以上步驟後,寫請求就屬於被提交的事務,後面的讀請求就能讀取到寫請求的資料。這些步驟裡面都包含了HBase的各種特性,主要是為了保證可觀的寫請求的效能的同時,也確保行層級的事務ACID特性。接下來就具體分析一下一些主要步驟的具體情況。

HRegion的updatesLock

    步驟3中擷取HRegion的updatesLock,是為了防止MemStore在flush過程中和寫請求事務發生線程衝突。

    首先要知道MemStore在寫請求的作用。HBase為了提高讀效能,因此保證儲存在HDFS上的資料必須是有序的,這樣就能使用各種特性,如二分尋找,提升讀效能。但由於HDFS不支援修改,因此必須採用一種措施把隨機寫變為順序寫。MemStore就是為瞭解決這個問題。隨機寫的資料寫如MemStore中就能夠在記憶體中進行排序,當MemStore大小超過閥值就需要flush到HDFS上,以HFile格式進行儲存,顯然這個HFile的資料就是有序的,這樣就把隨機寫變為順序寫。另外,MemStore也是HBase的LSM樹(Log-Structured Merge Tree)的實現部分之一。

    在MemStore進行flush的時候,為了避免對讀請求的影響,MemStore會對當前記憶體資料kvset建立snapshot,並清空kvset的內容,讀請求在查詢KeyValue的時候也會同時查詢snapshot,這樣就不會受到太大影響。但是要注意,寫請求是把資料寫入到kvset裡面,因此必須加鎖避免線程訪問發生衝突。由於可能有多個寫請求同時存在,因此寫請求擷取的是updatesLock的readLock,而snapshot同一時間只有一個,因此擷取的是updatesLock的writeLock。

擷取MVCC寫序號

    MVCC是HBase為了保證行層級的事務一致性的同時,提升讀請求的一種並發事務控制的機制。MVCC的機制不難理解,可以參考這裡。

    MVCC的最大優勢在於,讀請求和寫請求之間不會互相阻塞衝突,因此讀請求一般不需要加鎖(只有兩個寫同一行資料的寫請求需要加鎖),只有當寫請求被提交了後,讀請求才能看到寫請求的資料,這樣就可以避免發生“髒讀”,保證了事務一致性。具體MVCC實現可以參考HBase的一位PMC Member的這篇文章。

WAL(Write-Ahead Logging) 與HLog

    WAL是HBase為了避免遇到節點故障無法服務的情況下,能讓其它節點進行資料恢複的機制。HBase進行寫請求操作的時候,預設都會把KeyValue資料寫入封裝成WALEdit對象,然後序列化到HLog中,在0.98.8版本裡採用ProtoBuf格式進行序列化WAL。HLog是記錄HBase修改的記錄檔,和資料檔案HFile一樣,也是儲存於HDFS上,因此保證了HLog檔案的可靠性。這樣如果機器發生宕機,儲存在MemStore的KeyValue資料就會丟失,HBase就可以利用HLog裡面記錄的修改日誌進行資料恢複。

    每個HRegionServer只有一個HLog對象,因此當前HRegionServer上所有的HRegion的修改都會記錄到同一個記錄檔中,在需要資料恢複的時候再慢慢按照HRegion分割HLog裡的修改日誌(Log Splitting)。

    整個寫請求裡,WALEdit對象序列化寫入到HLog是唯一會發生I/O的步驟,這個會大大影響寫請求的效能。當然,如果業務情境對資料穩定性要求不高,關鍵是寫入請求,那麼可以調用Put.setDurability(Durability.SKIP_WAL),這樣就可以跳過這個步驟。

   HBase為了減輕寫入HLog產生I/O的影響,採用了較為粒度較細的多線程併發模式(詳細可參考HBASE-8755)。HLog的實現為FSHLog,主要過程涉及三個對象:AsyncWriter、AsyncSyncer和AsyncNotifier。整個寫入過程涉及步驟5-8。

  1. HRegion調用FSHLog.appendNoSync,把修改記錄添加到本地buffer中,通知AsyncWriter有記錄插入,然後返回一個long型遞增的txid作為這條修改記錄。注意到這是一個非同步呼叫。
  2. HRegion之後會馬上釋放updatesLock的讀鎖以及獲得的行鎖,然後再調用FSHLog.sync(txid),來等待之前的修改記錄寫入到HLog中。
  3. AsyncWriter從本地buffer取出修改記錄,然後將記錄經過壓縮以及ProtoBuf序列化寫入到FSDataOutputStream的緩衝中,然後再通知AsyncSyncer。由於AsyncSyncer的工作量較大,因此總共有5條線程,AsyncWriter會選擇其中一條進行喚醒。
  4. AsyncSyncer判斷是否有其它AsyncSyncer線程已經完成了同步任務,如果是則繼續等待AsyncWriter的同步請求。否則的話就把FSDataOutputStream的緩衝寫入到HDFS中去,然後喚醒AsyncNotifier
  5. AsyncNotifier的任務較為簡單,只是把所有正在等待同步的寫請求線程喚醒,不過事實上該過程同樣較為耗時,因此另外分出AsyncNotifier線程,而不是在AsyncSyncer完成通知任務。
  6. HRegion被喚醒,發現自己的txid已經得到同步,也就是修改記錄寫入到HLog中,於是接著其它操作。

    在以上的寫入過程中,第2步裡HRegion先把記錄寫入HLog的buffer,然後再釋放之前獲得的鎖後才同步等待寫入完成,這樣可以有效降低鎖持有的時間,提高其它寫請求的並發。另外,AsyncWriter、AsyncSyncer和AsyncNotifier組成的新的寫模型主要負擔起HDFS寫操作的任務,對比起舊的寫模型(需要每個寫請求的線程來負責寫HDFS,大量的線程導致嚴重的鎖競爭),最主要是大大降低了線程同步過程中的鎖競爭,有效地提高了線程的輸送量。這個寫過程對於大批量寫請求來說,能夠提高輸送量,但對於寫請求並發量較小,線程競爭較低的環境下,由於每個寫請求必須等待Async*線程之間的同步,增加了線程環境切換的開銷,會導致效能稍微下降(在0.99版本裡採用了LMAX Disruptor同步模型,並把FSHLog進行了重構,HBASE-10156)。

MVCC讀序號前移

    完成HLog的寫之後,整個寫請求事務就已經完成流程,因此就需要提交事務,讓其它讀請求可以看到這個寫請求的資料。前面已經略微介紹過MVCC的作用,這裡關注一下MVCC是如何處理讀序號前移。

    MVCC在內部維持一個long型寫序號memstoreWrite,一個long型讀序號memstoreRead,還有一個隊列writeQueue。當HRegion調用beginMemStoreInsert要求分配一個寫序號的時候,就會把寫序號自增1,並返回,並同時把一個寫請求添加到writeQueue尾部。代碼如下:  

public WriteEntry beginMemstoreInsert() {  synchronized (writeQueue) {    long nextWriteNumber = ++memstoreWrite;    WriteEntry e = new WriteEntry(nextWriteNumber);    writeQueue.add(e);    return e;  }}

    HRegion把這個寫序號和每個新插入的KeyValue資料進行關聯。當寫請求完成的時候,HRegion調用completeMemstoreInsert請求讀序號前移,MVCC首先把寫請求記錄為完成,然後查看writeQueue隊列,從隊列頭部開始取出所有已經完成的寫請求,最後一個完成的寫請求的序號則會賦值給memstoreRead,表示這是當前最大可讀的讀序號,如果HRegion的寫請求的序號比讀序號要小,則完成了事務提交,否則HRegion會一直迴圈等待提交完成。相關代碼如下:

public void completeMemstoreInsert(WriteEntry e) {  advanceMemstore(e);  waitForRead(e);} boolean advanceMemstore(WriteEntry e) {  synchronized (writeQueue) {    e.markCompleted();    long nextReadValue = -1;    while (!writeQueue.isEmpty()) {      ranOnce=true;      WriteEntry queueFirst = writeQueue.getFirst();      //...      if (queueFirst.isCompleted()) {        nextReadValue = queueFirst.getWriteNumber();        writeQueue.removeFirst();      } else {        break;      }    }      if (nextReadValue > 0) {      synchronized (readWaiters) {        memstoreRead = nextReadValue;        readWaiters.notifyAll();      }    }    if (memstoreRead >= e.getWriteNumber()) {      return true;    }    return false;  }} public void waitForRead(WriteEntry e) {  boolean interrupted = false;  synchronized (readWaiters) {    while (memstoreRead < e.getWriteNumber()) {      try {        readWaiters.wait(0);      } catch (InterruptedException ie) {        //...      }    }  }}

    由此可見,MVCC保證了事務提交的串列順序性,如果有某個寫請求提交成功,則任何寫序號小於這個寫序號的寫請求必然提交成功。因此在讀請求的時候,只要擷取MVCC的讀請求序號則可以讀取任何最新提交成功寫請求的寫資料。另外,MVCC只限制在事務提交的這個過程的串列,在實際的寫請求過程中,其它步驟都是允許並發的,因此不會對效能造成太大的影響。

    至此,HBase的一個寫請求的事務提交過程就完成。在整個寫過程裡,都採用了大量的方法去避免鎖競爭、縮短擷取鎖的時間以及保證事務一致性等措施。由於MemStore在記憶體的緩衝上始終有大小限制,因此當MemStore超過閥值的時候,HBase就要重新整理資料到HDFS上,形成新的HFile。接下來看看這個過程。

MemStore的flush

    當大量的寫請求資料添加到MemStore上,MemStore超過閥值,HRegion就會請求把MemStore的資料flush到HDFS上。另外要注意到的是,這裡flush的單位是單個HRegion,也就是說如果有多個HStore,只要有一個MemStore超過閥值,這個HRegion所屬的所有HStore都要執行flush操作。

  • HRegion首先要擷取updatesLock的寫鎖,這樣就防止有新的寫請求到來
  • 請求擷取MVCC的寫序號
  • 請求MemStore產生snapshot
  • 釋放updatesLock的寫鎖
  • 提交之前擷取的MVCC寫序號,等待之前的事務完成,防止復原事務寫入HFile
  • 把snapshot的KeyValue資料寫入到HFile裡

    主要集中來看看把snapshot的KeyValue資料寫入HFile部分。先來看看HFile的格式:

    之前在讀請求文章裡已經介紹個HFile的這個格式。HFile要保證每個HBlock大小約為64KB,採用DataBlock多級索引和BloomFilter一級索引的方法來構成HFile結構。整個寫過程比較簡單,在迴圈裡便利擷取MemStore的snapshot的KeyValue資料,然後不斷寫DataBlock裡,如果當前DataBlock的總大小超過64KB,則DataBlock就停止添加資料(設定了壓縮會進行壓縮處理),同時計算DataBlock的索引,並添加到記憶體中,另外如果開啟了BloomFilter屬性也要寫入對應的BloomBlock,這個過程中會注意儲存未壓縮大小等FileInfo資料。

    當所有的snapshot資料都寫入完DataBlock中後,就要開始寫入DataBlock的多級索引了。HBase會根據之前儲存的索引計算多級索引的級數,如果索引數量不多,則有可能只有RootIndexBlock一個層級。同時也會根據RookIndexBlock獲得MidKey的資料。最後就按照順序寫入FileInfo以及BloomFilter的索引,還有Trailer。

總結

    HBase採用了MemStore把隨機寫變為順序寫,這樣有助於提高讀請求的效率。另外也為了避免資料丟失使用HLog來記錄修改日誌。在整個寫過程中,使用了多種手段減輕了鎖競爭,提高了線程輸送量,也注意縮短鎖擷取的時間,儘可能地提高並發。通過利用MVCC也避免了讀寫請求之間的影響。

著作權聲明:本文為博主原創文章,未經博主允許不得轉載。

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.