在分佈式集羣中,咱們介紹了分片,把它描述爲底層的工做單元。但分片究竟是什麼,它怎樣工做?在這章節,咱們將回答這些問題:html
爲何搜索是近實時的?
爲何文檔的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更重要,哪些文檔更重要,也就是相關性。數據結構
須要意識到,爲了實現倒排索引預期的功能,它必需要知道集合中全部的文檔。分佈式
在全文檢索的早些時候,會爲整個文檔集合創建一個大索引,而且寫入磁盤。只有新的索引準備好了,它就會替代舊的索引,最近的修改才能夠被檢索。ide
寫入磁盤的倒排索引是不可變的,它有以下好處:性能
1.不須要鎖。若是歷來不須要更新一個索引,就沒必要擔憂多個程序同時嘗試修改。
2.一旦索引被讀入文件系統的緩存(譯者:在內存),它就一直在那兒,由於不會改變。
只要文件系統緩存有足夠的空間,大部分的讀會直接訪問內存而不是磁盤。這有助於性能提高。
3.在索引的聲明週期內,全部的其餘緩存均可用。它們不須要在每次數據變化了都重建,由於數據不會變。
4.寫入單個大的倒排索引,能夠壓縮數據,較少磁盤IO和須要緩存索引的內存大小。
複製代碼
固然,不可變的索引有它的缺點,首先是它不可變!你不能改變它。若是想要搜索一個新文檔,必須重見整個索引。這不只嚴重限制了一個索引所能裝下的數據,還有一個索引能夠被更新的頻次。優化
下一個須要解決的問題是如何在保持不可變好處的同時更新倒排索引。答案是,使用多個索引。ui
不是重寫整個倒排索引,而是增長額外的索引反映最近的變化。每一個倒排索引均可以按順序查詢,從最老的開始,最後把結果聚合。
Elasticsearch底層依賴的Lucene,引入了per-segment search
的概念。一個段(segment
)是有完整功能的倒排索引,可是如今Lucene中的索引指的是段的集合,再加上提交點(commit point
,包括全部段的文件),如圖1所示。新的文檔,在被寫入磁盤的段以前,首先寫入內存區的索引緩存,如圖二、圖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:緩存內容已經寫到段中,可是還沒提交
在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:
圖2:通過一次refresh,緩存被清除,但事務日誌沒有
3.隨着更多的文檔加入到緩存區,寫入日誌,這個過程會繼續
圖3:事務日誌會記錄增加的文檔
4.不時地,好比日誌很大了,新的日誌會建立,會進行一次全提交:
事務日誌記錄了沒有flush到硬盤的全部操做。當故障重啓後,ES會用最近一次提交點從硬盤恢復全部已知的段,而且從日誌裏恢復全部的操做。
事務日誌還用來提供實時的CRUD操做。當你嘗試用ID進行CRUD時,它在檢索相關段內的文檔前會首先檢查日誌最新的改動。這意味着ES能夠實時地獲取文檔的最新版本。
圖4:flush事後,段被全提交,事務日誌清除
在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.下圖描述了合併後的操做:
圖2:段合併完後,舊的段被刪除
合併大的段會消耗不少IO和CPU,若是不檢查會影響到搜素性能。默認狀況下,ES會限制合併過程,這樣搜索就能夠有足夠的資源進行。
optimize API最好描述爲強制合併段API。它強制分片合併段以達到指定max_num_segments
參數。這是爲了減小段的數量(一般爲1)達到提升搜索性能的目的。
警告
不要在動態的索引(正在活躍更新)上使用optimize API。
後臺的合併處理已經作的很好了,優化命令會阻礙它的工做。不要干涉!
複製代碼
在特定的環境下,optimize API是有用的。典型的場景是記錄日誌,這中狀況下日誌是按照天天,周,月存入索引。舊的索引通常是隻可讀的,它們是不可能修改的。 這種狀況下,把每一個索引的段降至1是有效的。搜索過程就會用到更少的資源,性能更好:
POST /logstash-2014-10/_optimize?max_num_segments=1 <1>
複製代碼
<1> 把索引中的每一個分片都合併成一個段
參考:es權威指南