HBase高性能複雜條件查詢引擎(轉)

寫在前面



本文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

 

相關閱讀:

OpenTSDB設計解讀

HBase Block Cache的重要實現細節和In-Memory Cache的特色

關於近期HBase系統設計開發和性能調優的一些小結

Hadoop源碼解析之: HBase Security

相關文章
相關標籤/搜索