在分散式叢集中,我們介紹了分片,把它描述為底層的工作單元。但分片到底是什麼,它怎樣工作?在這章節,我們將回答這些問題:
為什麼搜尋是近實時的?
為什麼文件的CRUD操作是實時的?
ES怎樣保證更新持久化,即使斷電也不會丟失?
為什麼刪除文件不會立即釋放空間?
什麼是refresh,flush, optimize API,以及什麼時候你該使用它們?
複製程式碼
為了理解分片如何工作,最簡單的方式是從一堂歷史課開始。我們將會看下,為了提供一個有近實時搜尋和分析功能的分散式、持久化的搜尋引擎需要解決哪些問題。
使文字可以被搜尋
第一個不得不解決的挑戰是如何讓文字變得可搜尋。在傳統的資料庫中,一個欄位存一個值,但是這對於全文搜尋是不足的。想要讓文字中的每個單詞都可以被搜尋,這意味這資料庫需要存多個值。
支援一個欄位多個值的最佳資料結構是倒排索引。倒排索引包含了出現在所有文件中唯一的值或詞的有序列表,以及每個詞所屬的文件列表。
Term | Doc 1 | Doc 2 | Doc 3 | ...
------------------------------------
brown | X | | X | ...
fox | X | X | X | ...
quick | X | X | | ...
the | X | | X | ...
複製程式碼
倒排索引儲存了比包含了一個特定term的文件列表多地多的資訊。它可能儲存包含每個term的文件數量,一個term出現在指定文件中的頻次,每個文件中term的順序,每個文件的長度,所有文件的平均長度,等等。這些統計資訊讓Elasticsearch知道哪些term更重要,哪些文件更重要,也就是相關性。
需要意識到,為了實現倒排索引預期的功能,它必須要知道集合中所有的文件。
在全文檢索的早些時候,會為整個文件集合建立一個大索引,並且寫入磁碟。只有新的索引準備好了,它就會替代舊的索引,最近的修改才可以被檢索。
不可變性
寫入磁碟的倒排索引是不可變的,它有如下好處:
1.不需要鎖。如果從來不需要更新一個索引,就不必擔心多個程式同時嘗試修改。
2.一旦索引被讀入檔案系統的快取(譯者:在記憶體),它就一直在那兒,因為不會改變。
只要檔案系統快取有足夠的空間,大部分的讀會直接訪問記憶體而不是磁碟。這有助於效能提升。
3.在索引的宣告週期內,所有的其他快取都可用。它們不需要在每次資料變化了都重建,因為資料不會變。
4.寫入單個大的倒排索引,可以壓縮資料,較少磁碟IO和需要快取索引的記憶體大小。
複製程式碼
當然,不可變的索引有它的缺點,首先是它不可變!你不能改變它。如果想要搜尋一個新文件,必須重見整個索引。這不僅嚴重限制了一個索引所能裝下的資料,還有一個索引可以被更新的頻次。
動態索引
下一個需要解決的問題是如何在保持不可變好處的同時更新倒排索引。答案是,使用多個索引。
不是重寫整個倒排索引,而是增加額外的索引反映最近的變化。每個倒排索引都可以按順序查詢,從最老的開始,最後把結果聚合。
Elasticsearch底層依賴的Lucene,引入了per-segment search
的概念。一個段(segment
)是有完整功能的倒排索引,但是現在Lucene中的索引指的是段的集合,再加上提交點(commit point
,包括所有段的檔案),如圖1所示。新的文件,在被寫入磁碟的段之前,首先寫入記憶體區的索引快取,如圖2、圖3所示。
圖1:一個提交點和三個索引的Lucene
索引vs分片
為了避免混淆,需要說明,Lucene索引是Elasticsearch中的分片,Elasticsearch中的索引是分片的集合。
當Elasticsearch搜尋索引時,它傳送查詢請求給該索引下的所有分片,然後過濾這些結果,聚合成全域性的結果。
複製程式碼
一個per-segment search
如下工作:
1.新的文件首先寫入記憶體區的索引快取。
2.不時,這些buffer被提交:
一個新的段——額外的倒排索引——寫入磁碟。
新的提交點寫入磁碟,包括新段的名稱。
磁碟是fsync(檔案同步)——所有寫操作等待檔案系統快取同步到磁碟,確保它們可以被物理寫入。
3.新段被開啟,它包含的文件可以被檢索
4.記憶體的快取被清除,等待接受新的文件。
複製程式碼
圖2:記憶體快取區有即將提交文件的Lucene索引
圖3:提交後,新的段加到了提交點,快取被清空
當一個請求被接受,所有段依次查詢。所有段上的Term統計資訊被聚合,確保每個term和文件的相關性被正確計算。通過這種方式,新的文件以較小的代價加入索引。
刪除和更新
段是不可變的,所以文件既不能從舊的段中移除,舊的段也不能更新以反映文件最新的版本。相反,每一個提交點包括一個.del檔案,包含了段上已經被刪除的文件。
當一個文件被刪除,它實際上只是在.del檔案中被標記為刪除,依然可以匹配查詢,但是最終返回之前會被從結果中刪除。
文件的更新操作是類似的:當一個文件被更新,舊版本的文件被標記為刪除,新版本的文件在新的段中索引。也許該文件的不同版本都會匹配一個查詢,但是更老版本會從結果中刪除。
近實時搜尋
因為per-segment search
機制,索引和搜尋一個文件之間是有延遲的。新的文件會在幾分鐘內可以搜尋,但是這依然不夠快。
磁碟是瓶頸。提交一個新的段到磁碟需要fsync
操作,確保段被物理地寫入磁碟,即時電源失效也不會丟失資料。但是fsync
是昂貴的,它不能在每個文件被索引的時就觸發。
所以需要一種更輕量級的方式使新的文件可以被搜尋,這意味這移除fsync
。
位於Elasticsearch和磁碟間的是檔案系統快取。如前所說,在記憶體索引快取中的文件(圖1)被寫入新的段(圖2),但是新的段首先寫入檔案系統快取,這代價很低,之後會被同步到磁碟,這個代價很大。但是一旦一個檔案被快取,它也可以被開啟和讀取,就像其他檔案一樣。
圖1:記憶體快取區有新文件的Lucene索引
Lucene允許新段寫入開啟,好讓它們包括的文件可搜尋,而不用執行一次全量提交。這是比提交更輕量的過程,可以經常操作,而不會影響效能。
圖2:快取內容已經寫到段中,但是還沒提交
refeash API
在Elesticsearch中,這種寫入開啟一個新段的輕量級過程,叫做refresh。預設情況下,每個分片每秒自動重新整理一次。這就是為什麼說Elasticsearch是近實時的搜尋了:文件的改動不會立即被搜尋,但是會在一秒內可見。
這會困擾新使用者:他們索引了個文件,嘗試搜尋它,但是搜不到。解決辦法就是執行一次手動重新整理,通過API:
POST /_refresh <1>
POST /blogs/_refresh <2>
複製程式碼
<1> refresh所有索引
<2> 只refresh 索引blogs
不是所有的使用者都需要每秒重新整理一次。也許你使用ES索引百萬日誌檔案,你更想要優化索引的速度,而不是進實時搜尋。你可以通過修改配置項refresh_interval減少重新整理的頻率:
PUT /my_logs
{
"settings": {
"refresh_interval": "30s" <1>
}
}
複製程式碼
<1> 每30s refresh一次my_logs
refresh_interval
可以在存在的索引上動態更新。你在建立大索引的時候可以關閉自動重新整理,在要使用索引的時候再開啟它。
PUT /my_logs/_settings
{ "refresh_interval": -1 } <1>
PUT /my_logs/_settings
{ "refresh_interval": "1s" } <2>
複製程式碼
<1> 禁用所有自動refresh
<2> 每秒自動refresh
持久化變更
沒用fsync
同步檔案系統快取到磁碟,我們不能確保電源失效,甚至正常退出應用後,資料的安全。為了ES的可靠性,需要確保變更持久化到磁碟。
我們說過一次全提交同步段到磁碟,寫提交點,這會列出所有的已知的段。在重啟,或重新開啟索引時,ES使用這次提交點決定哪些段屬於當前的分片。
當我們通過每秒的重新整理獲得近實時的搜尋,我們依然需要定時地執行全提交確保能從失敗中恢復。但是提交之間的文件怎麼辦?我們也不想丟失它們。
ES增加了事務日誌(translog
),來記錄每次操作。有了事務日誌,過程現在如下:
1.當一個文件被索引,它被加入到記憶體快取,同時加到事務日誌。
圖1:新的文件加入到記憶體快取,同時寫入事務日誌
2.refresh使得分片的進入如下圖描述的狀態。每秒分片都進行refeash:
- 記憶體緩衝區的文件寫入到段中,但沒有fsync。
- 段被開啟,使得新的文件可以搜尋。
- 快取被清除
圖2:經過一次refresh,快取被清除,但事務日誌沒有
3.隨著更多的文件加入到快取區,寫入日誌,這個過程會繼續
圖3:事務日誌會記錄增長的文件
4.不時地,比如日誌很大了,新的日誌會建立,會進行一次全提交:
- 記憶體快取區的所有文件會寫入到新段中。
- 清除快取
- 一個提交點寫入硬碟
- 檔案系統快取通過fsync操作flush到硬碟
- 事務日誌被清除
事務日誌記錄了沒有flush到硬碟的所有操作。當故障重啟後,ES會用最近一次提交點從硬碟恢復所有已知的段,並且從日誌裡恢復所有的操作。
事務日誌還用來提供實時的CRUD操作。當你嘗試用ID進行CRUD時,它在檢索相關段內的文件前會首先檢查日誌最新的改動。這意味著ES可以實時地獲取文件的最新版本。
圖4:flush過後,段被全提交,事務日誌清除
flush API
在ES中,進行一次提交併刪除事務日誌的操作叫做 flush
。分片每30分鐘,或事務日誌過大會進行一次flush
操作。
flush API可用來進行一次手動flush:
POST /blogs/_flush <1>
POST /_flush?wait_for_ongoing <2>
複製程式碼
<1> flush索引blogs
<2> flush所有索引,等待操作結束再返回
你很少需要手動flush,通常自動的就夠了。
當你要重啟或關閉一個索引,flush該索引是很有用的。當ES嘗試恢復或者重新開啟一個索引時,它必須重放所有事務日誌中的操作,所以日誌越小,恢復速度越快。
合併段
通過每秒自動重新整理建立新的段,用不了多久段的數量就爆炸了。有太多的段是一個問題。每個段消費檔案控制程式碼,記憶體,cpu資源。更重要的是,每次搜尋請求都需要依次檢查每個段。段越多,查詢越慢。
ES通過後臺合併段解決這個問題。小段被合併成大段,再合併成更大的段。
這是舊的文件從檔案系統刪除的時候。舊的段不會再複製到更大的新段中。
這個過程你不必做什麼。當你在索引和搜尋時ES會自動處理。這個過程如圖:兩個提交的段和一個未提交的段合併為了一個更大的段所示:
1.索引過程中,refresh會建立新的段,並開啟它。
2.合併過程會在後臺選擇一些小的段合併成大的段,這個過程不會中斷索引和搜尋。
圖1:兩個提交的段和一個未提交的段合併為了一個更大的段
3.下圖描述了合併後的操作:
- 新的段flush到了硬碟。
- 新的提交點寫入新的段,排除舊的段。
- 新的段開啟供搜尋。
- 舊的段被刪除。
圖2:段合併完後,舊的段被刪除
合併大的段會消耗很多IO和CPU,如果不檢查會影響到搜素效能。預設情況下,ES會限制合併過程,這樣搜尋就可以有足夠的資源進行。
optimize API
optimize API最好描述為強制合併段API。它強制分片合併段以達到指定max_num_segments
引數。這是為了減少段的數量(通常為1)達到提高搜尋效能的目的。
警告
不要在動態的索引(正在活躍更新)上使用optimize API。
後臺的合併處理已經做的很好了,優化命令會阻礙它的工作。不要干涉!
複製程式碼
在特定的環境下,optimize API是有用的。典型的場景是記錄日誌,這中情況下日誌是按照每天,周,月存入索引。舊的索引一般是隻可讀的,它們是不可能修改的。 這種情況下,把每個索引的段降至1是有效的。搜尋過程就會用到更少的資源,效能更好:
POST /logstash-2014-10/_optimize?max_num_segments=1 <1>
複製程式碼
<1> 把索引中的每個分片都合併成一個段
參考:es權威指南