寫在前面
本文2014年7月份發表於InfoQ。HBase的PMC成員Ted Yu先生參與了審稿並於給予了肯定。
該方案設計之初僅寄希望於通過二級索引提升查詢性能,由於在前期架構時充分考慮了通用性以及對復雜條件的支持,在后來的演變中逐漸被剝離出來形成了一個通用的查詢引擎。
Ted Yu對“查詢決策器”表示了關心,他指出類似的組件同一時候也是Phoenix, Impala用於支持SQL查詢的核心組件。可是這類組件非常難引入到HBase中。由於HBase專注於byte[]的操作。對此,方案在設計時避開了“SQL解析”和“在各種數據類型與byte[]之間進行轉化”的棘手問題,而是使用了一組能夠描寫敘述查詢的Query API,這與Hibernate中提供Criteria接口的做法非常類似,在Hibernate中既支持HQL語句的查詢又支持使用Criteria接口以編程方式描寫敘述的查詢,對於我們來說選擇類似后者的做法實現起來要高速和easy的多,而查詢條件中的值在構造之初就以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攔截,托付“索引構造器”依據“索引配置文件”創建指向當前主數據的全部索引,然后一同插入到數據表中。
讓我們來深入了解一下引擎的幾個核心組件。對於引擎的client來講,最重要的組件是一套用於表達復雜查詢請求的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:引擎client的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。
相關閱讀:
HBase Block Cache的重要實現細節和In-Memory Cache的特點