ElastricSearch第三彈之存儲原理

我們上文中介紹的ES內(nèi)部索引的寫處理流程是在ES的內(nèi)存中執(zhí)行的,而數(shù)據(jù)被分配到特定的主、副分片上之后,最終是存儲到磁盤上的,這樣在斷電的時候就不會丟失數(shù)據(jù)。具體的存儲路徑可在配置文件 ../config/elasticsearch.yml 中進行設(shè)置,默認(rèn)存儲在安裝目錄的 Data文件夾下。建議不要使用默認(rèn)值,因為若 ES 進行了升級,則有可能導(dǎo)致數(shù)據(jù)全部丟失。文件配置如下:

path.data: /path/to/data  //索引數(shù)據(jù)
path.logs: /path/to/logs  //日志記錄

那么ES是怎么將索引從內(nèi)存中同步到磁盤上的呢?今天我們就來說一下ES的存儲原理(搬著小板凳坐好)。

我們先設(shè)想一下,ES是否是直接調(diào)用 Fsync 物理性地寫入磁盤?答案是否定的,如果是直接寫入磁盤,磁盤的 I/O 消耗會嚴(yán)重影響性能,那么當(dāng)寫數(shù)據(jù)量大的時候會造成 ES 停頓卡死,查詢也無法做到快速響應(yīng), ES 就不會被稱為近實時全文搜索引擎了。那么問題來了,ES 是采用什么方式存儲的呢?

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

后期阿Q的主要精力會放到gzh中,這邊文章更新不及時,請移步gzh“阿Q說”查看。

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

特點

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

那么問題來了,不能修改,如何實現(xiàn)增刪改呢?

  • 新增:新增很好處理,由于數(shù)據(jù)是新的,所以只需要對當(dāng)前文檔新增一個段就可以了。
  • 刪除:段是不可改變的,所以既不能把文檔從舊的段中移除,也不能修改舊的段來進行文檔的更新。取而代之的是每個提交點(定義會在下邊給出)會包含一個 .del 文件,文件中會列出這些被刪除文檔的段信息。當(dāng)一個文檔被 “刪除” 時,它實際上只是在 .del 文件中被標(biāo)記刪除。一個被標(biāo)記刪除的文檔仍然可以被查詢匹配到,但它會在最終結(jié)果被返回前從結(jié)果集中移除。
  • 更新:更新相當(dāng)于是刪除和新增這兩個動作組成。當(dāng)一個文檔被更新時,舊版本文檔被標(biāo)記刪除,文檔的新版本被索引到一個新的段中。可能兩個版本的文檔都會被一個查詢匹配到,但被刪除的那個舊版本文檔在結(jié)果集返回前就已經(jīng)被移除。

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

段的優(yōu)勢
  • 不需要鎖。如果你從來不更新索引,你就不需要擔(dān)心多進程同時修改數(shù)據(jù)的問題。
  • 一旦索引被讀入內(nèi)核的文件系統(tǒng)緩存,便會留在哪里,由于其不變性。只要文件系統(tǒng)緩存中還有足夠的空間,那么大部分讀請求會直接請求內(nèi)存,而不會命中磁盤。這提供了很大的性能提升。
  • 其它緩存(像 Filter 緩存),在索引的生命周期內(nèi)始終有效。它們不需要在每次數(shù)據(jù)改變時被重建,因為數(shù)據(jù)不會變化。
  • 寫入單個大的倒排索引允許數(shù)據(jù)被壓縮,減少磁盤 I/O 和需要被緩存到內(nèi)存的索引的使用量。
段的缺點
  • 當(dāng)對舊數(shù)據(jù)進行刪除時,舊數(shù)據(jù)不會馬上被刪除,而是在 .del 文件中被標(biāo)記為刪除。而舊數(shù)據(jù)只能等到段更新時才能被移除,這樣會造成大量的空間浪費。
  • 若有一條數(shù)據(jù)頻繁的更新,每次更新都是新增新的標(biāo)記舊的,則會有大量的空間浪費。
  • 每次新增數(shù)據(jù)時都需要新增一個段來存儲數(shù)據(jù)。當(dāng)段的數(shù)量太多時,對服務(wù)器的資源例如文件句柄的消耗會非常大。
  • 在查詢的結(jié)果中包含所有的結(jié)果集,需要排除被標(biāo)記刪除的舊數(shù)據(jù),這增加了查詢的負擔(dān)。

Refresh(刷新)

ES 中,寫入和打開一個新段的輕量的過程叫做 Refresh (即ES內(nèi)存刷新到文件緩存系統(tǒng))。ES首先會將文檔加載到ES的內(nèi)存緩沖區(qū)(當(dāng)段在內(nèi)存中時,就只有寫的權(quán)限,而不具備讀數(shù)據(jù)的權(quán)限,意味著不能被檢索),當(dāng)達到默認(rèn)的時間(1 秒鐘)或者內(nèi)存的數(shù)據(jù)達到一定量時,會觸發(fā)一次刷新(Refresh),這時數(shù)據(jù)就會被加載到文件緩存系統(tǒng)(操作系統(tǒng)的內(nèi)存),創(chuàng)建新的段并將段打開以供搜索使用。這就是為什么我們說 ES 是近實時搜索,因為文檔的變化并不是立即對搜索可見,但會在一秒之內(nèi)變?yōu)榭梢?。這就會存在一個問題:當(dāng)你索引了一個文檔然后嘗試搜索它,但卻沒有搜到。這個問題的解決辦法是用 refresh API 執(zhí)行一次手動刷新。配置如下:

POST /_refresh         //刷新(Refresh)所有的索引。
POST /blogs/_refresh   //只刷新(Refresh) blogs 索引。

注: 當(dāng)寫測試的時候,手動刷新很有用,但是不要在生產(chǎn)環(huán)境下每次索引一個文檔都去手動刷新。

盡管刷新是比提交輕量很多的操作,它還是會有性能開銷,并不是所有的情況都需要每秒刷新:當(dāng)你使用 ES 索引大量的日志文件時,你可能想優(yōu)化索引速度而不是近實時搜索,這時可以在創(chuàng)建索引時在 Settings 中通過調(diào)大 refresh_interval = "30s" 的值,降低每個索引的刷新頻率,設(shè)值時需要注意后面帶上時間單位,否則默認(rèn)是毫秒,如果是1毫秒無疑會使你的集群陷入癱瘓。當(dāng) refresh_interval=-1 時表示關(guān)閉索引的自動刷新。配置如下:

PUT /my_logs
{
  "settings": {
    "refresh_interval": "1s"   //每秒刷新 my_logs 索引
  }
}

refresh_interval 可以在既存索引上進行動態(tài)更新。 在生產(chǎn)環(huán)境中,當(dāng)你正在建立一個大的新索引時,可以先關(guān)閉自動刷新,待開始使用該索引時,再把它們調(diào)回來。

段合并

由于自動刷新流程每秒會創(chuàng)建一個新的段,這樣會導(dǎo)致短時間內(nèi)的段數(shù)量暴增。而段數(shù)目太多會帶來較大的麻煩。每一個段都會消耗文件句柄、內(nèi)存和 CPU 運行周期。更重要的是,每個搜索請求都必須輪流檢查每個段然后合并查詢結(jié)果,所以段越多,搜索也就越慢。ES 通過在后臺定期進行段合并來解決這個問題。小的段被合并到大的段,然后這些大的段再被合并到更大的段(這些段既可以是未提交的也可以是已提交的)。

兩個提交了的段和一個未提交的段正在被合并到一個更大的段

啟動段合并不需要你做任何事,進行索引和搜索時會自動進行:

1、 當(dāng)索引的時候,刷新(refresh)操作會創(chuàng)建新的段并將段打開以供搜索使用;

2、 合并進程選擇一小部分大小相似的段,并且在后臺將它們合并到更大的段中,這并不會中斷索引和搜索;

3、 “一旦合并結(jié)束,老的段被刪除” 說明合并完成時的活動:新的段被刷新(flush)到了磁盤,寫入一個包含新段且排除舊的和較小的段的新提交點,那些舊的已刪除文檔從文件系統(tǒng)中清除,被刪除的文檔(或被更新文檔的舊版本)不會被拷貝到新的大段中。

一旦合并結(jié)束,老的段被刪除

段合并的計算量龐大,需要消耗大量的I/O和CPU資源,并會拖累寫入速率,如果任其發(fā)展會影響搜索性能。ES 在默認(rèn)情況下會對合并流程進行資源限制,所以搜索仍然有足夠的資源很好地執(zhí)行。限流閾值默認(rèn)是20MB/s,如果是SSD,可以考慮100-200MB/s;如果是機械磁盤而非SSD,需要增加設(shè)置 index.merge.scheduler.max_thread_count: 1。因為機械磁盤在并發(fā) I/O 支持方面比較差,所以我們需要降低每個索引并發(fā)訪問磁盤的線程數(shù)。這個設(shè)置允許 max_thread_count + 2 個線程同時進行磁盤操作,也就是設(shè)置為 1 允許三個線程,SSD默認(rèn)是 Math.min(3, Runtime.getRuntime().availableProcessors() / 2),支持很好;如果在做批量導(dǎo)入,不在意搜索,可以設(shè)置為none。配置如下:

PUT /_cluster/settings
{
    "persistent" : {
        "indices.store.throttle.max_bytes_per_sec" : "100mb"
    }
 }
optimize API

optimize API大可看做是強制合并 API。它會將一個分片強制合并到 max_num_segments 參數(shù)指定大小的段數(shù)目。這樣做的意圖是減少段的數(shù)量(通常減少到一個)來提升搜索性能。

optimize API不應(yīng)該被用在一個活躍的索引--一個正積極更新的索引:后臺合并流程已經(jīng)可以很好地完成工作,optimizing 會阻礙這個進程,不要干擾它!在特定情況下,使用 optimize API 頗有益處。例如在日志這種用例下,每天、每周、每月的日志被存儲在一個索引中,老的索引實質(zhì)上是只讀的;它們也并不太可能會發(fā)生變化。在這種情況下,使用optimize優(yōu)化老的索引,將每一個分片合并為一個單獨的段就很有用了,這樣既可以節(jié)省資源,也可以使搜索更加快速。

POST /logstash-2014-10/_optimize?max_num_segments=1 //合并索引中的每個分片為一個單獨的段

請注意,使用 optimize API 觸發(fā)段合并的操作不會受到任何資源上的限制。這可能會消耗掉你節(jié)點上全部的I/O資源,使其沒有余力來處理搜索請求,從而有可能使集群失去響應(yīng)。 如果你想要對索引執(zhí)行 optimize,你需要先使用分片分配把索引移到一個安全的節(jié)點,再執(zhí)行。

Translog

為了提升寫的性能,ES 并沒有每新增一條數(shù)據(jù)就增加一個段到磁盤上,而是采用延遲寫的策略。等文件系統(tǒng)中有新段生成之后,在稍后的時間里再被刷新到磁盤中并生成提交點。雖然通過延時寫的策略可以減少數(shù)據(jù)往磁盤上寫的次數(shù)提升了整體的寫入能力,但是我們知道文件緩存系統(tǒng)也是內(nèi)存空間,屬于操作系統(tǒng)的內(nèi)存,只要是內(nèi)存都存在斷電或異常情況下丟失數(shù)據(jù)的危險。為了避免丟失數(shù)據(jù),ES 添加了事務(wù)日志(Translog),事務(wù)日志記錄了所有還沒有持久化到磁盤的數(shù)據(jù)。

translog 默認(rèn)是每5秒被 fsync 刷新到硬盤,或者在每次寫請求完成之后執(zhí)行(index, delete, update, bulk)操作也可以刷新到磁盤。在每次請求后都執(zhí)行一個 fsync 會帶來一些性能損失,盡管實踐表明這種損失相對較小(特別是bulk導(dǎo)入,它在一次請求中平攤了大量文檔的開銷)。對于一些大容量的偶爾丟失幾秒數(shù)據(jù)問題也并不嚴(yán)重的集群,使用異步的 fsync 還是比較有益的。我們可以通過設(shè)置 durability 參數(shù)為 async 來啟用:

PUT /my_index/_settings
{
    "index.translog.durability": "async",
    "index.translog.sync_interval": "5s"
}

這個選項可以針對索引單獨設(shè)置,并且可以動態(tài)進行修改。如果你決定使用異步 translog 的話,你需要保證在發(fā)生crash時,丟失掉 sync_interval 時間段的數(shù)據(jù)也無所謂。如果你不確定這個行為的后果,最好是使用默認(rèn)的參數(shù)( "index.translog.durability": "request" )來避免數(shù)據(jù)丟失。

Flush

執(zhí)行一個提交并且截斷 translog 的行為在ES中被稱作一次flush。分片每30分鐘被自動刷新(flush)或者在 translog 太大的時候也會刷新。可以通過設(shè)置translog 文檔來控制這些閾值,flush API 可以被用來執(zhí)行一個手工的刷新(flush):

POST /blogs/_flush                //刷新(flush) blogs 索引。
POST /_flush?wait_for_ongoing     //刷新(flush)所有的索引并且并且等待所有刷新在返回前完成。

總結(jié)

最后我們來說一下添加了事務(wù)日志后的整個存儲的流程吧:

ES存儲原理.png
  • 一個新文檔被索引之后,先被寫入到內(nèi)存中,但是為了防止數(shù)據(jù)的丟失,會追加一份數(shù)據(jù)到事務(wù)日志中。不斷有新的文檔被寫入到內(nèi)存,同時也都會記錄到事務(wù)日志中(日志默認(rèn)存儲到文件緩存系統(tǒng),每五秒刷新一下到本地磁盤,但是會導(dǎo)致數(shù)據(jù)丟失,也可以設(shè)置參數(shù)每個請求都同步,但是性能下降)。這時新數(shù)據(jù)還不能被檢索和查詢。
  • 當(dāng)達到默認(rèn)的刷新時間或內(nèi)存中的數(shù)據(jù)達到一定量后,會觸發(fā)一次 Refresh,將內(nèi)存中的數(shù)據(jù)以一個新段形式刷新到文件緩存系統(tǒng)中并清空內(nèi)存。這時雖然新段未被提交到磁盤,但是可以提供文檔的檢索功能且不能被修改。
  • 隨著新文檔索引不斷被寫入,當(dāng)日志數(shù)據(jù)大小超過 512M 或者時間超過 30 分鐘時,會觸發(fā)一次 Flush。內(nèi)存中的數(shù)據(jù)被寫入到一個新段同時被寫入到文件緩存系統(tǒng),文件系統(tǒng)緩存中數(shù)據(jù)通過 Fsync 刷新到磁盤中,生成提交點,日志文件被刪除,創(chuàng)建一個空的新日志。
  • 通過這種方式當(dāng)斷電或需要重啟時,ES 不僅要根據(jù)提交點去加載已經(jīng)持久化過的段,還需要讀取 Translog 里的記錄,把未持久化的數(shù)據(jù)重新持久化到磁盤上,避免了數(shù)據(jù)丟失的可能。

阿Q正在將ES的知識做一個系統(tǒng)的學(xué)習(xí)與講解,后續(xù)還會持續(xù)輸出ES的相關(guān)知識,如果你感興趣的話,可以關(guān)注gzh“阿Q說”!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、段 倒排索引被寫入磁盤后是不可變的,ES解決不變性和更新索引的方式是使用多個索引,利用新增的索引來反映修改,在...
    冰河winner閱讀 4,580評論 0 8
  • 1. 使文檔可被搜索 ?? 傳統(tǒng)的數(shù)據(jù)庫每個字段存儲單個值,但這對全文檢索并不夠。文本字段中的每個單詞需要被搜索,...
    布魯斯理閱讀 1,111評論 0 0
  • 生活中的數(shù)據(jù) 搜索引擎是對數(shù)據(jù)的檢索,所以我們先從生活中的數(shù)據(jù)說起。我們生活中的數(shù)據(jù)總體分為兩種: 結(jié)構(gòu)化數(shù)據(jù) 非...
    小蘇c閱讀 669評論 0 0
  • 關(guān)系圖: Segment(段):Lucene里面的一個數(shù)據(jù)集概念提交點文件:有一個列表存放著所有已知的所有段ES底...
    hellokitty小丸子閱讀 4,414評論 0 3
  • 基本概念 索引(Index) ES將數(shù)據(jù)存儲于一個或多個索引中,索引是具有類似特性的文檔的集合。類比傳統(tǒng)的關(guān)系型數(shù)...
    timothyue1閱讀 1,342評論 0 2

友情鏈接更多精彩內(nèi)容