1. 程式人生 > >並發拉取HBase大量指定列數據時卡住的問題排查

並發拉取HBase大量指定列數據時卡住的問題排查

mem 功能 generate ets 動態 invoke current 可能 tro

最近遇到一例,並發拉取HBase大量指定列數據時,導致應用不響應的情形。記錄一下。

背景

退款導出中,為了獲取商品規格編碼,需要從HBase表 T 裏拉取對應的數據。 T 對商品數據的存儲采用了 表名:字段名:id 的列存儲方式。由於這個表很大,且為詳情公用,因此不方便使用 scan 的方式,擔心帶來集群的不穩定,進而影響詳情和導出的整體穩定性。

要用 multiGet 的方式來獲取多個訂單的這個列的數據。 就必須動態生成相應的列,然後在 HBase 獲取數據的時候指定列集合。 現有記錄集合 List , 其中 Record 含有 id 字段。 這樣,可以從 Record 中把 id 字段的值提取出來,結合列模板 tablename:fieldname:id 來生成所要獲取的HBase列名集合。

然而,當 HBase指定列名集合比較大的時候, 似乎是有問題的。導致了堆內存爆了。
技術分享圖片

CPU 曲線也是直線上升
技術分享圖片

排查與解決

從錯誤日誌上看,是因為 HBase 獲取數據卡住了。 而此次的變更在於是增加了一個可以並發獲取HBase指定列數據的插件。 因為某種原因,必須生成和指定大量列去查詢HBase數據, 因此疑點主要鎖定在 HBase 獲取大量動態列數據是不是有問題,是不是因為指定列太多了。

原來的代碼如下:

private List<Result> fetchDataFromHBase(List<OneRecord> data, List<String> rowKeys, HBaseDataConf hbaseDataConf) {
  List<Result> hbaseResults = multiTaskExecutor.exec(rowKeys,
                                                    subRowkeys -> haHbaseService.getRawData(subRowkeys, hbaseDataConf.getTable(),
                                                                                            "cf", generateCols(hbaseDataConf.getFetchDataConf(), data), "", true), 200);
 
  return hbaseResults;
}

問題出在 subRowkeys -> haHbaseService.getRawData(subRowkeys, hbaseDataConf.getTable(), "cf", generateCols(hbaseDataConf.getFetchDataConf(), data) 這一行上。 data 是記錄全集,這樣 generalCols 會拿到所有訂單的商品ID 對應的列集合。而 subRowkeys 是按照指定任務數分割後的 HBase Rowkeys 子集合。 假如 data 有 8000 條記錄,那麽 generateCols(hbaseDataConf.getFetchDataConf(), data) 會生成幾萬條動態列,而 subRowkeys 只有不足 200 條。 顯然, generateCols 裏的 data 應該是對應劃分後的 subRowkeys 的那些記錄,而不是全部記錄。

修改後的代碼如下:

private List<Result> fetchDataFromHBase(List<OneRecord> data, HBaseDataConf hbaseDataConf) {
  List<Result> hbaseResults = multiTaskExecutor.exec(data,
                                                    partData -> fetchDataFromHBasePartially(partData, hbaseDataConf), 200);
 
  return hbaseResults;
}
 
private List<Result> fetchDataFromHBasePartially(List<OneRecord> partData, HBaseDataConf hbaseDataConf) {
  List<String> rowKeys = RowkeyUtil.buildRowKeys(partData, hbaseDataConf.getRowkeyConf());
  logger.info("hbase-rowkeys: {}", rowKeys.size());
  return haHbaseService.getRawData(rowKeys, hbaseDataConf.getTable(),
                            "cf", generateCols(hbaseDataConf.getFetchDataConf(), partData), "", true);
}

這裏,generalCols 用來生成的動態列就只對應分割後的記錄集合。修改後,問題就解決了。

原因

那麽,為什麽數萬條指定列會導致HBase獲取數據時內存爆掉了呢?

在 獲取 HBase 數據的地方打日誌:

String cf = (cfName == null) ? "cf" : cfName;
logger.info("columns: {}", columns);
List<Get> gets = buildGets(rowKeyList, cf, columns, columnPrefixFilters);
logger.info("after buildGet: {}", gets.size());
Result[] results = getFromHbaseFunc.apply(tableName, gets);
logger.info("after getHBase: {}", results.length);

發現: columns 日誌打出來了, after buildGet 沒有打出來。程序卡住了。可以推斷,是 buildGets 這一步卡住了。為什麽卡在了 buildGets 這一步呢?

鎖定嫌疑

寫一個單測,做個小實驗。 先弄個串行的實驗。 1000個訂單, 列數從 2000 增長 24000

@Test
def "testMultiGetsSerial"() {
    expect:
    def columnSize = 12
    def rowkeyNums = 1000
    def rowkeys = (1..rowkeyNums).collect { "E001" + it }
    (1..columnSize).each { colsSize ->
        def columns = (1..(colsSize*2000)).collect { "tc_order_item:sku_code:" + it }
 
        def start = System.currentTimeMillis()
        List<Get> gets = new HAHbaseService().invokeMethod("buildGets", [rowkeys, "cf", columns, null])
        gets.size() == rowkeyNums
        def end = System.currentTimeMillis()
        def cost = end - start
        println "num = $rowkeyNums , colsSize = ${columns.size()}, cost (ms) = $cost"
    }
 
}

耗時如下:

num = 1000 , colsSize = 2000, cost (ms) = 2143
num = 1000 , colsSize = 4000, cost (ms) = 3610
num = 1000 , colsSize = 6000, cost (ms) = 5006
num = 1000 , colsSize = 8000, cost (ms) = 8389
num = 1000 , colsSize = 10000, cost (ms) = 8921
num = 1000 , colsSize = 12000, cost (ms) = 12467
num = 1000 , colsSize = 14000, cost (ms) = 11845
num = 1000 , colsSize = 16000, cost (ms) = 12589
num = 1000 , colsSize = 18000, cost (ms) = 20068
 
java.lang.OutOfMemoryError: GC overhead limit exceeded

按照實際運行的並發情況做個實驗。 從 1000 到 6000 個訂單,列集合數量 從 1000 - 10000。 用並發來構建 gets 。

@Test
def "testMultiGetsConcurrent"() {
    expect:
    def num = 4
    def columnSize = 9
    (1..num).each { n ->
        def rowkeyNums = n*1000
        def rowkeys = (1..rowkeyNums).collect { "E001" + it }
        (1..columnSize).each { colsSize ->
            def columns = (1..(colsSize*1000)).collect { "tc_order_item:sku_code:" + it }
 
            def start = System.currentTimeMillis()
            List<Get> gets = taskExecutor.exec(
                    rowkeys,  { new HAHbaseService().invokeMethod("buildGets", [it, "cf", columns, null]) } as Function, 200)
            gets.size() == rowkeyNums
            def end = System.currentTimeMillis()
            def cost = end - start
            println "num = $rowkeyNums , colsSize = ${columns.size()}, cost (ms) = $cost"
            println "analysis:$rowkeyNums,${columns.size()},$cost"
        }
 
    }
 
}

耗時如下:

num = 1000 , colsSize = 1000, cost (ms) = 716
num = 1000 , colsSize = 2000, cost (ms) = 1180
num = 1000 , colsSize = 3000, cost (ms) = 1378
num = 1000 , colsSize = 4000, cost (ms) = 2632
num = 1000 , colsSize = 5000, cost (ms) = 2130
num = 1000 , colsSize = 6000, cost (ms) = 4328
num = 1000 , colsSize = 7000, cost (ms) = 4524
num = 1000 , colsSize = 8000, cost (ms) = 5612
num = 1000 , colsSize = 9000, cost (ms) = 5804
num = 2000 , colsSize = 1000, cost (ms) = 1416
num = 2000 , colsSize = 2000, cost (ms) = 1486
num = 2000 , colsSize = 3000, cost (ms) = 2434
num = 2000 , colsSize = 4000, cost (ms) = 4925
num = 2000 , colsSize = 5000, cost (ms) = 5176
num = 2000 , colsSize = 6000, cost (ms) = 7217
num = 2000 , colsSize = 7000, cost (ms) = 9298
num = 2000 , colsSize = 8000, cost (ms) = 11979
num = 2000 , colsSize = 9000, cost (ms) = 20156
num = 3000 , colsSize = 1000, cost (ms) = 1837
num = 3000 , colsSize = 2000, cost (ms) = 2460
num = 3000 , colsSize = 3000, cost (ms) = 4516
num = 3000 , colsSize = 4000, cost (ms) = 7556
num = 3000 , colsSize = 5000, cost (ms) = 6169
num = 3000 , colsSize = 6000, cost (ms) = 19211
num = 3000 , colsSize = 7000, cost (ms) = 180950
……

可見,耗時隨著rowkey 數應該是線性增長; 而隨著指定列集合的增大,會有超過線性的增長和波動。超線性增長是算法引起的,波動應該是由線程池執行引起的。

如果有 8800 個訂單,指定 24000 個列, 可想而知,有多慢了。

查看buildGets代碼,其中嫌疑最大的就是 addColumn 方法。這個方法添加列時,將列加入了 NavigableSet<byte[]> 這個數據結構裏。NavigableSet是一個排序的集合。HBase 的 NavigableSet 實現類是 TreeSet, 是基於紅黑樹實現的。紅黑樹查詢一個元素的復雜度是在O(Log2n) 。添加N個元素的復雜度在 n*O(Log2n) 。 如果添加大量列,就可能導致CPU計算消耗大,並發的情況會加劇。

為什麽HBase要用NavigableSet

那麽, HBase 列數據集的結構為什麽要用排序的Set 而不用普通的 Set 呢?是因為指定列從HBase獲取數據時,HBase會將滿足條件的數據拿出來,依次與指定列進行匹配過濾,這時候要應用到查找列功能。當指定列非常大時,TreeSet 的效率比 HashSet 的要大。

結語

因為一個低級的錯誤,導致堆內存爆了; 又因為這個錯誤,深入了解了 HBase 讀取列數據的一些內幕。 幸~~ 後續還需要深入學習下 紅黑樹的算法實現。

【完】

並發拉取HBase大量指定列數據時卡住的問題排查