以往工作中HBase存儲海量數據時候,因為歷史原因主鍵使用自增長序列,數據遷移到HBase中時,并沒有改變主鍵策略。導致數據全部寫入一個region,造成數據熱點。當時也沒有采用預分區,數據增長過快,region過大時,在系統低負載時段需手動切分region。這些原因導致集群整體效率低。
現在要求
目前正好有一個新的數據存儲場景開始設計,對數據需要增量順序讀(如:增量構建全文索引等),中等規模隨機讀。能按照區間范圍順序查詢,并能控制查詢的開始和結束。以方便增量或重新構建索引或控制區間化數據讀取需求等等。總結歷史經驗和教訓, 現在需要考慮的2種場景:
- 海量文本數據A,天入庫數據量在30-200W左右,文本格式長,需要保存原文。(數量現對小,單文本容量比較大,需要保存原文)
- 海量文本數據B,天數據量在500w-5000w之間,文本格式短小。(數量現對多,單文本容量相對小 )
HBase基本情況
表和索引組成
HBase一般由行鍵(row key)、列鍵(column qualifier )、列族(column family)組成。行鍵對應關鍵數據庫中主鍵,HBase為行鍵建立了索引,列鍵歸屬列族。通過行鍵/列鍵/列族定位到一個唯一記錄。
HBase中使用.META內部表存儲region的分布情況以及每個region的詳細信息。region中記錄了rowkey范圍,region分散在不同服務器中。通過region server提供訪問數據訪問服務,region server可服務多個region,來自不同region server上的region組合成表格的整體邏輯視圖。
獲取記錄方式
- 通過get方式,指定rowkey獲取唯一記錄
- 通過scan方式,設置startRow和stopRow參數進行范圍匹配
- 通過scan方式,全表掃描,并通過RowFilter過濾出數據
基本可歸納為順序讀和隨機讀。
rowkey原則
長度原則
行鍵長度盡量短和合理,因為持久化文件HFile中使用KeyValue形式保存數據,column family/column qualifier/rowkey會記錄到每一條數據中,導致存儲文件過大,也會導MemStore內存有效利用率降低。rowkey使用byte[]保存數據,使用數值(long)比字符(String)占用更小空間。64為系統內存8字節對齊,控制在16個字節,盡量使用8為倍數。
散列原則避免熱點
- 加鹽
在rowkey前按規則加隨機數,使數據分散到不同region上避免熱點。也可通過業務規則分段比如:userid-service-timestamp,把不同userid或不同業務數據切分到不同region。 - 哈希
生成哈希或使用UUID散列它。 - 反轉
如果是順序序列可以反轉它,讓他經常改變的部分排到前面避免數據集中。時間反轉也可以考慮Long.Max_Value - timestamp。
唯一原則
主鍵都必須保證唯一。
分布式主鍵算法
分布式主鍵算法要求
- 毫秒級的快速響應
- 可用性強
- prefix有連續性方便DB順序存儲
- 體積小,8字節為佳
目前分布式主鍵算法比較
UUID
16字節,JAVA自帶,好用,占用空間大。
Twitter Snowflake
Snowflake: timestamp + work id + seq number
8字節,可用性強,速度快。占用空間小,如果考慮復雜環境work id需要更好處理。twitter默認實現需要引入zookeeper 和獨立的snowflake專用服務器,UC實現通過配置文件確定work id。
MongoDB ObjectId
ObjectId:timestamp + machine + pid + seq number
12字節,可用性強,速度快。占用空間中等,用空間降低實現復雜度,基本沒有其他依賴。
業務rowkey設計
業務的需求
- 大文本A每分鐘最多1388條,每秒23條。
- 小文本B每分鐘最多34722條,每秒578條。
考慮到大量順序讀,需要做到局部連續,全局分散。每秒極端情況寫數據量不多,可考慮按照分鐘分區。一共60個分區。獲取一天數據時候通過60*24=1440 按照1440個局部連續批來獲取數據。
rowkey規則1
參考snowflake和ObjectId原理,感覺它并不好直觀分區,所以:
partition + timestamp + work id+ seq number 增加分區,減少時間和work id范圍。
0-000000-01111111 10111111 11111000 00111100-00000000 0-00000000 00000000
1bit 不用
6bit 分區,可支持63個分區,可使用秒或分鐘做分區
32bit 時間 System.currentTimeMillis()到秒(可以用到2099年)
9bit 區分機器和進程,需要在存儲空間和復雜度上找平衡
16bit seq(最大65535)
全長64Bit,8Byte可做到不依賴其他服務。
分區可表示為:
0-000000-0000000 000000000 00000000 00000000-00000000 0-00000000 00000000
0-000001-0000000 000000000 00000000 00000000-00000000 0-00000000 00000000
0-000010-0000000 000000000 00000000 00000000-00000000 0-00000000 00000000
0-000011-0000000 000000000 00000000 00000000-00000000 0-00000000 00000000
rowkey規則2
規則1太復雜實現和可讀性低簡,簡化下
reverse timestamp(mmHHddMMyyyy)+ seq number(0-99999)
seq number使用redis保證全局唯一,每個客戶端使用步長減少redis訪問頻次。
例如:rowkey=20170719213012345表示為:30211907201712345使用分鐘做分區30表示分區。
分區可表示為:
1000000000000000L:11100011010111111010100100110001101000000000000000
3000000000000000L:111000110101111110101001001100011010000000000000000
59000000000000000L:11010001100111000010111111111001101111111000000000000000
56Bit,7Byte 依賴redis服務 簡單直觀可讀性好。
rowkey規則2順序讀取數據方式
假如我需要scan查詢2017/07/19數據,我需要從201707190000~201707192359
60*24=1440做循環。2017/07/19/ 21:35為例子, StartRow和StopRow設置如下:
StartRow=35211907201700000L
StopRow=35211907201799999L
最終選擇規則2作為rowkey規則,隨然依賴redis服務,但是存儲空間小、可讀性高、可理解性好、方便使用和維護。
其他
不考慮HBase RowFilter方式,希望的效果就是直接利用rowkey內部索引和.META表。對于其他復雜組合查詢,我傾向使用全文索引ES或Solr。
可以參考算法源碼
UC Snowflake
https://github.com/sumory/uc/blob/master/src/com/sumory/uc/id/IdWorker.java
MaongoDB ObjectID
https://github.com/mongodb/mongo-java-driver/blob/master/bson/src/main/org/bson/types/ObjectId.java
Email:wei.liu@qq.com
劉威 2017年7月19日 長沙