Rowkey设计_HBase表设计
在HBase中我們都知道rowkey的設計尤為重要,其設計原則分為三種:
- 長度原則
- 散列原則
- 唯一原則
那么下面對rowkey的設計做一個詳細的闡述:?
HBase的rowkey設計可以說是使用HBase最為重要的事情,直接影響到HBase的性能,常見的RowKey的設計問題及對應訪問為:
?
Hotspotting
的行由行鍵按字典順序排序,這樣的設計優化了掃描,允許存儲相關的行或者那些將被一起讀的鄰近的行。然而,設計不好的行鍵是導致 hotspotting 的常見原因。當大量的客戶端流量( traffic )被定向在集群上的一個或幾個節點時,就會發生 hotspotting。這些流量可能代表著讀、寫或其他操作。流量超過了承載該region的單個機器所能負荷的量,這就會導致性能下降并有可能造成region的不可用。在同一 RegionServer 上的其他region也可能會受到其不良影響,因為主機無法提供服務所請求的負載。設計使集群能被充分均勻地使用的數據訪問模式是至關重要的。
為了防止在寫操作時出現 hotspotting ,設計行鍵時應該使得數據盡量同時往多個region上寫,而避免只向一個region寫,除非那些行真的有必要寫在一個region里。
下面介紹了集中常用的避免 hotspotting 的技巧,它們各有優劣:
Salting
Salting 從某種程度上看與加密無關,它指的是將隨機數放在行鍵的起始處。進一步說,salting給每一行鍵隨機指定了一個前綴來讓它與其他行鍵有著不同的排序。所有可能前綴的數量對應于要分散數據的region的數量。如果有幾個“hot”的行鍵模式,而這些模式在其他更均勻分布的行里反復出現,salting就能到幫助。下面的例子說明了salting能在多個RegionServer間分散負載,同時也說明了它在讀操作時候的負面影響。
假設行鍵的列表如下,表按照每個字母對應一個region來分割。前綴‘a’是一個region,‘b’就是另一個region。在這張表中,所有以‘f’開頭的行都屬于同一個region。這個例子關注的行和鍵如下:
foo0001foo0002foo0003foo0004現在,假設想將它們分散到不同的region上,就需要用到四種不同的 salts :a,b,c,d。在這種情況下,每種字母前綴都對應著不同的一個region。用上這些salts后,便有了下面這樣的行鍵。由于現在想把它們分到四個獨立的區域,理論上吞吐量會是之前寫到同一region的情況的吞吐量的四倍。?
a-foo0003b-foo0001c-foo0004d-foo0002如果想新增一行,新增的一行會被隨機指定四個可能的salt值中的一個,并放在某條已存在的行的旁邊。
a-foo0003b-foo0001c-foo0003c-foo0004d-foo0002由于前綴的指派是隨機的,因而如果想要按照字典順序找到這些行,則需要做更多的工作。從這個角度上看,salting增加了寫操作的吞吐量,卻也增大了讀操作的開銷。
Hashing
可用一個單向的 hash 散列來取代隨機指派前綴。這樣能使一個給定的行在“salted”時有相同的前綴,從某種程度上說,這在分散了RegionServer間的負載的同時,也允許在讀操作時能夠預測。確定性hash( deterministic hash )能讓客戶端重建完整的行鍵,以及像正常的一樣用Get操作重新獲得想要的行。
考慮和上述salting一樣的情景,現在可以用單向hash來得到行鍵foo0003,并可預測得‘a’這個前綴。然后為了重新獲得這一行,需要先知道它的鍵。可以進一步優化這一方法,如使得將特定的鍵對總是在相同的region。
Reversing the Key(反轉鍵)
第三種預防hotspotting的方法是反轉一段固定長度或者可數的鍵,來讓最常改變的部分(最低顯著位, the least significant digit )在第一位,這樣有效地打亂了行鍵,但是卻犧牲了行排序的屬性
單調遞增行鍵/時序數據
在一個集群中,一個導入數據的進程鎖住不動,所有的client都在等待一個region(因而也就是一個單個節點),過了一會后,變成了下一個region… 如果使用了單調遞增或者時序的key便會造成這樣的問題。使用了順序的key會將本沒有順序的數據變得有順序,把負載壓在一臺機器上。所以要盡量避免時間戳或者序列(e.g. 1, 2, 3)這樣的行鍵。
如果需要導入時間順序的文件(如log)到HBase中,可以學習OpenTSDB的做法。它有一個頁面來描述它的HBase模式。OpenTSDB的Key的格式是[metric_type][event_timestamp],乍一看,這似乎違背了不能將timestamp做key的建議,但是它并沒有將timestamp作為key的一個關鍵位置,有成百上千的metric_type就足夠將壓力分散到各個region了。因此,盡管有著連續的數據輸入流,Put操作依舊能被分散在表中的各個region中
簡化行和列
在HBase中,值是作為一個單元(Cell)保存在系統的中的,要定位一個單元,需要行,列名和時間戳。通常情況下,如果行和列的名字要是太大(甚至比value的大小還要大)的話,可能會遇到一些有趣的情況。在HBase的存儲文件( storefiles )中,有一個索引用來方便值的隨機訪問,但是訪問一個單元的坐標要是太大的話,會占用很大的內存,這個索引會被用盡。要想解決這個問題,可以設置一個更大的塊大小,也可以使用更小的行和列名 。壓縮也能得到更大指數。
大部分時候,細微的低效不會影響很大。但不幸的是,在這里卻不能忽略。無論是列族、屬性和行鍵都會在數據中重復上億次。
列族
盡量使列族名小,最好一個字符。(如:f 表示)
屬性
詳細屬性名 (如:”myVeryImportantAttribute”) 易讀,最好還是用短屬性名 (e.g., “via”) 保存到HBase.
行鍵長度
?讓行鍵短到可讀即可,這樣對獲取數據有幫助(e.g., Get vs. Scan)。短鍵對訪問數據無用,并不比長鍵對get/scan更好。設計行鍵需要權衡
字節模式
long類型有8字節。8字節內可以保存無符號數字到18,446,744,073,709,551,615。如果用字符串保存——假設一個字節一個字符——需要將近3倍的字節數。
下面是示例代碼,可以自己運行一下:???????
long?l?=?1234567890L;byte[] lb = Bytes.toBytes(l);System.out.println("long?bytes?length:?"?+?lb.length); String s = String.valueOf(l);byte[] sb = Bytes.toBytes(s);System.out.println("long?as?string?length:?"?+?sb.length); MessageDigest md = MessageDigest.getInstance("MD5");byte[] digest = md.digest(Bytes.toBytes(s));System.out.println("md5 digest bytes length: " + digest.length);String sDigest = new String(digest);byte[] sbDigest = Bytes.toBytes(sDigest);System.out.println("md5?digest?as?string?length:?"?+?sbDigest.length);不幸的是,用二進制表示會使數據在代碼之外難以閱讀。下例便是當需要增加一個值時會看到的shell:???????
hbase(main):001:0>?incr?'t',?'r',?'f:q',?1COUNTER?VALUE?=?1 hbase(main):002:0> get 't', 'r'COLUMN CELL f:q timestamp=1369163040570, value=\x00\x00\x00\x00\x00\x00\x00\x011?row(s)?in?0.0310?seconds?
這個shell盡力在打印一個字符串,但在這種情況下,它決定只將進制打印出來。當在region名內行鍵會發生相同的情況。如果知道儲存的是什么,那自是沒問題,但當任意數據都可能被放到相同單元的時候,這將會變得難以閱讀。這是最需要權衡之處。
?
倒序時間戳
一個數據庫處理的通常問題是找到最近版本的值。采用倒序時間戳作為鍵的一部分可以對此特定情況有很大幫助。該技術包含追加( Long.MAX_VALUE - timestamp ) 到key的后面,如 [key][reverse_timestamp] 。
表內[key]的最近的值可以用[key]進行Scan,找到并獲取第一個記錄。由于HBase行鍵是排序的,該鍵排在任何比它老的行鍵的前面,所以是第一個。
該技術可以用于代替版本數,其目的是保存所有版本到“永遠”(或一段很長時間) 。同時,采用同樣的Scan技術,可以很快獲取其他版本。
行鍵和列族
行鍵在列族范圍內。所以同樣的行鍵可以在同一個表的每個列族中存在而不會沖突。
行鍵不可改
行鍵不能改變。唯一可以“改變”的方式是刪除然后再插入。這是一個常問問題,所以要注意開始就要讓行鍵正確(且/或在插入很多數據之前)。
?
行鍵和region split的關系
如果已經 pre-split (預裂)了表,接下來關鍵要了解行鍵是如何在region邊界分布的。為了說明為什么這很重要,可考慮用可顯示的16位字符作為鍵的關鍵位置(e.g., “0000000000000000” to “ffffffffffffffff”)這個例子。通過 Bytes.split來分割鍵的范圍(這是當用 Admin.createTable(byte[] startKey, byte[] endKey, numRegions) 創建region時的一種拆分手段),這樣會分得10個region。???????
48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 // 054 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 // 661 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -68 // =68 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -126 // D75 75 75 75 75 75 75 75 75 75 75 75 75 75 75 72 // K82 18 18 18 18 18 18 18 18 18 18 18 18 18 18 14 // R88 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -44 // X95 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -102 // _102 102 102 102 102 102 102 102 102 102 102 102 102 102 102 102 // f但問題在于,數據將會堆放在前兩個region以及最后一個region,這樣就會導致某幾個region由于數據分布不均勻而特別忙。為了理解其中緣由,需要考慮ASCII Table的結構。根據ASCII表,“0”是第48號,“f”是102號;但58到96號是個巨大的間隙,考慮到在這里僅[0-9]和[a-f]這些值是有意義的,因而這個區間里的值不會出現在鍵空間( keyspace ),進而中間區域的region將永遠不會用到。為了pre-split這個例子中的鍵空間,需要自定義拆分。
教程1:預裂表( pre-splitting tables ) 是個很好的實踐,但pre-split時要注意使得所有的region都能在鍵空間中找到對應。盡管例子中解決的問題是關于16位鍵的鍵空間,但其他任何空間也是同樣的道理。
教程2:16位鍵(通常用到可顯示的數據中)盡管通常不可取,但只要所有的region都能在鍵空間找到對應,它依舊能和預裂表配合使用。
一下case說明了如何16位鍵預分區
public?static?boolean?createTable(Admin?admin,?HTableDescriptor?table,?byte[][]?splits)throws IOException { try { admin.createTable( table, splits ); return true; } catch (TableExistsException e) { logger.info("table " + table.getNameAsString() + " already exists"); return false; }} public static byte[][] getHexSplits(String startKey, String endKey, int numRegions) { byte[][] splits = new byte[numRegions-1][]; BigInteger lowestKey = new BigInteger(startKey, 16); BigInteger highestKey = new BigInteger(endKey, 16); BigInteger range = highestKey.subtract(lowestKey); BigInteger regionIncrement = range.divide(BigInteger.valueOf(numRegions)); lowestKey = lowestKey.add(regionIncrement); for(int i=0; i < numRegions-1;i++) { BigInteger key = lowestKey.add(regionIncrement.multiply(BigInteger.valueOf(i))); byte[] b = String.format("%016x", key).getBytes(); splits[i] = b; } return splits;}摘自:https://help.aliyun.com/document_detail/59035.html?spm=a2c4g.11186623.6.773.3c3e7bb29cWzA0?
總結
以上是生活随笔為你收集整理的Rowkey设计_HBase表设计的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大剑无锋之什么是值传递和引用传递?【面试
- 下一篇: 大剑无锋之你了解HTTPS吗?那么它为什