天天看點

HBase - 資料寫入流程解析

衆所周知,HBase預設适用于寫多讀少的應用,正是依賴于它相當出色的寫入性能:一個100台RS的叢集可以輕松地支撐每天10T的寫入量。當然,為了支援更高吞吐量的寫入,HBase還在不斷地進行優化和修正,這篇文章結合0.98版本的源碼全面地分析HBase的寫入流程,全文分為三個部分,第一部分介紹用戶端的寫入流程,第二部分介紹伺服器端的寫入流程,最後再重點分析WAL的工作原理。

用戶端流程解析

(1)使用者送出put請求後,HBase用戶端會将put請求添加到本地buffer中,符合一定條件就會通過AsyncProcess異步批量送出。HBase預設設定autoflush=true,表示put請求直接會送出給伺服器進行處理;使用者可以設定autoflush=false,這樣的話put請求會首先放到本地buffer,等到本地buffer大小超過一定門檻值(預設為2M,可以通過配置檔案配置)之後才會送出。很顯然,後者采用group commit機制送出請求,可以極大地提升寫入性能,但是因為沒有保護機制,如果用戶端崩潰的話會導緻送出的請求丢失。

(2)在送出之前,HBase會在中繼資料表.meta.中根據rowkey找到它們歸屬的region server,這個定位的過程是通過HConnection的locateRegion方法獲得的。如果是批量請求的話還會把這些rowkey按照HRegionLocation分組,每個分組可以對應一次RPC請求。

(3)HBase會為每個HRegionLocation構造一個遠端RPC請求MultiServerCallable<Row>,然後通過rpcCallerFactory.<MultiResponse> newCaller()執行調用,忽略掉失敗重新送出和錯誤處理,用戶端的送出操作到此結束。

伺服器端流程解析

伺服器端RegionServer接收到用戶端的寫入請求後,首先會反序列化為Put對象,然後執行各種檢查操作,比如檢查region是否是隻讀、memstore大小是否超過blockingMemstoreSize等。檢查完成之後,就會執行如下核心操作:

HBase - 資料寫入流程解析

(1)擷取行鎖、Region更新共享鎖: HBase中使用行鎖保證對同一行資料的更新都是互斥操作,用以保證更新的原子性,要麼更新成功,要麼失敗。

(2)開始寫事務:擷取write number,用于實作MVCC,實作資料的非鎖定讀,在保證讀寫一緻性的前提下提高讀取性能。

(3)寫緩存memstore:HBase中每列族都會對應一個store,用來存儲該列資料。每個store都會有個寫緩存memstore,用于緩存寫入資料。HBase并不會直接将資料落盤,而是先寫入緩存,等緩存滿足一定大小之後再一起落盤。

(4)Append HLog:HBase使用WAL機制保證資料可靠性,即首先寫日志再寫緩存,即使發生當機,也可以通過恢複HLog還原出原始資料。該步驟就是将資料構造為WALEdit對象,然後順序寫入HLog中,此時不需要執行sync操作。0.98版本采用了新的寫線程模式實作HLog日志的寫入,可以使得整個資料更新性能得到極大提升,具體原理見下一個章節。

(5)釋放行鎖以及共享鎖

(6)Sync HLog:HLog真正sync到HDFS,在釋放行鎖之後執行sync操作是為了盡量減少持鎖時間,提升寫性能。如果Sync失敗,執行復原操作将memstore中已經寫入的資料移除。

(7)結束寫事務:此時該線程的更新操作才會對其他讀請求可見,更新才實際生效。具體分析見上一篇文章《HBase – 并發控制深度解析》

(8)flush memstore:當寫緩存滿64M之後,會啟動flush線程将資料重新整理到硬碟。重新整理操作涉及到HFile相關結構,後面會詳細對此進行介紹。

WAL機制解析

WAL(Write-Ahead Logging)是一種高效的日志算法,幾乎是所有非記憶體資料庫提升寫性能的不二法門,基本原理是在資料寫入之前首先順序寫入日志,然後再寫入緩存,等到緩存寫滿之後統一落盤。之是以能夠提升寫性能,是因為WAL将一次随機寫轉化為了一次順序寫加一次記憶體寫。提升寫性能的同時,WAL可以保證資料的可靠性,即在任何情況下資料不丢失。假如一次寫入完成之後發生了當機,即使所有緩存中的資料丢失,也可以通過恢複日志還原出丢失的資料。

WAL持久化等級

HBase中可以通過設定WAL的持久化等級決定是否開啟WAL機制、以及HLog的落盤方式。WAL的持久化等級分為如下四個等級:

1. SKIP_WAL:隻寫緩存,不寫HLog日志。這種方式因為隻寫記憶體,是以可以極大的提升寫入性能,但是資料有丢失的風險。在實際應用過程中并不建議設定此等級,除非确認不要求資料的可靠性。

2. ASYNC_WAL:異步将資料寫入HLog日志中。

3. SYNC_WAL:同步将資料寫入日志檔案中,需要注意的是資料隻是被寫入檔案系統中,并沒有真正落盤。

4. FSYNC_WAL:同步将資料寫入日志檔案并強制落盤。最嚴格的日志寫入等級,可以保證資料不會丢失,但是性能相對比較差。

5. USER_DEFAULT:預設如果使用者沒有指定持久化等級,HBase使用SYNC_WAL等級持久化資料。

使用者可以通過用戶端設定WAL持久化等級,代碼:put.setDurability(Durability. SYNC_WAL );

HLog資料結構

HBase中,WAL的實作類為HLog,每個Region Server擁有一個HLog日志,所有region的寫入都是寫到同一個HLog。下圖表示同一個Region Server中的3個 region 共享一個HLog。當資料寫入時,是将資料對<HLogKey,WALEdit>按照順序追加到HLog中,以擷取最好的寫入性能。

HBase - 資料寫入流程解析

上圖中HLogKey主要存儲了log sequence number,更新時間 write time,region name,表名table name以及cluster ids。其中log sequncece number作為HFile中一個重要的中繼資料,和HLog的生命周期息息相關,後續章節會詳細介紹;region name和table name分别表征該段日志屬于哪個region以及哪張表;cluster ids用于将日志複制到叢集中其他機器上。

WALEdit用來表示一個事務中的更新集合,在之前的版本,如果一個事務中對一行row R中三列c1,c2,c3分别做了修改,那麼hlog中會有3個對應的日志片段如下所示:

<logseq1-for-edit1>:<keyvalue-for-edit-c1>

<logseq2-for-edit2>:<keyvalue-for-edit-c2>

<logseq3-for-edit3>:<keyvalue-for-edit-c3>

然而,這種日志結構無法保證行級事務的原子性,假如剛好更新到c2之後發生當機,那麼就會産生隻有部分日志寫入成功的現象。為此,hbase将所有對同一行的更新操作都表示為一個記錄,如下:

<logseq#-for-entire-txn>:<WALEdit-for-entire-txn>

其中WALEdit會被序列化為格式<-1, # of edits, <KeyValue>, <KeyValue>, <KeyValue>>,比如<-1, 3, <keyvalue-for-edit-c1>, <keyvalue-for-edit-c2>, <keyvalue-for-edit-c3>>,其中-1作為标示符表征這種新的日志結構。

WAL寫入模型

了解了HLog的結構之後,我們就開始研究HLog的寫入模型。HLog的寫入可以分為三個階段,首先将資料對<HLogKey,WALEdit>寫入本地緩存,然後再将本地緩存寫入檔案系統,最後執行sync操作同步到磁盤。在以前老的寫入模型中,上述三步都由工作線程獨自完成,如下圖所示:

HBase - 資料寫入流程解析

上圖中,本地緩存寫入檔案系統那個步驟工作線程需要持有updateLock執行,不同工作線程之間必然會惡性競争;不僅如此,在Sync HDFS這步中,工作線程之間需要搶占flushLock,因為Sync操作是一個耗時操作,搶占這個鎖會導緻寫入性能大幅降低。

所幸的是,來自中國(準确的來說,是來自小米,鼓掌)的3位工程師意識到了這個問題,進而提出了一種新的寫入模型并被官方采納。根據官方測試,新寫入模型的吞吐量比之前提升3倍多,單台RS寫入吞吐量介于12150~31520,5台RS組成的叢集寫入吞吐量介于22000~70000(見HBASE-8755)。下圖是小米官方給出來的對比測試結果:

HBase - 資料寫入流程解析

在新寫入模型中,本地緩存寫入檔案系統以及Sync HDFS都交給了新的獨立線程完成,并引入一個Notify線程通知工作線程是否已經Sync成功,采用這種機制消除上述鎖競争,具體如下圖所示:

HBase - 資料寫入流程解析

1. 上文中提到工作線程在寫入WALEdit之後并沒有進行Sync,而是等到釋放行鎖阻塞在syncedTillHere變量上,等待AsyncNotifier線程喚醒。

2. 工作線程将WALEdit寫入本地Buffer之後,會生成一個自增變量txid,攜帶此txid喚醒AsyncWriter線程

3. AsyncWriter線程會取出本地Buffer中的所有WALEdit,寫入HDFS。注意該線程會比較傳入的txid和已經寫入的最大txid(writtenTxid),如果傳入的txid小于writteTxid,表示該txid對應的WALEdit已經寫入,直接跳過

4. AsyncWriter線程将所有WALEdit寫入HDFS之後攜帶maxTxid喚醒AsyncFlusher線程

5. AsyncFlusher線程将所有寫入檔案系統的WALEdit統一Sync重新整理到磁盤

6. 資料全部落盤之後調用setFlushedTxid方法喚醒AyncNotifier線程

7. AyncNotifier線程會喚醒所有阻塞在變量syncedTillHere的工作線程,工作線程被喚醒之後表示WAL寫入完成,後面再執行MVCC結束寫事務,推進全局讀取點,本次更新才會對使用者可見

通過上述過程的梳理可以知道,新寫入模型采取了多線程模式獨立完成寫檔案系統、sync磁盤操作,避免了之前多工作線程惡性搶占鎖的問題。同時,工作線程在将WALEdit寫入本地Buffer之後并沒有馬上阻塞,而是釋放行鎖之後阻塞等待WALEdit落盤,這樣可以盡可能地避免行鎖競争,提高寫入性能。

總結

本文首先介紹了HBase的寫入流程,之後重點分析了WAL的寫入模型以及相關優化。希望借此能夠對HBase寫入的高性能特性能夠了解。後面一篇文章會接着介紹寫入到memstore的資料如何真正的落盤,敬請期待!

本文轉載自:http://hbasefly.com

<a href="http://hbasefly.com/2016/03/23/hbase_writer/" target="_blank">原文連結</a>