1. 程式人生 > >探祕Hadoop生態5:Hbase讀寫流程詳解

探祕Hadoop生態5:Hbase讀寫流程詳解

如果將上篇內容理解為一個冗長的"鋪墊",那麼,從本文開始,劇情才開始正式展開。本文基於提供的樣例資料,介紹了寫資料的介面,RowKey定義,資料在客戶端的組裝,資料路由,打包分發,以及RegionServer側將資料寫入到Region中的全部流程。

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

NoSQL漫談

本文整體思路

  1. 前文內容回顧

  2. 示例資料

  3. HBase可選介面介紹

  4. 表服務介面介紹

  5. 介紹幾種寫資料的模式

  6. 如何構建Put物件(包含RowKey定義以及列定義)

  7. 資料路由

  8. Client側的分組打包

  9. Client發RPC請求到RegionServer

  10. 安全訪問控制

  11. RegionServer側處理:Region分發

  12. Region內部處理:寫WAL

  13. Region內部處理:寫MemStore

為了保證"故事"的完整性,導致本文篇幅過長,非常抱歉,讀者可以按需跳過不感興趣的內容。

前文回顧

上篇文章《一條資料的HBase之旅,簡明HBase入門教程-開篇》主要介紹瞭如下內容:

  • HBase專案概況(搜尋引擎熱度/社群開發活躍度)

  • HBase資料模型(RowKey,稀疏矩陣,Region,Column Family,KeyValue)

  • 基於HBase的資料模型,介紹了HBase的適合場景(以實體/事件為中心的簡單結構的資料)

  • 介紹了HBase與HDFS的關係,叢集關鍵角色以及部署建議

  • 寫資料前的準備工作:建立連線,建表

示例資料

(上篇文章已經提及,這裡再複製一次的原因,一是為了讓下文內容更容易理解,二是個別字段名稱做了調整)

給出一份我們日常都可以接觸到的資料樣例,先簡單給出示例資料的欄位定義:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

示例資料欄位定義

本文力求簡潔,僅給出了最簡單的幾個欄位定義。如下是”虛構”的樣例資料:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

示例資料

在本文大部分內容中所涉及的一條資料,是上面加粗的最後一行"Mobile1"為"13400006666"這行記錄。在下面的流程圖中,我們使用下面這樣一個紅色小圖示來表示該資料所在的位置:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

資料位置標記

可選介面

HBase中提供瞭如下幾種主要的介面:

  • Java Client API

    HBase的基礎API,應用最為廣泛。

  • HBase Shell

    基於Shell的命令列操作介面,基於Java Client API實現。

  • Restful API

    Rest Server側基於Java Client API實現。

  • Thrift API

    Thrift Server側基於Java Client API實現。

  • MapReduce Based Batch Manipulation API

    基於MapReduce的批量資料讀寫API。

除了上述主要的API,HBase還提供了基於Spark的批量操作介面以及C++ Client介面,但這兩個特性都被規劃在了3.0版本中,當前尚在開發中。

無論是HBase Shell/Restful API還是Thrift API,都是基於Java Client API實現的。因此,接下來關於流程的介紹,都是基於Java Client API的呼叫流程展開的。

關於表服務介面的抽象

同步連線與非同步連線,分別提供了不同的表服務介面抽象:

  • Table 同步連線中的表服務介面定義

  • AsyncTable 非同步連線中的表服務介面定義

非同步連線AsyncConnection獲取AsyncTable例項的介面預設實現:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

Create AsyncTable

同步連線ClusterConnection的實現類ConnectionImplementation中獲取Table例項的介面實現:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

Create Table

寫資料的幾種方式

  • Single Put

    單條記錄單條記錄的隨機put操作。Single Put所對應的介面定義如下:

    在AsyncTable介面中的定義:

    CompletableFuture<Void> put(Put put);

    在Table介面中的定義:

    void put(Put put) throws IOException;
  • Batch Put

    匯聚了幾十條甚至是幾百上千條記錄之後的小批次隨機put操作。

    Batch Put只是本文對該型別操作的稱法,實際的介面名稱如下所示:

    在AsyncTable介面中的定義:

    List<CompletableFuture<Void>> put(List<Put> puts);

    在Table介面中的定義:

    void put(List<Put> puts) throws IOException;
  • Bulkload

    基於MapReduce API提供的資料批量匯入能力,匯入資料量通常在GB級別以上,Bulkload能夠繞過Java Client API直接生成HBase的底層資料檔案(HFile)。

構建Put物件

設計合理的RowKey

RowKey通常是一個或若干個欄位的直接組合或經一定處理後的資訊,因為一個表中所有的資料都是基於RowKey排序的,RowKey的設計對讀寫都會有直接的效能影響。

我們基於本文的樣例資料,先給出兩種RowKey的設計,並簡單討論各自的優缺點:

樣例資料:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

示例資料

RowKey Format 1: Mobile1 + StartTime

為了方便讀者理解,我們在兩個欄位之間添加了連線符”^”。如下是RowKey以及相關排序結果:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

RowKey Format 1

RowKey Format 2: StartTime + Mobile1

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

RowKey Format 2

從上面兩個表格可以看出來,不同的欄位組合順序設計,帶來截然不同的排序結果,我們將RowKey中的第一個欄位稱之為“先導欄位”。第一種設計,有利於查詢”手機號碼XXX的在某時間範圍內的資料記錄”,但不利於查詢”某段時間範圍內有哪些手機號碼撥出了電話?”,而第二種設計卻恰好相反。

上面是兩種設計都是兩個欄位的直接組合,這種設計在實際應用中,會帶來讀寫熱點問題,難以保障資料讀寫請求在所有Regions之間的負載均衡。避免熱點的常見方法有如下幾種:

Reversing

如果先導欄位本身會帶來熱點問題,但該欄位尾部的資訊卻具備良好的隨機性,此時,可以考慮將先導欄位做反轉處理,將尾部幾位直接提前到前面,或者直接將整個欄位完全反轉。

將先導欄位Mobile1翻轉後,就具備非常好的隨機性。

例如:

13400001111^201803010800

將先導欄位Mobile1反轉後的RowKey變為:

11110000431^201803010800

Salting

Salting的原理是在RowKey的前面新增固定長度的隨機Bytes,隨機Bytes能保障資料在所有Regions間的負載均衡。

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

RowKey With Salting

Salting能很好的保障寫入時將資料均勻分散到各個Region中,但對於讀取卻是不友好的,例如,如果讀取Mobile1為”13400001111″在20180301這一天的資料記錄時,因為Salting Bytes資訊是隨機選擇新增的,查詢時並不知道前面新增的Salting Bytes是”A”,因此{“A”, “B”, “C”}所關聯的Regions都得去檢視一下。

Hashing

Hashing是將一個RowKey通過一個Hash函式生成一組固定長度的bytes,Hash函式能保障所生成的隨機bytes具備良好的離散度,從而也能夠均勻打散到各個Region中。Hashing既有利於隨機寫入,又利於基於知道RowKey各欄位的確切資訊之後的隨機讀取操作,但如果是基於RowKey範圍的Scan或者是RowKey的模糊資訊進行查詢的話,就會帶來顯著的效能問題,因為原來在字典順序相鄰的RowKey列表,通過Hashing打散後導致這些資料被分散到了多個Region中。

因此,RowKey的設計,需要充分考慮業務的讀寫特點。

本文內容假設RowKey設計:reversing(Mobile1) +StartTime

也就是說,RowKey由反轉處理後的Mobile1與StartTime組成。對於我們所關注的這行資料:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

關注的資料記錄

RowKey應該為: 66660000431^201803011300

因為建立表時預設的Region與RowKey強相關,我們現在才可以給出本文樣例所需要建立的表的”Region分割點“資訊:

假設,Region分割點為“1,2,3,4,5,6,7,8,9”,基於這9個分割點,可以預先建立10個Region,這10個Region的StartKey和StopKey如下所示:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

Region劃分資訊

  • 第一個Region的StartKey為空,最後一個Region的StopKey為空

  • 每一個Region區間,都包含StartKey本身,但不包含StopKey

  • 由於Mobile1欄位的最後一位是0~9之間的隨機數字,因此,可以均勻打散到這10個Region中

定義列

每一個列在HBase中體現為一個KeyValue,而每一個KeyValue擁有特定的組成結構,這一點在上一篇文章的資料模型章節已經提到過。

所謂的定義列,就是需要定義出每一個列要存放的列族(Column Family)以及列標識(Qualifier)資訊。

我們假設,存放樣例資料的這個表名稱為”TelRecords” ,為了簡單起見,僅僅設定了1個名為”I”的列族。

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

Column Family以及列標識定義

因為Mobile1與StartTime都已經被包含在RowKey中,所以,不需要再在列中儲存一份。關於列族名稱與列標識名稱,建議應該簡短一些,因為這些資訊都會被包含在KeyValue裡面,過長的名稱會導致資料膨脹。

基於RowKey和列定義資訊,就可以組建HBase的Put物件,一個Put物件用來描述待寫入的一行資料,一個Put可以理解成與某個RowKey關聯的1個或多個KeyValue的集合。

至此,這條資料已經轉變成了Put物件,如下圖所示:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

Put

資料路由

初始化ZooKeeper Session

因為meta Region存放於ZooKeeper中,在第一次從ZooKeeper中讀取META Region的地址時,需要先初始化一個ZooKeeper Session。ZooKeeper Session是ZooKeeper Client與ZooKeeper Server端所建立的一個會話,通過心跳機制保持長連線。

獲取Region路由資訊

通過前面建立的連線,從ZooKeeper中讀取meta Region所在的RegionServer,這個讀取流程,當前已經是非同步的。獲取了meta Region的路由資訊以後,再從meta Region中定位要讀寫的RowKey所關聯的Region資訊。如下圖所示:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

Region Routing

因為每一個使用者表Region都是一個RowKey Range,meta Region中記錄了每一個使用者表Region的路由以及狀態資訊,以RegionName(包含表名,Region StartKey,Region ID,副本ID等資訊)作為RowKey。基於一條使用者資料RowKey,快速查詢該RowKey所屬的Region的方法其實很簡單:只需要基於表名以及該使用者資料RowKey,構建一個虛擬的Region Key,然後通過Reverse Scan的方式,讀到的第一條Region記錄就是該資料所關聯的Region。如下圖所示:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

Location User Region

Region只要不被遷移,那麼獲取的該Region的路由資訊就是一直有效的,因此,HBase Client有一個Cache機制來快取Region的路由資訊,避免每次讀寫都要去訪問ZooKeeper或者meta Region。

進階內容1:meta Region究竟在哪裡?

meta Region的路由資訊存放在ZooKeeper中,但meta Region究竟在哪個RegionServer中提供讀寫服務?

在1.0版本中,引入了一個新特性,使得Master可以”兼任”一個RegionServer角色(可參考HBASE-5487, HBASE-10569),從而可以將一些系統表的Region分配到Master的這個RegionServer中,這種設計的初衷是為了簡化/優化Region Assign的流程,但這依然帶來了一系列複雜的問題,尤其是Master初始化和RegionServer初始化之間的Race,因此,在2.0版本中將這個特性暫時關閉了。詳細資訊可以參考:HBASE-16367,HBASE-18511,HBASE-19694,HBASE-19785,HBASE-19828

客戶端側的資料分組“打包”

如果這條待寫入的資料採用的是Single Put的方式,那麼,該步驟可以略過(事實上,單條Put操作的流程相對簡單,就是先定位該RowKey所對應的Region以及RegionServer資訊後,Client直接傳送寫請求到RegionServer側即可)。

但如果這條資料被混雜在其它的資料列表中,採用Batch Put的方式,那麼,客戶端在將所有的資料寫到對應的RegionServer之前,會先分組”打包”,流程如下:

  1. 按Region分組:遍歷每一條資料的RowKey,然後,依據meta表中記錄的Region資訊,確定每一條資料所屬的Region。此步驟可以獲取到Region到RowKey列表的對映關係。

  2. 按RegionServer”打包”:因為Region一定歸屬於某一個RegionServer(注:本文內容中如無特殊說明,都未考慮Region Replica特性),那屬於同一個RegionServer的多個Regions的寫入請求,被打包成一個MultiAction物件,這樣可以一併傳送到每一個RegionServer中。

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

資料分組與打包

Client傳送寫資料請求到RegionServer

類似於Client傳送建表到Master的流程,Client傳送寫資料請求到RegionServer,也是通過RPC的方式。只是,Client到Master以及Client到RegionServer,採用了不同的RPC服務介面。

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

Client Send Request To RegionServer

single put請求與batch put請求,兩者所呼叫的RPC服務介面方法是不同的,如下是Client.proto中的定義:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

Client Proto定義

安全訪問控制

如何保障UserA只能寫資料到UserA的表中,以及禁止UserA改寫其它User的表的資料,HBase提供了ACL機制。ACL通常需要與Kerberos認證配合一起使用,Kerberos能夠確保一個使用者的合法性,而ACL確保該使用者僅能執行許可權範圍內的操作。

HBase將許可權分為如下幾類:

  • READ(‘R’)

  • WRITE(‘W’)

  • EXEC(‘X’)

  • CREATE(‘C’)

  • ADMIN(‘A’)

可以為一個使用者/使用者組定義整庫級別的許可權集合,也可以定義Namespace、表、列族甚至是列級別的許可權集合。

RegionServer端處理:Region分發

RegionServer的RPC Server側,接收到來自Client端的RPC請求以後,將該請求交給Handler執行緒處理。

如果是single put,則該步驟比較簡單,因為在傳送過來的請求引數MutateRequest中,已經攜帶了這條記錄所關聯的Region,那麼直接將該請求轉發給對應的Region即可。

如果是batch puts,則接收到的請求引數為MultiRequest,在MultiRequest中,混合了這個RegionServer所持有的多個Region的寫入請求,每一個Region的寫入請求都被包裝成了一個RegionAction物件。RegionServer接收到MultiRequest請求以後,遍歷所有的RegionAction,而後寫入到每一個Region中,此過程是序列的:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

Write Per Region

從這裡可以看出來,並不是一個batch越大越好,大的batch size甚至可能導致吞吐量下降。

Region內部處理:寫WAL

HBase也採用了LSM-Tree的架構設計:LSM-Tree利用了傳統機械硬碟的“順序讀寫速度遠高於隨機讀寫速度”的特點。隨機寫入的資料,如果直接去改寫每一個Region上的資料檔案,那麼吞吐量是非常差的。因此,每一個Region中隨機寫入的資料,都暫時先快取在記憶體中(HBase中存放這部分記憶體資料的模組稱之為MemStore,這裡僅僅引出概念,下一章節詳細介紹),為了保障資料可靠性,將這些隨機寫入的資料順序寫入到一個稱之為WAL(Write-Ahead-Log)的日誌檔案中,WAL中的資料按時間順序組織:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

MemStore And WAL

如果位於記憶體中的資料尚未持久化,而且突然遇到了機器斷電,只需要將WAL中的資料回放到Region中即可:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

WAL Replay

在HBase中,預設一個RegionServer只有一個可寫的WAL檔案。WAL中寫入的記錄,以Entry為基本單元,而一個Entry中,包含:

  • WALKey 包含{Encoded Region Name,www.365soke.cn  Table Name,www.hbs90.cn Sequence ID,Timestamp}等關鍵資訊,其中,Sequence ID在維持資料一致性方面起到了關鍵作用,可以理解為一個事務ID。

  • WALEdit WALEdit中直接儲存待寫入資料的所有的KeyValues,www.boshenyl.cn  而這些KeyValues可能來自一個Region中的多行資料。

也就是說,通常,一個Region中的一個batch put請求,會被組裝成一個Entry,寫入到WAL中:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

Write into WAL

將Entry寫到檔案中時是支援壓縮的,但該特性預設未開啟。

WAL進階內容

WAL Roll and Archive

當正在寫的WAL檔案達到一定大小以後,會建立一個新的WAL檔案,上一個WAL檔案依然需要被保留,因為這個WAL檔案中所關聯的Region中的資料,尚未被持久化儲存,因此,該WAL可能會被用來回放資料。

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

Roll WAL

如果一個WAL中所關聯的所有的Region中的資料,都已經被持久化儲存了,那麼,這個WAL檔案會被暫時歸檔到另外一個目錄中:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

WAL Archive

注意,這裡不是直接將WAL檔案刪除掉,這是一種穩妥且合理的做法,原因如下:

  • 避免因為邏輯實現上的問題導致WAL被誤刪,暫時歸檔到另外一個目錄,為錯誤發現預留了一定的時間視窗

  • 按時間維度組織的WAL資料檔案還可以被用於其它用途,如增量備份,跨叢集容災等等,因此,這些WAL檔案通常不允許直接被刪除,至於何時可以被清理,還需要額外的控制邏輯

另外,如果對寫入HBase中的資料的可靠性要求不高,那麼,HBase允許通過配置跳過寫WAL操作。

思考:put與batch put的效能為何差別巨大?

在網路分發上,batch put已經具備一定的優勢,因為batch put是打包分發的。

而從寫WAL這塊,看的出來,www.taohuayuan178.com batch put寫入的一小批次Put物件,可以通過一次sync就持久化到WAL檔案中了,有效減少了IOPS。

但前面也提到了,batch size並不是越大越好,因為每一個batch在RegionServer端是被序列處理的。

利用Disruptor提升寫併發效能

在高併發隨機寫入場景下,會帶來大量的WAL Sync操作,HBase中採用了Disruptor的RingBuffer來減少競爭,思路是這樣:如果將瞬間併發寫入WAL中的資料,合併執行Sync操作,可以有效降低Sync操作的次數,來提升寫吞吐量。

Multi-WAL

預設情形下,一個RegionServer只有一個被寫入的WAL Writer,儘管WAL Writer依靠順序寫提升寫吞吐量,在基於普通機械硬碟的配置下,此時只能有單塊盤發揮作用,其它盤的IOPS能力並沒有被充分利用起來,這是Multi-WAL設計的初衷。Multi-WAL可以在一個RegionServer中同時啟動幾個WAL Writer,可按照一定的策略,將一個Region與其中某一個WAL Writer繫結,這樣可以充分發揮多塊盤的效能優勢。

關於WAL的未來

WAL是基於機械硬碟的IO模型設計的,而對於新興的非易失性介質,如3D XPoint,WAL未來可能會失去存在的意義,關於這部分內容,請參考文章《從HBase中移除WAL?3D XPoint技術帶來的變革》。

Region內部處理:寫MemStore

每一個Column Family,在Region內部被抽象為了一個HStore物件,而每一個HStore擁有自身的MemStore,用來快取一批最近被隨機寫入的資料,這是LSM-Tree核心設計的一部分。

MemStore中用來存放所有的KeyValue的資料結構,稱之為CellSet,而CellSet的核心是一個ConcurrentSkipListMap,我們知道,ConcurrentSkipListMap是Java的跳錶實現,資料按照Key值有序存放,而且在高併發寫入時,效能遠高於ConcurrentHashMap。

因此,寫MemStore的過程,事實上是將batch put提交過來的所有的KeyValue列表,寫入到MemStore的以ConcurrentSkipListMap為組成核心的CellSet中:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

Write Into MemStore

MemStore因為涉及到大量的隨機寫入操作,會帶來大量Java小物件的建立與消亡,會導致大量的記憶體碎片,給GC帶來比較重的壓力,HBase為了優化這裡的機制,借鑑了作業系統的記憶體分頁的技術,增加了一個名為MSLab的特性,通過分配一些固定大小的Chunk,來儲存MemStore中的資料,這樣可以有效減少記憶體碎片問題,降低GC的壓力。當然,ConcurrentSkipListMap本身也會建立大量的物件,這裡也有很大的優化空間,去年阿里的一篇文章透露了阿里如何通過優化ConcurrentSkipListMap的結構來有效減少GC時間。

進階內容2:先寫WAL還是先寫MemStore?

在0.94版本之前,Region中的寫入順序是先寫WAL再寫MemStore,這與WAL的定義也相符。

但在0.94版本中,將這兩者的順序顛倒了,當時顛倒的初衷,是為了使得行鎖能夠在WAL sync之前先釋放,從而可以提升針對單行資料的更新效能。詳細問題單,請參考HBASE-4528。

在2.0版本中,這一行為又被改回去了,原因在於修改了行鎖機制以後(下面章節將講到),發現了一些效能下降,而HBASE-4528中的優化卻無法再發揮作用,詳情請參考HBASE-15158。改動之後的邏輯也更簡潔了。

進階內容3:關於行級別的ACID

在之前的版本中,行級別的任何併發寫入/更新都是互斥的,由一個行鎖控制。但在2.0版本中,這一點行為發生了變化,多個執行緒可以同時更新一行資料,這裡的考慮點為:

  • 如果多個執行緒寫入同一行的不同列族,是不需要互斥的

  • 多個執行緒寫同一行的相同列族,也不需要互斥,即使是寫相同的列,也完全可以通過HBase的MVCC機制來控制資料的一致性

  • 當然,CAS操作(如checkAndPut)或increment操作,依然需要獨佔的行鎖

更多詳細資訊,可以參考HBASE-12751。

至此,這條資料已經被同時成功寫到了WAL以及MemStore中:

一條資料的HBase之旅,簡明HBase入門教程-Write全流程

Data Written In HBase

總結

本文主要內容總結如下:

  • 介紹HBase寫資料可選介面以及介面定義。

  • 通過一個樣例,介紹了RowKey定義以及列定義的一些方法,以及如何組裝Put物件

  • 資料路由,資料分發、打包,以及Client通過RPC傳送寫資料請求至RegionServer

  • RegionServer接收資料以後,將資料寫到每一個Region中。寫資料流程先寫WAL再寫MemStore,這裡展開了一些技術細節

  • 簡單介紹了HBase許可權控制模型

需要說明的一點,本文所講到的MemStore其實是一種"簡化"後的模型,在2.0版本中,這裡已經變的更加複雜,這些內容將在下一篇介紹Flush與Compaction的流程中詳細介紹。