摘要: 前言 Elasticsearch是一個很火的分佈式搜索系統,提供了很是強大並且易用的查詢和分析能力,包括全文索引、模糊查詢、多條件組合查詢、地理位置查詢等等,並且具備必定的分析聚合能力。由於其查詢場景很是豐富,因此若是泛泛的分析其查詢性能是一個很是複雜的事情,並且除了場景以外,還有不少影響因素,包括機型、參數配置、集羣規模等等。數據庫
Elasticsearch是一個很火的分佈式搜索系統,提供了很是強大並且易用的查詢和分析能力,包括全文索引、模糊查詢、多條件組合查詢、地理位置查詢等等,並且具備必定的分析聚合能力。由於其查詢場景很是豐富,因此若是泛泛的分析其查詢性能是一個很是複雜的事情,並且除了場景以外,還有不少影響因素,包括機型、參數配置、集羣規模等等。本文主要是針對幾種主要的查詢場景,從查詢原理的角度分析這個場景下的查詢開銷,並給出一個大概的性能數字,供你們參考。數組
本節主要是一些Lucene的背景知識,瞭解這些知識的同窗能夠略過。服務器
Elasticsearch的底層是Lucene,能夠說Lucene的查詢性能就決定了Elasticsearch的查詢性能。關於Lucene的查詢原理你們能夠參考如下這篇文章:數據結構
Lucene查詢原理分佈式
Lucene中最重要的就是它的幾種數據結構,這決定了數據是如何被檢索的,本文再簡單描述一下幾種數據結構:性能
瞭解了Lucene的數據結構和基本查詢原理,咱們知道:測試
如今的問題是,若是給一個組合查詢條件,Lucene怎麼對各個單條件的結果進行組合,獲得最終結果。簡化的問題就是如何求兩個集合的交集和並集。優化
上面Lucene原理分析的文章中講過,N個倒排鏈求交集,能夠採用skipList,有效的跳過無效的doc。阿里雲
處理方式一:仍然保留多個有序列表,多個有序列表的隊首構成一個優先隊列(最小堆),這樣後續能夠對整個並集進行iterator(堆頂的隊首出堆,隊列裏下一個docID入堆),也能夠經過skipList的方式向後跳躍(各個子列表分別經過skipList跳)。這種方式適合倒排鏈數量比較少(N比較小)的場景。spa
處理方式二:倒排鏈若是比較多(N比較大),採用方式一就不夠划算,這時候能夠直接把結果合併成一個有序的docID數組。
處理方式三:方式二中,直接保存原始的docID,若是docID很是多,很消耗內存,因此當doc數量超過必定值時(32位docID在BitSet中只須要一個bit,BitSet的大小取決於segments裏的doc總數,因此能夠根據doc總數和當前doc數估算是否BitSet更加划算),會採用構造BitSet的方式,很是節約內存,並且BitSet能夠很是高效的取交/並集。
經過BKD-Tree查找到的docID是無序的,因此要麼先轉成有序的docID數組,或者構造BitSet,而後再與其餘結果合併。
若是採用多個條件進行查詢,那麼先查詢代價比較小的,再從小結果集上進行迭代,會更優一些。Lucene中作了不少這方面的優化,在查詢前會先估算每一個查詢的代價,再決定查詢順序。
默認狀況下,Lucene會按照Score排序,即算分後的分數值,若是指定了其餘的Sort字段,就會按照指定的字段排序。那麼,排序會很是影響性能嗎?首先,排序並不會對全部命中的doc進行排序,而是構造一個堆,保證前(Offset+Size)個數的doc是有序的,因此排序的性能取決於(Size+Offset)和命中的文檔數,另外就是讀取docValues的開銷。由於(Size+Offset)並不會太大,並且docValues的讀取性能很高,因此排序並不會很是的影響性能。
上一節講了一些查詢相關的理論知識,那麼本節就是理論結合實踐,經過具體的一些測試數字來分析一下各個場景的性能。測試採用單機單Shard、64核機器、SSD磁盤,主要分析各個場景的計算開銷,不考慮操做系統Cache的影響,測試結果僅供參考。
這個請求耗費了233ms,而且返回了符合條件的數據總數:5184867條。
對於Tag1="a"這個查詢條件,咱們知道是查詢Tag1="a"的倒排鏈,這個倒排鏈的長度是5184867,是很是長的,主要時間就花在掃描這個倒排鏈上。其實對這個例子來講,掃描倒排鏈帶來的收益就是拿到了符合條件的記錄總數,由於條件中設置了constant_score,因此不須要算分,隨便返回一條符合條件的記錄便可。對於要算分的場景,Lucene會根據詞條在doc中出現的頻率來計算分值,並取分值排序返回。
目前咱們獲得一個結論,233ms時間至少能夠掃描500萬的倒排鏈,另外考慮到單個請求是單線程執行的,能夠粗略估算,一個CPU核在一秒內掃描倒排鏈內doc的速度是千萬級的。
咱們再換一個小一點的倒排鏈,長度爲1萬,總共耗時3ms。
{"took":3,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":10478,"max_score":1.0,"hits":...}
首先考慮兩個Term查詢求交集:
這個請求耗時21ms,主要是作兩個倒排鏈的求交操做,所以咱們主要分析skipList的性能。
這個例子中,倒排鏈長度是1萬、500萬,合併後仍有5000多個doc符合條件。對於1萬的倒排鏈,基本上不進行skip,由於一半的doc都是符合條件的,對於500萬的倒排鏈,平均每次skip1000個doc。由於倒排鏈在存儲時最小的單位是BLOCK,一個BLOCK通常是128個docID,BLOCK內不會進行skip操做。因此即便可以skip到某個BLOCK,BLOCK內的docID仍是要順序掃描的。因此這個例子中,實際掃描的docID數粗略估計也有幾十萬,因此總時間花費了20多ms也符合預期。
對於Term查詢求並集呢,將上面的bool查詢的must改爲should,查詢結果爲:
{"took":393,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":5190079,"max_score":1.0,"hits":...}
花費時間393ms,因此求並集的時間是多於其中單個條件查詢的時間。
這個查詢咱們主要分析FST的查詢性能,從上面的結果中咱們能夠看到,FST的查詢性能相比掃描倒排鏈要差許多,一樣掃描500萬的數據,倒排鏈掃描只須要不到300ms,而FST上的掃描花費了3秒,基本上是慢十倍的。對於UUID長度的字符串來講,FST範圍掃描的性能大概是每秒百萬級。
這個例子中,查詢消耗時間的大頭仍是在掃描FST的部分,經過FST掃描出符合條件的Term,而後讀取每一個Term對應的docID列表,構造一個BitSet,再與兩個TermQuery的倒排鏈求交集。
這個場景咱們主要測試BKD-Tree的性能,能夠看到BKD-Tree查詢的性能仍是不錯的,查找500萬個doc花費了500多ms,只比掃描倒排鏈差一倍,相比FST的性能有了很大的提高。地理位置相關的查詢也是經過BKD-Tree實現的,性能很高。
這個結果出乎咱們的意料,居然只須要27ms!由於在上一個例子中,數字Range查詢耗時500多ms,而咱們增長兩個Term條件後,時間居然變爲27ms,這是爲什麼呢?
實際上,Lucene在這裏作了一個優化,底層有一個查詢叫作IndexOrDocValuesQuery,會自動判斷是查詢Index(BKD-Tree)仍是DocValues。在這個例子中,查詢順序是先對兩個TermQuery求交集,獲得5000多個docID,而後讀取這個5000多個docID對應的docValues,從中篩選符合數字Range條件的數據。由於只須要讀5000多個doc的docValues,因此花費時間不多。
最後結尾再放一個彩蛋,既然掃描數據越多,性能越差,那麼可否獲取到足夠數據就提早終止呢,下一篇文章我會介紹一種這方面的技術,能夠極大的提升不少場景下的查詢性能。
阿里雲雙十一1折拼團活動:已滿6人,都是最低折扣了
【滿6人】1核2G雲服務器99.5元一年298.5元三年 2核4G雲服務器545元一年 1227元三年
【滿6人】1核1G MySQL數據庫 119.5元一年
【滿6人】3000條國內短信包 60元每6月
參團地址:http://click.aliyun.com/m/1000020293/