天天看點

面對千萬級資料查詢,CK、ES、RediSearch誰才是王炸?

作者:dbaplus社群

前言

在開發中遇到一個業務訴求,需要在千萬量級的底池資料中篩選出不超過 10W 的資料,并根據配置的權重規則進行排序、打散(如同一個類目下的商品資料不能連續出現 3 次)。

下面對該業務訴求的實作,設計思路和方案優化進行介紹,對「千萬量級資料中查詢 10W 量級的資料」設計了如下方案:

  • 多線程 + CK 翻頁方案
  • ES scroll scan 深翻頁方案
  • ES + Hbase 組合方案
  • RediSearch + RedisJSON 組合方案

一、初版設計方案

整體方案設計為:

  • 先根據配置的「篩選規則」,從底池表中篩選出「目标資料」。
  • 在根據配置的「排序規則」,對「目标資料」進行排序,得到「結果資料」。

技術方案如下:

  • 每天運作導數任務,把現有的千萬量級的底池資料(Hive 表)導入到 Clickhouse 中,後續使用 CK 表進行資料篩選。
  • 将業務配置的篩選規則和排序規則,建構為一個「篩選 + 排序」對象 SelectionQueryCondition。
  • 從 CK 底池表取「目标資料」時,開啟多線程,進行分頁篩選,将擷取到的「目标資料」存放到 result 清單中。
//分頁大小  預設 5000
int pageSize = this.getPageSize();
//頁碼數
int pageCnt = totalNum / this.getPageSize() + 1;


List<Map<String, Object>> result = Lists.newArrayList();
List<Future<List<Map<String, Object>>>> futureList = new ArrayList<>(pageCnt);


//開啟多線程調用
for (int i = 1; i <= pageCnt; i++) {
    //将業務配置的篩選規則和排序規則 建構為 SelectionQueryCondition 對象
    SelectionQueryCondition selectionQueryCondition = buildSelectionQueryCondition(selectionQueryRuleData);
    selectionQueryCondition.setPageSize(pageSize);
    selectionQueryCondition.setPage(i);
    futureList.add(selectionQueryEventPool.submit(new QuerySelectionDataThread(selectionQueryCondition)));
}




for (Future<List<Map<String, Object>>> future : futureList) {
    //RPC 調用
    List<Map<String, Object>> queryRes = future.get(20, TimeUnit.SECONDS);
    if (CollectionUtils.isNotEmpty(queryRes)) {
        // 将目标資料存放在 result 中
        result.addAll(queryRes);
    }
}           
  • 對目标資料 result 進行排序,得到最終的「結果資料」。

二、CK分頁查詢

在「初版設計方案」章節的第 3 步提到了「從 CK 底池表取目标資料時,開啟多線程,進行分頁篩選」。此處對 CK 分頁查詢進行介紹。

  • 封裝了 queryPoolSkuList 方法,負責從 CK 表中獲得目标資料。該方法内部調用了 sqlSession.selectList 方法。
public List<Map<String, Object>> queryPoolSkuList( Map<String, Object> params ) {
    List<Map<String, Object>> resultMaps = new ArrayList<>();




    QueryCondition queryCondition = parseQueryCondition(params);
    List<Map<String, Object>> mapList = lianNuDao.queryPoolSkuList(getCkDt(),queryCondition);
    if (CollectionUtils.isNotEmpty(mapList)) {
        for (Map<String,Object> data : mapList) {
            resultMaps.add(camelKey(data));
        }
    }
    return resultMaps;
}           
// lianNuDao.queryPoolSkuList


@Autowired
@Qualifier("ckSqlNewSession")
private SqlSession sqlSession;


public List<Map<String, Object>> queryPoolSkuList( String dt, QueryCondition queryCondition ) {
    queryCondition.setDt(dt);
    queryCondition.checkMultiQueryItems();
    return sqlSession.selectList("LianNu.queryPoolSkuList",queryCondition);
}           
  • sqlSession.selectList 方法中調用了和 CK 互動的 queryPoolSkuList 查詢方法,部分代碼如下。
<select id="queryPoolSkuList" parameterType="com.jd.bigai.domain.liannu.QueryCondition" resultType="java.util.Map">
    select sku_pool_id,i
    tem_sku_id,
    skuPoolName,
    price,
    ...
    ...
    businessType
    from liannu_sku_pool_indicator_all
    where
    dt=#{dt}
    and
    <foreach collection="queryItems" separator=" and " item="queryItem" open=" " close=" " >
        <choose>
            <when test="queryItem.type == 'equal'">
                ${queryItem.field} = #{queryItem.value}
            </when>
            ...
            ...
        </choose>
    </foreach>
    <if test="orderBy == null">
        group by sku_pool_id,item_sku_id
    </if>
    <if test="orderBy != null">
        group by sku_pool_id,item_sku_id,${orderBy} order by ${orderBy} ${orderAd}
    </if>
    <if test="limitEnd != 0">
        limit #{limitStart},#{limitEnd}
    </if>
</select>           
  • 可以看到,在 CK 分頁查詢時,是通過 limit #{limitStart},#{limitEnd} 實作的分頁。

limit 分頁方案,在「深翻頁」時會存在性能問題。初版方案上線後,在 1000W 量級的底池資料中篩選 10W 的資料,最壞耗時會達到 10s~18s 左右。

三、使用ES Scroll Scan優化深翻頁

對于 CK 深翻頁時候的性能問題,進行了優化,使用 Elasticsearch 的 scroll scan 翻頁方案進行優化。

1、ES的翻頁方案

ES 翻頁,有下面幾種方案:

  • from + size 翻頁
  • scroll 翻頁
  • scroll scan 翻頁
  • search after 翻頁
面對千萬級資料查詢,CK、ES、RediSearch誰才是王炸?

對上述幾種翻頁方案,查詢不同數目的資料,耗時資料如下表。

面對千萬級資料查詢,CK、ES、RediSearch誰才是王炸?

2、耗時資料

此處,分别使用 Elasticsearch 的 scroll scan 翻頁方案、初版中的 CK 翻頁方案進行資料查詢,對比其耗時資料。

面對千萬級資料查詢,CK、ES、RediSearch誰才是王炸?
面對千萬級資料查詢,CK、ES、RediSearch誰才是王炸?

如上測試資料,可以發現,以十萬,百萬,千萬量級的底池為例。

  • 底池量級越大,查詢相同的資料量,耗時越大。
  • 查詢結果 3W 以下時,ES 性能優;查詢結果 5W 以上時,CK 多線程性能優。

四、ES+Hbase組合查詢方案

在「使用 ES Scroll Scan 優化深翻頁」中,使用 Elasticsearch 的 scroll scan 翻頁方案對深翻頁問題進行了優化,但在實作時為單線程調用,是以最終測試耗時資料并不是特别理想,和 CK 翻頁方案性能差不多。

在調研階段發現,從底池中取出 10W 的目标資料時,一個商品包含多個字段的資訊(CK 表中一行記錄有 150 個字段資訊),如價格、會員價、學生價、庫存、好評率等。對于一行記錄,當減少擷取字段的個數時,查詢耗時會有明顯下降。如對 sku1的商品,從之前擷取價格、會員價、學生價、親友價、庫存等 100 個字段資訊,縮減到隻擷取價格、庫存這兩個字段資訊。

如下圖所示,使用 ES 查詢方案,對查詢同樣條數的場景(從千萬級底池中篩選出 7W+ 條資料),擷取的每條記錄的字段個數從 32 縮減到 17,再縮減到 1個(其實是兩個字段,一個是商品唯一辨別 sku_id,另一個是 ES 對每條文檔記錄的 doc_id)時,查詢的耗時會從 9.3s 下降到 4.2s,再下降到 2.4s。

面對千萬級資料查詢,CK、ES、RediSearch誰才是王炸?

從中可以得出如下結論:

  • 一次 ES 查詢中,若查詢字段和資訊較多,fetch 階段的耗時,遠大于 query 階段的耗時。
  • 一次 ES 查詢中,若查詢字段和資訊較多,通過減少不必要的查詢字段,可以顯著縮短查詢耗時。

下面對結論中涉及的 query 和 fetch 查詢階段進行補充說明。

1、ES查詢的兩個階段:query和fetch

在 ES 中,搜尋一般包括兩個階段,query 和 fetch 階段。

  • query 階段

根據查詢條件,确定要取哪些文檔(doc),篩選出文檔 ID(doc_id)。

  • fetch 階段

根據 query 階段傳回的文檔 ID(doc_id),取出具體的文檔(doc)。

2、ES的filesystem cache

1)ES 會将磁盤中的資料自動緩存到 filesystem cache,在記憶體中查找,提升了速度。

2)若 filesystem cache 無法容納索引資料檔案,則會基于磁盤查找,此時查詢速度會明顯變慢。

3)若數量兩過大,基于「ES 查詢的的 query 和 fetch 兩個階段」,可使用 ES + HBase 架構,保證 ES 的資料量小于 filesystem cache,保證查詢速度。

3、組合使用Hbase

在上文調研的基礎上,發現「減少不必要的查詢展示字段」可以明顯縮短查詢耗時。沿着這個優化思路,參考相關資料設計了一種新的查詢方案。

  • ES 僅用于條件篩選,ES 的查詢結果僅包含記錄的唯一辨別 sku_id(其實還包含 ES 為每條文檔記錄的 doc_id)。
  • Hbase 是列存儲資料庫,每列資料有一個 rowKey。利用 rowKey 篩選一條記錄時,複雜度為 O(1)。(類似于從 HashMap 中根據 key 取 value)。
  • 根據 ES 查詢傳回的唯一辨別 sku_id,作為 Hbase 查詢中的 rowKey,在 O(1) 複雜度下擷取其他資訊字段,如價格,庫存等。
面對千萬級資料查詢,CK、ES、RediSearch誰才是王炸?

使用 ES + Hbase 組合查詢方案,線上上進行了小規模的灰階測試。在 1000W 量級的底池資料中篩選 10W 的資料,對比 CK 翻頁方案,最壞耗時從 10~18s 優化到了 3~6s 左右。

也應該看到,使用 ES + Hbase 組合查詢方案,會增加系統複雜度,同時資料也需要同時存儲到 ES 和 Hbase。

五、RediSearch+RedisJSON優化方案

RediSearch 是基于 Redis 建構的分布式全文搜尋和聚合引擎,能以極快的速度在 Redis 資料集上執行複雜的搜尋查詢。RedisJSON 是一個 Redis 子產品,在 Redis 中提供 JSON 支援。RedisJSON 可以和 RediSearch 無縫配合,實作索引和查詢 JSON 文檔。

根據一些參考資料,RediSearch + RedisJSON 可以實作極高的性能,可謂碾壓其他 NoSQL 方案。在後續版本疊代中,可考慮使用該方案來進一步優化。

下面給出 RediSearch + RedisJSON 的部分性能資料。

1、RediSearch 性能資料

在同等伺服器配置下索引了 560 萬個文檔 (5.3GB),RediSearch 建構索引的時間為 221 秒,而 Elasticsearch 為 349 秒。RediSearch 比 ES 快了 58%。

資料建立索引後,使用 32 個用戶端對兩個單詞進行檢索,RediSearch 的吞吐量達到 12.5K ops/sec,ES 的吞吐量為 3.1K ops/sec,RediSearch 比ES 要快 4 倍。同時,RediSearch 的延遲為 8ms,而 ES 為 10ms,RediSearch 延遲稍微低些。

面對千萬級資料查詢,CK、ES、RediSearch誰才是王炸?

2、RedisJSON 性能資料

根據官網的性能測試報告,RedisJson + RedisSearch 可謂碾壓其他 NoSQL。

  • 對于隔離寫入(isolated writes),RedisJSON 比 MongoDB 快 5.4 倍,比 ES 快 200 倍以上。
  • 對于隔離讀取(isolated reads),RedisJSON 比 MongoDB 快 12.7 倍,比 ES 快 500 倍以上。

在混合工作負載場景中,實時更新不會影響 RedisJSON 的搜尋和讀取性能,而 ES 會受到影響。

  • RedisJSON 支援的操作數/秒比 MongoDB 高約 50 倍,比 ES 高 7 倍/秒。
  • RedisJSON 的延遲比 MongoDB 低約 90 倍,比 ES 低 23.7 倍。

此外,RedisJSON 的讀取、寫入和負載搜尋延遲,在更高的百分位數中遠比 ES 和 MongoDB 穩定。當增加寫入比率時,RedisJSON 還能處理越來越高的整體吞吐量。而當寫入比率增加時,ES 會降低它可以處理的整體吞吐量。

總結

本文從一個業務訴求觸發,對「千萬量級資料中查詢 10W 量級的資料」介紹了不同的設計方案。對于「在 1000W 量級的底池資料中篩選 10W 的資料」的場景,不同方案的耗時如下:

  • 多線程 + CK 翻頁方案,最壞耗時為 10s~18s。
  • 單線程 + ES scroll scan 深翻頁方案,相比 CK 方案,并未見到明顯優化。
  • ES + Hbase 組合方案,最壞耗時優化到了 3s~6s。
  • RediSearch + RedisJSON 組合方案,後續會實測該方案的耗時。

>>>>

參考資料

  • ES翻頁優化和性能優化 | 掘金
  • ES 億級資料檢索優化,三秒傳回突破性能瓶頸 | InfoQ
  • RedisJson 橫空出世,性能碾壓 ES 和 Mongo | 掘金

作者丨變速風聲

來源丨網址:

https://juejin.cn/post/7104090532015505416

dbaplus社群歡迎廣大技術人員投稿,投稿郵箱:[email protected]

更多精彩内容

dbaplus社群最新一期直播【話題接力丨大資料上雲,如何發揮出1+1>2的優勢?】将于今晚8點開播,dbaplus社群邀請到攜程 研發總監-周昕毅、騰訊 PCG大資料平台部SRE負責人-童光輝在雲上彙聚,希望通過彙集兩位大資料專家的研究成果和實踐積累,給大家進一步明确大資料平台發展的方向,提供可參考、可落地的大資料上雲實戰經驗。

直播位址:話題接力丨大資料上雲,如何發揮出1+1>2的優勢?

面對千萬級資料查詢,CK、ES、RediSearch誰才是王炸?

關于我們

dbaplus社群是圍繞Database、BigData、AIOps的企業級專業社群。資深大咖、技術幹貨,每天精品原創文章推送,每周線上技術分享,每月線下技術沙龍,每季度Gdevops&DAMS行業大會。

關注公衆号【dbaplus社群】,擷取更多原創技術文章和精選工具下載下傳

繼續閱讀