千萬級索引的聚合性能優化

當搜索引擎 ElasticSearch 面對千萬級索引量的去重統計時,該如何實現快速的響應。本文將結合我們的親身經歷,爲讀者朋友呈現生產環境中遇到這類問題時的解決思路。

1. 背景

在數週前的某一天,交易團隊的同學發現運行在某雲上面的訂單索引存在嚴重的增量同步延遲問題,且已經對業務造成了影響。所以將該索引的流量切換到自研搜索平臺中,雖說自研搜索不存在增量延遲情況,但卻發現查詢的 RT 竟高達十幾秒,依舊無法解決業務面臨的困境。

當時獲知該情況時還是比較錯愕的,接口 RT 的增長通常是個漸進式的過程,既然存在性能問題應該在早期就有所表現,不至於突然暴漲至十幾秒。進一步瞭解情況後,得知一年前由於當時的自研搜索平臺基建不夠成熟,該索引的查詢流量便一直由三方雲服務承接。過去這麼長時間,自研搜索平臺僅保留着該索引的增量功能,但從未對外提供檢索服務。而如今突然將查詢流量導入自研平臺,不曾預料到會存在如此嚴重的查詢性能問題。

ElasticSearch 作爲一款能夠輕鬆應對上億規模檢索的分佈式搜索引擎,卻發生如此“反常”的表現,下意識就覺得癥結應該出在我們的使用方式上。在抓取了相關的 DSL 語句後很快便定位到了問題根源,主要由於業務場景中會對某個查詢字段作去重後的計數統計,用到了 ES 中 cardinality 這一項聚合功能。這原本是個非常普通的操作,然而由於匹配到的訂單索引數高達千萬級,此時的聚合操作需要消耗大量的計算資源,以致RT暴漲。

2. 原因

爲了證實慢查是因聚合所致,我們先後做了兩次對比實驗。

第一次包含去重查詢,涉及數據量1450W,用時超 7 秒。

(爲什麼沒有上文提到的超10秒?這是因爲期間我們做過一次ES集羣擴容,增加節點和分片數後執行效率有所提升)

第二次移除聚合語句,此時的查詢耗時僅 184 毫秒。

要了解背後的差異,我們需要先對cardinality建立認知。它是基於 HyperLogLog++ (HLL)算法實現的一種近似度量算法。這涉及到對輸入條件作 hash 運算,然後根據哈希運算的結果中的 bits 做概率估算從而得到基數。

HLL 本身便是一種非常高效的算法,可畢竟還是需要對全量的數據集合都做一遍 hash 運算。如果是幾萬、幾十萬的統計量,用戶對於由此產生額外的幾十毫秒,甚至上百毫秒性能開銷是不太敏感的。可倘若統計量達到百萬級,乃至千萬級,計算時長增加了幾十倍,上百倍,慢查的體感則非常明顯。

至此,我們可以得出一個結論:引發慢查的直接原因是由於需要參與 hash 運算的數據集合過於龐大。

3. 步入誤區

找到原因後,便可以對症下藥。不過很遺憾,當時我們採取的第一個解決方案不僅沒有獲得預期的效果,反而引發了其他問題。

起初我們認爲,既然導致慢查的直接原因是參與 hash 運算的數據量太多了,那我們是否可以在保證不改變召回結果的前提下,通過減少參與聚合統計的數據量來改善性能。

雖然這種聚合方式會導致統計結果失真,但由於系統本就要求召回結果限制在1萬以內( 比如匹配查詢條件的索引數有2萬條,但系統提供的分頁能力最多查詢前1萬條索引),這意味着只需針對排序的前1萬條記錄作聚合也是可被接受的。

順着這個思路,我從 ES 文檔中找到了 terminate_after。該屬性會限制查詢請求在每個索引分片中召回的記錄數,緩解了因匹配索引數過多而引發的資源開銷。

乍一看這確實是我們需要的解決方案,可是上線後才發現使用 terminate_after之後召回的結果不是“最佳”匹配,僅僅是符合過濾條件而已,最直觀的表現便是排序效果失效,因此不得已只能棄用該方案。

4. 尋找正解

既然無法減少聚合數據集,我們便只能從節省 hash 運算開銷這個方向入手。幸運的是, ElasticSearch 已經爲此提供了很好的解決方案,即:hash預運算。

這是一種將 Hash 運算的過程從查詢階段的實時計算,前置到索引創建階段的策略。在創建索引的同時,計算出待聚合字段的 hash 值並寫入索引文件,查詢環節便可直接對 hash 進行統計。該策略尤爲適用讀多寫少,且聚合基數龐大的場景。

而要啓用該策略,我們還需要對ES集羣和搜索工程做一些調整。具體如下:

  1. 在ES集羣中安裝mapper-murmur3插件並重啓服務。

    sudo bin/elasticsearch-plugin install mapper-murmur3

     

  2. 修改索引模板,爲聚合字段設置 hash field。

    {
    	"mappings": {
            "trade_id": {
        	    "type" : "keyword",
                "fields" : {
                    "hash" : {
                        "type" : "murmur3"
                    }
                }
            }
        }
    }

     

  3. 改寫查詢時的DSL語句,採用<聚合字段>.hash的形式查詢。

通過 hash 預運算的方式,我們可以看到查詢性能由原先的 7 秒多驟降至不到500ms,幾乎可以算是完美解決了聚合的性能問題。

5. 思考

雖說本次遇到的問題算是得到了圓滿的解決,但是在我看來 hash 預運算也只能作爲階段性方案。假設我們需要統計的數據集合高達數億、數十億、百億,屆時我們依舊陷入了因量變而導致質變的局面。

所以,我認爲最終的解決方案還是需要在數據的統計量上作出妥協,迴歸到一開始誤入的那條“歧途“。只不過目前還未找到一種即保留排序效果,又能限制聚合索引數量的最優解。如對此有研究的朋友,歡迎留言交流。

<著作權歸作者所有,未經允許請勿轉載>

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章