ElasticSearch第三彈之存儲原理

咱們上文中介紹的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 是採用什麼方式存儲的呢?服務器

首先咱們先來講幾個概念,而後再具體介紹下它的整個流程及細節處理,方便你們更好的理解。微信

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

特色

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

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

  • 新增:新增很好處理,因爲數據是新的,因此只須要對當前文檔新增一個段就能夠了。
  • 刪除:段是不可改變的,因此既不能把文檔從舊的段中移除,也不能修改舊的段來進行文檔的更新。取而代之的是每一個提交點(定義會在下邊給出)會包含一個 .del 文件,文件中會列出這些被刪除文檔的段信息。當一個文檔被 「刪除」 時,它實際上只是在 .del 文件中被標記刪除。一個被標記刪除的文檔仍然能夠被查詢匹配到,但它會在最終結果被返回前從結果集中移除。
  • 更新:更新至關因而刪除和新增這兩個動做組成。當一個文檔被更新時,舊版本文檔被標記刪除,文檔的新版本被索引到一個新的段中。可能兩個版本的文檔都會被一個查詢匹配到,但被刪除的那個舊版本文檔在結果集返回前就已經被移除。

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

段的優點
  • 不須要鎖。若是你歷來不更新索引,你就不須要擔憂多進程同時修改數據的問題。
  • 一旦索引被讀入內核的文件系統緩存,便會留在哪裏,因爲其不變性。只要文件系統緩存中還有足夠的空間,那麼大部分讀請求會直接請求內存,而不會命中磁盤。這提供了很大的性能提高。
  • 其它緩存(像 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 經過在後臺按期進行段合併來解決這個問題。小的段被合併到大的段,而後這些大的段再被合併到更大的段(這些段既能夠是未提交的也能夠是已提交的)。

啓動段合併不須要你作任何事,進行索引和搜索時會自動進行:

一、 當索引的時候,刷新(refresh)操做會建立新的段並將段打開以供搜索使用;

二、 合併進程選擇一小部分大小類似的段,而且在後臺將它們合併到更大的段中,這並不會中斷索引和搜索;

三、 「一旦合併結束,老的段被刪除」 說明合併完成時的活動:新的段被刷新(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)全部的索引而且而且等待全部刷新在返回前完成。

總結

最後咱們來講一下添加了事務日誌後的整個存儲的流程吧:

  • 一個新文檔被索引以後,先被寫入到內存中,可是爲了防止數據的丟失,會追加一份數據到事務日誌中。不斷有新的文檔被寫入到內存,同時也都會記錄到事務日誌中(日誌默認存儲到文件緩存系統,每五秒刷新一下到本地磁盤,可是會致使數據丟失,也能夠設置參數每一個請求都同步,可是性能降低)。這時新數據還不能被檢索和查詢。
  • 當達到默認的刷新時間或內存中的數據達到必定量後,會觸發一次 Refresh,將內存中的數據以一個新段形式刷新到文件緩存系統中並清空內存。這時雖然新段未被提交到磁盤,可是能夠提供文檔的檢索功能且不能被修改。
  • 隨着新文檔索引不斷被寫入,當日志數據大小超過 512M 或者時間超過 30 分鐘時,會觸發一次 Flush。內存中的數據被寫入到一個新段同時被寫入到文件緩存系統,文件系統緩存中數據經過 Fsync 刷新到磁盤中,生成提交點,日誌文件被刪除,建立一個空的新日誌。
  • 經過這種方式當斷電或須要重啓時,ES 不只要根據提交點去加載已經持久化過的段,還須要讀取 Translog 裏的記錄,把未持久化的數據從新持久化到磁盤上,避免了數據丟失的可能。

阿Q正在將ES的知識作一個系統的學習與講解,後續還會持續輸出ES的相關知識,若是你感興趣的話,能夠關注gzh「阿Q說代碼」!也能夠加我微信qingqing-4132,期待你的到來!

相關文章
相關標籤/搜索