最近在做ES搜索調優,看了一些lucene搜索的文檔和代碼,本文用於總結調優過程中學到的知識和自己的思考。
在抓到ES慢查詢之后,會通過profile或者kibana的Search Profiler console查看具體慢在了哪里。一般在執行profile search之前,需要稍微改變query語句里的查詢內容,防止cache影響測試效果。
profile主要包括shard級別的query耗時、query語句rewrite耗時以及最終lucene collector的耗時。
一般主要關注query的耗時,profile返回的query部分詳細的展示了被rewrite后的查詢語句以及每個子查詢的耗時。包括子查詢的類型(type),子查詢語句(description),子查詢耗時(time_in_nanos)和一個breakdown集合包含了lucene segements search各階段的耗時。從這個breakdown集合中能比較清晰的看到耗時的原因,當然,前提是要理解breakdown里每個指標代表什么意思和內部的實現邏輯。
breakdown里的主要指標及lucene中的實現:
build_scorer:構造一個scorer的耗時。scorer主要用於對matching的doc進行打分和排序。build_scorer內部構造了迭代器,這個迭代器可以遍歷所有matched document,構造迭代器是非常耗時的操作,因為涉及到對各子查詢的docId結果集構造倒排鏈或bitset,並且做conjunction生成最終可被迭代的docId bitset或倒排鏈。大多數查詢主要耗時在這一步。
next_doc: 尋找下一個匹配的document Id。這里keyword, text等文本類型的字段會利用skipList,數值類型的數據會利用Tree結構快速找的下一個匹配的docoument Id。同時,這里會記錄該doc命中的子查詢數量,用於最終的min_should_match之類的過濾。
advance: 類似於一個low level的next_doc。並不是所有的query都能實現next_doc,比如must查詢走的advance去找下一個匹配的文檔。
score: 記錄socrer中對文檔打分的耗時,通過Freq,normal等數據結合tf-idf等算法計算出得分。
match: 記錄第二階段打分的耗時。有些查詢需要兩階段打分,比如短語查詢(phrase query) "chinese love china", 第一階段先找所有包含“chinese”、“love”、“china”三個term的文檔。第二階段再在第一階段匹配到到的所有文檔中計算“chinese”,"love","china"三個單詞的位置和順序是否滿足條件,這一操作非常耗時,所以通過第一階段縮小匹配文檔的范圍。
create_weight: 創建weight過程的耗時,weight就相當於lucene查詢的context,里面包含了query,collector,indexreader等。
*_count: 記錄方法調用次數,比如next_doc_count:2,代表next_doc方法被調用了兩次。
除了query過程的詳細統計,還包括:
rewrite_time: query語句被重寫的耗時,lucene自己維護了一套查詢語句重寫邏輯,比如terms查詢中如果要查詢的terms個數小於16,會被重寫成多個TermQuery做or結合;如果大於16會被重寫成TermInSetQuery。
collector: query數據收集階段的各種指標。包括query用到的collector的個數,類型和耗時。ES默認使用的是SimpleTopScoreDocCollector。lucene的collector主要通過reduce方法對每個segment上匹配的結果進行合並和排序,返回topN。
故障診斷過程中,除了通過profile API定位慢查詢,也需要關注ES集群的整體資源使用情況,比如data node的CPU, Mem, 磁盤IO是否有瓶頸,單節點shard個數是否過多等。一般可以通過cerebro或者elasticsearch_exporter+Prometheus來監控集群狀態, 也可以通過ES API查看相關指標。