天天看點

HBase原理-遲到的‘資料讀取流程’部分細節

簡單地回顧一下scan的整個流程,如下圖所示:

HBase原理-遲到的‘資料讀取流程’部分細節

上圖是一個簡單的示意圖,使用者如果對整個流程比較感興趣,可以閱讀之前的文章,本文将會關注于隐藏在這個示意圖中的核心細節。這裡筆者挑出了其中五個比較重要的問題來說明,這些問題都是本人之前或早或晚比較困惑的問題,拿出來與大家分享。當然,如果大家有回報想了解的其他細節,也可以單獨交流探讨。

1. 常說HBase資料讀取要讀Memstore、HFile和Blockcache,為什麼上面Scanner隻有StoreFileScanner和MemstoreScanner兩種?沒有BlockcacheScanner?

HBase中資料僅僅獨立地存在于Memstore和StoreFile中,Blockcache中的資料隻是StoreFile中的部分資料(熱點資料),即所有存在于Blockcache的資料必然存在于StoreFile中。是以MemstoreScanner和StoreFileScanner就可以覆寫到所有資料。實際讀取時StoreFileScanner通過索引定位到待查找key所在的block之後,首先檢查該block是否存在于Blockcache中,如果存在直接取出,如果不存在再到對應的StoreFile中讀取。

2.  資料更新操作先将資料寫入Memstore,再落盤。落盤之後需不需要更新Blockcache中對應的kv?如果不更新,會不會讀到髒資料?

如果理清楚了第一個問題,相信很容易得出這個答案:不需要更新Blockcache中對應的kv,而且不會讀到髒資料。資料寫入Memstore落盤會形成新的檔案,和Blockcache裡面的資料是互相獨立的,以多版本的方式存在。

3. 讀取流程中如何使用BloomFilter(簡稱BF)對StoreFile進行過濾?

過濾StoreFile發生在上圖中第三步,過濾手段主要有三種:根據KeyRange過濾、根據TimeRange過濾、根據BF過濾。下面分别進行介紹:

(1)根據KeyRange過濾:因為StoreFile是中所有KV資料都是有序排列的,是以如果待檢索row範圍[startrow,stoprow]與檔案起始key範圍[firstkey,lastkey]沒有交集,比如stoprow < firstkey 或者 startrow > lastkey,就可以過濾掉該StoreFile。 

(2)根據TimeRange過濾:StoreFile中中繼資料有一個關于該File的TimeRange屬性[minimumTimestamp, maxmumTimestamp],是以待檢索的TimeRange如果與該檔案時間範圍沒有交集,就可以過濾掉該StoreFile;另外,如果該檔案所有資料已經過期,也可以過濾淘汰。

現在來看看HBase中如何利用BF對StoreFile進行過濾(注:接下來所有關于HBase BF的說明都按照Row類型來,Row-Column類型類似),原理其實很簡單:首先把BF資料加載到記憶體;然後使用hash函數對待檢索row進行hash,根據hash後的結果在BF資料中進行尋址檢視即可确定是否存在該HFile。第二步就是BF的原理,并沒有什麼好講的,主要來看看HBase是如何将BF資料加載到記憶體的。

HBase原理-遲到的‘資料讀取流程’部分細節

HFile組織結構中關于BF有兩個非常重要的結構-Bloom Block與Bloom Index。Bloom Block主要存儲BF的實際資料,可能這會大家要問為什麼Bloom Block要分布在整個HFile?分布的具體位置如何确定?其實很簡單,HBase在寫資料的時候就會根據row生成對應的BF資訊并寫到一個Block中,随着使用者資料的不斷寫入,這個BF Block就會不斷增大,當增大到一定門檻值之後系統就會重新生成一個新Block,舊Block就會順序加載到Data Block之後。這裡隐含了一個關鍵的資訊,随着單個檔案的增大,BF資訊會逐漸變的很大,并不适合一次性全部加載到記憶體,更适合的使用方式是使用哪塊加載哪塊!

這些Bloom Block分散在HFile中的各個角落,就會帶來一個問題:如何有效快速定位到這些BF Block?這就是Bloom Index的核心作用,與Data Index相同,Bloom Index也是一顆B+樹,Bloom Index Block結構如下圖所示:

HBase原理-遲到的‘資料讀取流程’部分細節

上圖需要重點關注Bloom Block的Block Key:Block中第一個原始KV的RowKey。這樣給定一個待檢索的 rowkey,就可以很容易地通過Bloom Index定位到具體的Bloom Block,将Block加載到記憶體進行過濾。通常情況下,熱點Bloom Block會常駐記憶體的!

到此為止,筆者已經解釋清楚了HBase是如何利用BF在讀取資料時根據rowkey過濾StoreFile的,相信Kudu、RocksDB中BF的原理基本相同。

再回到出發的地方,我們說在實際scan之前就要使用BF對StoreFile進行過濾,那仔細想下,到底用哪個rowkey過濾?實際實作中系統使用scan的startrow作為過濾條件進行過濾,這是不是有問題?舉個簡單的例子,假設小明檢索的資料為[row1, row4],如果此檔案不包含row1,而包含row2,這樣在scan前你利用row1就把該檔案淘汰掉了,row2這條資料怎麼辦?不是會被遺漏?

這裡系統實作有個隐藏點,scan之前使用BF進行過濾隻針對get查詢以及scan單條資料的場景,scan多條資料并不會執行實際的BF過濾,而是在實際seek到新一行的時候才會啟用BF根據新一行rowkey對所有StoreFile過濾。

4. 最小堆中彈出cell之後如何對該cell進行檢查過濾,確定滿足使用者設定條件?檢查過濾之後是繼續彈出下一個cell,還是跳過部分cell重新seek到下一列或者下一行?

scan之是以複雜,很大程度上是因為scan可以設定的條件非常之多,下面所示代碼為比較正常的一些設定:

在整個Scan流程的第6步,将堆頂kv元素出堆進行檢查,實際上主要檢查兩個方面,其一是非使用者條件檢查,比如kv是否已經過期(列族設定TTL)、kv是否已經被删除,這些檢查和使用者設定查詢條件沒有任何關系;其二就是檢查該kv是否滿足使用者設定的這些查詢條件,代碼邏輯還是比較清晰的,在此不再贅述。核心代碼主要參考ScanQueryMatcher.match(cell)方法。

相比堆頂元素檢查流程,筆者更想探讨堆頂元素kv檢查之後的傳回值-MatchCode,這個Code可不簡單,它會告訴scanner是繼續seek下一個cell,還是直接跳過部分cell直接seek到下一列(對應INCLUDE_AND_SEEK_NEXT_COL或SEEK_NEXT_COL),抑或是直接seek到下一行(對應INCLUDE_AND_SEEK_NEXT_ROW或SEEK_NEXT_ROW)。還是舉一個簡單的例子:

HBase原理-遲到的‘資料讀取流程’部分細節

上圖是待查表,含有一個列族cf,列族下有四個列[c1, c2, c3, c4],列族設定MaxVersions為2,即允許最多存在2個版本。現在簡單構造一個查詢語句如下:

下面分别模拟直接跳過部分紀錄seek到下一列(INCLUDE_AND_SEEK_NEXT_COL)的場景以及跳過部分列直接seek到下一行(INCLUDE_AND_SEEK_NEXT_ROW)的場景:

(1)假設目前檢索r1行,堆頂元素為cf:c1下的kv1(版本為v1),按照設定條件中檢索的最大版本号為1,其他條件都滿足的情況下就可以直接跳過kv2直接seek到下一列-c2列。這種場景下就會傳回INCLUDE_AND_SEEK_NEXT_COL。

(2)假設目前檢索r1行,堆頂元素為cf:c2下的kv3(僅有1個版本),滿足設定的版本條件,系統檢測到c2是檢索的最後一列之後(c3、c4并不需要檢索),就會傳回訓示-略過c3、c4直接seek到下一行。這種場景下就會傳回INCLUDE_AND_SEEK_NEXT_ROW。

至此,筆者針對scan流程中的第6步進行了比較詳細的解讀,對認為比較重要的點進行了簡單示範。其實還是有很多内容,但大多都大同小異,原理類似。有興趣讀HBase源碼的同學可以參考這裡的解讀,相信會有所幫助。

5. 每次seek(key)指令是不是都需要所有scanner真正seek到指定key?延遲seek是如何優化讀性能的?

這是本文探讨的最後一個話題,嚴格來說,這個話題并不涉及scan的流程,而僅僅是對scan的一項優化。但是個人認為了解這項優化對scan的流程了解有着相當重要的意義,同時也是閱讀HBase-Scan子產品源碼必須要邁過的一道坎。

先回到scan的流程,根據之前的了解,如果堆頂元素出堆檢查之後訓示scanner需要跳過部分cell直接seek到下一列或者下一行,此時所有scanner都需要按照訓示執行seek指令定位指定位置,這本身沒有毛病。然而這可能并不高效,試想這麼一種場景:

(1)目前有3個StoreFile檔案,是以對應3個StoreFileScanner,現在接到訓示需要seek 到(rowk, cf:c1)位置,剛好這三個檔案中都存在這樣的KV,差别在于時間戳不同

(2)于是這3個Scanner很順從地在檔案中找到指定位置,然後等待最小KV出堆接受檢查

(3)最小KV出堆接受檢查之後滿足使用者條件,而且使用者隻需要檢索最新版本。是以檢查之後告訴所有scanner直接seek到下一行。

有沒有發現一些小小的問題?沒錯,3個scanner隻有1個scanner做了’有用功’,其他兩個scanner都做了’無用seek’。這很顯然一定程度上會影響scan性能。

HBase提出了一個很巧妙的應對方案-延遲seek,就是3個scanner接到seek訓示的時候,實際上并沒有真正去做,而隻是将scanner的指針指向指定位置。那童鞋就會問了,那什麼時候真正去seek呢?隻需要在堆頂元素彈出來的時候真正去執行就可以。這樣,就可以有效避免大量’無用seek’。

好了,本文核心内容就基本介紹完了,接下來扯點閑篇。任何存儲系統的核心子產品無非讀寫子產品,但不同類型的資料庫側重不同。MySQL類系統(Oracle、SQLServer等)側重于寫,寫就是它的靈魂!為了實作事務原子性,資料更新之前要先寫undo log,為了實作資料持久性,又引入redo log,為了實作事務隔離性,還需要實作各種鎖,還有類似double write等一系列機制…… ,個人認為,搞懂了資料更新寫入流程基本就搞懂了MySQL存儲引擎。與MySQL相對,HBase類系統(RocksDB、Kudu )更側重讀,讀是它的靈魂!HBase将所有更新操作、删除操作都簡單的當作寫入操作來處理,對于更新删除來說确實簡單了,但卻給資料讀取帶來了極大的負擔,資料讀取的時候需要額外過濾删除資料、處理多版本資料,除此之外,LSM所特有的多檔案存儲、BloomFilter過濾特性支援等等無不增加了資料讀取的難度。個人認為,隻有搞懂了資料讀取才可能真正了解HBase核心。

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

<a href="http://hbasefly.com/2017/06/11/hbase-scan-2/" target="_blank">原文連結</a>