天天看點

ES 18 - (底層原理) Elasticsearch寫入索引資料的過程 以及優化寫入過程

Elasticsearch是如何通過Lucene把索引資料寫入磁盤的? 為了實作更快的實時性、更可靠的資料持久化, 以及更高效的大量segment檔案的歸并, 還能不能優化這個過程? 本片文章介紹一些優化實踐, 歡迎交流呀( ⊙ o ⊙ )

目錄

  • 1 Lucene操作document的流程
    • 1.1 添加document的流程
    • 1.2 删除document的流程
  • 2 優化寫入流程 - 實作近實時搜尋
    • 2.1 流程的改進思路
    • 2.2 設定refresh的間隔
  • 3 優化寫入流程 - 實作持久化變更
    • 3.1 文檔持久化到磁盤的流程
    • 3.2 基于translog和commit point的資料恢複
  • 4 優化寫入流程 - 實作海量segment檔案的歸并
    • 4.1 存在的問題
    • 4.2 merge操作的流程
    • 4.3 優化merge的配置項
    • 4.4 optimize接口的使用
  • 參考資料
  • 版權聲明

Lucene将index資料分為segment(段)進行存儲和管理.

Lucene中, 反向索引一旦被建立就不可改變, 要添加或修改文檔, 就需要重建整個反向索引, 這就對一個index所能包含的資料量, 或index可以被更新的頻率造成了很大的限制.

為了在保留不變性的前提下實作反向索引的更新, Lucene引入了一個新思路: 使用更多的索引, 也就是通過增加新的補充索引來反映最新的修改, 而不是直接重寫整個反向索引.

—— 這樣就能確定, 從最早的版本開始, 每一個反向索引都會被查詢到, 查詢完之後再對結果進行合并.

① 将資料寫入buffer(記憶體緩沖區);

② 執行commit操作: buffer空間被占滿, 其中的資料将作為新的 index segment 被commit到檔案系統的cache(緩存)中;

③ cache中的index segment通過

fsync

強制flush到系統的磁盤上;

④ 寫入磁盤的所有segment将被記錄到commit point(送出點)中, 并寫入磁盤;

④ 新的index segment被打開, 以備外部檢索使用;

⑤ 清空目前buffer緩沖區, 等待接收新的文檔.

說明:

(a)

fsync

是一個Unix系統調用函數, 用來将記憶體緩沖區buffer中的資料存儲到檔案系統. 這裡作了優化, 是指将檔案緩存cache中的所有segment重新整理到磁盤的操作.

(b) 每個Shard都有一個送出點(commit point), 其中儲存了目前Shard成功寫入磁盤的所有segment.

① 送出删除操作, 先查詢要删除的文檔所屬的segment;

② commit point中包含一個

.del

檔案, 記錄哪些segment中的哪些document被标記為

deleted

了;

③ 當

.del

檔案中存儲的文檔足夠多時, ES将執行實體删除操作, 徹底清除這些文檔.

  • 在删除過程中進行搜尋操作:

    依次查詢所有的segment, 取得結果後, 再根據

    .del

    檔案, 過濾掉标記為

    deleted

    的文檔, 然後傳回搜尋結果. —— 也就是被标記為delete的文檔, 依然可以被查詢到.
  • 在删除過程中進行更新操作:

    将舊文檔标記為

    deleted

    , 然後将新的文檔寫入新的index segment中. 執行查詢請求時, 可能會比對到舊版本的文檔, 但由于

    .del

    檔案的存在, 不恰當的文檔将被過濾掉.

(1) 現有流程的問題:

插入的新文檔必須等待

fsync

操作将segment強制寫入磁盤後, 才可以提供搜尋.而

fsync

操作的代價很大, 使得搜尋不夠實時.

(2) 改進寫入流程:

② 不等buffer空間被占滿, 而是每隔一定時間(預設1s), 其中的資料就作為新的index segment被commit到檔案系統的cache(緩存)中;

③ index segment 一旦被寫入cache(緩存), 就立即打開該segment供搜尋使用;

④ 清空目前buffer緩沖區, 等待接收新的文檔.

—— 這裡移除了

fsync

操作, 便于後續流程的優化.

優化的地方: 過程②和過程③:

segment進入作業系統的緩存中就可以提供搜尋, 這個寫入和打開新segment的輕量過程被稱為

refresh

.

Elasticsearch中, 每個Shard每秒都會自動refresh一次, 是以ES是近實時的, 資料插入到可以被搜尋的間隔預設是1秒.

(1) 手動refresh —— 測試時使用, 正式生産中請減少使用:

# 重新整理所有索引:
POST _refresh
# 重新整理某一個索引: 
POST employee/_refresh
           

(2) 手動設定refresh間隔 —— 若要優化索引速度, 而不注重實時性, 可以降低重新整理頻率:

# 建立索引時設定, 間隔1分鐘: 
PUT employee
{
    "settings": {
        "refresh_interval": "1m"
    }
}
# 在已有索引中設定, 間隔10秒: 
PUT employee/_settings
{
    "refresh_interval": "10s"
}
           

(3) 當你在生産環境中建立一個大的新索引時, 可以先關閉自動重新整理, 要開始使用該索引時再改回來:

# 關閉自動重新整理: 
PUT employee/_settings
{
    "refresh_interval": -1 
} 
# 開啟每秒重新整理: 
PUT employee/_settings
{
    "refresh_interval": "1s"
} 
           

Elasticsearch通過事務日志(

translog

)來防止資料的丢失 —— durability持久化.

① 索引資料在寫入記憶體buffer(緩沖區)的同時, 也寫入到translog日志檔案中;

② 每隔

refresh_interval

的時間就執行一次refresh:

(a) 将buffer中的資料作為新的 index segment, 刷到檔案系統的cache(緩存)中;

(b) index segment一旦被寫入檔案cache(緩存), 就立即打開該segment供搜尋使用;

③ 清空目前記憶體buffer(緩沖區), 等待接收新的文檔;

④ 重複①~③, translog檔案中的資料不斷增加;

⑤ 每隔一定時間(預設30分鐘), 或者當translog檔案達到一定大小時, 發生flush操作, 并執行一次全量送出:

(a) 将此時記憶體buffer(緩沖區)中的所有資料寫入一個新的segment, 并commit到檔案系統的cache中;

(b) 打開這個新的segment, 供搜尋使用;

(c) 清空目前的記憶體buffer(緩沖區);

(d) 将translog檔案中的所有segment通過

fsync

強制刷到磁盤上;

(e) 将此次寫入磁盤的所有segment記錄到commit point中, 并寫入磁盤;

(f) 删除目前translog, 建立新的translog接收下一波建立請求.

擴充: translog也可以被用來提供實時CRUD.

當通過id查詢、更新、删除一個文檔時, 從segment中檢索之前, 先檢查translog中的最新變化 —— ES總是能夠實時地擷取到文檔的最新版本.

共計:3599 個字

(1) 關于translog的配置:

flush操作 = 将translog中的記錄刷到磁盤上 + 更新commit point資訊 + 清空translog檔案.

Elasticsearch預設: 每隔30分鐘就flush一次;

或者: 當translog檔案的大小達到上限(預設為512MB)時主動觸發flush.

相關配置為:

# 發生多少次操作(累計多少條資料)後進行一次flush, 預設是unlimited: 
index.translog.flush_threshold_ops

# 當translog的大小達到此預設值時, 執行一次flush操作, 預設是512MB: 
index.translog.flush_threshold_size

# 每隔多長時間執行一次flush操作, 預設是30min:
index.translog.flush_threshold_period

# 檢查translog、并執行一次flush操作的間隔. 預設是5s: ES會在5-10s之間進行一次操作: 
index.translog.interval
           

(2) 資料的故障恢複:

① 增删改操作成功的标志: segment被成功重新整理到Primary Shard和其對應的Replica Shard的磁盤上, 對應的操作才算成功.

② translog檔案中存儲了上一次flush(即上一個commit point)到目前時間的所有資料的變更記錄. —— 即translog中存儲的是還沒有被刷到磁盤的所有最新變更記錄.

③ ES發生故障, 或重新開機ES時, 将根據磁盤中的commit point去加載已經寫入磁盤的segment, 并重做translog檔案中的所有操作, 進而保證資料的一緻性.

(3) 異步重新整理translog:

為了保證不丢失資料, 就要保護translog檔案的安全:

Elasticsearch 2.0之後, 每次寫請求(如index、delete、update、bulk等)完成時, 都會觸發

fsync

将translog中的segment刷到磁盤, 然後才會傳回

200 OK

的響應;

或者: 預設每隔5s就将translog中的資料通過

fsync

強制重新整理到磁盤.

—— 提高資料安全性的同時, 降低了一點性能.

==> 頻繁地執行

fsync

操作, 可能會産生阻塞導緻部分操作耗時較久. 如果允許部分資料丢失, 可設定異步重新整理translog來提高效率.

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

由上述近實時性搜尋的描述, 可知ES預設每秒都會産生一個新的segment檔案, 而每次搜尋時都要周遊所有的segment, 這非常影響搜尋性能.

為解決這一問題, ES會對這些零散的segment進行merge(歸并)操作, 盡量讓索引中隻保有少量的、體積較大的segment檔案.

這個過程由獨立的merge線程負責, 不會影響新segment的産生.

同時, 在merge段檔案(segment)的過程中, 被标記為deleted的document也會被徹底實體删除.

① 選擇一些有相似大小的segment, merge成一個大的segment;

② 将新的segment重新整理到磁盤上;

③ 更新commit檔案: 寫一個新的commit point, 包括了新的segment, 并删除舊的segment;

④ 打開新的segment, 完成搜尋請求的轉移;

⑤ 删除舊的小segment.

segment的歸并是一個非常消耗系統CPU和磁盤IO資源的任務, 是以ES對歸并線程提供了限速機制, 確定這個任務不會過分影響到其他任務.

(1) 歸并線程的速度限制:

限速配置

indices.store.throttle.max_bytes_per_sec

的預設值是20MB, 這對寫入量較大、磁盤轉速較高的伺服器來說明顯過低.

對ELK Stack應用, 建議将其調大到100MB或更高. 可以通過API設定, 也可以寫在配置檔案中:

PUT _cluster/settings
{
    "persistent" : {
        "indices.store.throttle.max_bytes_per_sec" : "100mb"
    }
}
// 響應結果如下: 
{
    "acknowledged": true,
    "persistent": {
        "indices": {
            "store": {
                "throttle": {
                    "max_bytes_per_sec": "100mb"
                }
            }
        }
    },
    "transient": {}
}
           

(2) 歸并線程的數目:

推薦設定為CPU核心數的一半, 如果磁盤性能較差, 可以适當降低配置, 避免發生磁盤IO堵塞:

PUT employee/_settings
{
    "index.merge.scheduler.max_thread_count" : 8
}
           

(3) 其他政策:

# 優先歸并小于此值的segment, 預設是2MB:
index.merge.policy.floor_segment

# 一次最多歸并多少個segment, 預設是10個: 
index.merge.policy.max_merge_at_once

# 一次直接歸并多少個segment, 預設是30個
index.merge.policy.max_merge_at_once_explicit

# 大于此值的segment不參與歸并, 預設是5GB. optimize操作不受影響
index.merge.policy.max_merged_segment
           

segment的預設大小是5GB, 在非常龐大的索引中, 仍然會存在很多segment, 這對檔案句柄、記憶體等資源都是很大的浪費.

但由于歸并任務非常消耗資源, 是以一般不會選擇加大

index.merge.policy.max_merged_segment

配置, 而是在負載較低的時間段, 通過

optimize

接口強制歸并segment:

# 強制将segment歸并為1個大的segment: 
POST employee/_optimize?max_num_segments=1

# 在終端中的操作方法: 
curl -XPOST http://ip:5601/employee/_optimize?max_num_segments=1
           

optimize線程不會受到任何資源上的限制, 是以不建議對還在寫入資料的熱索引(動态索引)執行這個操作.

實戰建議: 對一些很少發生變化的老索引, 如日志資訊, 可以将每個Shard下的segment合并為一個單獨的segment, 節約資源, 還能提高搜尋效率.

Elasticsearch 基礎理論 & 配置調優

https://www.elastic.co/guide/cn/elasticsearch/guide/current/making-text-searchable.html

https://www.elastic.co/guide/cn/elasticsearch/guide/current/near-real-time.html

https://www.elastic.co/guide/cn/elasticsearch/guide/current/translog.html

https://www.elastic.co/guide/cn/elasticsearch/guide/current/merge-process.html

作者: 馬瘦風

出處: 部落格園 馬瘦風的部落格

感謝閱讀, 如果文章有幫助或啟發到你, 點個[好文要頂👆] 或 [推薦👍] 吧😜

本文版權歸部落客所有, 歡迎轉載, 但[必須在文章頁面明顯位置給出原文連結], 否則部落客保留追究相關人員法律責任的權利.

繼續閱讀