剖析 Elasticsearch 的索引原理

JaJian發表於2019-05-13

前言

建立索引的時候,我們通過Mapping 對映定義好索引的基本結構資訊,接下來我們肯定需要往 ES 裡面新增業務文件資料了,例如使用者,日誌等業務資料。新增的業務資料,我們根據 Mapping 來生成對應的倒排索引資訊 。

我們一直說,Elasticsearch是一個基於Apache Lucene 的開源搜尋引擎。Elasticsearch的搜尋高效的原因並不是像Redis那樣重依賴記憶體的,而是通過建立特殊的索引資料結構--倒排索引實現的。由於它的使用場景:處理PB級結構化或非結構化資料,資料量大且需要持久化防止斷電丟失,所以 Elasticsearch 的資料和索引儲存是依賴於伺服器的硬碟。這也是為什麼我們在ES效能調優的時候可以將使用SSD硬碟儲存作為其中一個優化項來考慮。

倒排索引的概念,我相信大家都已經知道了,這裡就不在贅述,倒排索引可以說是Elasticsearch搜尋高效和支援非結構化資料檢索的主要原因了,但是倒排索引被寫入磁碟後是不可改變 的:它永遠不會修改

段和提交點

倒排索引的不可變性,這點主要是因為 Elasticsearch 的底層是基於 Lucene,而在 Lucene 中提出了按段搜尋的概念,將一個索引檔案拆分為多個子檔案,則每個子檔案叫作,每個段都是一個獨立的可被搜尋的資料集,並且段具有不變性,一旦索引的資料被寫入硬碟,就不可再修改。

的概念提出主要是因為:在早期全文檢索中為整個文件集合建立了一個很大的倒排索引,並將其寫入磁碟中。如果索引有更新,就需要重新全量建立一個索引來替換原來的索引。這種方式在資料量很大時效率很低,並且由於建立一次索引的成本很高,所以對資料的更新不能過於頻繁,也就不能保證時效性。

而且在底層採用了分段的儲存模式,使它在讀寫時幾乎完全避免了鎖的出現,大大提升了讀寫效能。說到這,你們可能會想到 ConcurrentHashMap 的分段鎖 的概念,其實原理有點類似。

而且 Elasticsearch 中的倒排索引被設計成不可變的,有以下幾個方面優勢

  • 不需要鎖。如果你從來不更新索引,你就不需要擔心多程式同時修改資料的問題。
  • 一旦索引被讀入核心的檔案系統快取,便會留在哪裡。由於其不變性,只要檔案系統快取中還有足夠的空間,那麼大部分讀請求會直接請求記憶體,而不會命中磁碟。這提供了很大的效能提升。
  • 其它快取(像filter快取),在索引的生命週期內始終有效。它們不需要在每次資料改變時被重建,因為資料不會變化。
  • 寫入單個大的倒排索引允許資料被壓縮,減少磁碟 I/O 和 需要被快取到記憶體的索引的使用量。

每一個本身都是一個倒排索引,但索引在 Lucene 中除表示所有的集合外,還增加了提交點的概念。

為了提升寫的效能,Lucene並沒有每新增一條資料就增加一個段,而是採用延遲寫的策略,每當有新增的資料時,就將其先寫入記憶體中,然後批量寫入磁碟中。若有一個段被寫到硬碟,就會生成一個提交點,提交點就是一個列出了所有已知段和記錄所有提交後的段資訊的檔案

剖析 Elasticsearch 的索引原理

寫索引的流程

上面說過 ES 的索引的不變性,還有段和提交點的概念。那麼它的具體實現細節和寫入磁碟的過程是怎樣的呢?

剖析 Elasticsearch 的索引原理

  • 使用者建立了一個新文件,新文件被寫入到一個新段中,然後首先被新增到記憶體索引快取中。

  • 不時地, 快取被提交,這時快取中新段會被先寫入到檔案快取系統而不是直接被刷到磁碟。
    這是因為,提交一個新的段到磁碟需要一個fsync 來確保段被物理性地寫入磁碟,這樣在斷電的時候就不會丟失資料。 但是 fsync 操作代價很大;如果每次索引一個文件都去執行一次的話會造成很大的效能問題,但是這裡新段會被先寫入到檔案系統快取,這一步代價會比較低。

  • 新的段被寫入到檔案快取系統,這時記憶體快取被清空。在檔案快取系統會存在一個未提交的段。雖然新段未被提交(刷到磁碟),但是檔案已經在快取中了, 此時就可以像其它檔案一樣被開啟和讀取了。
  • 到目前為止索引的段還未被重新整理到磁碟,如果沒有用 fsync 把資料從檔案系統快取刷(flush)到硬碟,我們不能保證資料在斷電甚至是程式正常退出之後依然存在。Elasticsearch 增加了一個 translog ,或者叫事務日誌,在每一次對 Elasticsearch 進行操作時均進行了日誌記錄。如上圖所示,一個文件被索引之後,就會被新增到記憶體緩衝區,並且同時追加到了 translog。
  • 每隔一段時間,更多的文件被新增到記憶體緩衝區和追加到事務日誌(translog),之後新段被不斷從記憶體快取區被寫入到檔案快取系統,這時記憶體快取被清空,但是事務日誌不會。隨著 translog 變得越來越大,達到一定程度後索引被重新整理,在重新整理(flush)之後,段被全量提交,一個提交點被寫入硬碟,並且事務日誌被清空。

剖析 Elasticsearch 的索引原理

從整個流程我們可以瞭解到以下幾個問題:

  • 為什麼說 ES 搜尋是近實時的
    因為文件索引在從記憶體快取被寫入到檔案快取系統時,雖然還沒有進行提交未被 flush 到磁碟,但是緩衝區的內容已經被寫入一個段(segment6)中且新段可被搜尋。這就是為什麼我們說 Elasticsearch 是近實時搜尋: 文件的變化並不是立即對搜尋可見,但會在一秒之內變為可見。
  • Elasticsearch 是怎樣保證更新被持久化在斷電時也不丟失資料?
    新索引文件被寫入到記憶體快取時,同時會記錄一份到事務日誌(translog)中,translog 提供所有還沒有被刷到磁碟的操作的一個持久化紀錄。當 Elasticsearch 啟動的時候, 它會從磁碟中使用最後一個提交點去恢復已知的段,並且會重放 translog 中所有在最後一次提交後發生的變更操作。
    translog 也被用來提供實時 CRUD 。當你試著通過ID查詢、更新、刪除一個文件,它會在嘗試從相應的段中檢索之前, 首先檢查 translog 任何最近的變更。這意味著它總是能夠實時地獲取到文件的最新版本。

段合併

剖析 Elasticsearch 的索引原理

由於自動重新整理流程每秒會建立一個新的段 ,這樣會導致短時間內的段數量暴增。而段數目太多會帶來較大的麻煩。 每一個段都會消耗檔案控制程式碼、記憶體和cpu執行週期。更重要的是,每個搜尋請求都必須輪流檢查每個段;所以段越多,搜尋也就越慢。

Elasticsearch通過在後臺進行段合併來解決這個問題。小的段被合併到大的段,然後這些大的段再被合併到更大的段。

剖析 Elasticsearch 的索引原理

段合併的時候會將那些舊的已刪除文件 從檔案系統中清除。 被刪除的文件(或被更新文件的舊版本)不會被拷貝到新的大段中。

如何更新索引

上文闡述了索引的持久化流程和倒排索引被設定為不可修改以及這樣設定的好處。因為它是不可變的,你不能修改它。但是如果你需要讓一個新的文件可被搜尋,這就涉及到索引的更新了,索引不可被修改但又需要更新,這種看似矛盾的要求,我們需要怎麼做呢?

ES 的解決方法就是:用更多的索引。什麼意思?就是原來的索引不變,我們對新的文件再建立一個索引。這樣說完不知道大家有沒有疑惑或者沒理解,我們通過圖表的方式說明下。

假如我們現有兩個日誌資訊的文件,資訊如下:

  • Doc 1:the request param is name = 'zhang san' and age is 20.
  • Doc 2:the response result is code = 0000 and msg = 'success'.

這時候我們得到的倒排索引內容(省略一部分)是:

詞項(term) 文件(Doc)
the doc 1,doc 2
request doc 1
param doc 1,doc 2
is doc 1,doc 2
name doc 1
response doc 2
result doc 2
... ...

如果我們這時新增一個文件 doc 3:the request param is name = 'li si' and sex is femal,或者修改文件 doc 2的內容為:the response result is code = 9999 and msg = 'false'。這時 ES 是如何處理的呢?

正如上文所述的,為了保留索引不變性,ES 會建立一個新的索引,對於新增的文件索引資訊如下:

詞項(term) 文件(Doc)
the doc 3
request doc 3
param doc 3
is doc 3
name doc 3
sex doc 3
... ...

對於修改的文件索引資訊如下;

詞項(term) 文件(Doc)
the doc 2
response doc 2
result doc 2
is doc 2
code doc 2
sex doc 2
... ...

通過增加新的補充索引來反映新近的修改,而不是直接重寫整個倒排索引。每一個倒排索引都會被輪流查詢到(從最早的開始),查詢完後再對結果進行合併。

正如上文所述那樣,對於修改的場景來說,同一個文件這時磁碟中同時會有兩個索引資料一個是原來的索引,另一個是修改之後的索引。

以正常邏輯來看,我們知道搜尋的時候肯定以新的索引為標準,但是段是不可改變的,所以既不能從把文件從舊的段中移除,也不能修改舊的段來進行反映文件的更新。 取而代之的是,每個提交點會包含一個 .del檔案,檔案中會列出這些被刪除文件的段資訊。

當一個文件被 “刪除” 時,它實際上只是在.del 檔案中被 標記 刪除。一個被標記刪除的文件仍然可以被查詢匹配到, 但它會在最終結果被返回前從結果集中移除。

文件更新也是類似的操作方式:當一個文件被更新時,舊版本文件被標記刪除,文件的新版本被索引到一個新的段中。 可能兩個版本的文件都會被一個查詢匹配到,但被刪除的那個舊版本文件在結果集返回前就已經被移除。

相關文章