再談|Rowkey設計_HBase表設計
- 2019 年 12 月 15 日
- 筆記
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。這個例子關注的行和鍵如下:
foo0001 foo0002 foo0003 foo0004
現在,假設想將它們分散到不同的region上,就需要用到四種不同的 salts :a,b,c,d。在這種情況下,每種字母前綴都對應著不同的一個region。用上這些salts後,便有了下面這樣的行鍵。由於現在想把它們分到四個獨立的區域,理論上吞吐量會是之前寫到同一region的情況的吞吐量的四倍。
a-foo0003 b-foo0001 c-foo0004 d-foo0002
如果想新增一行,新增的一行會被隨機指定四個可能的salt值中的一個,並放在某條已存在的行的旁邊。
a-foo0003 b-foo0001 c-foo0003 c-foo0004 d-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', 1 COUNTER VALUE = 1 hbase(main):002:0> get 't', 'r' COLUMN CELL f:q timestamp=1369163040570, value=x00x00x00x00x00x00x00x01 1 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 // 0 54 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 // 6 61 -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 // D 75 75 75 75 75 75 75 75 75 75 75 75 75 75 75 72 // K 82 18 18 18 18 18 18 18 18 18 18 18 18 18 18 14 // R 88 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -44 // X 95 -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
但問題在於,數據將會堆放在前兩個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