記錄一次 ElasticSearch 的查詢性能優化
問題: 慢查詢
搜索平臺的公共集羣,由於業務衆多,對業務的 es 查詢語法缺少約束,導致問題頻發。業務可能寫了一個巨大的查詢直接把集羣打掛掉,但是我們平臺人力投入有限,也不可能一條條去審覈業務的 es 查詢語法,只能通過後置的手段去保證整個集羣的穩定性,通過 slowlog 分析等,下圖中 cpu 已經 100% 了。
昨天剛好手頭有一點點時間,就想着能不能針對這些情況,把影響最壞的業務抓出來,進行一些改善,於是昨天花了 2 小時分析了一下,找到了一些共性的問題,可以通過平臺來很好的改善這些情況。
首先通過 slowlog 抓到一些耗時比較長的查詢,例如下面這個索引的查詢耗時基本都在 300ms 以上:
{
"from": 0,
"size": 200,
"timeout": "60s",
"query": {
"bool": {
"must": \[
{
"match": {
"source": {
"query": "5",
"operator": "OR",
"prefix\_length": 0,
"fuzzy\_transpositions": true,
"lenient": false,
"zero\_terms\_query": "NONE",
"auto\_generate\_synonyms\_phrase\_query": "false",
"boost": 1
}
}
},
{
"terms": {
"type": \[
"21"
\],
"boost": 1
}
},
{
"match": {
"creator": {
"query": "0d754a8af3104e978c95eb955f6331be",
"operator": "OR",
"prefix\_length": 0,
"fuzzy\_transpositions": "true",
"lenient": false,
"zero\_terms\_query": "NONE",
"auto\_generate\_synonyms\_phrase\_query": "false",
"boost": 1
}
}
},
{
"terms": {
"status": \[
"0",
"3"
\],
"boost": 1
}
},
{
"match": {
"isDeleted": {
"query": "0",
"operator": "OR",
"prefix\_length": 0,
"fuzzy\_transpositions": "true",
"lenient": false,
"zero\_terms\_query": "NONE",
"auto\_generate\_synonyms\_phrase\_query": "false",
"boost": 1
}
}
}
\],
"adjust\_pure\_negative": true,
"boost": 1
}
},
"\_source": {
"includes": \[
\],
"excludes": \[\]
}
}
這個查詢比較簡單,翻譯一下就是:
SELECT guid FROM xxx WHERE source=5 AND type=21 AND creator='0d754a8af3104e978c95eb955f6331be' AND status in (0,3) AND isDeleted=0;
慢查詢分析
這個查詢問題還挺多的,不過不是今天的重點。比如這裏面不好的一點是還用了模糊查詢 fuzzy_transpositions, 也就是查詢 ab 的時候,ba 也會被命中,其中的語法不是今天的重點,可以自行查詢,我估計這個是業務用了 SDK 自動生成的,裏面很多都是默認值。
第一反應是當然是用 filter 來代替 match 查詢,一來 filter 可以緩存,另外避免這種無意義的模糊匹配查詢,但是這個優化是有限的,並不是今天講解的關鍵點,先忽略。
錯用的數據類型
我們通過 kibana 的 profile 來進行分析,耗時到底在什麼地方?es 有一點就是開源社區很活躍,文檔齊全,配套的工具也非常的方便和齊全。
可以看到大部分的時間都花在了 PointRangQuery 裏面去了,這個是什麼查詢呢?爲什麼這麼耗時呢?這裏就涉及到一個 es 的知識點,那就是對於 integer 這種數字類型的處理。在 es2.x 的時代,所有的數字都是按 keyword 處理的,每個數字都會建一個倒排索引,這樣查詢雖然快了,但是一旦做範圍查詢的時候。比如 type>1 and type<5 就需要轉成 type in (1,2,3,4,5) 來進行,大大的增加了範圍查詢的難度和耗時。
之後 es 做了一個優化,在 integer 的時候設計了一種類似於 b-tree 的數據結構,加速範圍的查詢,詳細可以參考 (https://elasticsearch.cn/article/446)
所以在這之後,所有的 integer 查詢都會被轉成範圍查詢,這就導致了上面看到的 isDeleted 的查詢的解釋。那麼爲什麼範圍查詢在我們這個場景下,就這麼慢呢?能不能優化。
明明我們這個場景是不需要走範圍查詢的,因爲如果走倒排索引查詢就是 O(1) 的時間複雜度,將大大提升查詢效率。由於業務在創建索引的時候,isDeleted 這種字段建成了 Integer 類型,導致最後走了範圍查詢,那麼只需要我們將 isDeleted 類型改成 keyword 走 term 查詢,就能用上倒排索引了。
實際上這裏還涉及到了 es 的一個查詢優化。類似於 isDeleted 這種字段,毫無區分度的倒排索引的時候,在查詢的時候,es 是怎麼優化的呢?
多個 Term 查詢的順序問題
實際上,如果有多個 term 查詢並列的時候,他的執行順序,既不是你查詢的時候,寫進去的順序。
例如上面這個查詢,他既不是先執行 source=5 再執行 type=21 按照你代碼的順序執行過濾,也不是同時併發執行所有的過濾條件,然後再取交集。es 很聰明,他會評估每個 filter 的條件的區分度,把高區分度的 filter 先執行,以此可以加速後面的 filter 循環速度。比如 creator=0d754a8af3104e978c95eb955f6331be 查出來之後 10 條記錄,他就會優先執行這一條。
怎麼做到的呢?其實也很簡單,term 建的時候,每一個 term 在寫入的時候都會記錄一個詞頻,也就是這個 term 在全部文檔裏出現的次數,這樣我們就能判斷當前的這個 term 他的區分度高低了。
爲什麼 PointRangeQuery 在這個場景下非常慢
上面提到了這種查詢的數據結構類似於 b-tree, 他在做範圍查詢的時候,非常有優勢,Lucene 將這顆 B-tree 的非葉子結點部分放在內存裏,而葉子結點緊緊相鄰存放在磁盤上。當作 range 查詢的時候,內存裏的 B-tree 可以幫助快速定位到滿足查詢條件的葉子結點塊在磁盤上的位置,之後對葉子結點塊的讀取幾乎都是順序的。
總結就是這種結構適合範圍查詢,且磁盤的讀取是順序讀取的。但是在我們這種場景之下,term 查詢可就麻煩了,數值型字段的 TermQuery 被轉換爲了 PointRangeQuery。這個 Query 利用 Block k-d tree 進行範圍查找速度非常快,但是滿足查詢條件的 docid 集合在磁盤上並非向 Postlings list 那樣按照 docid 順序存放,也就無法實現 postings list 上藉助跳錶做蛙跳的操作。
要實現對 docid 集合的快速 advance 操作,只能將 docid 集合拿出來,做一些再處理。這個處理過程在 org.apache.lucene.search.PointRangeQuery#createWeight 這個方法裏可以讀取到。這裏就不貼冗長的代碼了,主要邏輯就是在創建 scorer 對象的時候,順帶先將滿足查詢條件的 docid 都選出來,然後構造成一個代表 docid 集合的 bitset,這個過程和構造 Query cache 的過程非常類似。之後 advance 操作,就是在這個 bitset 上完成的。所有的耗時都在構建 bitset 上,因此可以看到耗時主要在 build_scorer 上了。
驗證
找到原因之後,就可以開始驗證了。將原來的 integer 類型全部改成 keyword 類型,如果業務真的有用到範圍查詢,應該會報錯。通過搜索平臺的平臺直接修改配置,修改完成之後,重建索引就生效了。
索引切換之後的效果也非常的明顯,通過 kibana 的 profile 分析可以看到,之前需要接近 100ms 的 PointRangQuery 現在走倒排索引,只需要 0.5ms 的時間。
之前這個索引的平均 latency 在 100ms+,這個是 es 分片處理的耗時, 從搜索行爲開始,到搜索行爲結束的打點,不包含網絡傳輸時間和連接建立時間,單純的分片內的函數的處理時間的平均值,正常情況在 10ms 左右。
經過調整之後的耗時降到了 10ms 內。
通過監控查看慢查詢的數量,立即減少到了 0。
未來
後續將通過搜索平臺側的能力來保證業務的查詢,所有的 integer 我們會默認你記錄的是狀態值,不需要進行範圍查詢,默認將會修改爲 keyword 類型,如果業務確實需要範圍查詢,則可以通過後臺再修改回 integer 類型,這樣可以保證在業務不瞭解 es 機制的情況下,也能擁有較好的性能,節省機器計算資源。
目前還遇到了很多問題需要優化。例如重建索引的時候,機器負載太高。公共集羣的機器負載分佈不均衡的問題,業務的查詢和流量不可控等各種各樣的問題,要節省機器資源就一定會面對這種各種各樣的問題,除非土豪式做法,每個業務都擁有自己的機器資源,這裏面有很多很多頗具技術挑戰的事情。
實際上,在這一塊還是非常利於積累經驗,對於 es 的瞭解和成長也非常快,在查問題的過程中,對於搜索引擎的使用和了解會成長的非常快。不僅如此,很多時候,我們用心的看到生產的問題,持續的跟蹤,一定會有所收穫。大家遇到生產問題的時候,務必不要放過任何細節,這個就是你收穫的時候,比你寫 100 行的 CRUD 更有好處。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/mrf6kYFCUzQwBjxk096yMw