HBase RowKey設計的那些事
通過實戰經歷分享HBase RowKey設計的技巧與方法 在說 rowkey設計之前,先回答一下大家配置 HBase時可能有的疑問,關于 HBase是否需要單獨的 ZooKeeper托管?嗯,如果只是部署 HBase,我建議不要用單獨的 ZooKeeper進行托管,用 HBase自帶的 ZooKeeper就可以,假如要部署其他應用,比如 Spark等可以單獨部署一個 ZooKeeper集群。好,廢話不多說了,下面說說 RowKey設計的事。
先談HBase底層架構
對于新手來說,RowKey的設計是比較陌生的一件事,看上去很簡單的東西,其實非常復雜,RowKey的設計基本上可以劃分成兩大影響,分別是分析維度、查詢性能。為什么要這樣分呢?我們再回頭看看HBase系統架構圖:
這種設計看上去并沒有什么問題,但是這種設計隱藏了非常多陷阱,假如CompanyCode字段非常固定,而TimeStamp變化比較大的話,會造成單個Region連續地存儲這些數據,數據量非常大的時候,這個Region會集中了這些數據,當有應用需要訪問這些數據時,造成了RPC timeout,甚至應用程序直接報錯,無法執行。
合理的RowKey設計方法
基于上面的原因,我們需要考慮單點集中以及數據查詢兩方面的因素,因此,在RowKey上我們要針對這兩個問題進行方案設計。
首先是單點集中問題,我們出現這樣單點集中的原因大概有以下幾種:
l RowKey前面的字符過于固定
l 集群結點數量過少
集群結點數量是由我們自身硬件資源限制的,這個我們不考慮在內,我們主要考慮RowKey設計。既然是因為前面字符過于集中,那么我們可以通過在RowKey前面添加隨機的一個字符串,下面是引自《HBase Essential》里面的一個隨機字符計算方法:
int saltNumber = new Long(new Long(timestamp).hashCode()) %<number of region servers>
用這種方法,我們在插入數據的時候可以人為地隨機把一斷時間內的數據打散,分布到各個RegionServer下的Region中,充分利用分布式的優勢,這樣做不緊可以加快數據的讀寫訪問,也解決了數據集中的問題。
改良后的RowKey設計方案
通過上面的技術研討,可以制定出以下的RowKey設計方案了:
隨機字符(2位) + 時間位(14位)+ CompanyCode(4位)
我在實際測試過程中,前后兩種方案對比,前者的MR程序跑了1個小時,后者只花了5分鐘。
合理地編寫查詢代碼
我們完成數據存儲之后,假如要取出某部分數值,需要設置Scan查詢,以下是我在實戰中用到的部分代碼,僅供參考:
public class HBaseTableDriver extends Configured implements Tool {
public int run(String[] arg0) throws Exception {
if(arg0.length < 4 || arg0.length > 5)
throw new IllegalArgumentException("The input argument need:start && stop && farmid && turbineNum && calid");
if(arg0[0].length() != 8 || arg0[1].length() != 8)
throw new IllegalArgumentException("The date format should be yyyyMMdd");
Configuration conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.quorum", ConstantValues.QUOREM);
conf.set("hbase.zookeeper.property.clientPort", ConstantValues.CLIENT_PORT);
//extract table && tagid && start time && end time
conf.set("start", arg0[0]);
conf.set("stop", arg0[1]);
conf.set("farmid", arg0[2]);
conf.set("turbineNum", arg0[3]);
conf.set("calid", arg0[4]);
String startRow = "0" + arg0[0] + " 000000" + arg0[2] + "001";
String stopRow = "2" + arg0[1] + " 235959" + arg0[2] + RowKeyGenerator.addZero(Integer.parseInt(arg0[3]));
String targetKpiTableName = "kpi2";
Job job = Job.getInstance(conf, "KPIExtractor");
job.setJarByClass(KPIExtractor.class);
job.setNumReduceTasks(6);
Scan scan = new Scan();
scan.addColumn("f".getBytes(), "v".getBytes());
String regEx = "^\\d{1}(?:" + arg0[0].substring(0, 4) + "|" + arg0[1].substring(0, 4) + ")\\d{17}";
switch(arg0[4]){
case "1":
regEx = regEx + "(?:823|834)$";
startRow = startRow + "823";
stopRow = stopRow + "834";
break;
case "2":
regEx = regEx + "211$";
startRow = startRow + "211";
stopRow = stopRow + "211";
break;
case "3":
regEx = regEx + "544$";
startRow = startRow + "544";
stopRow = stopRow + "544";
break;
case "4":
regEx = regEx + "208$";
startRow = startRow + "208";
stopRow = stopRow + "208";
break;
case "5":
regEx = regEx + "(?:739|823)$";
startRow = startRow + "739";
stopRow = stopRow + "823";
break;
case "6":
regEx = regEx + "(?:211|823)$";
startRow = startRow + "211";
stopRow = stopRow + "823";
break;
case "7":
regEx = regEx + "708$";
startRow = startRow + "708";
stopRow = stopRow + "708";
break;
case "8":
regEx = regEx + "822$";
startRow = startRow + "822";
stopRow = stopRow + "822";
break;
case "9":
regEx = regEx + "211$";
startRow = startRow + "211";
stopRow = stopRow + "211";
break;
default:
throw new IllegalArgumentException("UnKnown Argument calid:"+arg0[4]+",it should be between 1~9");
}
scan.setStartRow(startRow.getBytes());
scan.setStopRow(stopRow.getBytes());
scan.setFilter(new RowFilter(CompareOp.EQUAL, new RegexStringComparator(regEx)));
TableMapReduceUtil.initTableMapperJob("hellowrold", scan , KPIMapper.class, ImmutableBytesWritable.class, ImmutableBytesWritable.class, job);
TableMapReduceUtil.initTableReducerJob(targetKpiTableName, KPIReducer.class, job);
job.waitForCompletion(true);
return 0;
}
}注意點:
l 這里主要用到了RowFilter對RowKey進行過濾,并且我在查閱相關資料的時候,別人建議不要在大數據量下使用ColumnFilter,性能非常低。
l 可以通過Configuration為Map/Reduce傳輸參數值。
來自:http://my.oschina.net/lanzp/blog/477732