Rowkey(行鍵)設計

2018-03-17 11:19 更新

本節(jié)介紹了 HBase 中的行鍵(Rowkey)設計。

Hotspotting

HBase 中的行按行鍵按順序排序。這種設計優(yōu)化了掃描(scan),允許您將相關的行或彼此靠近的行一起讀取。但是,設計不佳的行鍵是 hotspotting 的常見來源。當大量客戶端通信針對群集中的一個節(jié)點或僅少數(shù)幾個節(jié)點時,會發(fā)生 Hotspotting。此通信量可能表示讀取、寫入或其他操作。通信量壓倒負責托管該區(qū)域的單個機器,從而導致性能下降并可能導致區(qū)域不可用性。這也會對由同一臺區(qū)域服務器托管的其他區(qū)域產生不利影響,因為該主機無法為請求的負載提供服務。設計數(shù)據(jù)訪問模式以使群集得到充分和均勻利用非常重要。

為了防止 hotspotting 寫入,請設計行鍵,使真正需要在同一個區(qū)域中的行成為行,但是從更大的角度來看,數(shù)據(jù)將被寫入整個群集中的多個區(qū)域,而不是一次。以下描述了避免 hotspotting 的一些常用技術,以及它們的一些優(yōu)點和缺點。

Salting

從這個意義上說,Salting 與密碼學無關,而是指將隨機數(shù)據(jù)添加到行鍵的開頭。在這種情況下,salting 是指為行鍵添加一個隨機分配的前綴,以使它的排序方式與其他方式不同??赡艿那熬Y數(shù)量對應于要傳播數(shù)據(jù)的區(qū)域數(shù)量。如果你有一些“hotspotting”行鍵模式,反復出現(xiàn)在其他更均勻分布的行中,那么 Salting 可能會有幫助。請考慮以下示例,該示例顯示 salting 可以跨多個 RegionServer 傳播寫入負載,并說明讀取的一些負面影響。

使用實例

假設您有以下的行鍵列表,并且您的表格被拆分,以便字母表中的每個字母都有一個區(qū)域。前綴'a'是一個區(qū)域,前綴'b'是另一個區(qū)域。在此表中,所有以'f'開頭的行都在同一個區(qū)域中。本示例重點關注具有以下鍵的行:

foo0001
foo0002
foo0003
foo0004

現(xiàn)在,想象你想要在四個不同的地區(qū)傳播這些信息。您決定使用四個不同的 Salting:a,b,c 和 d。在這種情況下,每個這些字母前綴將位于不同的區(qū)域。應用 Salting 后,您可以使用以下 rowkeys。由于您現(xiàn)在可以寫入四個不同的區(qū)域,因此理論上寫入時的吞吐量是吞吐量的四倍,如果所有寫入操作都在同一個區(qū)域,則會有這樣的吞吐量。

A-foo0003
B-foo0001
C-foo0004
d-foo0002

然后,如果添加另一行,它將隨機分配四種可能的 Salting 值中的一種,并最終靠近現(xiàn)有的一行。

A-foo0003
B-foo0001
C-foo0003
C-foo0004
d-foo0002

由于這個任務是隨機的,如果你想按字典順序檢索行,你需要做更多的工作。以這種方式,Salting 試圖增加寫入吞吐量,但在讀取期間會產生成本。

Hashing

除了隨機分配之外,您可以使用單向 Hashing,這會導致給定的行總是被相同的前綴“salted”,其方式會跨 RegionServer 傳播負載,但允許在讀取期間進行預測。使用確定性  Hashing 允許客戶端重建完整的 rowkey 并使用 Get 操作正常檢索該行。

Hashing 示例

考慮到上述 salting 示例中的相同情況,您可以改為應用單向 Hashing,這會導致帶有鍵的行 foo0003 始終處于可預見的狀態(tài)并接收 前綴。

然后,為了檢索該行,您已經知道了密鑰。

例如,您也可以優(yōu)化事物,以便某些鍵對總是在相同的區(qū)域中。

反轉關鍵

防止熱點的第三種常用技巧是反轉固定寬度或數(shù)字行鍵,以便最經常(最低有效位數(shù))改變的部分在第一位。這有效地使行鍵隨機化,但犧牲了行排序屬性。

單調遞增行鍵/時間序列數(shù)據(jù)

在 Tom White 的書“Hadoop: The Definitive Guide”(O'Reilly)的一章中,有一個優(yōu)化筆記,關注一個現(xiàn)象,即導入過程與所有客戶一起敲擊表中的一個區(qū)域(并且因此是單個節(jié)點),然后移動到下一個區(qū)域等等。隨著單調遞增的行鍵(即,使用時間戳),這將發(fā)生。通過將輸入記錄隨機化為不按排序順序排列,可以緩解由單調遞增密鑰帶來的單個區(qū)域上的堆積,但通常最好避免使用時間戳或序列(例如1,2,3)作為行鍵。

如果您確實需要將時間序列數(shù)據(jù)上傳到 HBase 中,則應將 OpenTSDB 作為一個成功的示例進行研究。它有一個描述它在 HBase 中使用的模式的頁面。OpenTSDB 中的關鍵格式實際上是 [metric_type] [event_timestamp],它會在第一眼看起來與之前關于不使用時間戳作為關鍵的建議相矛盾。但是,區(qū)別在于時間戳不在密鑰的主導位置,并且設計假設是有幾十個或幾百個(或更多)不同的度量標準類型。因此,即使連續(xù)輸入數(shù)據(jù)和多種度量類型,Puts也會分布在表中不同的地區(qū)。

盡量減少行和列的大小

在 HBase 中,值總是隨著坐標而運行;當單元格值通過系統(tǒng)時,它將始終伴隨其行,列名稱和時間戳。如果你的行和列的名字很大,特別是與單元格的大小相比,那么你可能會遇到一些有趣的場景。其中之一就是 Marc Limotte 在 HBASE-3551 尾部描述的情況。其中,保存在 HBase商店文件( StoreFile(HFile))以方便隨機訪問可能最終占用 HBase 分配的 RAM 的大塊,因為單元值坐標很大。上面引用的注釋中的標記建議增加塊大小,以便存儲文件索引中的條目以更大的間隔發(fā)生,或者修改表模式,以便使用較小的行和列名稱。壓縮也會使更大的指數(shù)。在用戶郵件列表中查看線程問題 storefileIndexSize。

大多數(shù)時候,小的低效率并不重要。不幸的是,這是他們的情況。無論為 ColumnFamilies,屬性和 rowkeys 選擇哪種模式,都可以在數(shù)據(jù)中重復數(shù)十億次。

列族

盡量保持 ColumnFamily 名稱盡可能小,最好是一個字符(例如,"d" 用于 data 或者 default)。

屬性

雖然詳細的屬性名稱(例如,“myVeryImportantAttribute”)更易于閱讀,但更喜歡使用較短的屬性名稱(例如,“via”)來存儲在 HBase 中。

Rowkey長度

保持它們盡可能短,這樣它們仍然可以用于所需的數(shù)據(jù)訪問(例如,Get 和 Scan)。對數(shù)據(jù)訪問無用的短密鑰并不比具有更好的 get/scan 屬性的更長密鑰更好。在設計行鍵時需要權衡。

字節(jié)模式

長為8個字節(jié)。您可以在這八個字節(jié)中存儲最多18,446,744,073,709,551,615的未簽名數(shù)字。如果您將此數(shù)字作為字符串存儲 - 假定每個字符有一個字節(jié) - 則需要接近3倍的字節(jié)。

以下是您可以自行運行的一些示例代碼:

// long
//
long l = 1234567890L;
byte[] lb = Bytes.toBytes(l);
System.out.println("long bytes length: " + lb.length);   // returns 8

String s = String.valueOf(l);
byte[] sb = Bytes.toBytes(s);
System.out.println("long as string length: " + sb.length);    // returns 10

// hash
//
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(Bytes.toBytes(s));
System.out.println("md5 digest bytes length: " + digest.length);    // returns 16

String sDigest = new String(digest);
byte[] sbDigest = Bytes.toBytes(sDigest);
System.out.println("md5 digest as string length: " + sbDigest.length);    // returns 26

不幸的是,使用類型的二進制表示會使您的數(shù)據(jù)難以在代碼之外讀取。例如,這是您在增加值時在 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=\x00\x00\x00\x00\x00\x00\x00\x01
1 row(s) in 0.0310 seconds

shell 會盡最大努力打印一個字符串,并且它決定只打印十六進制。區(qū)域名稱內的行鍵也會發(fā)生同樣的情況。如果您知道存儲的內容可能沒問題,但如果可以將任意數(shù)據(jù)放入同一個單元格中,它可能也是不可讀的。這是主要的權衡。

反向時間戳

反向掃描 API

HBASE-4811 實現(xiàn)一個 API,以反向掃描表中的表或區(qū)域,從而減少了為正向或反向掃描優(yōu)化模式的需要。此功能在 HBase 0.98 和更高版本中可用。

數(shù)據(jù)庫處理中的一個常見問題是快速找到最新版本的值。使用反向時間戳作為密鑰的一部分的技術可以幫助解決這個問題的一個特例。在 Tom White 的書籍“Hadoop:The Definitive Guide(O'Reilly)”的 HBase 章節(jié)中也有介紹,該技術包括附加 Long.MAX_VALUE - timestamp 到任何密鑰的末尾(例如,[key][reverse_timestamp])。

通過執(zhí)行 Scan [key] 并獲取第一條記錄,可以找到表格中 [key] 的最新值。由于 HBase 密鑰的排序順序不同,因此該密鑰在 [key] 的任何較舊的行鍵之前排序,因此是第一個。

這種技術將被用來代替使用版本號,其意圖是永久保存所有版本(或者很長時間),同時通過使用相同的掃描技術來快速獲得對任何其他版本的訪問。

Rowkeys和ColumnFamilies

行鍵的范圍為 ColumnFamilies。因此,相同的 rowkey 可以存在于沒有碰撞的表中存在的每個 ColumnFamily 中。

Rowkeys的不變性

行鍵無法更改。他們可以在表格中“更改”的唯一方法是該行被刪除然后重新插入。這是 HBase dist-list 上的一個相當常見的問題,所以在第一次(或在插入大量數(shù)據(jù)之前)獲得 rowkeys 是值得的。

RowKeys與區(qū)域分割之間的關系

如果您預先拆分表格,了解您的 rowkey 如何在區(qū)域邊界上分布是非常重要的。作為重要的一個例子,考慮使用可顯示的十六進制字符作為鍵的前導位置(例如,“0000000000000000” 到 “ffffffffffffffff”)的示例。通過這些關鍵范圍 Bytes.split(這是在 Admin.createTable(byte[] startKey, byte[] endKey, numRegions) 為10個區(qū)域創(chuàng)建區(qū)域時使用的分割策略)將生成以下分割:

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 // 6
61 = 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 // f

(注意:前導字節(jié)作為注釋列在右側。)鑒于第一個分割是'0'而最后一個分割是'f',一切都很好,但是還沒有結束。

其中的問題是,所有的數(shù)據(jù)都會堆積在前兩個區(qū)域和最后一個區(qū)域,從而產生一個“塊狀(lumpy)”(也可能是“hot”)區(qū)域問題。'0'是字節(jié)48,'f'是字節(jié)102,但字節(jié)值(字節(jié)58到96)之間存在巨大的差距,永遠不會出現(xiàn)在這個密鑰空間中,因為唯一的值是 [0-9] 和 [af]。因此,中間地區(qū)將永遠不會被使用。要使用此示例鍵空間進行預分割工作,需要分割的自定義定義(即,不依賴于內置拆分方法)。

第1課:預分割表通常是最佳做法,但您需要預先拆分它們,以便可以在密鑰空間中訪問所有區(qū)域。雖然此示例演示了十六進制密鑰空間的問題,但任何密鑰空間都會出現(xiàn)同樣的問題。了解你的數(shù)據(jù)。

第2課:盡管通常不可取,但只要所有創(chuàng)建的區(qū)域都可在密鑰空間中訪問,則使用十六進制鍵(更一般而言,可顯示的數(shù)據(jù))仍可用于預分割表。

為了總結這個例子,以下是如何為十六進制密鑰預先創(chuàng)建恰當?shù)姆指畹睦樱?/p>

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");
    // the table 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;
}
以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號