標籤:style blog http io ar os 使用 sp for
二級索引與索引Join是多數業務系統要求儲存引擎提供的基本特性,RDBMS早已支援,NOSQL陣營也在摸索著符合自身特點的最佳解決方案。
這篇文章會以HBase做為對象來討論如何基於Hbase構建二級索引與實現索引join。文末同時會列出目前已知的包括0.19.3版secondary index, ITHbase, Facebook方案和官方Coprocessor的介紹。
理論目標
在HBase中實現二級索引與索引Join需要考慮三個目標:
1,高效能的範圍檢索。
2,資料的低冗餘(儲存所佔的資料量)。
3,資料的一致性。
效能與資料冗餘,一致性是相互制約的關係。
如果你實現了高效能地範圍檢索,必然需要靠冗餘索引資料來提升效能,而資料冗餘會導致更新資料時難以實現一致性,特別是分布式情境下。
如果你不要求高效地範圍檢索,那麼可以不考慮產生冗餘資料,一致性問題也可以間接避免,畢竟share nothing是公認的最簡單有效解決方案。
理論結合實際,下文會以執行個體的方式來闡述各個方案是如何選擇偏重點。
這些方案是經過筆者資料查閱和同事的不斷交流後得出的結論,如有錯誤,歡迎指正:
1,按索引建表
每一個索引建立一個表,然後依靠表的row key來實現範圍檢索。row key在HBase中是以B+ tree結構化有序儲存的,所以scan起來會比較效率。
單表以row key儲存索引,column value儲存id值或其他資料 ,這就是Hbase索引表的結構。
如何Join?
多索引(多表)的join情境中,主要有兩種參考方案:
1,按索引的種類掃描各自獨立的單索引表,最後將掃描結果merge。
這個方案的特點是簡單,但是如果多個索引掃描結果資料量比較大的話,merge就會遇到瓶頸。
比如,現在有一張1億的使用者資訊表,建有出生地和年齡兩個索引,我想得到一個條件是在杭州出生,年齡為20歲的按使用者id正序排列前10個的使用者列表。
有一種方案是,系統先掃描出生地為杭州的索引,得到一個使用者id結果集,這個集合的規模假設是10萬。
然後掃描年齡,規模是5萬,最後merge這些使用者id,去重,排序得到結果。
這明顯有問題,如何改良?
保證出生地和年齡的結果是排過序的,可以減少merge的資料量?但Hbase是按row key排序,value是不能排序的。
變通一下 – 將使用者id冗餘到row key裡?OK,這是一種解決方案了,這個方案的圖示如下:
merge時提取交集就是所需要的列表,順序是靠索引增加了_id,以字典序保證的。
2, 按索引查詢種類建立複合式索引。
在方案1的情境中,想象一下,如果單索引數量多達10個會怎麼樣?10個索引,就要merge 10次,效能可想而知。
解決這個問題需要參考RDBMS的複合式索引實現。
比如出生地和年齡需要同時查詢,此時如果建立一個出生地和年齡的複合式索引,查詢時效率會高出merge很多。
當然,這個索引也需要冗餘使用者id,目的是讓結果自然有序。結構圖示如下:
這個方案的優點是查詢速度非常快,根據查詢條件,只需要到一張表中檢索即可得到結果list。缺點是如果有多個索引,就要建立多個與查詢條件一一對應的複合式索引,儲存壓力會增大。
在制定Schema設計方案時,設計人員需要充分考慮情境的特點,結合方案一和二來使用。下面是一個簡單的對比:
| |
單索引 |
複合式索引 |
| 檢索效能 |
優異 |
優異 |
| 儲存 |
資料不冗餘,節省儲存。 |
資料冗餘,儲存比較浪費。 |
| 事務性 |
多個索引保證事務性比較困難。 |
多個索引保證事務性比較困難。 |
| join |
效能較差 |
效能優異 |
| count,sum,avg,etc |
合格結果集全表掃描 |
合格結果集全表掃描 |
從上表中可以得知,方案1,2都存在更新時事務性保證比較困難的問題。如果業務系統可以接受最終一致性的話,事務性會稍微好做一些。否則只能藉助於複雜的分散式交易,比如JTA,Chubby等技術。
count, sum, avg, max, min等彙總功能,Hbase只能通過硬掃的方式,並且很悲劇,你可能需要做一些hack操作(比如加一個CF,value為null),否則你在掃描時可能需要往用戶端傳回所有資料。
當然你可以在這個情境上做一些最佳化,比如增加狀態表等,但複雜性帶來的風險會更高。
還有一種終極解決方案就是在業務上只提供上一頁和下一頁,這或許是最簡單有效方案了。
2,單張表多個列族,索引基於列
Hbase提供了列族Column Family特性。
列索引是將Column Family做為index,多個index值散落到Qualifier,多個column值依據version排列(CF, Qualifer, Version Hbase會保證有序,其中CF和Qualifier正序,Version倒序)。
舉個典型的例子,就是使用者賣了很多商品,這些商品的標題title需要支援like %title%查詢。傳統基於RDMBS就是模糊查詢,基於search engine就是分詞+倒排表。
在HBase中,模糊查詢顯然不滿足我們的要求,接下來只能通過分詞+倒排的方式來儲存。基於CF的倒排表索引結構見:
取資料的時候,只需要根據使用者id(row key)定位到一個row,然後根據分詞定位到qualifier,再通過version的有序list,取top n條記錄即可。不過大家可能會發現個問題,version list的總數量是需要scan全version list才能知道的,這裡需要業務系統本身做一些改進。
如何join?
實現方式同方案1裡的join,多個CF列索引掃描結果後,需要走merge,將多個索引的查詢結果conjunction。
兩個方案的對比似乎變化就是一個表,一個列,但其實這個方案有個最大的好處,就是解決了事務性的問題,因為所有的索引都是跟單個row key綁定的,我們知道單個row的更新,在hbase中是保證原子更新的,這就是這個方案的天然優勢。當你在考慮單索引時,使用基於列的索引會比單表索 引有更好的適用性。
而複合式索引在以列為儲存粒度的方案裡,也同樣可以折中實現。理解這種儲存模式的同學可能已經猜到了,就是基於qualifier。
下表對比了表索引和列索引的優缺點:
| |
列索引 |
表索引 |
| 檢索效能 |
檢索資料需要走多次scan,第一次scan row key,第二次scan qualifier,第三次scan version。 |
只需要走一次row key的scan即可。 |
| 儲存 |
在沒有複合式索引時,儲存較節省 |
在沒有複合式索引時,儲存較節省 |
| 事務性 |
容易保證 |
保證事務性比較困難 |
| join |
效能較差,只有在建立組合條件Qualifier的時候效能會有所改善 |
效能較差,只有在建立組合表索引的時候效能會有所改善 |
| 額外的問題 |
1,同一個row裡每個qualifier的version是有大小限制的,不能超過Int的最大值。(別以為這個值很大,對于海量資料存放區,上億很平常)
2,version的count總數需要額外做處理擷取。
3,單個row資料超過split大小時,會導致不能compaction或compaction記憶體吃緊,增加風險。 |
|
| count,sum,avg,etc |
合格結果集全表掃描 |
合格結果集全表掃描 |
雖然列索引缺點這麼多,但是儲存節省帶來的成本優勢有時還是值得我們去這麼做的,何況它還解決了事務性問題,需要使用者自己去權衡。
值得一提的是,Facebook的訊息應用伺服器就是基於類似的方案來實現的。
3,ITHBase
方案一中的多表,解決了效能問題,同時帶來了儲存冗餘和資料一致性問題。這兩個問題中,只要解決其中一項,其實也就滿足了大多數業務情境。
本方案中,著重關注的是資料一致性。ITHbase的全稱是 Indexed Transactional HBase,從名字中就能看出,事務性是它的重要特性。
ITHBase的事務原理簡介
建一張事務表__GLOBAL_TRX_LOG__,每次開啟事務時,在表中選項組。因為是基於Hbase的HTable,事務表同樣會寫WAL用於恢複,不過這個日誌格式被ITHbase改造過,它稱之為THLog。
用戶端對多張表更新時,先啟動事務,然後每次PUT,將事務id傳遞給HRegionServer。
ITHbase通過繼承HRegionServer和HReogin類,重寫了大多數操作介面方法,比如put, update, delete, 用於擷取transactionalId和狀態。
當server收到操作和事務id後,先確認服務端收到,標記當前事務為待寫入狀態(需要再發起一次PUT)。當所有表的操作完成後,由用戶端統一做commit寫入,做二階段提交。
4,Map-reduce
這個方案沒什麼好說的,儲存節省,也不需要建索引表,只需要靠強大的叢集計算能力即可匯出結果。但一般不適合online業務。
5,Coprocessor副處理器
官方0.92.0新版正在開發中的新功能-Coprocessor,支援region層級索引。詳見:
https://issues.apache.org/jira/browse/HBASE-2038
副處理器的機制可以理解為,server端添加了一些回呼函數。這些回呼函數如下:
The Coprocessor interface defines these hooks:
- preOpen, postOpen: Called before and after the region is reported as online to the master.
- preFlush, postFlush: Called before and after the memstore is flushed into a new store file.
- preCompact, postCompact: Called before and after compaction.
- preSplit, postSplit: Called after the region is split.
- preClose and postClose: Called before and after the region is reported as closed to the master.
The RegionObserver interface is defines these hooks:
- preGet, postGet: Called before and after a client makes a Get request.
- preExists, postExists: Called before and after the client tests for existence using a Get.
- prePut and postPut: Called before and after the client stores a value.
- preDelete and postDelete: Called before and after the client deletes a value.
- preScannerOpen postScannerOpen: Called before and after the client opens a new scanner.
- preScannerNext, postScannerNext: Called before and after the client asks for the next row on a scanner.
- preScannerClose, postScannerClose: Called before and after the client closes a scanner.
- preCheckAndPut, postCheckAndPut: Called before and after the client calls checkAndPut().
- preCheckAndDelete, postCheckAndDelete: Called before and after the client calls checkAndDelete().
利用這些hooks可以實現region級二級索引,實現count, sum, avg, max, min等彙總操作而不需要返回所有的資料,詳見https://issues.apache.org/jira/browse/HBASE-1512。
二級索引的原理猜測
因為coprocessor的最終方案還未公布,就提供的這些hooks來說,二級索引的實現應該是攔截同一個region的put, get, scan, delete等操作。與此同時在同一個reigon裡維護一個索引CF,建立對應的索引表。
基於region的索引表其實有很多局限性,比如全域排序就很難做。
不過我覺得Coprocessor最大的好處在於其提供了server端的完全擴充能力,這對於Hbase來說是一個大的躍進。
如何join?
目前還未發布,不過就瞭解很難從本質上有所突破。解決方案無非就是merge和composite index,同樣事務性是需要解決的難題之一。
業界已經公開的二級索引方案羅列:
0.19.3版Secondary Index
一直關注HBase的同學,或許知道,早在HBase 0.19.3版發布時,曾經加入過secondary index的功能,Issue詳見這裡。
它的使用例子也很簡單:http://blog.rajeevsharma.in/2009/06/secondary-indexes-in-hbase.html
0.19.3版Secondary Index通過將列值以row key方法儲存,提供索引scan。
但HBase早期的需求主要來自Hadoop。事務的複雜性以及當時發現hadoop-core裡有個很難解決的與ITHBase相容的問題,致使官方在0.20.0版將其核心代碼移出了hbase-core,改為contrib第三方擴充,Issue詳見這裡。
Transactional tableindexed-ITHBase
這個方案就是在0.19.3版被官方剝離出核心的第三方擴充,它的方案上面已經介紹過了。目前支援最新的Hbase 0.90。
是否具備工業強度的穩定性是使用者選擇它的主要障礙。
https://github.com/hbase-trx/hbase-transactional-tableindexed
Facebook方案
facebook採用的是單表多列索引的解決方案,上面已經提到過了。很完美地解決了資料一致性問題,這主要跟他們的使用情境有關。
感興趣的同學可以看下這篇blog,本文不作詳述:
blog.huihoo.com/?p=688
HBase官方方案 0.92.0 版開發中 – Coprocessor副處理器
還未發布,不過hbase官方blog有篇介紹:http://hbaseblog.com/2010/11/30/hbase-coprocessors
Lily Hbase indexing Library
這是一個索引構建,查詢,管理的架構。結構上,就是通過一張indexmeta表管理多張indexdata索引表。
特點是,有一套非常完善的針對int, string, utf-8, decimal等類型的row key排序機制。這個機制在這篇博文中有詳細介紹:
http://brunodumon.wordpress.com/2010/02/17/building-indexes-using-hbase-mapping-strings-numbers-and-dates-onto-bytes/
此外,架構針對join情境(原理=merge),提供了封裝好的conjunction和disjunction工具類。
針對索引構建情境,Hbase indexing library也提供了很方便的介面。
IHbase
IHBase非常類似ITHBase。IHBase同樣從HBase源碼層級進行了擴充了,重新定義和實現了一些Server,Client端處理邏輯,所以,它是具備強侵入性的。不幸的是,這個工具在fix完Hbase 0.20.5版相容bug以後再也沒更新。是否支援0.90以上版本,筆者還未嘗試。IHBase與ITHBase的一個對比(仁者見仁)
| Feature |
ITHBase |
IHBase |
Comment |
| global ordering |
yes |
no |
IHBase has an index for each region. The flip side of not having global ordering is compatibility with the good old HRegion: results are coming back in row order (and not value order as in ITHBase) |
| Full table scan? |
no |
no |
THbase does a partial scan on the index table. ITHBase supports specifying start/end rows to limit the number of scanned regions |
| Multiple Index Usage |
no |
yes |
IHBase can take advantage of multiple indexes in the same scan. IHBase IdxScan object accepts an Expression which allows intersection/unison of several indexed column criteria |
| Extra disk storage |
yes |
no |
IHBase indexes are created when the region starts/flushes and do not require any extra storage |
| Extra RAM |
yes |
yes |
IHBase indexes are in memory and hence increase the memory overhead. THBbase indexes increase the number of regions each region server has to support thus costing memory too |
| Parallel scanning support |
no |
yes |
In ITHBase the index table needs to be consulted and then GETs are issued for each matching row. The behavior of IHBase (as perceived by the client) is no different than a regular scan and hence supports parallel scanning seamlessly. parallel GET can be implemented to speedup THbase scans |
原理簡介在Memstore滿了以後刷磁碟時,IHBase會進行攔截請求並為這個memstore的資料構建索引。索引另一個CF的方式儲存在表內。不過只支援region層級(類似coprocessor)
scan的時候,IHBase會結合索引列中的標記,來加速scan。http://github.com/ykulbak/ihbase
Hbase二級索引與join