當前位置:
首頁 > 知識 > 聚合查詢慢——詳解Global Ordinals與High Cardinality

聚合查詢慢——詳解Global Ordinals與High Cardinality

Elasticsearch中的概念很多,本文將從筆者在實踐過程中遇到的問題出發,逐步詳細介紹 Global Ordinals 和 High Cardinality ,這也是筆者的認知過程。文中的Elasticsearch 版本為5.5。

背景

故事是這樣的,因為業務需要,我們在項目中設計了一種針對Elasticsearch數據的非同步去重方法(註:關於Elasticsearch數據去重,筆者會在另一篇博文中更加詳細介紹),基本思路是:

為每一條數據計算hash值,作為document的一個欄位(keyword類型)插入到Elasticsearch中,數據格式簡化如下:

{

"timestamp": 1540099182,

"msgType": 1210,

......

"hash": "31a2c683dccb83ef8b8d1ee43290df62"

}

1

2

3

4

5

6

每隔一段時間,運行一次檢測腳本,檢查Elasticsearch中的數據是否有重複,相關查詢語句如下(這裡,terms聚合用於發現給定時間範圍內是否有超過2條hash值一樣的數據,top_hits聚合用於找出重複數據組中的具體數據信息,然後刪除掉重複的數據即可):

{

"size": 0,

"query": {

"bool": {

"filter": [

{

"range":{

"timestamp":{

"gte": 1540087200,

"lt": 1540087500

}

}

}

]

}

},

"aggs": {

"duplications": {

"terms": {

"field": "hash",

"min_doc_count": 2,

"size": 500

},

"aggs": {

"top_duplications": {

"top_hits": {

"size": 3

}

}

}

}

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

這樣一個方案,因為只是在數據集中增加了一個hash欄位,並且去重是非同步的,不會影響到原有的設計,所以在通過相關的功能性測試後就上線了。然而,運行一段時間後,出現了嚴重問題:

隨著新數據的寫入,上述的查詢語句變得越來越慢,從秒級逐步變成要20多秒,並且在數據量超過10億條後,每次查詢都會使內存超過80%;

index的存儲空間比原先增加了近一倍

對於類似上述的查詢語句,Elasticsearch會先根據Filter條件找出匹配的document,然後再進行聚合運算。在我們的業務中,每次查詢2小時內的數據,並且數據的寫入是勻速的,這意味著每次匹配出來的document個數基本是固定的,那麼為何會出現這個查詢越來越慢的問題?而且,我們發現,即使Filter匹配的document個數為0,也同樣需要很久才能返回結果。

聚合查詢慢——詳解Global Ordinals與High Cardinality

另一方面,經過對比驗證,可以確定是新增加的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

1

2

3

4

5

6

為了減少內存使用,考慮將字元串排序後進行編號,形成一張映射表,然後在每條數據中使用相應字元串的序號來表示。通過這樣的設計,可以將所需內存從15 GB減少為1 GB左右。

這裡的映射表,或者說映射表中的序號,就是Ordinals。

Ordinal | Term

-------------------------------

0 | status_deleted

1 | status_pending

2 | status_published

Doc | Ordinal

-------------------------------

0 | 0 # deleted

1 | 2 # published

2 | 1 # pending

3 | 0 # deleted

1

2

3

4

5

6

7

8

9

10

11

12

什麼是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與High Cardinality

為何會影響聚合查詢?

構建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小時內的數據量。這也是當前我們的優化方法。

GET /_search

{

"aggs" : {

"tags" : {

"terms" : {

"field" : "status",

"execution_hint": "map"

}

}

}

}

1

2

3

4

5

6

7

8

9

10

11

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的數據量。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序員小新人學習 的精彩文章:

python如何進行內存管理
為什麼我們做分散式的使用都是用Redis?

TAG:程序員小新人學習 |