转-聚合查询变慢-详解Elasticsearch的Global Ordinals与High Cardinality
轉自: https://blog.csdn.net/zwgdft/article/details/83215977
?
【README】
?Elasticsearch中的概念很多,本文將從筆者在實踐過程中遇到的問題出發,逐步詳細介紹 Global Ordinals 和 High Cardinality ,這也是筆者的認知過程。文中的Elasticsearch 版本為5.5。
背景
??故事是這樣的,因為業務需要,我們在項目中設計了一種針對Elasticsearch數據的異步去重方法(注:關于Elasticsearch數據去重,筆者會在另一篇博文中更加詳細介紹),基本思路是:
- 為每一條數據計算hash值,作為document的一個字段(keyword類型)插入到Elasticsearch中,數據格式簡化如下:
- ?
- 每隔一段時間,運行一次檢測腳本,檢查Elasticsearch中的數據是否有重復,相關查詢語句如下(這里,terms聚合用于發現給定時間范圍內是否有超過2條hash值一樣的數據,top_hits聚合用于找出重復數據組中的具體數據信息,然后刪除掉重復的數據即可):
- ?
??這樣一個方案,因為只是在數據集中增加了一個hash字段,并且去重是異步的,不會影響到原有的設計,所以在通過相關的功能性測試后就上線了。然而,運行一段時間后,出現了嚴重問題:
- 隨著新數據的寫入,上述的查詢語句變得越來越慢,從秒級逐步變成要20多秒,并且在數據量超過10億條后,每次查詢都會使內存超過80%;
- index的存儲空間比原先增加了近一倍
??對于類似上述的查詢語句,Elasticsearch會先根據Filter條件找出匹配的document,然后再進行聚合運算。在我們的業務中,每次查詢2小時內的數據,并且數據的寫入是勻速的,這意味著每次匹配出來的document個數基本是固定的,那么為何會出現這個查詢越來越慢的問題?而且,我們發現,即使Filter匹配的document個數為0,也同樣需要很久才能返回結果。
??另一方面,經過對比驗證,可以確定是新增加的hash字段導致了數據存儲空間比原先增加了近一倍。
??帶著這些問題,筆者進行了詳細的調研,最終鎖定Global Ordinals與High Cardinality兩個核心概念。其中,github上面的一個issue Terms aggregation speed is proportional to field cardinality 給了很大的啟發。
Global Ordinals
什么是Ordinals??
??假設有10億條數據,每條數據有一個字段status(keyword類型),其值有三種可能性:status_pending、status_published、status_deleted,那么每條數據至少需要14-16 Bytes,也就是說需要將近15GB內存才能裝下所有數據。
Doc | Term ------------------------------- 0 | status_pending 1 | status_deleted 2 | status_published 3 | status_pending- ?
??為了減少內存使用,考慮將字符串排序后進行編號,形成一張映射表,然后在每條數據中使用相應字符串的序號來表示。通過這樣的設計,可以將所需內存從15 GB減少為1 GB左右。
??這里的映射表,或者說映射表中的序號,就是Ordinals。
- ?
什么是Global Ordinals??
??當我們對status字段做Terms聚合查詢時,請求會透過Coordinate Node分散到Shard所在的Node中執行,而針對每個Shard的查詢又會分散到多個Segment中去執行。
??上述的Ordinals是per-segment ordinals,是針對每個Segment里面的數據而言,意味著同一個字符串在不同的per-segment ordinals中可能對應的序號是不同的。比如,在Segment 1中只有status_deleted(0)和status_published(1)兩個值,而Segment 2中有3個值:status_deleted(0),status_pending(1),status_published(2)。
??這樣就面臨一個抉擇:方案一,在完成per-segment的查詢后,將相應的序號轉換成字符串,返回到Shard層面進行合并;方案二,構建一個Shard層面的Global Ordinals,實現與per-segment ordinals的映射,就可以在Shard層面完成聚合后再轉換成字符串。
??經過權衡,Elasticsearch(Lucene)選擇了方案二作為默認方法:構建Global Ordinals。
為何會影響聚合查詢?
??構建Global Ordinals的目的是為了減少內存使用、加快聚合統計,在大多數情況下其表現出來的性能都非常好。之所以會影響到查詢性能,與其構建時機有關:
- 由于Global Ordinals是Shard級別的,因此當一個Shard的Segment發生變動時就需要重新構建Global Ordinals,比如有新數據寫入導致產生新的Segment、Segment Merge等情況。當然,如果Segment沒有變動,那么構建一次后就可以一直利用緩存了(適用于歷史數據)。
- 默認情況下,Global Ordinals是在收到聚合查詢請求并且該查詢會命中相關字段時構建,而構建動作是在查詢最開始做的,即在Filter之前。
??這樣的構建方式,在遇到某個字段的值種類很多(即下文所述的High Cardinary問題)時會變的非常慢,會嚴重影響聚合查詢速度,即使Filter出來的document很少也需要花費很久,也就是上文筆者遇到的問題,即在High Cardinary情況下,構建Global Ordinals非常慢。因為我們新加的hash字段對于每條數據都不一樣,所以當寫入越來越多的數據后,聚合查詢越來越慢(大概超過5000W條之后)。
有哪些優化方法?
??雖然在Lucene 7.1中,針對global ordinals的構建有些優化(LUCENE-7905),但是仍然不能避免這樣的問題。目前有這樣幾種優化方法(或者說是緩解之法,目前尚未發現完美的方法):
- 增加Shard個數。因為Global Ordinals是Shard層面的,增加Shard個數也許可以緩解問題,前提是:第一,要能確定有問題的字段的值種類可以通過該方式減少在單個Shard中的量;第二,確保Shard的個數增加不會影響到整體的性能。
- 延長refresh interval,即減少構建Global Ordinals的次數來緩解其影響,前提是要能接受數據的非實時性。
- 修改execution_hint的值。在Terms聚合中,可以設置執行方式是map還是global_ordinals,前者的意思是直接使用該字段的字符串值來做聚合,即無需構建Global Ordinals。這樣的方式,適用于可以確定匹配文檔數據量的場景,并且不會引起內存的暴增,比如在筆者的業務場景中,每次只查詢2小時內的數據量。這也是當前我們的優化方法。
- ?
High Cardinality?
??相信看完上文,讀者已經知道什么是High Cardinality了。所謂High Cardinality,指的是Large Number of Unique Values,即某個字段的值有很多很多種,比如筆者業務中的那個hash字段。在Elasticsearch,High Cardinality會帶來各種問題,百害而無一利,所以應該盡量避免,避免不了也要做到心中有數,在出問題時可以及時調整。
- High Cardinality會導致構建Global Ordinals過程變慢,從而導致聚合查詢變慢、內存使用過高。
- High Cardinality會導致壓縮比率降低,從而導致存儲空間增加,特別是像hash值這樣完全隨機的字符串。
- 對High Cardinality字段執行Cardinality聚合查詢時,會受到精度控制從而導致結果不精確。
?
??本文結合筆者在實踐過程中遇到的由High Cardinality引起Global Ordinals構建過慢,從而導致聚合查詢變慢的問題,闡述了Global Ordinals和High Cardinality兩個核心概念,希望對遇到類似問題的人有所幫助。目前,針對我們的業務場景,相關的調整有:第一,使用"execution_hint": "map"來避免構建Global Ordinals;第二,嘗試在數據上傳端增加對壓縮友好的唯一鍵來作為去重對象,比如uuid4;第三,減小index的切割時間,比如從weekly index變成daily index,從而降低index中單個shard的數據量。
?
?
總結
以上是生活随笔為你收集整理的转-聚合查询变慢-详解Elasticsearch的Global Ordinals与High Cardinality的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: dreamweaver怎么打开整站(dr
- 下一篇: 怎么提交网页(怎么提交网页文件)