漫談HBase Filter


                                                          初衷


對數據庫來講,知足業務多樣化的查詢方式很是重要。若是說有人設計了一個KV數據庫,只提供了Get/Put/Scan這三種接口,估計要被用戶吐槽到死,畢竟現實的業務場景並不簡單。就以訂單系統來講,查詢給定用戶最近三個月的歷史訂單,這裏面的過濾條件就至少有2個:1. 查指定用戶的訂單;2. 訂單必須是最近是三個月的。此外,這裏的過濾條件還必須是用AND來鏈接的。若是經過Scan先把整個訂單表信息加載到客戶端,再按照條件過濾,這會給數據庫系統形成極大壓力。所以,在服務端實現一個數據過濾器是必須的。數據庫

除了上例查詢需求,相似小明或小黃最近三個月的歷史訂單這樣的查詢需求,一樣很常見。這兩個查詢需求,本質上前者是一個AND鏈接的多條件查詢,後者是一個OR鏈接的多條件查詢,現實場景中AND和OR混合鏈接的多條件查詢需求也不少。所以,HBase設計了Filter以及用AND或OR來鏈接Filter的FilterList。bash

例以下面的過濾器,表示用戶將讀到rowkey以abc爲前綴且值爲testA的那些cell。框架

fl = new FilterList(MUST_PASS_ALL,     
     new PrefixFilter("abc"),     
     new ValueFilter(EQUAL, 
     new BinaryComparator(Bytes.toBytes("testA"))))複製代碼

實際上,FilterList內部的子Filter也能夠是一個FilterList。例以下面過濾器表示用戶將讀到那些rowkey以abc爲前綴且值爲testA或testB的f列cell列表。模塊化

fl = new FilterList(MUST_PASS_ALL,     
     new PrefixFilter("abc"),     
     new FamilyFilter(EQUAL, new BinaryComparator(Bytes.toBytes("f"))),     
     new FilterList(MUST_PASS_ONE,      
     new ValueFilter(EQUAL, new BinaryComparator(Bytes.toBytes("testA"))),     
     new ValueFilter(EQUAL, new BinaryComparator(Bytes.toBytes("testB")))));複製代碼

所以,FilterList的結構實際上是一顆多叉樹。每個葉子節點都是一個具體的Filter,例如PrefixFilter、ValueFilter等;全部的非葉子節點都是一個FilterList,各個子樹對應各自的子filter邏輯。對應的圖示以下:性能

固然,HBase還提供了NOT語義的SkipFilter,例如用戶想拿到那些rowkey以abc爲前綴但value既不等於testA又不等於testB的f列的cell列表,可用以下FilterList來表示:測試

fl = new FilterList(MUST_PASS_ALL,     
     new PrefixFilter("abc"),     
     new FamilyFilter(EQUAL, new BinaryComparator("f")),     
     new SkipFilter(     
     new FilterList(MUST_PASS_ONE,     
     new ValueFilter(EQUAL, new BinaryComparator(Bytes.toBytes("testA"))),     
     new ValueFilter(EQUAL, new BinaryComparator(Bytes.toBytes("testB"))))));複製代碼


                                                    實現


Filter和FilterList做爲一個通用的數據過濾框架,提供了一系列的接口,供用戶來實現自定義的Filter。固然,HBase自己也提供了一系列的內置Filter,例如:PrefixFilter、RowFilter、FamilyFilter、QualifierFilter、ValueFilter、ColumnPrefixFilter等。優化

事實上,不少Filter都沒有必要在服務端從Scan的startRow一直掃描到endRow,中間有不少數據是能夠根據Filter具體的語義直接跳過,經過減小磁盤IO和比較次數來實現更高的性能的。以PrefixFilter(「333」)爲例,須要返回的是rowkey以「333」爲前綴的數據。spa

實際的掃描流程如上圖所示:設計

(1)碰到rowkey=111的行時,發現111比前綴333小,所以直接跳過111這一行去掃下一行,返回狀態碼NEXT_ROW;
(2)下一個Cell的rowkey=222,仍然比前綴333小,所以繼續掃下一行,返回狀態NEXT_ROW;
(3)下一個Cell的rowkey=333,前綴和333匹配,返回column=f:ddd這個cell個用戶,返回狀態碼爲INCLUDE;
(4)下一個Cell的rowkey仍爲333,前綴和333匹配,返回column=f:eee這個cell給用戶,返回狀態碼爲INCLUDE;
(5)下一個Cell的rowkey爲444,前綴比333大,再也不繼續掃描數據。code

這個流程中,每碰到一個Cell,返回的狀態碼NEXT_ROW、INCLUDE等,就告訴了RegionServer掃描框架下一個Cell的位置。例如在第2步中,返回狀態碼NEXT_ROW,那麼下一個Cell的rowkey必須是比222大的,因而就跳過了column=f:ccc這個Cell,直接定位到了rowkey=333的行,繼續掃描數據。

在實際的Filter設計中,共引入了INCLUDE、INCLUDE_AND_NEXT_COL、SKIP、NEXT_COL、NEXT_ROW、SEEK_NEXT_USING_HINT共6種狀態碼。其中INCLUDE表示當前Cell應該返回給用戶,同時自動讀下一個Cell;INCLUDE_AND_NEXT_COL相似,表示當前Cell返回給用戶,同時須要切換到下一個Column的Cell,也就是跟當前Cell相同Column的Cell都被跳過。SEEK_NEXT_USING_HINT表示下一個待讀取的Cell是用戶根據Filter語義自定義的一個Cell,例如對PrefixFilter(333)來講,碰到rowkey=111的行時,實際上是能夠根據前綴爲333直接定位到下一個rowkey=333的Cell,只是當前的PrefixFilter沒有作這個優化。

FilterList在處理狀態碼時則要稍微複雜一點,由於對同一個Cell每一個子Filter的狀態碼均可能不同,所以須要對多個子Filter的狀態碼進行合併。例如:

fl = new FilterList(MUST_PASS_ALL,     
     new PrefixFilter("abc"),     
     new ValueFilter(EQUAL, new BinaryComparator(Bytes.toBytes("testA"))));複製代碼

在碰到rowkey=abb且value=testA的Cell(記爲Cell-A)時,PrefixFilter返回的狀態碼應該是NEXT_ROW,而ValueFilter返回的狀態碼應該是INCLUDE。對於用AND來鏈接的FilterList來講,應該取各狀態碼中跳躍步數最大的狀態碼,所以對Cell-A來講,fl這個FilterList獲得的狀態碼將會是:max(NEXT_ROW, INCLUDE) = NEXT_ROW。

簡單來講,對某個Cell,用AND鏈接的FilterList,必須選各子Filter狀態碼跳躍步數最大的那個狀態碼;而用OR鏈接的FilterList,必須選各子Filter狀態碼跳躍步數最小的那個狀態碼。這實際上是FilterList對比正常Filter來講,須要實現的一個最核心的工做,咱們很早以前就在HBASE-18410將這塊代碼進行模塊化重構,HBase1.4.x以後的版本都是使用重構以後的代碼,用戶在使用新版本FilterList時能夠得到更精準的語義保證了。


                                                      一些優化

1.經過設置StartRow和StopRow替換PrefixFilter

PrefixFilter是將rowkey前綴爲指定字節串的數據都過濾出來並返回給用戶。例如,以下scan會返回全部rowkey前綴爲’def’的數據。

Scan scan = new Scan();
scan.setFilter(new PrefixFilter(Bytes.toBytes("def")));複製代碼

注意,這個scan雖然能拿到預期的效果,但卻並不高效。由於對於rowkey在區間(-oo, def)的數據,scan會一條條依次掃描一次,發現前綴不爲def,就讀下一行,直到找到第一個rowkey前綴爲def的行爲止。

這主要是由於目前HBase的PrefixFilter設計的相對簡單粗暴,沒有根據具體的Filter作過多的查詢優化。這種問題其實很好解決,在scan中簡單加一個startRow便可,RegionServer在發現scan設了StartRow,首先尋址定位到這個StartRow,而後從這個位置開始掃描數據,這樣就跳過了大量的(-oo, def)的數據。代碼以下:

Scan scan = new Scan();
scan.setStartRow(Bytes.toBytes("def"));
scan.setFilter(new PrefixFilter(Bytes.toBytes("def")));複製代碼

固然,更簡單直接的方式,就是將PrefixFilter直接展開成掃描[def, deg)這個區間的數據,這樣效率是最高的,代碼以下:

Scan scan = new Scan();
scan.setStartRow(Bytes.toBytes("def"));
scan.setStopRow(Bytes.toBytes("deg"));複製代碼

2.用MultipleColumnPrefixFilter來替換掉FilterList(OR, ColumnPrefixFilter, ColumnPrefixFilter, …)

在HBASE-22448,有用戶提到,寫一個以下的FilterList會顯的特別慢:

fl = new FilterList(MUST_PASS_ONE,      
     new ColumnPrefixFilter(Bytes.toBytes("aaa")),     
     new ColumnPrefixFilter(Bytes.toBytes("bbb")),    
     ...     
     new ColumnPrefixFilter(Bytes.toBytes("zzz")))複製代碼

這是由於採用FilterList(OR, ColumnPrefixFilter,…)的比較次數以下圖所示,每一個橙色的實心圓圈表示一次Cell的Compare操做。

通過討論和測試後,發現實際上是能夠用MultipleColumnPrefixFilter來替換上述FilterList(OR,ColumnPrefixFilter,…)的。代碼以下:

fl = new MultipleColumnPrefixFilter(byte[][] {    
     Bytes.toBytes("aaa"),    
     Bytes.toBytes("bbb"), 
     ...,    
     Bytes.toBytes("zzz")});複製代碼

經過評估,咱們發現Cell的比較次數以下圖所示:

兩者對比發現,採用MultipleColumnPrefixFilter以後能夠減小大量的比較次數。事實上,用HBASE-22448上的測試數據對比,發現優化後的性能快20倍:

這個案例帶給咱們的啓發是,若是發現某些場景下采用通用的FilterList框架沒法知足業務的性能需求,那麼實際上能夠嘗試採用自定義Filter的方式來知足更高的性能需求。由於在自定義的Filter中,咱們能夠經過更少的比較次數來實現優化,而FilterList框架爲了保證通用邏輯的正確性則沒法實現。

3.關於SingleColumnValueFilter的語義

這個Filter的定義比較複雜,讓人有點難以理解。舉例來講:

Scan scan = new Scan();
SingleColumnValueFilter scvf = new SingleColumnValueFilter(
Bytes.toBytes("family"),
Bytes.toBytes("qualifier"), 
CompareOp.EQUAL, 
Bytes.toBytes("value"));
scan.setFilter(scvf);複製代碼

這個例子表面上是將列簇爲family、列爲qualifier且值爲value的cell返回給用戶。但事實上,對那些不包含family:qualifier這一列的行,也會被默認返回給用戶。若是用戶不但願讀取那些不包含family:qualifier的數據,須要設計以下scan:

Scan scan = new Scan();
SingleColumnValueFilter scvf = new SingleColumnValueFilter(
Bytes.toBytes("family"),
Bytes.toBytes("qualifier"), 
CompareOp.EQUAL, 
Bytes.toBytes("value"));
scvf.setFilterIfMissing(true); // 跳過不包含對應列的數據
scan.setFilter(scvf);複製代碼

另外,當SingleColumnValueFilter設置filterIfMissing爲true時,和其餘Filter組合成FilterList時,可能致使返回結果不正確(參見HBASE-20151)。由於filterIfMissing設爲true時,SingleColumnValueFilter必需要遍歷一行數據中的每個cell, 才能肯定是否過濾,但在filterList中,若是其餘的Filter返回NEXT_ROW會直接跳過某個列簇的數據,致使SingleColumnValueFilter沒法遍歷一行全部的cell,從而致使返回結果不符合預期。對於這個問題,我的建議是:不要使用SingleColumnValueFilter和其餘Filter組合成FilterList。儘可能經過ValueFilter來替換掉SingleColumnValueFilter。

4.關於PageFilter

在HBASE-21332中,有一位用戶說,有一個表,表裏面有5個Region,分別爲(-oo, 111), [111, 222), [222, 333), [333, 444), [444, +oo)。表中這5個Region,每一個Region都有超過10000行的數據。他發現經過以下scan掃描出來的數據竟然超過了3000行:


Scan scan = new Scan();
scan.withStartRow(Bytes.toBytes("111"));
scan.withStopRow(Bytes.toBytes("4444"));
scan.setFilter(new PageFilter(3000));複製代碼

乍一看確實很詭異,由於PageFilter就是用來作數據分頁功能的,應該要保證每一次掃描最多返回不超過3000行。可是須要注意的是,HBase裏面Filter狀態所有都是Region內有效的,也就是說,Scan一旦從一個Region切換到另外一個Region以後, 以前那個Filter的內部狀態就無效了,新Region內用的實際上是一個全新的Filter。具體這個問題來講,就是PageFilter內部計數器從一個Region切換到另外一個Region以後,計數器已經被清0。所以,這個Scan掃描出來的數據將會是:

在[111,222)區間內掃描3000行數據,切換到下一個region [222, 333)。

在[222,333)區間內掃描3000行數據,切換到下一個region [333, 444)。

在[333,444)區間內掃描3000行數據,發現已經到達stopRow,終止。

所以,最終將返回9000行數據。理論上說,這應該算是HBase的一個缺陷,PageFilter並無實現全局的分頁功能,由於Filter沒有全局的狀態。我我的認爲,HBase也是考慮到了全局Filter的複雜性,因此暫時沒有提供這樣的實現。固然若是想實現分頁功能,能夠不經過Filter,而直接經過limit來實現,代碼以下:

Scan scan = new Scan();
scan.withStartRow(Bytes.toBytes("111"));
scan.withStopRow(Bytes.toBytes("4444"));
scan.setLimit(1000);複製代碼

因此,正常狀況下對用戶來講,PageFilter並無太多存在的價值。


                                                         總結


這些年社區的Filter和FilterList模塊基本上是由小米HBase團隊來負責維護和改進的。總結起來,一方面,自己HBase讀路徑上的概念繁多,諸如版本號、DeleteMarker、TTL、行、Family、列等,爲實現這些功能讀路徑已經較爲複雜;另外一方面,Filter自己是一個高度抽象的框架,用戶能夠基於這個抽象的框架實現各類各樣自定義的過濾器,實現須要考慮各類現實場景的適用性。對普通用戶來講,正確和高效的使用,有必定的小門檻,所以寫了這篇文章,但願對用戶正確的使用和理解Filter以及FilterList有所幫助。

                                           Apache HBaseConAsia 2019峯會

                                                 將於【7月20日上午9:00】

                                                     北京金隅喜來登酒店

                                                             隆重召開!

                                     現場不只能和HBase行業內大牛面對面交流

                                               還能得到大會限量版記念T恤

                                                       最後50個開放名額

                                      

                         

相關文章
相關標籤/搜索