在HBase中,定位一條數(shù)據(jù)(即一個(gè)Cell)需要4個(gè)維度的限定:行鍵(RowKey)、列族(Column Family)、列限定符(Column Qualifier)、時(shí)間戳(Timestamp)。其中,RowKey是最容易出現(xiàn)問題的。除了根據(jù)業(yè)務(wù)和查詢需求來設(shè)計(jì)之外,還需要注意以下三點(diǎn)。
-
打散RowKey
HBase中的行是按照RowKey字典序排序的。這對(duì)Scan操作非常友好,因?yàn)镽owKey相近的行總是存儲(chǔ)在相近的位置,順序讀的效率比隨機(jī)讀要高。但是,如果大量的讀寫操作總是集中在某個(gè)RowKey范圍,那么就會(huì)造成Region熱點(diǎn),拖累RegionServer的性能。因此,要適當(dāng)?shù)貙owKey打散。
-
加鹽(salting)+哈希(hashing)
這里的“加鹽”與密碼學(xué)中的“加鹽”不是一回事。它是指在RowKey的前面增加一些前綴。加鹽的前綴種類越多,RowKey就被打得越散。
前綴不可以是隨機(jī)的,因?yàn)楸仨氁尶蛻舳四軌蛲暾刂貥?gòu)RowKey。我們一般會(huì)拿原RowKey或其一部分計(jì)算hash值,然后再對(duì)hash值做運(yùn)算作為前綴。 -
反轉(zhuǎn)固定格式的數(shù)值
以手機(jī)號(hào)為例,手機(jī)號(hào)的前綴變化比較少(如152、185等),但后半部分變化很多。如果將它反轉(zhuǎn)過來,可以有效地避免熱點(diǎn)。不過其缺點(diǎn)就是失去了有序性。 -
反轉(zhuǎn)時(shí)間
這個(gè)操作嚴(yán)格來講不算“打散”,但可以調(diào)整數(shù)據(jù)的時(shí)間排序。如果將時(shí)間按照字典序排列,最近產(chǎn)生的數(shù)據(jù)會(huì)排在舊數(shù)據(jù)后面。如果用一個(gè)大值減去時(shí)間(比如用99999999減去yyyyMMdd,或者Long.MAX_VALUE減去時(shí)間戳),最新的數(shù)據(jù)就可以排在前面了。
-
控制RowKey長度
在HBase中,RowKey、列族、列名等都是以byte[]形式傳輸?shù)摹owKey的最大長度限制為64KB,但在實(shí)際應(yīng)用中最多不會(huì)超過100B。設(shè)計(jì)短RowKey有以下兩方面考慮:
-
在HBase的底層存儲(chǔ)HFile中,RowKey是KeyValue結(jié)構(gòu)中的一個(gè)域。假設(shè)RowKey長度100B,那么1000萬條數(shù)據(jù)中,只算RowKey就占用掉將近1G空間,會(huì)影響HFile的存儲(chǔ)效率。
HFile簡單結(jié)構(gòu)示意 -
HBase中設(shè)計(jì)有MemStore和BlockCache,分別對(duì)應(yīng)列族/Store級(jí)別的寫入緩存,和RegionServer級(jí)別的讀取緩存。如果RowKey過長,緩存中存儲(chǔ)數(shù)據(jù)的密度就會(huì)降低,影響數(shù)據(jù)落地或查詢效率。
MemStore與BlockCache
另外,我們目前使用的服務(wù)器操作系統(tǒng)都是64位系統(tǒng),內(nèi)存是按照8B對(duì)齊的,因此設(shè)計(jì)RowKey時(shí)一般做成8B的整數(shù)倍,如16B或者24B,可以提高尋址效率。
同樣地,列族、列名的命名在保證可讀的情況下也應(yīng)盡量短。HBase官方不推薦使用3個(gè)以上列族,因此實(shí)際上列族命名幾乎都用一個(gè)字母,比如‘c’或‘f’。
-
保證RowKey唯一性
這個(gè)就是顯而易見的了,不再贅述。
舉個(gè)例子
我們的業(yè)務(wù)中,有一部分是用戶在日歷上記錄自己的行為。需要儲(chǔ)存在RowKey中的維度有:用戶ID(uid,不會(huì)超過十億)、日歷上的日期(date,yyyyMMdd格式)、記錄行為的類型(type,0~99之間)。記錄的詳細(xì)數(shù)據(jù)則存儲(chǔ)在列f:data中。根據(jù)查詢邏輯,我們設(shè)計(jì)的RowKey格式如下:
9~79809782~05~0008839540
長度正好是24B。以字符‘~’為分界(‘~’的ASCII碼是最大的,方便),各個(gè)部分的含義如下:
uid.toString().hashCode() % 1099999999 - dateStringUtils.leftPad(type, 2, "0")StringUtils.leftPad(uid, 10, "0")
基于這種設(shè)計(jì),我們在建表階段就可以將其預(yù)分區(qū),使得數(shù)據(jù)在一開始就均勻分布在不同的Region上。建表語句參考:
create 'user_calendar_record', {
NAME => 'f',
VERSIONS => '1',
BLOCKCACHE => 'true',
BLOCKSIZE => '65536',
BLOOMFILTER => 'row',
COMPRESSION => 'SNAPPY'
}, {
SPLITS => ['1', '2', '3', '4', '5', '6', '7', '8', '9']
}
如果不做預(yù)分區(qū),那么表剛開始只會(huì)有一個(gè)Region。隨著數(shù)據(jù)量增大,就會(huì)頻繁觸發(fā)Region split,影響效率。關(guān)于Region split應(yīng)該另外寫文章討論,這里就不提了。

