寫在前面
本文2014年7月份發表於InfoQ,HBase的PMC成員Ted Yu先生參與了審稿並於給予了肯定。該方案設計之初僅寄希望於通過二級索引提升查詢性能,由於在前期架構時充分考慮了通用性以及對復雜條件的支持,在后來的演變中逐漸被剝離出來形成了一個通用的查詢引擎。Ted Yu對“查詢決策器”表示了關心,他指出類似的組件同時也是Phoenix, Impala用於支持SQL查詢的核心組件,但是這類組件很難引入到HBase中,因為HBase專注於byte[]的操作。對此,方案在設計時避開了“SQL解析”和“在各種數據類型與byte[]之間進行轉化”的棘手問題,而是使用了一組可以描述查詢的Query API,這與Hibernate中提供Criteria接口的做法非常相似,在Hibernate中既支持HQL語句的查詢又支持使用Criteria接口以編程方式描述的查詢,對於我們來說選擇類似后者的做法實現起來要快速和容易的多,而查詢條件中的值在構造之初就以byte[]的形式傳遞,避免了決策器解析時的類型判定和轉化問題。本文原文出處:http://blog.csdn.net/bluishglc/article/details/31799255 嚴禁任何形式的轉載,否則將委托CSDN官方維護權益!
題記
——索引的實質是另一種編排形式的數據冗余,高效的檢索源自於面向查詢特別設計的編排形式,如果再輔以分布式的計算框架,就可以支撐起高性能的大數據查詢。
正文
Apache HBase™是一個分布式、可伸縮的NoSQL數據庫,它構建在Hadoop基礎設施之上,依托於Hadoop的迅猛發展,HBase在大數據領域的應用越來越廣泛,成為目前NoSQL數據庫中表現最耀眼,呼聲最高的產品之一。像其他NoSQL數據庫一樣,HBase也有其適用范圍,就應對復雜條件的查詢來說,一般認為它並不是非常適合[i],熟悉HBase的開發人員對此應該有一定的體會,但是基於普遍的需求,開發者們希望HBase在保持高性能優勢的同時能對復雜條件的查詢給予一定的支持,而本文將要介紹的正是一種在HBase現行機制下以非侵入式實現的基於二級多列索引的高性能復雜條件查詢引擎。
問題
目前HBase主要應用在結構化和半結構化的大數據存儲上,其在插入和讀取上都具有極高的性能表現,這與它的數據組織方式有着密切的關系,在邏輯上,HBase的表數據按RowKey進行字典排序, RowKey實際上是數據表的一級索引(Primary Index),由於HBase本身沒有二級索引(Secondary Index)機制,基於索引檢索數據只能單純地依靠RowKey,為了能支持多條件查詢,開發者需要將所有可能作為查詢條件的字段一一拼接到RowKey中,這是HBase開發中極為常見的做法,但是無論怎樣設計,單一RowKey固有的局限性決定了它不可能有效地支持多條件查詢。通常來說,RowKey只能針對條件中含有其首字段的查詢給予令人滿意的性能支持,在查詢其他字段時,表現就差強人意了,在極端情況下某些字段的查詢性能可能會退化為全表掃描的水平,這是因為字段在RowKey中的地位是不等價的,它們在RowKey中的排位決定了它們被檢索時的性能表現,排序越靠前的字段在查詢中越具有優勢,特別是首位字段具有特別的先發優勢,如果查詢中包含首位字段,檢索時就可以通過首位字段的值確定RowKey的前綴部分,從而大幅度地收窄檢索區間,如果不包含則只能在全體數據的RowKey上逐一查找,由此可以想見兩者在性能上的差距。
受限於單一RowKey在復雜查詢上的局限性,基於二級索引(Secondary Index)的解決方案成為最受關注的研究方向,並且開源社區已經在這方面已經取得了一定的成果,像ITHBase、IHBase以及華為的hindex項目,這些產品和框架都按照自己的方式實現了二級索引,各自具有不同的優勢,同時也都有一定局限性,本文闡述的方案借鑒了它們的一些優點,在確保非侵入的前提下,以高性能為首要目標,通過建立二級多列索引實現了對復雜條件查詢的支持,同時通過提供通用的查詢API,以及完全基於配置的索引結構,完全封裝了索引的創建和使用細節,使之成為一種通用的查詢引擎。
原理
“二級多列索引”是針對目標記錄的某個或某些列建立的“鍵-值”數據,以列的值為鍵,以記錄的RowKey為值,當以這些列為條件進行查詢時,引擎可以通過檢索相應的“鍵-值”數據快速找到目標記錄。由於HBase本身並沒有索引機制,為了確保非侵入性,引擎將索引視為普通數據存放在數據表中,所以,如何解決索引與主數據的划分存儲是引擎第一個需要處理的問題,為了能獲得最佳的性能表現,我們並沒有將主數據和索引分表儲存,而是將它們存放在了同一張表里,通過給索引和主數據的RowKey添加特別設計的Hash前綴,實現了在Region切分時,索引能夠跟隨其主數據划歸到同一Region上,即任意Region上的主數據其索引也必定駐留在同一Region上,這樣我們就能把從索引抓取目標主數據的性能損失降低到最小。與此同時,特別設計的Hash前綴還在邏輯上把索引與主數據進行了自動的分離,當全體數據按RowKey排序時,排在前面的都是索引,我們稱之為索引區,排在后面的均為主數據,我們稱之為主數據區。最后,通過給索引和主數據分配不同的Column Family,又在物理存儲上把它們隔離了起來。邏輯和物理上的雙重隔離避免了將兩類數據存放在同一張表里帶來的副作用,防止了它們之間的相互干擾,降低了數據維護的復雜性,可以說這是在性能和可維護性上達到的最佳平衡。

圖1:Sample表Region 1的數據邏輯視圖
讓我們通過一個示例來詳細了解一下二級多列索引表的結構,假定有一張Sample表,使用四位數字構成Hash前綴[ii],范圍從0000到9999,規划切分100個Region,則100個Region的RowKey區間分別為[0000,0099],[0100,0199],……,[9900,9999],以第一個Region為例,請看圖1,所有數據按RowKey進行字典排序,自動分成了索引區和主數據區兩段,主數據區的Column Family是d,下轄q1,q2,q3等Qualifier,為了簡單起見,我們假定q1,q2,q3的值都是由兩位數字組成的字符串,索引區的Column Family是i,它不含任何Qualifier,這是一個典型的“Dummy Column Family“,作為區別於d的另一個Column Family,它的作用就是讓索引獨立於主數據單獨存儲。接下來是最重要的部分,即索引和主數據的RowKey,我們先看主數據的RowKey,它由四位Hash前綴和原始ID兩部分組成,其中Hash前綴是由引擎分配的一個范圍在0000到9999之間的隨機值,通過這個隨機的Hash前綴可以讓主數據均勻地散列到所有的Region上,我們看圖1,因為Region 1的RowKey區間是[0000,0099],所以沒有任何例外,凡是且必須是前綴從0000到0099的主數據都被分配到了Region 1上。接下來看索引的RowKey,它的結構要相對復雜一些,格式為:RegionStartKey-索引名-索引鍵-索引值,與主數據不同,索引RowKey的前綴部分雖然也是由四位數字組成,但卻不是隨機分配的,而是固定為當前Region的StartKey,這是非常重要而巧妙的設計,一方面,這個值處在Region的RowKey區間之內,它確保了索引必定跟隨其主數據被划分到同一個Region里;另一方面,這個值是RowKey區間內的最小值,這保證了在同一Region里所有索引會集中排在主數據之前。接下來的部分是“索引名”,這是引擎給每類索引添加的一個標識,用於區分不同類型的索引,圖1中展示了兩種索引:a和b,索引a是為字段q1和q2設計的兩列聯合索引,索引b是為字段q2和q3設計的兩列聯合索引,依次類推,我們可以根據需要設計任意多列的聯合索引。再接下來就是索引的鍵和值了,索引鍵是由目標記錄各對應字段的值組成,而索引值就是這條記錄的RowKey。
現在,假定需要查詢滿足條件q1=01 and q2=02的Sample記錄,分析查詢字段和索引匹配情況可知應使用索引a,也就是說我們首先確定了索引名,於是在Region 1上進行scan的區間將從主數據全集收窄至[0000-a, 0000-b),接着拼接查詢字段的值,我們得到了索引鍵:0102,scan區間又進一步收窄為[0000-a-0102, 0000-a-0103),於是我們可以很快地找到0000-a-0102-0000|63af51b2這條索引,進而得到了索引值,也就是目標數據的RowKey:0000|63af51b2,通過在Region內執行Get操作,最終得到了目標數據。需要特別說明的是這個Get操作是在本Region上執行的,這和通過HTable發出的Get有很大的不同,它專門用於獲取Region的本地數據,其執行效率是非常高的,這也是為什么我們一定要將索引和它的主數據放在同一張表的同一個Region上的原因。
架構
在了解了引擎的工作原理之后來我們來看一下它的整體架構:

圖2:引擎的整體架構
引擎構建在HBase的Coprocessor機制之上,由Client端和Server端兩部分構成,對於查詢而言,查詢請求從Client端經由HTable的coprocessorExec方法推送到所有的RegionServer上,RegionServer接收到查詢請求后使用“查詢決策器”分析查詢條件,比對索引元數據,在找到適合該查詢的最優索引后,解析索引區間,然后委托“索引查詢器”基於給定的最優索引和解析區間進行數據檢索,如果沒有找到合適的索引則委托“全表查詢器”進行全表掃描。當各RegionServer的局部查詢結果返回之后,引擎的Client端還負責對它們並進行合並匯總和排序,從而得到最終的結果集。對於插入而言,當主數據試圖寫入時會被Coprocessor攔截,委托“索引構造器”根據“索引配置文件”創建指向當前主數據的所有索引,然后一同插入到數據表中。
讓我們來深入了解一下引擎的幾個核心組件。對於引擎的客戶端來講,最重要的組件是一套用於表達復雜查詢請求的Query API,在這套API的設計上我們借鑒了IHBase的一些做法,通過對查詢條件(Condition)進行抽象和建模,得到一套典型的基於“復合模式”(Composite Pattern)的Class Hierarchy,使之能夠優雅地表達基於AND和OR的多重復合條件。以圖1所示的Sample表為例,使用Query API構造一個查詢條件為“(q1=01 and q2<02) or (q1=03 and q2>04)”的Java代碼如下:

圖3:引擎客戶端的Query API示意代碼
查詢請求到達Server端以后,由Coprocessor委派查詢決策器進行分析以確定使用何種查詢策略應對,這是查詢處理流程上的一個關鍵結點。查詢決策器需要分析查詢請求的各項細節,包括條件字段、排序字段和排序,然后和索引的元數據進行比對找出性能最優的索引,有時候對於一個查詢請求可能會有多個適用索引,但是查詢性能卻有高下之分,因此需要對每一個候選索引進行性能評估,找出最優者,性能評估的方法是看哪個索引能最大限度地收窄檢索區間。索引的元數據來自於索引配置文件,圖4展示了一份簡單的索引配置,配置中描述的正是圖1中使用的索引a和b的元數據,索引元數據主要是由索引名和一組field組成,filed描述的是索引針對的目標列(ColumnFamily:Qualifier)。實際的索引配置通常比我們看到的這份要復雜,因為在生成索引時有很多細節需要通過索引配置給出指引,比如如何處理不定長字段,目標列使用正序還是倒序(例如時間數據在HBase中經常需要按補值進行倒序處理),是否需要使用自定義格式化器對目標列的值進行格式化等等,完全配置化的索引元數據使創建和維護索引的成本大大降低,為上層應用根據實際需求靈活設計索引提供了保障。

圖4:一份簡單的索引配置文件
在確定最優索引之后,查詢決策器開始基於最優索引對查詢條件進行解析,解析的結果是一組索引區間,區間內的數據未必都滿足查詢條件,但卻是通過計算所能得到的最小區間,索引查詢器就在這些區間上進行檢索,通過配備的專用Filter對區間內的每一條數據進行最后的匹配判斷。圖5展示了一個條件為q1=01 and 01<=q2<=03的查詢請求在Sample表Region 1上的解析和執行過程。

圖5 :查詢請求q1=01 and 01<=q2<=03在Sample表Region 1上的解析和執行過程示意
對於那些找不到索引的查詢請求來說,查詢決策器將委派全表查詢器處理,全表查詢器將跳過索引區,從主數據區開始通過配備的專用Filter進行全表掃描。顯然,相對於索引查詢,全表掃描的執行效率是很低的,它的存在是為了在所有索引都不適用的情況下起“托底”作用,以此保證任意復雜條件的查詢都能得到處理,所以這里引出一個非常重要的問題,就是在索引查詢和全表掃描之間的選擇與權衡問題。通常人們總是希望所有的查詢都越快越好,雖然從理論上講建立覆蓋任意條件查詢的索引是可能的,但這是不現實的,因為創建索引是有代價的,除了占用大量的存儲空間之外還會影響到數據插入的性能,所以不能無節制地創建索引,理性的做法是分析並篩選出最為常用的查詢,針對這些查詢建立相應的索引,優化查詢性能,而對於那些較為“生僻”的查詢則使用全表掃描的方式進行處理,以此在存儲成本、插入性能和查詢性能之間找到一種理想的平衡。最后要補充說明的是,不管是使用索引查詢還是進行全表掃描,這些動作都是通過Coprocessor機制分發到所有Region上去並發執行的,即使是全表掃描其性能也將遠超過HBase原生的Scan操作!
應用
由於引擎設計之初就以非侵入性為前提,所以引擎的部署與集成就與引入第三方類庫無異,唯一需要上層應用提供的是面向數據表的索引配置文件。設計索引主要以業務需求為導向,先分析並梳理出常用的查詢用例,然后針對查詢用例所涉及的字段和排序要求按相似性進行分組,盡可能讓單個索引同時支持多種相近的查詢,減少索引的種類和數量,提升索引復用率。在這方面如下設計原則可供參考(注:以下原則均以“不考慮排序”為前提):
- N個字段組合的查詢只需要建立一個包含該N個字段的索引,建立按這個N字段其他順序排列的索引是沒有意義的。因此,以N個字段組合為條件的查詢只需要C(n, n)=1個索引。
- 一個包含N個字段的索引同時是以從第1到第N-1個字段為條件的查詢索引,以及從第1到第N-2個字段為條件的查詢索引,依此類推,也是僅以第1個字段為條件的查詢索引。因此,包含N個字段的索引總計可以支持C(n,1)=n種查詢組合。
- 基於上述兩點,任意一個索引的字段組合不應該是另一個索引字段組合的前綴部分,這樣設計的索引才會有較高的復用率。
假如某表有A、B、C、D四個字段,在不考慮排序的前提下,如果要用索引支持以任意字段或字段組合為條件的查詢,則索引的設計方法如下:四字段索引只需要一個,假定取ABCD(它將同時支持ABCD、ABC、AB和A四種查詢)。三字段索引分別以A、B、C、D開頭向后循環取足三個字段,得到:ABC、BCD(它將同時支持BCD、BC和B三種查詢)、CDA(它將同時支持CDA、CD和C三種查詢)和DAB(它將同時支持DAB、DA和D三種查詢),其中ABC是ABCD的前綴,故舍棄。按照同樣的方法,兩字段索引要分別從保留下來的三個三字段索引中依次以每一個字段開頭取足兩個字段,然后去除重復和前綴重疊的索引,最終得到DB(它將同時支持DB和D兩種查詢)和AC(它將同時支持AC和A兩種查詢),總計是6個索引,最后可以再根據實際需求剪裁掉不需要的索引。
在上述原則的表述中特別注明了“不考慮排序“這個前提,對於索引來說,”排序“是一個很“敏感”的要求,索引本身只有一種排序(即按索引首字段進行的字典排序),如果查詢請求的排序與索引排序不同,則索引直接出局,即使它們的字段完全匹配,也就是說排序會極大地消弱索引的復用度,對於我們的引擎來說,排序字段應該受到嚴格的控制。實際上,很多大數據系統都需要對排序進行限制,比如淘寶上的商品檢索,可供排序的字段只有人氣,銷量,信用和價格,因為排序需要針對數據全集進行計算,如果不是針對有限的排序字段建立索引或是離線計算並緩存結果,按任意字段排序的查詢是很難在線返回的。
小結
綜合前文所述,方案主要有如下幾個顯著的優勢:
- 高性能:引擎的高性能源自兩方面,一是二級多列索引,二是基於Coprocessor的並行計算
- 非侵入性:引擎構建在HBase之上,既沒有對HBase進行任何改動,也不需要上層應用做任何妥協
- 高度可配置:索引元數據是完全基於配置的,可以輕便靈活地創建和維護索引
- 通用性:引擎的前端查詢接口和后端索引處理都是基於通用目標設計的,不依賴於任何具體表
限於HBase自身的特點,方案本身也有一定的局限性,一是它不能隨意地支持任意的條件查詢,這一點前文已經給出了分析和建議,二是在插入主數據時需要伴隨插入多份索引從而對寫入性能產生了一定的影響,如何控制寫入和查詢的競爭關系需要根據系統的讀寫比進行權衡,對於數據寫入實時性要求不高或者數據是離線導入的系統來說,可以考慮使用批量導入工具,特別是以直接生成HFile的方式導入的話可以在很大程度上消除引入索引后的寫入壓力。
[i]理論上基於HBase的 Filter機制可以實現任意復雜條件的查詢,但是那樣做就徹底放棄了RowKey作為索引的利用價值,大多數查詢的性能都將變得非常差。
[ii]Hash前綴的長度和Region數量有着密切的關系,由於索引和主數據的分配高度依賴RowKey前綴和Region的RowKey區間,引擎嚴禁Region進行自動切分,開發人員需要在前期對Region數量和前綴長度進行規划,本例中取四位前綴意味着最多可以支持10000個Region。
轉:http://blog.csdn.net/bluishglc/article/details/31799255
相關閱讀:
