Elasticsearch深入:數據持久化過程


前言
Elasticsearch 存儲的基本單元是shard, ES中一個Index 可能分為多個shard, 事實上每個shard 都是一個Lucence 的Index,並且每個Lucence Index 由多個Segment組成, 每個Segment事實上是一些倒排索引的集合, 每次創建一個新的Document, 都會歸屬於一個新的Segment, 而不會去修改原來的Segment; 且每次的文檔刪除操作,會僅僅標記Segment中該文檔為刪除狀態,而不會真正的立馬物理刪除, 所以說ES的index 可以理解為一個抽象的概念。

  • index:類似數據庫,是存儲、索引數據的地方
  • shard:index 由 shard 組成,一個 primary shard,其他是 replica shard
  • segment:shard 包含 segment,segment 內的文檔數量的上限是 2^31
  • 倒排索引:是 Lucene 中用於使數據可搜索的數據結構,存儲在segment中
  • translog:也稱“事務日志”,提供所有還沒有被刷到磁盤的操作的一個持久化紀錄
  • commit point:列出所有已知 segment 的文件及一個.del文件

一個 Elasticsearch Index 由一個或者多個 shard (分片) 組成。

而 Lucene 中的 Lucene index 相當於 ES 的一個 shard。

數據持久化過程

整體流程:

  • 數據首先寫入內存緩存區和Translog日志文件中。當你寫一條數據doc的時候,一方面寫入到內存緩沖區中,一方面同時寫入到Translog日志文件中。
  • 內存緩存區滿了或者每隔1秒(默認1秒),refresh將內存緩存區的數據生成index segment文件並寫入文件系統緩存區,此時index segment可被打開以供search查詢讀取,這樣文檔就可以被搜索到了(注意,此時文檔還沒有寫到磁盤上);然后清空內存緩存區供后續使用。可見,refresh實現的是文檔從內存緩存區移到文件系統緩存區的過程。
  • 重復上兩個步驟,新的segment不斷添加到文件系統緩存區,內存緩存區不斷被清空,而translog的數據不斷增加,隨着時間的推移,Translog文件會越來越大。
  • 當Translog長度達到一定程度的時候,會觸發flush操作,否則默認每隔30分鍾也會定時flush,其主要過程:
  1. 執行refresh操作將內存緩存區中的數據寫入到新的segment並寫入文件系統緩存區,然后打開本segment以供search使用,最后再次清空內存緩存區。
  2. 一個commit point被寫入磁盤,這個commit point中標明所有的index segment。
  3. 文件系統中緩存的所有的index segment文件被fsync強制刷到磁盤,當index segment被fsync強制刷到磁盤上以后,就會被打開,供查詢使用。
  4. translog被清空和刪除,創建一個新的translog。
  5. Refresh
  6. 先寫入內存 buffer(這時數據是搜索不到的),同時將數據寫入 translog 日志文件。每個shard都對應一個translog文件。

 

如果內存 buffer 快滿了或者到一定時間(1秒),將內存buffer 數據 refresh 到文件 cache 即文件緩存區。這時數據就可以被搜索到了:

  • 內存緩存區的文檔被寫入到一個新的 segment 中;
  • 新的segment 被打開以供搜索;
  • 內存緩存區清空;

總結:Refresh操作默認每秒執行一次, 將內存緩存區的數據寫入到文件緩存區的一個新的Segment中,同時清空內存緩存區。這個時候索引變成了可被檢索的。
Elasticsearch 中,默認情況下 _refresh 操作設置為每秒執行一次,可以通過參數index.refresh_interval來修改這個刷新間隔,refresh的開銷比較大,因此在批量構建索引時可以把refresh間隔設置成-1來臨時關閉refresh,等到索引都提交完成之后再打開refresh,可以通過如下接口修改這個參數:

curl -XPUT 'localhost:9200/test/_settings' -d '{
    "index" : {
        "refresh_interval" : "-1"
    }
}'


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

備注:
refresh_interval 需要一個 持續時間值, 例如 1s (1 秒) 或 2m (2 分鍾)。 一個絕對值 1 表示的是 1毫秒 --無疑會使你的集群陷入癱瘓。

通常 buffer 里的內容被寫入到 Segment 里去,有三個條件:

  • 由索引中的設置所指定的 refresh_interval 啟動的周期性的 refresh。在默認的情況下為1s
  • 在導入文檔時強制 refresh:PUT twitter/_doc/1?refresh=true
  • 當 In Memory Buffer 滿了,在默認的情況下為 node Heap 的 10%

另外當你在做批量索引時,可以考慮把副本數設置成0,因為document從主分片(primary shard)復制到從分片(replica shard)時,副本分片也要執行相同的分析、索引和合並過程,這樣的開銷比較大,你可以在構建索引之后再開啟副本,這樣只需要把數據從主分片拷貝到從分片:

curl -XPUT 'localhost:9200/my_index/_settings' -d ' {
    "index" : {
        "number_of_replicas" : 0
    }
}'

執行完批量索引之后,把刷新間隔改回來:

curl -XPUT 'localhost:9200/my_index/_settings' -d '{
    "index" : {
        "refresh_interval" : "1s"
    } 
}'

你還可以強制執行一次refresh以及索引分段的合並:

curl -XPOST 'localhost:9200/my_index/_refresh'
curl -XPOST 'localhost:9200/my_index/_forcemerge?max_num_segments=5'

盡管刷新是比提交輕量很多的操作,它還是會有性能開銷。當寫測試的時候, 手動刷新很有用,但是不要在生產環境下每次索引一個文檔都去手動刷新。 相反,你的應用需要意識到 Elasticsearch 的近實時的性質,並接受它的不足。

Flush
每隔 1 秒鍾,es 將 buffer 中的數據寫入一個新的 segment file ,每秒鍾會產生一個新的磁盤文件 segment file ,這個 segment file 中就存儲最近 1 秒內 buffer 中寫入的數據。但是如果 buffer 里面此時沒有數據,那當然不會執行 refresh 操作,如果 buffer 里面有數據,默認 1 秒鍾執行一次 refresh 操作,刷入一個新的 segment file 中。
文件緩存區:系統自動在內存區中為程序每一個正在使用的文件開辟開辟一個文件緩存區,從內存向磁盤輸出時必須優先充滿文件緩存區后,數據才會被一起送到磁盤。
文件緩沖區是用以暫時存放讀寫期間的文件數據而在內存區預留的一定空間。使用文件緩沖區可減少讀取硬盤的次數
為什么叫 es 是准實時的:NRT,全稱near real-time。默認是每隔 1 秒 refresh 一次的,所以 es 是准實時的,因為寫入的數據 1 秒之后才能被看到。可以通過 es 的restful api或者java api,手動執行一次 refresh 操作,就是手動將 buffer 中的數據刷入os cache中,讓數據立馬就可以被搜索到。只要數據被輸入os cache中,buffer 就會被清空了,因為不需要保留 buffer 了,數據在 translog 里面已經持久化到磁盤去一份了。

 

重復上面的步驟,新的數據不斷進入 buffer 和 translog,不斷將 buffer 數據寫入一個又一個新的 segment file 中去,每次 refresh 完 buffer 清空,translog 保留。隨着這個過程推進,translog 會變得越來越大。當 translog 達到一定長度的時候,就會觸發 commit 操作。commit 操作發生第一步,就是將 buffer 中現有數據 refresh 到 os cache 中去,清空 buffer。然后,將一個 commit point 寫入磁盤文件,里面標識着這個 commit point 對應的所有 segment file ,同時強行將 os cache 中目前所有的數據都 fsync 到磁盤文件中去。最后清空現有 translog 日志文件,重啟一個 translog,此時 commit 操作完成。這個 commit 操作叫做 flush 。默認 30 分鍾自動執行一次 flush ,但如果 translog 過大,也會觸發 flush 。flush 操作就對應着 commit 的全過程,我們可以通過 Es api,手動執行 flush 操作,將 os cache 中的數據 fsync 強刷到磁盤上去。
當translog達到一定長度(默認512m)的時候或者30分鍾之后,就會觸發flush操作(flush 完成了 Lucene 的 commit 操作):

  • 第一步將 內存緩存區 中現有數據 refresh 到 文件緩存區 中去,清空 內存緩存區;
  • 然后,將一個 commit point 寫入磁盤文件,同時強行將 文件緩存區 中目前所有的數據都 fsync 到磁盤文件中去;
  • 最后清空現有 translog 日志文件並重建一個。

 

commit point:記錄當前所有可用的segment,每個commit point都會維護一個.del文件(es刪除數據本質是不屬於物理刪除),當es做刪改操作時首先會在.del文件中聲明某個document已經被刪除,文件內記錄了在某個segment內某個文檔已經被刪除,當查詢請求過來時在segment中被刪除的文件是能夠查出來的,但是當返回結果時會根據commit point維護的那個.del文件把已經刪除的文檔過濾掉。
數據寫入 segment file 之后,同時就建立好了倒排索引,其實就是refresh之后就建立好倒排索引了。
總結:Flush 操作將內存緩存區的數據全都寫入文件緩存區新的Segments中, 並將文件緩存區中所有的Segments全部刷盤, 並且清空translog日志的過程。
每個 Shard 中都存在一個 translog,這意味着它與物理磁盤內存有關。 它是同步且安全的,因此即使對於尚未提交的文檔,您也可以獲得持久性。 如果發生問題,可以還原事務日志。 同樣,在每個設置的時間間隔內,或在成功完成請求(索引,批量,刪除或更新)后,將事務日志提交到磁盤。translog 提供所有還沒有被刷到磁盤的操作的一個持久化紀錄。當 Elasticsearch 啟動的時候, 它會從磁盤中使用最后一個提交點去恢復已知的段,並且會重放 translog 中所有在最后一次提交后發生的變更操作。
這個執行一個提交並且截斷 translog 的行為在 Elasticsearch 被稱作一次 flush 。 分片每30分鍾被自動刷新(flush),或者在 translog 太大的時候也會刷新。請查看 translog 文檔 來設置,它可以用來 控制這些閾值:

flush API 可以被用來執行一個手工的刷新(flush): POST /blogs/_flush # 刷新(flush) blogs 索引。 POST /_flush?wait_for_ongoing # 刷新(flush)所有的索引並且等待所有刷新在返回前完成。 translog 每 5s 刷新一次磁盤,所以故障重啟,可能會丟失 5s 的數據。 translog 執行 flush 操作,默認 30 分鍾一次,或者 translog 太大 也會執行。 注意:你很少需要自己手動執行 flush 操作;通常情況下,自動刷新就足夠了。

這就是說,在重啟節點或關閉索引之前執行flush有益於你的索引。當 Elasticsearch 嘗試恢復或重新打開一個索引, 它需要重放 translog 中所有的操作,所以如果日志越短,恢復越快。
Es有幾個條件來決定是否flush到磁盤,不同版本的es參數有所不同,大家可以參考es對應版本的文檔來查看這幾個參數:es translog,這里介紹下1.7版本的flush參數:

index.translog.flush_threshold_ops,執行多少次操作后執行一次flush,默認無限制
index.translog.flush_threshold_size,translog的大小超過這個參數后flush,默認512mb
index.translog.flush_threshold_period,多長時間強制flush一次,默認30m
index.translog.interval,es多久去檢測一次translog是否滿足flush條件

Translog
https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-translog.html

  • 同步間隔(index.translog.sync_interval)Translog的日志每次都會寫入到操作系統的緩存中,只有執行fsync刷盤后才是安全的。因此ES會每隔一段時間執行fsync刷盤。默認時間間隔是5s,最低不能低於100ms。注:該參數只針對異步落盤方式才生效。
  • 刷盤方式(index.translog.durability)Translog的刷盤方式有兩種:同步(request)和異步(async)ES默認使用的是request,即每次寫入、更新、刪除操作后立刻執行fsync落盤。如果使用異步的方式,則根據同步間隔周期性的刷盤。兩種方式各有千秋,同步刷盤具備更強數據可靠性保障,但同時帶來更高的IO開銷,性能更低。異步刷盤犧牲了一定的可靠性保障,但是降低了IO的開銷,性能更佳,因此需要根據不同的場景需求選擇合適的方式。
  • 大小閾值(index.translog.flush_threshold_size)我們不可能讓translog的大小無限增長(內存)。translog的大小過大會帶來如下問題:占用磁盤空間;節點恢復需要同步translog回放日志,如果太大會導致恢復時間過長。因此,需要設定一個閾值,當日志量達到該閾值時,觸發flush,生成一個新的提交點,提交點之前的日志便可以刪除了(具體能否刪除還需結合日志的保留時長和保留大小而定)。
  • 日志保留大小(index.translog.retention.size)它控制為每個分片保留的translog文件的總大小(磁盤)。保留更多的translog文件可以增加在恢復副本時執行基於操作的同步的機會。如果translog文件不夠,副本恢復將退回到基於文件的同步。默認為512mb。如果啟用了軟刪除,此設置將被忽略,並且不應設置。默認情況下,在Elasticsearch7.0.0及更高版本中創建的索引中啟用軟刪除。
  • 日志保留時長(index.translog.retention.age)它控制每個分片保存translog文件的最大持續時間(磁盤)。保留更多的translog文件可以增加在恢復副本時執行基於同步操作的機會。如果translog文件不夠,副本恢復將退回到基於文件的同步。默認為12h。如果啟用了軟刪除,此設置將被忽略,並且不應設置。默認情況下,在Elasticsearch7.0.0及更高版本中創建的索引中啟用軟刪除。
  • 日志generation閾值大小(index.translog.generation_threshold_size)為了避免translog越來越大,增加恢復的時間。在translog達到閾值時,會執行flush,觸發lucenecommit,並滾動translog生成新的文件,當前generation前的操作數據都會提交到lucene持久化,恢復時,只需恢復當前translog中的操作即可。

Elasticsearch采用另一種方法來解決持久性問題。它在每個分片中引入一個事務日志(transactionlog)。已建立索引的新文檔將傳遞到此事務日志和內存緩沖區中。此處可以聯想Mysql的binlog,ES中也存在一個translog用來失敗恢復。translog文件中存儲了上一次flush(即上一個commitpoint)到當前時間的所有數據的變更記錄——即translog中存儲的是還沒有被刷到磁盤的所有最新變更記錄。

  • Document不斷寫入到In-memorybuffer,此時也會追加translog。
  • 當buffer中的數據每秒refresh到cache中時,translog並沒有進入到刷新到磁盤,是持續追加的。
  • translog每隔5s會fsync到磁盤。
  • translog會繼續累加變得越來越大,當translog大到一定程度或者每隔一段時間,會執行flush。
"translog": {
     "generation_threshold_size": "64mb",  # 超過該閾值會產生新的 translog 文件
     "flush_threshold_size": "512mb",  # 可以適當調大,但不能超過 indexBufferSize*1.5 倍,否則會觸發限流,並導致 JVM 內存不釋放
     "sync_interval": "5s",  # 同步
     "retention": {
        "size": "512mb",
        "age": "12h"
      },
     "durability": "REQUEST"  # 每次請求執行一次flush
}

translog 其實也是先寫入 os cache 的,默認每隔 5 秒刷一次到磁盤中去,所以默認情況下,可能有 5 秒的數據會僅僅停留在 buffer 或者 translog 文件的 os cache 中,如果此時機器掛了,會丟失 5 秒鍾的數據。但是這樣性能比較好,最多丟 5 秒的數據。也可以將 translog 設置成每次寫操作必須是直接 fsync 到磁盤,但是性能會差很多。
默認情況下,如果index.translog.durability被設置為async的話,Elasticsearch每5秒鍾同步並提交一次translog。或者如果被設置為request(默認)的話,每次index,delete,update,bulk請求時就同步一次translog。更准確地說,如果設置為request, Elasticsearch只會在成功地在主分片和每個已分配的副本分片上fsync並提交translog之后,才會向客戶端報告index、delete、update、bulk成功。

PUT /my_index/_settings
{
    "index.translog.durability": "async", # 異步 這個參數有2個取值:request(每次請求都執行fsync,es要等translog fsync到磁盤后才會返回成功)和async(translog每隔5秒鍾fsync一次)
    "index.translog.sync_interval": "5s"  # 控制translog多久fsync到磁盤,最小為100ms
}

這個選項可以針對索引單獨設置,並且可以動態進行修改。如果你決定使用異步 translog 的話,你需要 保證 在發生crash時,丟掉 sync_interval 時間段的數據也無所謂。請在決定前知曉這個特性。
由於translog是追加寫入,因此性能要比隨機寫入要好。與傳統的分布式系統不同,這里是先寫入Lucene再寫入translog,原因是寫入Lucene可能會失敗,為了減少寫入失敗回滾的復雜度,因此先寫入Lucene。
軟刪除
軟刪除在最新版本中創建的索引上默認啟用,但是可以在創建索引時顯式啟用或禁用軟刪除。如果禁用了軟刪除,那么有時仍然可以通過從translog中復制丟失的操作來進行對等恢復,只要這些操作保留在translog中。如果禁用軟刪除,則跨集群復制將不起作用。

index.soft_deletes.enabled
在7.6.0棄用。禁用軟刪除創建索引已被棄用,並將在未來的Elasticsearch版本中刪除。指示是否對索引啟用軟刪除。軟刪除只能在創建索引時配置,並且只能在Elasticsearch 6.5.0上或之后創建的索引上配置。默認值為true。

如果一個索引沒有使用軟刪除來保留歷史操作,那么Elasticsearch通過重放主節點的translog操作來恢復每個副本分片。這意味着對於主節點來說,在它的translog中保留額外的操作是很重要的,以防它需要重建一個副本。此外,重要的是每個副本在其translog中保留額外的操作,以防它被提升為主副本,然后需要依次重建自己的副本。
Merge
我們已經知道在elasticsearch中每個shard每隔1秒都會refresh一次,每次refresh都會生成一個新的segment,按照這個速度過不了多久segment的數量就會爆炸,所以存在太多的segment是一個大問題,因為每一個segment都會占用文件句柄,內存資源,cpu資源,更加重要的是每一個搜索請求都必須訪問每一個segment,這就意味着存在的segment越多,搜索請求就會變的更慢。
為什么要這么麻煩進行段合並?

  • 首先,索引中存在的段越多,搜索響應速度就會越慢,內存占用也會越大。
  • 此外,段文件是無法改動的,因此段數據信息不會刪除。如果恰好刪除了索引中的很多文檔,在索引合並之前,這些文檔只是標記刪除,並非物理刪除。因此,當段合並時,標記刪除的文檔不會寫入到新的段中,通過這種方式實現真正的刪除,並縮減了段數據的大小。

從用戶的角度,段合並將產生以下兩種影響:

  • 當幾個段合並成一個段時,通過減少段的數量提升了搜索的性能
  • 段合並完成后,索引大小會由於標記刪除轉成物理刪除而有所縮減

要記住段合並也是有開銷的:段合並引|起的I/O操作可能會使系統變慢從而影響性能。
Segment merge操作對系統的IO和內存占用都比較高,從Es2.0開始,merge行為不再由Es控制,而是由Lucene控制。盡管段合並是Lucene的責任,ElasticSearch也允許用戶配置想用的段合並策略。到目前為止,有三種可用的合並策略:

  • tiered(默認-分層合並策略)
  • log_byte_size(字節大小對數合並策略)
  • log_doc(文檔數量對數合並策略)
配置合並策略:修改elasticsearch.yml
配置文件的index.merge.policy.type字段配置成我們期望的段合並策略類型(創建之后不可更改)。例如下面這樣:

index.merge.policy.type:tiered

內存緩存區 每 refresh 一次,就會產生一個 segment(默認情況下是 1 秒鍾產生一個),這樣 segment 會越來越多,為了解決這個問題,ES 會不斷在后台運行任務,主動將這些零散的 segment 做數據歸並,盡量讓索引內只保有少量的,每個都比較大的segment 文件。這個過程是有獨立的線程來進行的,並不影響新 segment 的產生。合並的segment可以是磁盤上已經commit過的索引,也可以在內存中還未commit的segment。

  • 將多個 segment 合並成一個,並將新的 segment 寫入磁盤;
  • 新增一個 commit point,標識所有新的 segment;
  • 新的 segment 被打開供搜索使用;
  • 刪除舊的 segment。

歸並過程中,尚未完成的較大的 segment 是被排除在檢索可見范圍之外的。當歸並完成,較大的這個 segment 刷到磁盤后,commit 文件做出相應變更,等檢索請求都從小 segment 轉到大segment 上以后,刪除沒用的小 segment,改成新的大 segment。這時候,索引里 segment 數量就下降了。

POST index/_forcemerge?only_expunge_deletes=true merge過程也是文檔刪除和更新操作后,舊的doc真正被刪除的時候。還可以手動調用_forcemerge API來主動觸發merge,以減少集群的segment個數和清理已刪除或更新的文檔。

總結:merge能夠通過減少segment數量來提高搜索速度。但是merge的過程會對索引吞吐量及搜索速度有一定的影響,因此需要配置適當的合並策略參數。對於資源不足的環境,最好禁止自動merge,選擇空閑時段手動進行merge。
緩存 

參考博客:https://cloud.tencent.com/developer/article/1765827


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM