ElasticSearch集群故障案例分析: 警惕通配符查詢


最近ElasticSearch集群出現了 https://elasticsearch.cn/article/171 文章中描述的情況,現在轉載全文警示下自己。

 

許多有RDBMS/SQL背景的開發者,在初次踏入ElasticSearch世界的時候,很容易就想到使用(Wildcard Query)來實現模糊查詢(比如用戶輸入補全),因為這是和SQL里like操作最相似的查詢方式,用起來感覺非常舒適。然而近期我們線上一個搜索集群的故障揭示了,濫用wildcard query可能帶來災難性的后果。

故障經過
線上有一個10來台機器組成的集群,用於某個產品線的產品搜索。數據量並不大,實時更新量也不高,並發搜索量在幾百次/s。通常業務高峰期cpu利用率不超過10%,系統負載看起來很低。 但最近這個集群不定期(1天或者隔幾天)會出現CPU沖高到100%的問題,持續時間從1分鍾到幾分鍾不等。最嚴重的一次持續了20來分鍾,導致大量的用戶搜索請無求響應,從而造成生產事故。

問題排查
細節太多,此處略過,直接給出CPU無故飆高的原因: 研發在搜索實現上,根據用戶輸入的關鍵詞,在首尾加上通配符,使用wildcard query來實現模糊搜索,例如使用"*迪士尼*"來搜索含有“迪士尼”關鍵字的產品。 然而用戶輸入的字符串長度沒有做限制,導致首尾通配符中間可能是很長的一個字符串。 后果就是對應的wildcard Query執行非常慢,非常消耗CPU。

復現方法
1. 創建一個只有一條文檔的索引

POST test_index/type1/?refresh=true { "foo": "bar" }

2. 使用wildcard query執行一個首尾帶有通配符*的長字符串查詢

POST /test_index/_search
{
  "query": { "wildcard": { "foo": { "value": "*在迪士尼樂園,點亮心中奇夢。它是一個充滿創造力、冒險精神與無窮精彩的快地。您可在此游覽全球最大的迪士尼城堡——奇幻童話城堡,探索別具一格又令人難忘的六大主題園區——米奇大街、奇想花園、夢幻世界、探險島、寶藏灣和明日世界,和米奇朋友在一起,感覺歡樂時光開業於2016年上海國際旅游度假區秀沿路亞朵酒店位於上海市浦東新區滬南公路(滬南公路與秀沿路交匯處),臨近周浦萬達廣場、地鐵11號線秀沿路站,距離上海南站、人民廣場約20公里,距離迪線距*" } } } }

3. 查看結果

{
  "took": 3445, "timed_out": false, "_shards": { "total": 5, "successful": 5, "failed": 0 }, "hits": { "total": 0, "max_score": null, "hits": } }

即使no hits,耗時卻是驚人的3.4秒 (測試機是macbook pro, i7 CPU),並且執行過程中,CPU有一個很高的尖峰。
 
線上的查詢比我這個范例要復雜得多,會同時查幾個字段,實際測試下來,一個查詢可能會執行十幾秒鍾。 在有比較多長字符串查詢的時候,集群可能就DOS了。

探查深層次根源
為什么對只有一條數據的索引做這個查詢開銷這么高? 直覺上應該是瞬間返回結果才對!

回答這個問題前,可以再做個測試,如果繼續加大查詢字符串的長度,到了一定長度后,ES直接拋異常了,服務器ES里異常給出的cause如下:


 
Caused by: org.apache.lucene.util.automaton.TooComplexToDeterminizeException: Determinizing automaton with 22082 states and 34182 transitions would result in more than 10000 states. at org.apache.lucene.util.automaton.Operations.determinize(Operations.java:741) ~[lucene-core-6.4.1.jar:6.4.1
 


該異常來自org.apache.lucene.util.automaton這個包,異常原因的字面含義是說“自動機過於復雜而無法確定狀態: 由於狀態和轉換太多,確定一個自動機需要生成的狀態超過10000個上限"

網上查找了大量資料后,終於搞清楚了問題的來龍去脈。為了加速通配符和正則表達式的匹配速度,Lucene4.0開始會將輸入的字符串模式構建成一個DFA (Deterministic Finite Automaton),帶有通配符的pattern構造出來的DFA可能會很復雜,開銷很大。這個鏈接的博客using-dfa-for-wildcard-matching-problem比較形象的介紹了如何為一個帶有通配符的pattern構建DFA。借用博客里的范例,a*bc構造出來的DFA如下圖:

屏幕快照_2017-05-11_18.56_.06_.png



Lucene構造DFA的實現
看了一下Lucene的里相關的代碼,構建過程大致如下:
1. org.apache.lucene.search.WildcardQuery里的toAutomaton方法,遍歷輸入的通配符pattern,將每個字符變成一個自動機(automaton),然后將每個字符的自動機鏈接起來生成一個新的自動機

public static Automaton toAutomaton(Term wildcardquery) { List<Automaton> automata = new ArrayList<>(); String wildcardText = wildcardquery.text(); for (int i = 0; i < wildcardText.length();) { final int c = wildcardText.codePointAt(i); int length = Character.charCount(c); switch(c) { case WILDCARD_STRING: automata.add(Automata.makeAnyString()); break; case WILDCARD_CHAR: automata.add(Automata.makeAnyChar()); break; case WILDCARD_ESCAPE: // add the next codepoint instead, if it exists if (i + length < wildcardText.length()) { final int nextChar = wildcardText.codePointAt(i + length); length += Character.charCount(nextChar); automata.add(Automata.makeChar(nextChar)); break; } // else fallthru, lenient parsing with a trailing \ default: automata.add(Automata.makeChar(c)); } i += length; } return Operations.concatenate(automata); }

2. 此時生成的狀態機是不確定狀態機,也就是Non-deterministic Finite Automaton(NFA)。
3. org.apache.lucene.util.automaton.Operations類里的determinize方法則會將NFA轉換為DFA  

/** * Determinizes the given automaton. * <p> * Worst case complexity: exponential in number of states. * @param maxDeterminizedStates Maximum number of states created when * determinizing. Higher numbers allow this operation to consume more * memory but allow more complex automatons. Use * DEFAULT_MAX_DETERMINIZED_STATES as a decent default if you don't know * how many to allow. * @throws TooComplexToDeterminizeException if determinizing a creates an * automaton with more than maxDeterminizedStates */ public static Automaton determinize(Automaton a, int maxDeterminizedStates) {

 代碼注釋里說這個過程的時間復雜度最差情況下是狀態數量的指數級別!為防止產生的狀態過多,消耗過多的內存和CPU,類里面對最大狀態數量做了限制

  /** * Default maximum number of states that {@link Operations#determinize} should create. */ public static final int DEFAULT_MAX_DETERMINIZED_STATES = 10000;

在有首尾通配符,並且字符串很長的情況下,這個determinize過程會產生大量的state,甚至會超過上限。
 
至於NFA和DFA的區別是什么? 如何相互轉換? 網上有很多數學層面的資料和論文,限於鄙人算法方面有限的知識,無精力去深入探究。 但是一個粗淺的理解是: NFA在輸入一個條件的情況下,可以從一個狀態轉移到多種狀態,而DFA只會有一個確定的狀態可以轉移,因此DFA在字符串匹配時速度更快。 DFA雖然搜索的時候快,但是構造方面的時間復雜度可能比較高,特別是帶有首部通配符+長字符串的時候。

回想Elasticsearch官方文檔里對於wildcard query有特別說明,要避免使用通配符開頭的term。


" Note that this query can be slow, as it needs to iterate over many terms. In order to prevent extremely slow wildcard queries, a wildcard term should not start with one of the wildcards * or ?."



結合對上面wildcard query底層實現的探究,也就不難理解這句話的含義了!

總結: wildcard query應杜絕使用通配符打頭,實在不得已要這么做,就一定需要限制用戶輸入的字符串長度。 最好換一種實現方式,通過在index time做文章,選用合適的分詞器,比如nGram tokenizer預處理數據,然后使用更廉價的term query來實現同等的模糊搜索功能。 對於部分輸入即提示的應用場景,可以考慮優先使用completion suggester, phrase/term suggeter一類性能更好,模糊程度略差的方式查詢,待suggester沒有匹配結果的時候,再fall back到更模糊但性能較差的wildcard, regex, fuzzy一類的查詢。
 
-----------
補記: 有同學問regex, fuzzy query是否有同樣的問題,答案是有,原因在於他們底層和wildcard一樣,都是通過將pattern構造成DFA來加速字符串匹配速度的。 

 

[攜程旅行網: 吳曉剛]


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM