前言
創建索引的時候,我們通過Mapping 映射定義好索引的基本結構信息,接下來我們肯定需要往 ES 里面新增業務文檔數據了,例如用戶,日志等業務數據。新增的業務數據,我們根據 Mapping 來生成對應的倒排索引信息 。
我們一直說,Elasticsearch是一個基於Apache Lucene 的開源搜索引擎。Elasticsearch的搜索高效的原因並不是像Redis那樣重依賴內存的,而是通過建立特殊的索引數據結構--倒排索引實現的。由於它的使用場景:處理PB級結構化或非結構化數據,數據量大且需要持久化防止斷電丟失,所以 Elasticsearch 的數據和索引存儲是依賴於服務器的硬盤。這也是為什么我們在ES性能調優的時候可以將使用SSD硬盤存儲作為其中一個優化項來考慮。
倒排索引的概念,我相信大家都已經知道了,這里就不在贅述,倒排索引可以說是Elasticsearch搜索高效和支持非結構化數據檢索的主要原因了,但是倒排索引被寫入磁盤后是不可改變 的:它永遠不會修改。
段和提交點
倒排索引的不可變性,這點主要是因為 Elasticsearch 的底層是基於 Lucene,而在 Lucene 中提出了按段搜索的概念,將一個索引文件拆分為多個子文件,則每個子文件叫作段,每個段都是一個獨立的可被搜索的數據集,並且段具有不變性,一旦索引的數據被寫入硬盤,就不可再修改。
段 的概念提出主要是因為:在早期全文檢索中為整個文檔集合建立了一個很大的倒排索引,並將其寫入磁盤中。如果索引有更新,就需要重新全量創建一個索引來替換原來的索引。這種方式在數據量很大時效率很低,並且由於創建一次索引的成本很高,所以對數據的更新不能過於頻繁,也就不能保證時效性。
而且在底層采用了分段的存儲模式,使它在讀寫時幾乎完全避免了鎖的出現,大大提升了讀寫性能。說到這,你們可能會想到 ConcurrentHashMap 的分段鎖 的概念,其實原理有點類似。
而且 Elasticsearch 中的倒排索引被設計成不可變的,有以下幾個方面優勢:
- 不需要鎖。如果你從來不更新索引,你就不需要擔心多進程同時修改數據的問題。
- 一旦索引被讀入內核的文件系統緩存,便會留在哪里。由於其不變性,只要文件系統緩存中還有足夠的空間,那么大部分讀請求會直接請求內存,而不會命中磁盤。這提供了很大的性能提升。
- 其它緩存(像filter緩存),在索引的生命周期內始終有效。它們不需要在每次數據改變時被重建,因為數據不會變化。
- 寫入單個大的倒排索引允許數據被壓縮,減少磁盤 I/O 和 需要被緩存到內存的索引的使用量。
每一個段本身都是一個倒排索引,但索引在 Lucene 中除表示所有段的集合外,還增加了提交點的概念。
為了提升寫的性能,Lucene並沒有每新增一條數據就增加一個段,而是采用延遲寫的策略,每當有新增的數據時,就將其先寫入內存中,然后批量寫入磁盤中。若有一個段被寫到硬盤,就會生成一個提交點,提交點就是一個列出了所有已知段和記錄所有提交后的段信息的文件。
寫索引的流程
上面說過 ES 的索引的不變性,還有段和提交點的概念。那么它的具體實現細節和寫入磁盤的過程是怎樣的呢?
-
用戶創建了一個新文檔,新文檔被寫入內存中。
-
不時地, 緩存被提交,這時緩存中數據會以段的形式被先寫入到文件緩存系統而不是直接被刷到磁盤。
這是因為,提交一個新的段到磁盤需要一個fsync
來確保段被物理性地寫入磁盤,這樣在斷電的時候就不會丟失數據。 但是fsync
操作代價很大;如果每次索引一個文檔都去執行一次的話會造成很大的性能問題,但是這里新段會被先寫入到文件系統緩存,這一步代價會比較低。 -
新的段被寫入到文件緩存系統,這時內存緩存被清空。在文件緩存系統會存在一個未提交的段。雖然新段未被提交(刷到磁盤),但是文件已經在緩存中了, 此時就可以像其它文件一樣被打開和讀取了。
-
到目前為止索引的段還未被刷新到磁盤,如果沒有用
fsync
把數據從文件系統緩存刷(flush)到硬盤,我們不能保證數據在斷電甚至是程序正常退出之后依然存在。Elasticsearch 增加了一個 translog ,或者叫事務日志,在每一次對 Elasticsearch 進行操作時均進行了日志記錄。如上圖所示,一個文檔被索引之后,就會被添加到內存緩沖區,並且同時追加到了 translog。 -
每隔一段時間,更多的文檔被添加到內存緩沖區和追加到事務日志(translog),之后新段被不斷從內存緩存區被寫入到文件緩存系統,這時內存緩存被清空,但是事務日志不會。隨着 translog 變得越來越大,達到一定程度后索引被刷新,在刷新(flush)之后,段被全量提交,一個提交點被寫入硬盤,並且事務日志被清空。
從整個流程我們可以了解到以下幾個問題:
- 為什么說 ES 搜索是近實時的?
因為文檔索引在從內存緩存被寫入到文件緩存系統時,雖然還沒有進行提交未被 flush 到磁盤,但是緩沖區的內容已經被寫入一個段(segment6)中且新段可被搜索。這就是為什么我們說 Elasticsearch 是近實時搜索: 文檔的變化並不是立即對搜索可見,但會在一秒之內變為可見。 - Elasticsearch 是怎樣保證更新被持久化在斷電時也不丟失數據?
新索引文檔被寫入到內存緩存時,同時會記錄一份到事務日志(translog)中,translog 提供所有還沒有被刷到磁盤的操作的一個持久化紀錄。當 Elasticsearch 啟動的時候, 它會從磁盤中使用最后一個提交點去恢復已知的段,並且會重放 translog 中所有在最后一次提交后發生的變更操作。
translog 也被用來提供實時 CRUD 。當你試着通過ID查詢、更新、刪除一個文檔,它會在嘗試從相應的段中檢索之前, 首先檢查 translog 任何最近的變更。這意味着它總是能夠實時地獲取到文檔的最新版本。
段合並
由於自動刷新流程每秒會創建一個新的段 ,這樣會導致短時間內的段數量暴增。而段數目太多會帶來較大的麻煩。 每一個段都會消耗文件句柄、內存和cpu運行周期。更重要的是,每個搜索請求都必須輪流檢查每個段;所以段越多,搜索也就越慢。
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
文件中被 標記 刪除。一個被標記刪除的文檔仍然可以被查詢匹配到, 但它會在最終結果被返回前從結果集中移除。
文檔更新也是類似的操作方式:當一個文檔被更新時,舊版本文檔被標記刪除,文檔的新版本被索引到一個新的段中。 可能兩個版本的文檔都會被一個查詢匹配到,但被刪除的那個舊版本文檔在結果集返回前就已經被移除。