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 文件中被標記為刪除。而舊數據只能等到段更新時才能被移除,這樣會造成大量的空間浪費。
若有一條數據頻繁的更新,每次更新都是新增新的標記舊的,則會有大量的空間浪費。
每次新增數據時都需要新增一個段來存儲數據。當段的數量太多時,對服務器的資源例如文件句柄的消耗會非常大。
在查詢的結果中包含所有的結果集,需要排除被標記刪除的舊數據,這增加了查詢的負擔。
在 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大可看做是強制合并 API。它會將一個分片強制合并到 max_num_segments 參數指定大小的段數目。這樣做的意圖是減少段的數量(通常減少到一個)來提升搜索性能。
optimize API不應該被用在一個活躍的索引–一個正積極更新的索引:后臺合并流程已經可以很好地完成工作,optimizing 會阻礙這個進程,不要干擾它!在特定情況下,使用 optimize API 頗有益處。例如在日志這種用例下,每天、每周、每月的日志被存儲在一個索引中,老的索引實質上是只讀的;它們也并不太可能會發生變化。在這種情況下,使用optimize優化老的索引,將每一個分片合并為一個單獨的段就很有用了,這樣既可以節省資源,也可以使搜索更加快速。
POST /logstash-2014-10/_optimize?max_num_segments=1 //合并索引中的每個分片為一個單獨的段
請注意,使用 optimize API 觸發段合并的操作不會受到任何資源上的限制。這可能會消耗掉你節點上全部的I/O資源,使其沒有余力來處理搜索請求,從而有可能使集群失去響應。 如果你想要對索引執行 optimize,你需要先使用分片分配把索引移到一個安全的節點,再執行。
為了提升寫的性能,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” )來避免數據丟失。
執行一個提交并且截斷 translog 的行為在ES中被稱作一次flush。分片每30分鐘被自動刷新(flush)或者在 translog 太大的時候也會刷新。可以通過設置translog 文檔來控制這些閾值,flush API 可以被用來執行一個手工的刷新(flush):
POST /blogs/_flush //刷新(flush) blogs 索引。 POST /_flush?wait_for_ongoing //刷新(flush)所有的索引并且并且等待所有刷新在返回前完成。
最后我們來說一下添加了事務日志后的整個存儲的流程吧:
一個新文檔被索引之后,先被寫入到內存中,但是為了防止數據的丟失,會追加一份數據到事務日志中。不斷有新的文檔被寫入到內存,同時也都會記錄到事務日志中(日志默認存儲到文件緩存系統,每五秒刷新一下到本地磁盤,但是會導致數據丟失,也可以設置參數每個請求都同步,但是性能下降)。這時新數據還不能被檢索和查詢。
當達到默認的刷新時間或內存中的數據達到一定量后,會觸發一次 Refresh,將內存中的數據以一個新段形式刷新到文件緩存系統中并清空內存。這時雖然新段未被提交到磁盤,但是可以提供文檔的檢索功能且不能被修改。
隨著新文檔索引不斷被寫入,當日志數據大小超過 512M 或者時間超過 30 分鐘時,會觸發一次 Flush。內存中的數據被寫入到一個新段同時被寫入到文件緩存系統,文件系統緩存中數據通過 Fsync 刷新到磁盤中,生成提交點,日志文件被刪除,創建一個空的新日志。
通過這種方式當斷電或需要重啟時,ES 不僅要根據提交點去加載已經持久化過的段,還需要讀取 Translog 里的記錄,把未持久化的數據重新持久化到磁盤上,避免了數據丟失的可能。
阿Q正在將ES的知識做一個系統的學習與講解,后續還會持續輸出ES的相關知識,如果你感興趣的話,可以關注微信公眾號“阿Q說”!你也可以后臺留言說出你的疑惑,阿Q將會在后期的文章中為你解答。
Elasticsearch
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。