剖析Elasticsearch的IndexSorting:一種查詢效能優化利器

亦徵發表於2018-11-10

前言

前兩週寫過一篇《基於Lucene查詢原理分析Elasticsearch的效能》,在最後留了一個彩蛋,說下一篇會介紹一種可以極大的優化查詢效能的技術。本文就來介紹這種技術——IndexSorting。

因為IndexSorting是在ES6.0之後才作為實驗性的功能加入,相關的介紹資料還比較少,所以大部分人對它不夠了解。另一方面是,想要理解它為什麼能夠優化效能、適合哪些場景、內部如何實現、有何副作用等,也需要花一翻功夫。所以本文專門對IndexSorting進行一個介紹,並分析它的作用、實現、適用場景等。如果你的場景能用上IndexSorting,那麼它肯定會給你帶來一個巨大的效能提升!

什麼是IndexSorting?

IndexSorting是ES的新功能

在Elasticsearch中,IndexSorting是一個很新的功能,6.0版本才引入,並且標記這個功能是Beta版(6.5版本可能會去掉Beta標記)。

在Lucene中,IndexSorting其實已經發展了一段時間。最早在10年,Lucene提供了一個IndexSorter的工具,作為一個離線工具可以對Index資料排序後生成一個新的Index。後來13年加入了SortingMergePolicy,在Segment進行merge的時候可以生成排好序的新Segment,在17年又加入了Sorting on flushed segment的功能,在Segment最初生成時就進行排序。另一方面是Lucene在查詢時也做了很多優化,如果有IndexSorting,很多地方做了提前中斷,後面會講提前中斷對查詢效能的巨大作用。經過幾次Lucene的改進和優化,IndexSorting這個功能也終於被整合進Elasticsearch。

IndexSorting是一種預排序

與查詢時的Sort不同,IndexSorting是一種預排序,即資料預先按照某種方式進行排序,它是Index的一個設定,不可更改。大家知道,Elasticsearch的底層是Lucene,Lucene中是以Segment為單位進行查詢的,這裡說的IndexSorting對資料進行預排序也是在每個Segment內有序的。

一個Segment中的每個文件,都會被分配一個docID,docID從0開始,順序分配。在沒有IndexSorting時,docID是按照文件寫入的順序進行分配的,在設定了IndexSorting之後,docID的順序就與IndexSorting的順序一致。

舉個例子來說,假如文件中有一列為Timestamp,我們在IndexSorting中設定按照Timestamp逆序排序,那麼在一個Segment內,docID越小,對應的文件的Timestamp越大,即按照Timestamp從大到小的順序分配docID。

為什麼IndexSorting可以優化效能?

提前中斷

IndexSorting能夠優化效能,主要就是靠四個字,“提前中斷”。但是提前中斷是有條件的,需要查詢的Sort順序與IndexSorting的順序相同,並且不需要獲取符合條件的記錄總數(TotalHits)。

Lucene中的倒排鏈都是按照docID從小到大的順序排列的,在進行組合條件查詢時,也是按照docID從小到大的順序選出符合條件的doc。那麼當查詢時的Sort順序與IndexSorting的順序相同時,會發生什麼呢?

比如查詢時希望按照Timestamp降序排序後返回100條結果,在Lucene中進行查詢時,發現docID對應的doc順序也剛好是Timestamp降序排序的,那麼查詢到前100個符合條件的結果即可返回,這100個一定也是Timestamp最大的100個,這就做到了提前中斷。

提前中斷可以極大的提升查詢效能,特別是當一個查詢條件命中的文件數量非常多的時候。在沒有IndexSorting時,必須把所有符合條件的文件的docID掃描一遍,並且讀取這些doc的一些欄位來排序,選出符合條件的doc。有了IndexSorting之後,只需要選出前Top個doc即可,避免了全部掃描,效能甚至可以提升幾個數量級。

IndexSorting提高了資料壓縮率

Lucene中使用了很多的壓縮演算法來對資料進行壓縮,壓縮一方面減少了磁碟使用量,另一方面也減少了查詢時讀取磁碟的資料量和IO次數等,對查詢效能有很大幫助。

Lucene中同時包括行存(Store)與列存(DocValues)的儲存方式,但不管是行存還是列存,當應用IndexSorting後,相鄰資料的相似度就會越高,也就越利於壓縮。這不僅僅是體現在排序欄位上,也體現在其他欄位的相似度上。比如時序場景,當按照時間排序後,各個Metrics的值的近似度也會越大。所以IndexSorting可以提高資料的壓縮率。

IndexSorting是否適合我的場景?

由排序條件決定是否適合

IndexSorting最大的作用就是優化查詢效能,其適用的場景就是能夠被其優化的場景,比如說:

  1. 查詢時需要根據某列或者某幾列排序後返回的場景:讓IndexSorting順序跟要查詢的Sort順序一致,讓Lucene能夠提前中斷來提升效能。
  2. 不關心結果順序的場景:也可以按照某列IndexSorting,查詢時也設定按照這一列Sort即可。
  3. 有幾種排序需求的場景:IndexSorting起碼可以優化一種排序需求,其餘的幾種需求可以考慮是否多建幾個索引,用空間換時間。

也有些場景不能被其優化,比如根據文件相似度分數排序的場景,這時候很難進行預排序,因為相似分數是每次查詢時才算出來的。

根據查詢原理看是否能優化

上面提到Lucene會按照docID從小到大的順序選出符合條件的doc,但是有時查詢並不是慢在這個篩選過程,而是構造docID列表的過程,這時IndexSorting帶來的優化效果會比較有限。

因為Lucene的查詢原理是比較複雜的,這裡只列舉兩個例子:

  1. 對於字串進行Range查詢,且Range範圍內有很多符合條件的Term的場景。這個場景下,查詢可能會慢在兩個地方,一個是Range範圍內符合條件的Term非常多,掃描FST耗時很大,另一個如果這些Term對應的doc數很多,要構造BitSet也會非常耗時。因為利用IndexSorting的提前中斷是發生在BitSet構造好之後,所以並不能優化到這個地方的效能。
  2. 對數字型別在BKD-Tree上進行範圍查詢時,因為BKD-Tree裡的docID不是順序排列的,所以並不像倒排鏈一樣可以順序讀取。如果BKD-Tree上符合條件的docID很多,構造BitSet也很耗時,也不是IndexSorting能夠優化到的。

考慮對寫入效能的影響

IndexSorting優化的是查詢效能,因為在寫入時需要對資料進行排序,所以降低了寫效能。如果寫效能是目前的效能瓶頸,或者看重寫效能要高於查詢效能,那麼不適合使用IndexSorting。

IndexSorting是如何實現的?

本文介紹一下IndexSorting的實現細節,這也有助於大家理解它對寫入效能產生的影響。IndexSorting可以保證在每個Segment中,資料都是按照設定的方式進行排序的,這要解決兩個問題:

  1. Lucene的Flush操作會生成Segment,這時候生成的Segment如何保證資料有序。
  2. 多個Segment進行合併時如何保證有序。

1. Flush時保證Segment內資料有序

大家知道,資料寫入Lucene後,並不是立即可查的,要生成Segment之後才能被查到。為了保證近實時的查詢,ES會每隔一秒進行一次Refresh,Refresh就會呼叫到Lucene的Flush生成新的Segment。額外說的一點是,Lucene的Flush不同於ES的Flush,ES的Flush保證資料落盤,呼叫的是Lucene的commit,裡面會呼叫fsync,這裡的關係值得額外寫一篇文章來說清楚。

我們需要先知道Flush前資料是一個什麼樣的狀態,才能知道Flush時如何對這些資料排序。每個doc寫入進來之後,按照寫入順序被分配一個docID,然後被IndexingChain處理,依次要對invert index、store fields、doc values和point values進行處理,有些資料會直接寫到檔案裡,主要是store field和term vector,其他的資料會放到memory buffer中。

在Flush時,首先根據設定的列排序,這個排序可以利用記憶體中的doc values,排序之後得到老的docID到新docID的對映,因為之前docID是按照寫入順序生成的,現在重排後,生成的是新的排列。如果排序後與原來順序完全一致,那麼什麼都不做,跟之前流程一樣進行Flush。

如果排序後順序發生變化,如何排序呢?對於已經寫到檔案中的資料,比如store field和term vector,需要從檔案中讀出來,重新排列後再寫到一個新檔案裡,原來的檔案就相當於一個臨時檔案。對於記憶體中的資料結構,直接在記憶體中重排後寫到檔案中。

相比沒有IndexSorting時,對效能影響比較大的一塊就是store field的重排,因為這部分需要從檔案中讀出再寫回,而其他部分都是記憶體操作,效能影響稍小一些。這裡我們也可以做一些思考,如果將store field和term vector這類資料也buffer在記憶體中,是否可以提升IndexSorting開啟時的寫入效能?

2. Merge時保證新的Segment資料有序

由於Flush時Segment已經是有序的了,所以在Merge時也就可以採用非常高效的Merge Sort的方式進行。

總結

IndexSorting是一種能夠極大提高查詢效率的技術,它通過預排序和提前中斷大大減少了需要掃描的資料量,而且附帶的優化是可以提高壓縮率,減少儲存空間。對於查詢時需要按照某列排序的場景,它非常有用,但對於相關性分數排序的場景則無法通過預排序來優化。IndexSorting的缺點是對寫入效能有影響,主要是體現在Segment的Flush和Merge階段,對於非常看重寫入效能的場景也不適合使用。總體上說,這是一項非常有用也很新的技術,相信它在Lucene和ES中的重要性會越來越強,也會有越來越多的業務場景受益於這個功能。

參考文件

  1. https://www.elastic.co/blog/index-sorting-elasticsearch-6-0
  2. https://www.elastic.co/blog/space-savings-a-lesser-known-benefit-of-index-sorting-in-elasticsearch
  3. https://issues.apache.org/jira/browse/LUCENE-7579
  4. https://zhuanlan.zhihu.com/p/35795070
  5. https://zhuanlan.zhihu.com/p/47951652


相關文章