ElastricSearch 第三彈之存儲原理
我們上文中介紹的ES內部索引的寫處理流程是在ES的內存中執行的,而數據被分配到特定的主、副分片上之後,最終是存儲到磁盤上的,這樣在斷電的時候就不會丟失數據。具體的存儲路徑可在配置文件 ../config/elasticsearch.yml 中進行設置,默認存儲在安裝目錄的 Data文件夾下。建議不要使用默認值,因爲若 ES 進行了升級,則有可能導致數據全部丟失。文件配置如下:
path.data: /path/to/data //索引數據
path.logs: /path/to/logs //日誌記錄
那麼ES是怎麼將索引從內存中同步到磁盤上的呢?今天我們就來說一下ES的存儲原理(搬着小板凳坐好)。
我們先設想一下,ES是否是直接調用 Fsync 物理性地寫入磁盤?答案是否定的,如果是直接寫入磁盤,磁盤的 I/O 消耗會嚴重影響性能, 那麼當寫數據量大的時候會造成 ES 停頓卡死,查詢也無法做到快速響應, ES 就不會被稱爲近實時全文搜索引擎了。那麼問題來了,ES 是採用什麼方式存儲的呢?
首先我們先來說幾個概念,然後再具體介紹下它的整個流程及細節處理,方便大家更好的理解。
段
索引文檔被拆分成多個子文檔,則每個子文檔叫作段。段提出來的原因是:在早期全文檢索中爲整個文檔集合建立了一個很大的倒排索引,並將其寫入磁盤中。如果索引有更新,就需要重新全量創建一個索引來替換原來的索引。這種方式在數據量很大時效率很低,並且由於創建一次索引的成本很高,所以對數據的更新不能過於頻繁,也就不能保證時效性。
特點
索引文檔是以段的形式存儲在磁盤上的,每一個段本身都是一個倒排索引,並且段具有不變性,一旦索引的數據被寫入硬盤,就不能再修改。
那麼問題來了,不能修改,如何實現增刪改呢?
-
新增:新增很好處理,由於數據是新的,所以只需要對當前文檔新增一個段就可以了。
-
刪除:段是不可改變的,所以既不能把文檔從舊的段中移除,也不能修改舊的段來進行文檔的更新。取而代之的是每個提交點(定義會在下邊給出)會包含一個 .del 文件,文件中會列出這些被刪除文檔的段信息。當一個文檔被 “刪除” 時,它實際上只是在 .del 文件中被標記刪除。一個被標記刪除的文檔仍然可以被查詢匹配到,但它會在最終結果被返回前從結果集中移除。
-
更新:更新相當於是刪除和新增這兩個動作組成。當一個文檔被更新時,舊版本文檔被標記刪除,文檔的新版本被索引到一個新的段中。可能兩個版本的文檔都會被一個查詢匹配到,但被刪除的那個舊版本文檔在結果集返回前就已經被移除。
★
一個
Lucene索引會包含一個提交點和多個段,段被寫入到磁盤後會生成一個提交點,提交點是一個用來記錄所有提交後段信息的文件。一個段一旦擁有了提交點,就說明這個段只有讀的權限,失去了寫的權限。ES在啓動或重新打開一個索引的過程中使用這個提交點來判斷哪些段隸屬於當前分片。”
段的優勢
-
不需要鎖。如果你從來不更新索引,你就不需要擔心多進程同時修改數據的問題。
-
一旦索引被讀入內核的文件系統緩存,便會留在那裏,由於其不變性。只要文件系統緩存中還有足夠的空間,那麼大部分讀請求會直接請求內存,而不會命中磁盤。這提供了很大的性能提升。
-
其它緩存 (像
Filter緩存),在索引的生命週期內始終有效。它們不需要在每次數據改變時被重建,因爲數據不會變化。 -
寫入單個大的倒排索引允許數據被壓縮,減少磁盤 I/O 和需要被緩存到內存的索引的使用量。
段的缺點
-
當對舊數據進行刪除時,舊數據不會馬上被刪除,而是在 .del 文件中被標記爲刪除。而舊數據只能等到段更新時才能被移除,這樣會造成大量的空間浪費。
-
若有一條數據頻繁的更新,每次更新都是新增新的標記舊的,則會有大量的空間浪費。
-
每次新增數據時都需要新增一個段來存儲數據。當段的數量太多時,對服務器的資源例如文件句柄的消耗會非常大。
-
在查詢的結果中包含所有的結果集,需要排除被標記刪除的舊數據,這增加了查詢的負擔。
Refresh(刷新)
在 ES 中,寫入和打開一個新段的輕量的過程叫做 Refresh (即 ES 內存刷新到文件緩存系統)。ES首先會將文檔加載到ES的內存緩衝區(當段在內存中時,就只有寫的權限,而不具備讀數據的權限,意味着不能被檢索),當達到默認的時間(1 秒鐘)或者內存的數據達到一定量時,會觸發一次刷新(Refresh),這時數據就會被加載到文件緩存系統(操作系統的內存),創建新的段並將段打開以供搜索使用。這就是爲什麼我們說 ES 是近實時搜索,因爲文檔的變化並不是立即對搜索可見,但會在一秒之內變爲可見。這就會存在一個問題:當你索引了一個文檔然後嘗試搜索它,但卻沒有搜到。這個問題的解決辦法是用 refresh API 執行一次手動刷新。配置如下:
POST /_refresh //刷新(Refresh)所有的索引。
POST /blogs/_refresh //只刷新(Refresh) blogs 索引。
注: 當寫測試的時候,手動刷新很有用,但是不要在生產環境下每次索引一個文檔都去手動刷新。
儘管刷新是比提交輕量很多的操作,它還是會有性能開銷,並不是所有的情況都需要每秒刷新:當你使用 ES 索引大量的日誌文件時,你可能想優化索引速度而不是近實時搜索,這時可以在創建索引時在 Settings 中通過調大 refresh_interval = "30s" 的值,降低每個索引的刷新頻率,設值時需要注意後面帶上時間單位,否則默認是毫秒,如果是 1 毫秒無疑會使你的集羣陷入癱瘓。當 refresh_interval=-1 時表示關閉索引的自動刷新。配置如下:
PUT /my_logs
{
"settings": {
"refresh_interval": "1s" //每秒刷新 my_logs 索引
}
}
★
refresh_interval 可以在既存索引上進行動態更新。在生產環境中,當你正在建立一個大的新索引時,可以先關閉自動刷新,待開始使用該索引時,再把它們調回來。
”
段合併
由於自動刷新流程每秒會創建一個新的段,這樣會導致短時間內的段數量暴增。而段數目太多會帶來較大的麻煩。每一個段都會消耗文件句柄、內存和 CPU 運行週期。更重要的是,每個搜索請求都必須輪流檢查每個段然後合併查詢結果,所以段越多,搜索也就越慢。ES 通過在後臺定期進行段合併來解決這個問題。小的段被合併到大的段,然後這些大的段再被合併到更大的段(這些段既可以是未提交的也可以是已提交的)。
兩個提交了的段和一個未提交的段正在被合併到一個更大的段
啓動段合併不需要你做任何事,進行索引和搜索時會自動進行:
1、 當索引的時候,刷新(refresh)操作會創建新的段並將段打開以供搜索使用;
2、 合併進程選擇一小部分大小相似的段,並且在後臺將它們合併到更大的段中,這並不會中斷索引和搜索;
3、 “一旦合併結束,老的段被刪除” 說明合併完成時的活動:新的段被刷新(flush)到了磁盤,寫入一個包含新段且排除舊的和較小的段的新提交點,那些舊的已刪除文檔從文件系統中清除,被刪除的文檔(或被更新文檔的舊版本)不會被拷貝到新的大段中。
一旦合併結束,老的段被刪除
段合併的計算量龐大,需要消耗大量的 I/O 和 CPU 資源,並會拖累寫入速率,如果任其發展會影響搜索性能。ES 在默認情況下會對合並流程進行資源限制,所以搜索仍然有足夠的資源很好地執行。限流閾值默認是 20MB/s,如果是SSD,可以考慮 100-200MB/s;如果是機械磁盤而非 SSD,需要增加設置 index.merge.scheduler.max_thread_count: 1。因爲機械磁盤在併發 I/O 支持方面比較差,所以我們需要降低每個索引併發訪問磁盤的線程數。這個設置允許 max_thread_count + 2 個線程同時進行磁盤操作,也就是設置爲 1 允許三個線程,SSD 默認是 Math.min(3, Runtime.getRuntime().availableProcessors() / 2),支持很好;如果在做批量導入,不在意搜索,可以設置爲 none。配置如下:
PUT /_cluster/settings
{
"persistent" : {
"indices.store.throttle.max_bytes_per_sec" : "100mb"
}
}
optimize API
optimize API大可看做是強制合併 API。它會將一個分片強制合併到 max_num_segments 參數指定大小的段數目。這樣做的意圖是減少段的數量(通常減少到一個)來提升搜索性能。
optimize API不應該被用在一個活躍的索引 -- 一個正積極更新的索引:後臺合併流程已經可以很好地完成工作,optimizing 會阻礙這個進程,不要干擾它!在特定情況下,使用 optimize API 頗有益處。例如在日誌這種用例下,每天、每週、每月的日誌被存儲在一個索引中,老的索引實質上是隻讀的;它們也並不太可能會發生變化。在這種情況下,使用 optimize 優化老的索引,將每一個分片合併爲一個單獨的段就很有用了,這樣既可以節省資源,也可以使搜索更加快速。
POST /logstash-2014-10/_optimize?max_num_segments=1 //合併索引中的每個分片爲一個單獨的段
★
請注意,使用
optimize API觸發段合併的操作不會受到任何資源上的限制。這可能會消耗掉你節點上全部的 I/O 資源,使其沒有餘力來處理搜索請求,從而有可能使集羣失去響應。如果你想要對索引執行optimize,你需要先使用分片分配把索引移到一個安全的節點,再執行。”
Translog
爲了提升寫的性能,ES 並沒有每新增一條數據就增加一個段到磁盤上,而是採用延遲寫的策略。等文件系統中有新段生成之後,在稍後的時間裏再被刷新到磁盤中並生成提交點。雖然通過延時寫的策略可以減少數據往磁盤上寫的次數提升了整體的寫入能力,但是我們知道文件緩存系統也是內存空間,屬於操作系統的內存,只要是內存都存在斷電或異常情況下丟失數據的危險。爲了避免丟失數據,ES 添加了事務日誌(Translog),事務日誌記錄了所有還沒有持久化到磁盤的數據。
translog 默認是每 5 秒被 fsync 刷新到硬盤,或者在每次寫請求完成之後執行 (index, delete, update, bulk) 操作也可以刷新到磁盤。在每次請求後都執行一個 fsync 會帶來一些性能損失,儘管實踐表明這種損失相對較小(特別是bulk導入,它在一次請求中平攤了大量文檔的開銷)。對於一些大容量的偶爾丟失幾秒數據問題也並不嚴重的集羣,使用異步的 fsync 還是比較有益的。我們可以通過設置 durability 參數爲 async 來啓用:
PUT /my_index/_settings
{
"index.translog.durability": "async",
"index.translog.sync_interval": "5s"
}
這個選項可以針對索引單獨設置,並且可以動態進行修改。如果你決定使用異步 translog 的話,你需要保證在發生crash時,丟失掉 sync_interval 時間段的數據也無所謂。如果你不確定這個行爲的後果,最好是使用默認的參數( "index.translog.durability": "request" )來避免數據丟失。
Flush
執行一個提交併且截斷 translog 的行爲在ES中被稱作一次flush。分片每 30 分鐘被自動刷新(flush)或者在 translog 太大的時候也會刷新。可以通過設置translog 文檔來控制這些閾值,flush API 可以被用來執行一個手工的刷新(flush):
POST /blogs/_flush //刷新(flush) blogs 索引。
POST /_flush?wait_for_ongoing //刷新(flush)所有的索引並且並且等待所有刷新在返回前完成。
總結
最後我們來說一下添加了事務日誌後的整個存儲的流程吧:
ES 存儲流程
-
一個新文檔被索引之後,先被寫入到內存中,但是爲了防止數據的丟失,會追加一份數據到事務日誌中。不斷有新的文檔被寫入到內存,同時也都會記錄到事務日誌中(日誌默認存儲到文件緩存系統,每五秒刷新一下到本地磁盤,但是會導致數據丟失,也可以設置參數每個請求都同步,但是性能下降)。這時新數據還不能被檢索和查詢。
-
當達到默認的刷新時間或內存中的數據達到一定量後,會觸發一次
Refresh,將內存中的數據以一個新段形式刷新到文件緩存系統中並清空內存。這時雖然新段未被提交到磁盤,但是可以提供文檔的檢索功能且不能被修改。 -
隨着新文檔索引不斷被寫入,當日志數據大小超過
512M或者時間超過30分鐘時,會觸發一次Flush。內存中的數據被寫入到一個新段同時被寫入到文件緩存系統,文件系統緩存中數據通過Fsync刷新到磁盤中,生成提交點,日誌文件被刪除,創建一個空的新日誌。 -
通過這種方式當斷電或需要重啓時,
ES不僅要根據提交點去加載已經持久化過的段,還需要讀取Translog裏的記錄,把未持久化的數據重新持久化到磁盤上,避免了數據丟失的可能。
阿 Q 正在將 ES 的知識做一個系統的學習與講解,後續還會持續輸出 ES 的相關知識,如果你感興趣的話,可以關注微信公衆號 “阿 Q 說”!你也可以後臺留言說出你的疑惑,阿 Q 將會在後期的文章中爲你解答。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Dteb6NOEMr3fjLKp5z8OoQ