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 是採用什麼方式存儲的呢?

首先我們先來說幾個概念,然後再具體介紹下它的整個流程及細節處理,方便大家更好的理解。

索引文檔被拆分成多個子文檔,則每個子文檔叫作段。段提出來的原因是:在早期全文檢索中爲整個文檔集合建立了一個很大的倒排索引,並將其寫入磁盤中。如果索引有更新,就需要重新全量創建一個索引來替換原來的索引。這種方式在數據量很大時效率很低,並且由於創建一次索引的成本很高,所以對數據的更新不能過於頻繁,也就不能保證時效性。

特點

索引文檔是以段的形式存儲在磁盤上的,每一個段本身都是一個倒排索引,並且段具有不變性,一旦索引的數據被寫入硬盤,就不能再修改。

那麼問題來了,不能修改,如何實現增刪改呢?

一個Lucene索引會包含一個提交點和多個段,段被寫入到磁盤後會生成一個提交點,提交點是一個用來記錄所有提交後段信息的文件。一個段一旦擁有了提交點,就說明這個段只有讀的權限,失去了寫的權限。ES在啓動或重新打開一個索引的過程中使用這個提交點來判斷哪些段隸屬於當前分片。

段的優勢
段的缺點

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 存儲流程

阿 Q 正在將 ES 的知識做一個系統的學習與講解,後續還會持續輸出 ES 的相關知識,如果你感興趣的話,可以關注微信公衆號 “阿 Q 說”!你也可以後臺留言說出你的疑惑,阿 Q 將會在後期的文章中爲你解答。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Dteb6NOEMr3fjLKp5z8OoQ