《HBase权威指南》读书笔记(二)
第9章 高級用法
9.1 行鍵設計
9.1.1 概念
HBase的表中的數據分割主要使用列族而不是列,這與一般的列式存儲數據庫的概念有所不同。
右上角的圖片展示了邏輯布局如何轉換為實際的物理存儲布局。每一行的單元格被有序存儲,同時不同列族的數據存儲在不同文件中。換句話說,磁盤上一個列族下所有的單元格都存儲在一個存儲文件(store file)中,不同列族的單元格不會出現在同一個存儲文件中。
因為HBase不存儲任何在表中沒有值的單元格(在RDBMS中,NULL可作為空值存儲),磁盤文件中也只有這些已經有值的單元格。同時每個單元格在實際存儲時也保存了行鍵和列鍵,所以每個單元格都單獨存儲了它在表中所處位置的相關信息。
此外,同一個單元格的多個版本被單獨存儲為連續的單元格,當單元格被存儲時還需要添加必要的時間戳。單元格按照時間戳降序排列,所以在HFile的Reader讀取數據時,最新的值先被讀到,這也是HBase設計模式中典型的讀取數據的方式。
9.1.2 高表與寬表
到目前為止,用戶可能會問該如何在HBase中存儲自己的數據。通常情況下,HBase中的表可以設計為高表和寬表兩類。前者指表中列少而行多,后者則正好相反。根據之前我們介紹過的KeyValue信息的篩選粒度信息,用戶應當盡量將需要查詢的維度或信息存儲在行鍵中,因為用它篩選數據的效率最高。
此外,HBase只能按行分片,因此高表更有優勢。設想用戶將一個其所有電子郵件都存在一行中。這在大部分情況下都是合適的,但也有些人的收件箱中有大量的郵件。大到一行數據就超過了最大HFile的限制,此時這個HFile無法拆分,同時也導致region無法在合適的位置進行拆分。
解決此問題的更好的方法是把一個用戶的每個電子郵件都存在單獨的一行中,而行鍵可以是用戶ID和消息ID的組合。以下是寬表在磁盤上的數據分布,并包括一些示例:
以下是高表在磁盤上的數據布局:
9.1.3 部分鍵掃描
使用高表的例子中,消息ID在行鍵中作為用戶ID的后綴。如果用戶沒有這兩個ID確切的值,用戶就無法得到一個特定的電子郵件。為避免出現這個問題,用戶可以使用包含部分鍵的掃描:用戶可以將掃描操作中鍵的開始和結束鍵設置為一個用戶的ID,當然結束鍵要設置為userId+1。
由于表中沒有等于起始鍵的行鍵,它會定位掃描下行:
<userId>-<lowest-messageId>
也就是說,它是排序時最小的用戶ID和消息ID的組合。此后掃描會遍歷這個用戶的所有郵件,同時用戶可以從行鍵中抽取消息ID。
用戶可以使用包含部分鍵的掃描機制設計出非常有效的左對齊索引(字典序從左到右排序),當一個字段被加到鍵中時就多了一個可以檢索的維度
<userId>-<date>-<messageId>-<attachmentId>
用戶需要保證行鍵中每個字段的值都被補齊到這個字段所設的長度,這
樣字典序才會按預期排列(按二進制內容比較,并升序排列)。用戶需要
為每個字段設定一個固定的長度來保證每個字段比較時只會與同字段內
容從左向右比較,否則可能出現溢出的情況。
之前我們采用的組合鍵看上去是一種通用的優秀方法,但其原子性是
個很突出的問題。由于一個收件箱中的數據現在分布在多行中,所以不
可能在一個簡單操作中修改一個收件箱的全局屬性。如果用戶不需要
次修改整個收件箱中所有郵件的消息,之前我們提到的高表設計就非常
適合。但如果用戶有這種修改需求,寬表可能更為適合,因為 HBase能
保證數據操作的行級原子性。
9.1.4 分頁
具體步驟如下:
客戶端可以把起始行設為用戶ID,同時將結束行設為用戶ID+1。然后使用上面提到的方法,當offset為0時,用戶可以讀取50封郵件。當用戶單擊Next按鈕時,offset設為50并跳過前50行,并返回第51~100行。
當行數很少時,這種方法可行。但是,當需要對幾千行數據進行分頁時,就需要采用其他方法了。用戶可以添加一個序列ID到行鍵中來幫助起始鍵定位到對應偏移量的位置。
9.1.5 時間序列
當處理流式事件時,最常見的數據就是按時間序列組織的數據。這些數據可能來自電網的一個傳感器、一個股票交易軟件或一個信息化的監控系統。這些數據的突出特點是它們的行鍵都代表了事件發生的時間。由于HBase的數據組織方式,這樣的數據在存儲時會出現一個問題:這些數據會被有序存儲到一個特定的范圍內,也就是一個有特定起始鍵和停止鍵的region中。
由于一個 region只能由一個服務器管理,所以所有的更新都會集中在一臺服務器上。這會導致系統產生讀寫熱點,并由于寫入數據過分集中而導致整個系統性能下降。
要解決這個問題,用戶需要想辦法將數據分散到所有的region服務器上去。有很多方法可以達到這個目的,例如,在行鍵前添加一個不連續的前綴。通常情況下有如下選擇。
salting方式
用戶可以使用salting前綴來保證數據分散到所有region服務器。例如:
byte prefix =(byte)(Long.hashCode(timestamp)%<number of region servers>);
byte [] rowKey = Bytes.add (Bytes. oBytes(prefix), Bytes.toBytes(timestamp);
這個公式將產生足夠的前綴數以確保將數據分散到所有 region服務器中去。當然,這個公式假定服務器數目固定,如果用戶的集群規模可能擴大就應當將前綴數翻倍。生成的行鍵可能如下:
0myrowkey-1, 1myrowkey-2, 2myrowkey-3, 0myrowkey-4, 1myrowkey-5,\ 2myrowkey-6,...
當鍵被排序之后并發送到對應的region時,它們的順序如下:
0myrowkey-1
0myrowkey-4
1myrowkey-2
1myrowkey-5
這樣做的缺點是當用戶要掃描一個連續的范圍時,可能需要對每個region服務器都發起請求(因為之前連續的數據,現在已經分散到不同的服務器中)。這樣也會帶來好處,用戶可以多線程并行地讀取數據。這有些類似于一個小規模的Mapreduce作業,這樣查詢的吞吐量會有所提高。
字段交換/提升權重
使用9.1.3節介紹的方法,用戶可以將時間戳字段移開或添加其他字段作為前綴。這樣做其實是想利用組合行鍵的思想來讓連續遞增的時間戳在行鍵中的位置從第位變到第二位。
如果用戶設計的行鍵已經包含多個字段了,則可以調整它們的位置。如果行鍵只包含了時間戳,則用戶應當將其他字段從列鍵或值中提取出來,然后放到行鍵的前端。
將時間戳從組合鍵的左邊向右移動也有缺點:用戶用戶不能訪問指定時間范圍內的數據。
隨機化
另一種完全不同的方式是將行鍵隨機化,例如:
byte[] rowKey =MD5(timestamp);
采用MD5之類的散列函數能將行鍵分散到所有的region服務器上。對于時間連續的數據,這種方法明顯不是個好方法。因為隨機化之后,用戶將不能再按時間范圍掃描數據。
另一方面,由于用戶可以用散列的方式重新生成行鍵,隨機化的方式很適合每次只讀取一行數據的應用。如果用戶的數據不需要連續掃描而只需隨機讀取,用戶就可以使用這種策略。
簡單總結以上方法后,用戶會發現在優化讀寫性能的同時找到正確的平衡點并不是件簡單的事情。它關系到用戶的數據訪問模式,該模式最終決定了用戶如何設計行鍵的結構。圖9-3展示了不同解決方案對讀寫性能的影響。
使用salt前綴或將某些不是連續取值的主鍵字段提前,可以使分散寫壓力并提高寫入性能,同時掃描連續的鍵子集也可以提高讀性能。但是,如果用戶只需要隨機讀取數據,那么隨機行鍵就更有用,因為它能完全避免某個 region成為讀寫熱點。
9.1.6 時間順序關系
以上數據都會按照產生的時間順序以獨立行插入到HBase中,但是也可以使用另一種方式,即將新的事件以發生時間為列進行插入。因為列在HBase中是按列族組織的,所以每個列族下的列可以作為一個輔助索引單獨進行排序,如同RDBMS。雖然這不是推薦的設計模式,但少量的索引可能正是用戶所需要的。
以之前的收件箱應用為例,它將用戶的所有郵件存在一行中。由于用戶需要按照收件順序顯示郵件,同時也有可能需要按照題目等順序進行排序。設計時可以利用基于列的排序來顯示收件箱的不同視圖。
記住,不要為一張表設置過多的列族,特別是當數據量大的列族和數據
量小的列族混用(大小指的是存儲的數據量),用戶可以把收件箱的郵件
存儲在一張表中,同時把輔助索引存在另一張表中。缺點是這樣用戶不能保證修改兩張表的原子性(HBase只保證行級原子性)。同時請參考9。3
節來克服這個限制。
郵件內容存儲在主要的列族下,索引被單獨存儲在另一個列族下。用戶可以把郵件題目提取出來并添加到列鍵的前面來建立輔助索引。如果用戶需要對題目進行降序排列,則需要再額外添加一個列族。
為了避免太多的列族,用戶可以把所有輔助索引存儲在一個單獨的列族下,同時列鍵(列名)的最左段使用索引ID這個前綴來表示不同的排序,例如,idx- subject-desc和idx-to-asc等。接下來用戶要填入實際的排序值。單元格的值是主索引的鍵,同時主索引中存儲著消息的信息。用戶需要從以下3種存儲方式中選擇一種:從主表(主索引)中讀取消息的內容,只展示輔助索引中存儲的信息,或把消息中的信息冗余地存儲到輔助索引中從而避免在主信息源中進行隨機讀取。反范式化在HBase中十分常見,它可用于減少讀取時間,從而大大提高用戶響應速度。
將以上的表設計模式付諸實施可以得到如下表:
在前面這段代碼中,一個索引(idx-from-asc)按照電子郵件地址升序排列,另一個索引(idx-subject-desc)按照題目降序排列。同時題目按照位反序排列以達到降序排列的目的,所以其內容已經不可讀了。
9.2 高級模式
略~
9.3 輔助索引
盡管HBase沒有為輔助索引提供原生支持,但是有些應用場景仍需要使用輔助索引。通常的需求是用戶能夠通過主坐標(行鍵、列族和列限定符)來查找一個單元格,也可以通過一個其他類型的坐標來進行查找。此外,用戶可以按照輔助索引的順序從主表掃描數據。
與關系型數據庫系統中的索引類似,輔助索引存儲了一個新坐標和現有的坐標之間的映射關系。以下列出了一些可行的解決方案。
由客戶端管理索引
把責任完全轉移到應用層的典型做法是把一個數據表和一個(或者多個)查找映射表結合起來。每當程序寫數據表時,它也同時更新映射表(也被稱為輔助索引表)。讀數據時可以直接在主表中進行查詢,從輔助索引表中先査找原表行鍵,再在原表中讀取實際數據。
這種做法既有優點也有缺點。首先,優點是整個邏輯都由客戶端代碼處理,用戶可以按照需求設計映射關系。不過這樣做的缺點更多,由于HBase中不能保證跨行操作的原子性,例如,以事務的角度來看,用戶不能保證主表和依賴表的一致性。用戶可以使用定期修剪工作來部分解決這個問題,例如,使用MapReduce程序來掃描表,刪除過時的條目,或增加缺失的條目。
缺少事務的支持可能導致數據被存儲在數據表中,但是在輔助索引表中沒有相應的映射。這種情況可能發生在主表被更新后且索引表被成功寫入前,如果這段時間出現了任何導致操作失敗的問題都會使輔助索引和主表不一致。這個問題可以通過先寫輔助索引表,在操作的最后再寫數據表來緩解。如果在這個過程中有任何操作失敗,就會出現孤立的映射,不過它們很容易被異步的定期修剪工作刪除掉。
帶索引的事務型HBase
開源的帶索引的事務型HBase(Indexed-Transactional HBase,簡稱為 ITHBase)項目提供了一個不同的解決方案。它擴展了HBase,并增加了特殊的客戶端和服務器端類的實現。最核心的擴展是增加了用來保證所有的輔助索引更新操作一致性的事務功能。
以上所述的客戶端管理索引在單獨的表中存儲主鍵和輔助鍵之間的映射關系,與之不同的是,這些操作在ITHBase中被自動處理,同時主表與輔助索引表之間的關系由相應的描述符定義。結合事務支持的索引更新,這個方案提供了個HBase輔助索引的完全實現。
帶索引的HBase
在HBase中增加輔助索引的另外一種解決方案是帶索引的HBase(簡稱IHBase)。這種解決方案放棄了為每個索引使用單獨的表,而是完全在內存中維護索引。當一個region第一次打開,或者一個memstore被刷寫到磁盤上時,用戶可以通過掃描整個region來建立索引。根據用戶配置的region大小的不同,這個操作可能花費大量的時間和IO資源。
當磁盤上的數據有索引時,內存中數據搜索的方式如下:它直接使用內存中的數據來搜索索引相關的詳細數據。這個方案的優點是索引永遠都是同步的,并且不需要額外的事務控制。
與基于表的索引相比,使用這種方法有兩方面不同。一方面它很快,所有需要的索引數據都在內存中,因此可以執行很快的二分查找來定位行。另外一方面,它也需要大量額外的堆空間來維護索引。由于用戶的需求和想要索引的數據量不同,IHBase有時并不能建立用戶需要的所有索引。
協處理器
目前也有一些方法基于協處理器來實現索引方案。使用協處理器框架提供的服務器端的鉤子函數可以實現類似于ITHBase和IHBase的索引,并且不用替換任何客戶端類和服務器端類。協處理器將為每一個region載入索引層,并維護索引。(類似MYSQL的觸發器。)
9.4 搜索集成
略~
9.5 事務
略~
9.6 布隆過濾器
使用布隆過濾器的根本原因是默認機制決定了一個存儲文件是否包含特定的受限于可用塊索引的行鍵,同時這個索引又是相當粗粒度的,該索引只存儲了文件包含塊的開始鍵。
使用布隆過濾器的好處是,用戶可以立即判斷一個文件是否包含特定的行
鍵。這個過濾器的特性是:如果這個文件不包含這個行,它會給你一個明確的答復。但是如果文件包含這一行時,答復卻可能有誤,即它聲稱文件包含這個數據而實際卻并非如此。錯誤肯定答復的數量可以被調整,通常被設置為1%,這意味著過濾器中關于一個文件包含一個請求行的報告中有1%是錯誤的,因此可能會有一個塊被錯誤地加載和檢查。
為了提高效率,用戶還必須使用一種特定的更新模式:如果用戶定期修改所有行,那么大部分的存儲文件都將包含用戶查找行的數據。這種場景不適
合使用布隆過濾器。但是,如果用戶批量更新數據,使得一行數據每次只被寫入到少數幾個存儲文件中,那么過濾器就能夠為減少整個系統IO操作的數量發揮很大作用。
圖9-5總結了不同級別布隆過濾器的選擇標準。
9.7 版本管理
9.7.1 隱式版本管理
在用戶確保具服務器上的時鐘是同步的之前,有些問題就已經被指出了。其中一個可能出現的問題是,當用戶跨服務器把數據存儲在多行中時,使用隱式的時間戳可能讓用戶最終得到完全不同的時間集。
用戶可以通過在存儲這些值時設置商定的或者共享的時間戳來避免這個問題。put操作允許用戶設置一個客戶端的時間戳作為替代,并以此覆蓋服務器的時間。
region被拆分暴露了服務器時間不一致引起的另一個問題。假設用戶把一個值存儲在一個比機群中所有其他服務器都超前一小時的服務器上,并且使用服務器隱式的時間戳。十分鐘后這個region被拆分了,并且用戶一半的數據更新被移到了另一個服務器上。5分鐘后,當用戶再向同樣的列中插入一個新值時,服務器會自動添加時間戳。現在新值被認為比之前添加的數據更老,因為第一個版本的時間戳比當前服務器的時間超前一小時。此時如果
用戶使用標準的get方法去檢索這個值的最新版本,則會得到第一個之前存的值。
例9.1 刪除顯式時間戳的示例應用程序
for(int count = 1; count <=6; count++)(①Put put = new Put(ROW1);put.add (COLFAM1, QUAL1, count, Bytes.toBytes ("val-"+ count));②table. put (put);Delete delete = new Delete(ROW1);③delete.deleteColumn(COLFAM1, QUAL1, 5);delete.deleteColumn(COLFAM1, QUAL1, 6);table.delete(delete);①存儲同一列6次
②使用循環變量把版本設為一個特殊的值。
③刪除最新的兩個版本。
當你運行這個例子時,你應該可以看到以下輸出:
After put calls... KV: row1/colfam1: qual1/6/Put/vlen=5, Value: val-6 KV: row1/colfam1: qual1/5/Put/vlen=5, Value: val-5 KV: row1/colfam1: qual1/5/Put/vlen=5, Value: val-4After put call... KV: row1/colfam1: qual1/4/Put/vlen=5, Value: val-4 KV: row1/colfam1: qual1/3/Put/vlen=5, Value: val-3 KV: row1/colfam1: qual1/2/Put/vlen=5, Value: val-2有趣的現象是,用戶使得版本2和版本3復活了。這是由于服務器把內部處理推遲到一個定義好的時間執行。該列的老版本仍然存在,因此刪除較新的版本會使它們再次出現。
這種情形只可能出現在major合并被執行之前,在這之后老版本會被永久刪除,而保留的版本數基于已配置的最大保留版本數。
最后,在處理時間戳時,還有另外需要注意的一個問題:刪除標記。在 HBase中,刪除操作的本質是添加一個帶有特定時間戳的墓碑標記到存儲中。在此基礎上,它屏蔽直接指定的對應版本的數據,或者一個列刪除標記會抹去比給定時間戳更老的所有版本的數據。例9.2用Shell展示了上述情況。
例9.2 使用顯式時間戳刪除可以屏蔽之前的put操作
hbase(main): 001: 0> create 'testtable', 'colfam1' 0 row(s)in 1. 1100 secondshbase(main): 002: 0>Time.now.to_i =>1308900346hbase(main): 003: 0> put 'testtable', 'row1', 'colfam1:qual', 'val1' ① 0 row (s)in 0.0290 secondshbase (main): 004: 0> scan 'testtable' ROW COLUMN+CELL row1column=colfam1:qual1, timestamp=1308900355026, value=val1 1 row(s)in 0.0360 secondshbase(main): 005: 0> delete 'testtable', 'row1', 'colfam1:qual1' ② 0 row(sin.0280 secondshbase(main 006: 0> scan 'testtable’ ROW COLUMN+CELL 0 row(s)in 0.0260 secondshbase(main): 007: 0> put ‘testtable’, 'row1‘, 'colfam1:qual·’,'val1',\ Time.now.to_i-50000 ③ 0 row(s)in 0.0260 secondshbase(main ) 008: 0> scan 'testtable' ROW COLUMN+CELL 0 row(s) 0.0260 secondshbase(main): 009: 0> flush 'tasttable' 0 row(sin. 2720 secondshbase(main):010: 0> major_ compact 'testtable' 0 row(sin.0420 secondshbase(main):01l: 0> put 'testtable', 'row1', 'colfam1:qual1', 'val1',\ Time.now.to_i-50000 ⑤ 0 row(s in 0.0280 secondshbase (main: 012: 0> scan 'testtable' ROW COLUMN+CELL row1 column=colfam1:qual1, timestamp=1308900423953, value=val1 1 row(s)in.0290 seconds①向新創建的表的列中存入一個值,運行掃描來驗證結果。
②刪除列的所有值,這將會設置一個帶有當前時間戳的刪除標記。
③再一次把值存入列中,但是使用一個過去的時間戳,隨后的掃描沒有返回被屏蔽的值。
④通過對表的刷寫和major合并來移除這個刪除標記。
⑤再次存儲帶有過去時間戳的值,隨后的掃描如預期一樣顯示了插入的值。
9.7.2 自定義版本控制
由于你可以指定自己時間戳的值,因此也可以創建自己的版本控制計劃。在覆蓋服務器端基于同步的服務器時間的時間戳時,用戶可以不使用基于時間的版本。
用戶必須為每個put操作都這么做,否則服務器將插入一個基于服務器端時間的時間戳。表或者列的描述中有一個標志可以表明你使用的是自定義的時間戳(即自定義的版本控制策略)。如果用戶沒有設置這個值,那么它將在后臺被服務器時間替換掉。
總結
以上是生活随笔為你收集整理的《HBase权威指南》读书笔记(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Codeforces Round #49
- 下一篇: 花生壳客户端下载