elasticsearch — 內部分片原理(十)

問題:

爲什麼ES 搜索是 近 實時的?
爲什麼文檔的 CRUD (創建-讀取-更新-刪除) 操作是 實時 的?
Elasticsearch是怎樣保證更新被持久化在斷電時也不丟失數據?
爲什麼刪除文檔不會立刻釋放空間?
refresh, flush, 和 optimize API都做了什麼, 什麼情況下應該使用他們?

1.使文本可被搜索

必須解決的第一個挑戰是如何使文本可被搜索。 傳統的數據庫每個字段存儲單個值,但這對全文檢索並不夠。文本字段中的每個單詞需要被搜索,對數據庫意味着需要單個字段有索引多值(這裏指單詞)的能力。

最好的支持 一個字段多個值 需求的數據結構 倒排索引 。 倒排索引包含一個有序列表,列表包含所有文檔出現過的不重複個體,或稱爲 詞項 ,對於每一個詞項,包含了它所有曾出現過文檔的列表:

Term  | Doc 1 | Doc 2 | Doc 3 | ...
------------------------------------
brown |   X   |       |  X    | ...
fox   |   X   |   X   |  X    | ...
quick |   X   |   X   |       | ...
the   |   X   |       |  X    | ...
當討論倒排索引時,我們會談到 文檔 標引,因爲歷史原因,倒排索引被用來對整個非結構
化文本文檔進行標引。 Elasticsearch 中的 文檔 是有字段和值的結構化 JSON 文檔。事實
上,在 JSON 文檔中, 每個被索引的字段都有自己的倒排索引。

這個倒排索引相比特定詞項出現過的文檔列表,會包含更多其它信息。它會保存每一個詞項出現過的文檔總數, 在對應的文檔中一個具體詞項出現的總次數,詞項在文檔中的順序,每個文檔的長度,所有文檔的平均長度,等等。這些統計信息允許 Elasticsearch 決定哪些詞比其它詞更重要,哪些文檔比其它文檔更重要;
倒排索引需要知道集合中的 所有文檔;早期的全文檢索會爲整個文檔集合建立一個很大的倒排索引並將其寫入到磁盤。 一旦新的索引就緒,舊的就會被其替換,這樣最近的變化便可以被檢索到。
不變性
倒排索引被寫入磁盤後是 不可改變 的:它永遠不會修改。 不變性有重要的價值:

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

當然,一個不變的索引也有不好的地方。主要事實是它是不可變的! 你不能修改它。如果你需要讓一個新的文檔 可被搜索,你需要重建整個索引。這要麼對一個索引所能包含的數據量造成了很大的限制,要麼對索引可被更新的頻率造成了很大的限制。

2.動態更新索引

下一個需要被解決的問題是怎樣在保留不變性的前提下實現倒排索引的更新?答案是: 用更多的索引

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

Elasticsearch 基於 Lucene, 這個 java 庫引入了 按段搜索 的概念。 每一 段 本身都是一個倒排索引, 但 索引 在 Lucene 中除表示所有 段 的集合外, 還增加了 提交點 的概念 :一個列出了所有已知段的文件,一個 Lucene 索引包含一個提交點和三個段。 一個在內存緩存中包含新文檔的 Lucene 索引,新的文檔首先被添加到內存索引緩存中,然後寫入到一個基於磁盤的段,在一次提交後,一個新的段被添加到提交點而且緩存被清空。
圖1

索引與分片的比較
被混淆的概念是,一個 Lucene 索引 我們在 Elasticsearch 稱作 分片 。 一個Elasticsearch 
索引 是分片的集合。 當 Elasticsearch 在索引中搜索的時候, 他發送查詢到每一個屬於索
引的分片(Lucene 索引),然後像 執行分佈式檢索 提到的那樣,合併每個分片的結果到一
個全局的結果集。

逐段搜索會以如下流程進行工作:

1.新文檔被收集到內存索引緩存,如下圖所示:
2.不時地, 緩存被提交 :

  • 一個新的段:一個追加的倒排索引—​被寫入磁盤。
  • 一個新的包含新段名字的 提交點 被寫入磁盤。
  • 磁盤進行同步: 所有在文件系統緩存中等待的寫入都刷新到磁盤,以確保它們被寫入物理文件。

3.新的段被開啓,讓它包含的文檔可見以被搜索。
4.內存緩存被清空,等待接收新的文檔。

一個在內存緩存中包含新文檔的 Lucene 索引
在這裏插入圖片描述
在一次提交後,一個新的段被添加到提交點而且緩存被清空。
在這裏插入圖片描述
當一個查詢被觸發,所有已知的段按順序被查詢。詞項統計會對所有段的結果進行聚合,以保證每個詞和每個文檔的關聯都被準確計算。 這種方式可以用相對較低的成本將新文檔添加到索引。

刪除和更新
段是不可改變的,所以既不能從把文檔從舊的段中移除,也不能修改舊的段來進行反映文檔的更新。 取而代之的是,每個提交點會包含一個 .del 文件,文件中會列出這些被刪除文檔的段信息。

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

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

3.近實時搜索

隨着按段(per-segment)搜索的發展,一個新的文檔從索引到可被搜索的延遲顯著降低了。新文檔在幾分鐘之內即可被檢索,但這樣還是不夠快。

磁盤在這裏成爲了瓶頸。提交(Commiting)一個新的段到磁盤需要一個 fsync 來確保段被物理性地寫入磁盤,這樣在斷電的時候就不會丟失數據。 但是 fsync 操作代價很大; 如果每次索引一個文檔都去執行一次的話會造成很大的性能問題。

我們需要的是一個更輕量的方式來使一個文檔可被搜索,這意味着 fsync 要從整個過程中被移除。

在Elasticsearch和磁盤之間是文件系統緩存。 像之前描述的一樣, 在內存索引緩衝區(“在內存緩衝區中包含了新文檔的 Lucene 索引” )中的文檔會被寫入到一個新的段中( “緩衝區的內容已經被寫入一個可被搜索的段中,但還沒有進行提交” )。 但是這裏新段會被先寫入到文件系統緩存—​這一步代價會比較低,稍後再被刷新到磁盤—​這一步代價比較高。不過只要文件已經在緩存中, 就可以像其它文件一樣被打開和讀取了。

在內存緩衝區中包含了新文檔的 Lucene 索引
在這裏插入圖片描述
Lucene 允許新段被寫入和打開—​使其包含的文檔在未進行一次完整提交時便對搜索可見。 這種方式比進行一次提交代價要小得多,並且在不影響性能的前提下可以被頻繁地執行。

緩衝區的內容已經被寫入一個可被搜索的段中,但還沒有進行提交
在這裏插入圖片描述
refresh API
在 Elasticsearch 中,寫入和打開一個新段的輕量的過程叫做 refresh 。 默認情況下每個分片會每秒自動刷新一次。這就是爲什麼我們說 Elasticsearch 是 近 實時搜索: 文檔的變化並不是立即對搜索可見,但會在一秒之內變爲可見。

這些行爲可能會對新用戶造成困惑: 他們索引了一個文檔然後嘗試搜索它,但卻沒有搜到。這個問題的解決辦法是用 refresh API 執行一次手動刷新:

POST /_refresh    //刷新(Refresh)所有的索引。
POST /blogs/_refresh   //只刷新(Refresh) blogs 索引。

並不是所有的情況都需要每秒刷新。可能你正在使用 Elasticsearch 索引大量的日誌文件, 你可能想優化索引速度而不是近實時搜索, 可以通過設置 refresh_interval , 降低每個索引的刷新頻率:

PUT /my_logs
{
  "settings": {
    "refresh_interval": "30s"  //每30秒刷新 my_logs 索引。
  }
}

refresh_interval 可以在既存索引上進行動態更新。 在生產環境中,當你正在建立一個大的新索引時,可以先關閉自動刷新,待開始使用該索引時,再把它們調回來:

PUT /my_logs/_settings
{ "refresh_interval": -1 }  //關閉自動刷新。

PUT /my_logs/_settings
{ "refresh_interval": "1s" }  //每秒自動刷新。

4.持續化變更

如果沒有用 fsync 把數據從文件系統緩存刷(flush)到硬盤,我們不能保證數據在斷電甚至是程序正常退出之後依然存在。爲了保證 Elasticsearch 的可靠性,需要確保數據變化被持久化到磁盤。

在 動態更新索引,我們說一次完整的提交會將段刷到磁盤,並寫入一個包含所有段列表的提交點。Elasticsearch 在啓動或重新打開一個索引的過程中使用這個提交點來判斷哪些段隸屬於當前分片。

即使通過每秒刷新(refresh)實現了近實時搜索,我們仍然需要經常進行完整提交來確保能從失敗中恢復。但在兩次提交之間發生變化的文檔怎麼辦?我們也不希望丟失掉這些數據。

Elasticsearch 增加了一個 translog ,或者叫事務日誌,在每一次對 Elasticsearch 進行操作時均進行了日誌記錄。通過 translog ,整個流程看起來是下面這樣:

1.一個文檔被索引之後,就會被添加到內存緩衝區,並且追加到了 translog 。新的文檔被添加到內存緩衝區並且被追加到了事務日誌:
在這裏插入圖片描述
2.刷新(refresh)完成後, 緩存被清空但是事務日誌不會,分片每秒被刷新(refresh)一次:

  • 這些在內存緩衝區的文檔被寫入到一個新的段中,且沒有進行 fsync 操作。
  • 這個段被打開,使其可被搜索。
  • 內存緩衝區被清空。
    在這裏插入圖片描述
    3.這個進程繼續工作,更多的文檔被添加到內存緩衝區和追加到事務日誌,事務日誌不斷積累文檔:
    在這裏插入圖片描述
    4.每隔一段時間—​例如 translog 變得越來越大—​索引被刷新(flush);一個新的 translog 被創建,並且一個全量提交被執行。在刷新(flush)之後,段被全量提交,並且事務日誌被清空:
  • 所有在內存緩衝區的文檔都被寫入一個新的段。
  • 緩衝區被清空。
  • 一個提交點被寫入硬盤。
  • 文件系統緩存通過 fsync 被刷新(flush)。
  • 老的 translog 被刪除。
    translog 提供所有還沒有被刷到磁盤的操作的一個持久化紀錄。當 Elasticsearch 啓動的時候, 它會從磁盤中使用最後一個提交點去恢復已知的段,並且會重放 translog 中所有在最後一次提交後發生的變更操作。

translog 也被用來提供實時 CRUD 。當你試着通過ID查詢、更新、刪除一個文檔,它會在嘗試從相應的段中檢索之前, 首先檢查 translog 任何最近的變更。這意味着它總是能夠實時地獲取到文檔的最新版本。
在這裏插入圖片描述
flush API
這個執行一個提交併且截斷 translog 的行爲在 Elasticsearch 被稱作一次 flush 。 分片每30分鐘被自動刷新(flush),或者在 translog 太大的時候也會刷新。它可以用來 控制這些閾值:
flush API 可以被用來執行一個手工的刷新(flush):

POST /blogs/_flush  //刷新(flush) blogs 索引。

POST /_flush?wait_for_ongoing //刷新(flush)所有的索引並且並且等待所有刷新在返回前完成。

很少需要自己手動執行 flush 操作;通常情況下,自動刷新就足夠了。
這就是說,在重啓節點或關閉索引之前執行 flush 有益於你的索引。當 Elasticsearch 嘗試恢復或重新打開一個索引, 它需要重放 translog 中所有的操作,所以如果日誌越短,恢復越快。

5.段合併

由於自動刷新流程每秒會創建一個新的段 ,這樣會導致短時間內的段數量暴增。而段數目太多會帶來較大的麻煩。 每一個段都會消耗文件句柄、內存和cpu運行週期。更重要的是,每個搜索請求都必須輪流檢查每個段;所以段越多,搜索也就越慢。

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

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

啓動段合併不需要你做任何事。進行索引和搜索時會自動進行。兩個提交了的段和一個未提交的段正在被合併到一個更大的段,如下圖所示:

1、 當索引的時候,刷新(refresh)操作會創建新的段並將段打開以供搜索使用。
2、 合併進程選擇一小部分大小相似的段,並且在後臺將它們合併到更大的段中。這並不會中斷索引和搜索。
在這裏插入圖片描述
3、一旦合併結束,老的段被刪除,說明合併完成時的活動:

  • 新的段被刷新(flush)到了磁盤。寫入一個包含新段且排除舊的和較小的段的新提交點。
  • 新的段被打開用來搜索。
  • 老的段被刪除。
    在這裏插入圖片描述

optimize API

optimize API大可看做是 強制合併 API。它會將一個分片強制合併到 max_num_segments 參數指定大小的段數目。 這樣做的意圖是減少段的數量(通常減少到一個),來提升搜索性能。

optimize API 不應該 被用在一個活躍的索引————一個正積極更新的索引。後臺合併流程已經可以很好地完成工作。 optimizing 會阻礙這個進程。不要干擾它!

在特定情況下,使用 optimize API 頗有益處。例如在日誌這種用例下,每天、每週、每月的日誌被存儲在一個索引中。 老的索引實質上是隻讀的;它們也並不太可能會發生變化。

在這種情況下,使用optimize優化老的索引,將每一個分片合併爲一個單獨的段就很有用了;這樣既可以節省資源,也可以使搜索更加快速:

POST /logstash-2014-10/_optimize?max_num_segments=1  //合併索引中的每個分片爲一個單獨的段
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章