Elasticsearch搜索調(diào)優(yōu)
最近把搜索后端從AWS cloudsearch遷到了AWS ES和自建ES集群。測試發(fā)現(xiàn)search latency高于之前的benchmark,可見模擬數(shù)據(jù)遠(yuǎn)不如真實(shí)數(shù)據(jù)來的實(shí)在。這次在產(chǎn)線的backup ES上直接進(jìn)行測試和優(yōu)化,通過本文記錄search調(diào)優(yōu)的主要過程。
問題1:發(fā)現(xiàn)AWS ES shard級(jí)別的search latency是非常小的,符合期望,但是最終的查詢耗時(shí)卻非常大(ES response的took), 整體的耗時(shí)比預(yù)期要高出200ms~300ms。
troubleshooting過程:開始明顯看出問題在coordinator node收集數(shù)據(jù)排序及fetch階段。開始懷疑是因?yàn)锳WS ES沒有dedicated coordinator節(jié)點(diǎn),data node的資源不足導(dǎo)致這部分耗時(shí)較多,后來給所有data node進(jìn)行來比較大的升級(jí),排除了CPU,MEM, search thread_pool等瓶頸,并且通過cloud watch排除了EBS IOPS配額不夠的可能,但是,發(fā)現(xiàn)search latency并沒有減少。然后就懷疑是network的延時(shí), 就把集群從3個(gè)AV調(diào)整到1個(gè)AV,發(fā)現(xiàn)問題依舊。無奈,聯(lián)系了AWS的support,AWS ES team拿我們的數(shù)據(jù)和query語句做了benchmark,發(fā)現(xiàn)沒有某方面的資源瓶頸。這個(gè)開始讓我們很疑惑,因?yàn)樵谧越‥S集群上search latency明顯小于AWS ES,兩個(gè)集群的版本,規(guī)格,數(shù)據(jù)量都差不多。后來AWS回復(fù)說是他們那邊的架構(gòu)問題,比之自建集群,AWS ES為了適應(yīng)公有云上的security, loadbalance要求,在整個(gè)請(qǐng)求鏈路上加了一些組件,導(dǎo)致了整體延時(shí)的增加。
確定方案:限于ES cluster不受控,我們只能從自身的數(shù)據(jù)存儲(chǔ)和查詢語句上去優(yōu)化。
存儲(chǔ)優(yōu)化:
1. index sort。我們的查詢結(jié)果返回都是按時(shí)間(created_time)排序的,所以存儲(chǔ)的時(shí)候即按created_time進(jìn)行有序存儲(chǔ),方便單segment內(nèi)的查詢提前中斷查詢,提升查詢效率。
2. segment merge。索引是按季度存儲(chǔ)的,把2019年之前的索引進(jìn)行了force merge,進(jìn)行段合并,2019年之前的索引確定都是只讀的。
3. 索引優(yōu)化。合并了一些小索引,2016,2017年的數(shù)據(jù)量比較少,把這兩年的索引進(jìn)行合并,減少總shard數(shù)。通過創(chuàng)建原索引的別名指向新索引,保證search和index的邏輯不用改動(dòng)。
查詢優(yōu)化。
先通過profile API定位耗時(shí)的子查詢語句。
1. 合并查詢字段。一個(gè)比較耗時(shí)子查詢查詢?nèi)缦拢ǔession_id的list size>100,receiver_id和sender_id也會(huì)匹配到n多條記錄。
{ "minimum_should_match": "1" "should": [ { "terms": { "session_id": [ "ab", "cd" ], "boost": 1 } }, { "term": { "receiver_id": { "value": "efg", "boost": 1 } } }, { "term": { "sender_id": { "value": "hij", "boost": 1 } } } ] }
新開一個(gè)字段session_receiver_sender_id,通過copy_to把每條記錄的session_id,receiver_id, sender_id都放到這個(gè)字段上。把query語句改為
{ "terms": { "session_receiver_sender_id": [ "ab", "cd", "efg", "hij" ], "boost": 1 } }
不過,測試結(jié)論顯示,合并之后query耗時(shí)并沒有明顯縮短,感覺改動(dòng)意義不大。推測可能是我們的BoolQuery字段并不多(就3個(gè)),但是terms的size很多(100以上),因?yàn)椴还苁嵌鄠€(gè)字段每個(gè)對(duì)應(yīng)一個(gè)termQuery,還是一個(gè)terms query, 都是轉(zhuǎn)成BoolQuery,最終都是多個(gè)termQuery做or。
2. 優(yōu)化date range查詢。另外一個(gè)比較耗時(shí)的查詢是date range。Lucene會(huì)rewrite成一個(gè)DocValuesFieldExistsQuery。
"filter": [ { "range": { "created_time": { "from": 1560993441118, "to": null, "include_lower": true, "include_upper": true, "boost": 1 } } }, ... ]
這里匹配到的docId的確非常多,date range結(jié)果在構(gòu)造docIdset與別的子查詢語句做conjunction耗時(shí)較大。
采用的一個(gè)解決方案是盡量對(duì)這個(gè)子查詢進(jìn)行緩存,把這個(gè)date range查詢拆成兩段,分為3個(gè)月前到昨天,昨天到今天兩段,一般昨天的數(shù)據(jù)不再變化,在沒有觸發(fā)segment merge的情況下3個(gè)月前到昨天到查詢結(jié)果應(yīng)該能緩存較長時(shí)間。
"constant_score": { "filter": { "bool": { "should": [ { "range": { "created_time": { "gte": "now-3M/d", "lte": "now-1d/d" } } }, { "range": { "created_time": { "gte": "now-1d/d", "lte": "now/d" } } } ] } } }
相應(yīng)的,在用戶可接受的前提下,調(diào)大索引的refresh_interval。
問題2: 在自建ES集群上,發(fā)現(xiàn)某個(gè)索引500ms以上的搜索耗時(shí)占比較多。
這個(gè)索引每日大概30w次查詢,落在100ms以內(nèi)的查詢超過90%,但是依舊有1%的查詢落在500ms以上。發(fā)現(xiàn)同樣的query語句模版,但如果某些子查詢條件匹配到的數(shù)據(jù)比較多,查詢會(huì)變對(duì)特別慢。
troubleshooting過程:同樣是通過profile參數(shù)分析比較耗時(shí)的查詢子句。發(fā)現(xiàn)一個(gè)PointInSetQuery非常耗時(shí),這個(gè)子查詢是對(duì)一個(gè)名為user_type的Integer字段做terms查詢,子查詢內(nèi)部又耗時(shí)在build_score階段。
通過查找lucene的代碼和相關(guān)文章,發(fā)現(xiàn)lucene把numeric類型的字段索引成BKD-tree,內(nèi)部的docId是無序的,與其他查詢結(jié)果做交集前構(gòu)造Bitset比較耗時(shí),從而把Integer類型改成keyword,把這個(gè)查詢轉(zhuǎn)成TermQuery,這樣哪怕命中的數(shù)據(jù)很多,在build_score的時(shí)候因?yàn)榈古沛湹膁ocId有序性,利用skiplist,可以更快速的構(gòu)建一個(gè)Bitset。在把這個(gè)字段改成keyword后,50th的查詢耗時(shí)并沒有多大差異,但是90th、99th的search latency明顯小于之前。

另一個(gè)優(yōu)化,這個(gè)索引里的每條數(shù)據(jù)都是一個(gè)非空的accout_id字段,accout_id在query語句里會(huì)用于terms查詢。遂把這個(gè)accout_id字段作為routing進(jìn)行存儲(chǔ)。同時(shí)可以對(duì)查詢語句進(jìn)行修改:
#原query "filter": [ { "terms": { "account_id": [ "abc123" ], "boost": 1 } } ... ] #改為 "filter": [ { "terms": { "_routing": [ "abc123" ] } } ... ]
查詢改為_routing之后,發(fā)現(xiàn)整體的search latency大幅降低。

經(jīng)過這兩次改動(dòng),針對(duì)這個(gè)索引的search latency基本滿足需求。
另外,還有一個(gè)小改動(dòng),通過preload docvalue, 可以減少首次查詢的耗時(shí)。

浙公網(wǎng)安備 33010602011771號(hào)