最近把搜索後端從AWS cloudsearch遷到了AWS ES和自建ES集羣。測試發現search latency高於以前的benchmark,可見模擬數據遠不如真實數據來的實在。此次在產線的backup ES上直接進行測試和優化,經過本文記錄search調優的主要過程。node
問題1:發現AWS ES shard級別的search latency是很是小的,符合指望,可是最終的查詢耗時卻很是大(ES response的took), 總體的耗時比預期要高出200ms~300ms。後端
troubleshooting過程:開始明顯看出問題在coordinator node收集數據排序及fetch階段。開始懷疑是由於AWS ES沒有dedicated coordinator節點,data node的資源不足致使這部分耗時較多,後來給全部data node進行來比較大的升級,排除了CPU,MEM, search thread_pool等瓶頸,而且經過cloud watch排除了EBS IOPS配額不夠的可能,可是,發現search latency並無減小。而後就懷疑是network的延時, 就把集羣從3個AV調整到1個AV,發現問題依舊。無奈,聯繫了AWS的support,AWS ES team拿咱們的數據和query語句作了benchmark,發現沒有某方面的資源瓶頸。這個開始讓咱們很疑惑,由於在自建ES集羣上search latency明顯小於AWS ES,兩個集羣的版本,規格,數據量都差很少。後來AWS回覆說是他們那邊的架構問題,比之自建集羣,AWS ES爲了適應公有云上的security, loadbalance要求,在整個請求鏈路上加了一些組件,致使了總體延時的增長。緩存
肯定方案:限於ES cluster不受控,咱們只能從自身的數據存儲和查詢語句上去優化。session
存儲優化:架構
1. index sort。咱們的查詢結果返回都是按時間(created_time)排序的,因此存儲的時候即按created_time進行有序存儲,方便單segment內的查詢提早中斷查詢,提高查詢效率。測試
2. segment merge。索引是按季度存儲的,把2019年以前的索引進行了force merge,進行段合併,2019年以前的索引肯定都是隻讀的。fetch
3. 索引優化。合併了一些小索引,2016,2017年的數據量比較少,把這兩年的索引進行合併,減小總shard數。經過建立原索引的別名指向新索引,保證search和index的邏輯不用改動。優化
查詢優化。ui
先經過profile API定位耗時的子查詢語句。spa
1. 合併查詢字段。一個比較耗時子查詢查詢以下,一般session_id的list size>100,receiver_id和sender_id也會匹配到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 } } } ] }
新開一個字段session_receiver_sender_id,經過copy_to把每條記錄的session_id,receiver_id, sender_id都放到這個字段上。把query語句改成
{ "terms": { "session_receiver_sender_id": [ "ab", "cd", "efg", "hij" ], "boost": 1 } }
不過,測試結論顯示,合併以後query耗時並無明顯縮短,感受改動意義不大。推測多是咱們的BoolQuery字段並很少(就3個),可是terms的size不少(100以上),由於不論是多個字段每一個對應一個termQuery,仍是一個terms query, 都是轉成BoolQuery,最終都是多個termQuery作or。
2. 優化date range查詢。另一個比較耗時的查詢是date range。Lucene會rewrite成一個DocValuesFieldExistsQuery。
"filter": [ { "range": { "created_time": { "from": 1560993441118, "to": null, "include_lower": true, "include_upper": true, "boost": 1 } } }, ... ]
這裏匹配到的docId的確很是多,date range結果在構造docIdset與別的子查詢語句作conjunction耗時較大。
採用的一個解決方案是儘可能對這個子查詢進行緩存,把這個date range查詢拆成兩段,分爲3個月前到昨天,昨天到今天兩段,通常昨天的數據再也不變化,在沒有觸發segment merge的狀況下3個月前到昨天到查詢結果應該能緩存較長時間。
"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" } } } ] } } }
相應的,在用戶可接受的前提下,調大索引的refresh_interval。
問題2: 在自建ES集羣上,發現某個索引500ms以上的搜索耗時佔比較多。
這個索引每日大概30w次查詢,落在100ms之內的查詢超過90%,可是依舊有1%的查詢落在500ms以上。發現一樣的query語句模版,但若是某些子查詢條件匹配到的數據比較多,查詢會變對特別慢。
troubleshooting過程:一樣是經過profile參數分析比較耗時的查詢子句。發現一個PointInSetQuery很是耗時,這個子查詢是對一個名爲user_type的Integer字段作terms查詢,子查詢內部又耗時在build_score階段。
經過查找lucene的代碼和相關文章,發現lucene把numeric類型的字段索引成BKD-tree,內部的docId是無序的,與其餘查詢結果作交集前構造Bitset比較耗時,從而把Integer類型改爲keyword,把這個查詢轉成TermQuery,這樣哪怕命中的數據不少,在build_score的時候由於倒排鏈的docId有序性,利用skiplist,能夠更快速的構建一個Bitset。在把這個字段改爲keyword後,50th的查詢耗時並無多大差別,可是90th、99th的search latency明顯小於以前。
另外一個優化,這個索引裏的每條數據都是一個非空的accout_id字段,accout_id在query語句裏會用於terms查詢。遂把這個accout_id字段做爲routing進行存儲。同時能夠對查詢語句進行修改:
#原query "filter": [ { "terms": { "account_id": [ "abc123" ], "boost": 1 } } ... ] #改成 "filter": [ { "terms": { "_routing": [ "abc123" ] } } ... ]
查詢改成_routing以後,發現總體的search latency大幅下降。
通過這兩次改動,針對這個索引的search latency基本知足需求。
另外,還有一個小改動,經過preload docvalue, 能夠減小首次查詢的耗時。