深入理解Elasticsearch寫入過程

Elasticsearch 是當前主流的搜索引擎,其具有擴展性好,查詢速度快,查詢結果近實時等優點,本文將對Elasticsearch的寫操作進行分析。

1. lucene的寫操作及其問題

Elasticsearch底層使用Lucene來實現doc的讀寫操作,Lucene通過

public long addDocument(...);
public long deleteDocuments(...);
public long updateDocument(...);

三個方法來實現文檔的寫入,更新和刪除操作。但是存在如下問題

  1. 沒有併發設計
    lucene只是一個搜索引擎庫,並沒有涉及到分佈式相關的設計,因此要想使用Lucene來處理海量數據,並利用分佈式的能力,就必須在其之上進行分佈式的相關設計。
  2. 非實時
    將文件寫入lucence後並不能立即被檢索,需要等待lucene生成一個完整的segment才能被檢索
  3. 數據存儲不可靠
    寫入lucene的數據不會立即被持久化到磁盤,如果服務器宕機,那存儲在內存中的數據將會丟失
  4. 不支持部分更新
    lucene中提供的updateDocuments僅支持對文檔的全量更新,對部分更新不支持

2. Elasticsearch的寫入方案

針對Lucene的問題,ES做了如下設計

2.1 分佈式設計:

爲了支持對海量數據的存儲和查詢,Elasticsearch引入分片的概念,一個索引被分成多個分片,每個分片可以有一個主分片和多個副本分片,每個分片副本都是一個具有完整功能的lucene實例。分片可以分配在不同的服務器上,同一個分片的不同副本不能分配在相同的服務器上。

在進行寫操作時,ES會根據傳入的_routing參數(或mapping中設置的_routing, 如果參數和設置中都沒有則默認使用_id), 按照公式shard_num = hash(\routing) % num_primary_shards,計算出文檔要分配到的分片,在從集羣元數據中找出對應主分片的位置,將請求路由到該分片進行文檔寫操作。

2.2 近實時性-refresh操作

當一個文檔寫入Lucene後是不能被立即查詢到的,Elasticsearch提供了一個refresh操作,會定時地調用lucene的reopen(新版本爲openIfChanged)爲內存中新寫入的數據生成一個新的segment,此時被處理的文檔均可以被檢索到。refresh操作的時間間隔由refresh_interval參數控制,默認爲1s, 當然還可以在寫入請求中帶上refresh表示寫入後立即refresh,另外還可以調用refresh API顯式refresh。

2.3 數據存儲可靠性

  1. 引入translog
    當一個文檔寫入Lucence後是存儲在內存中的,即使執行了refresh操作仍然是在文件系統緩存中,如果此時服務器宕機,那麼這部分數據將會丟失。爲此ES增加了translog, 當進行文檔寫操作時會先將文檔寫入Lucene,然後寫入一份到translog,寫入translog是落盤的(如果對可靠性要求不是很高,也可以設置異步落盤,可以提高性能,由配置index.translog.durabilityindex.translog.sync_interval控制),這樣就可以防止服務器宕機後數據的丟失。由於translog是追加寫入,因此性能要比隨機寫入要好。與傳統的分佈式系統不同,這裏是先寫入Lucene再寫入translog,原因是寫入Lucene可能會失敗,爲了減少寫入失敗回滾的複雜度,因此先寫入Lucene.
  2. flush操作
    另外每30分鐘或當translog達到一定大小(由index.translog.flush_threshold_size控制,默認512mb), ES會觸發一次flush操作,此時ES會先執行refresh操作將buffer中的數據生成segment,然後調用lucene的commit方法將所有內存中的segment fsync到磁盤。此時lucene中的數據就完成了持久化,會清空translog中的數據(6.x版本爲了實現sequenceIDs,不刪除translog)
  3. merge操作
    由於refresh默認間隔爲1s中,因此會產生大量的小segment,爲此ES會運行一個任務檢測當前磁盤中的segment,對符合條件的segment進行合併操作,減少lucene中的segment個數,提高查詢速度,降低負載。不僅如此,merge過程也是文檔刪除和更新操作後,舊的doc真正被刪除的時候。用戶還可以手動調用_forcemerge API來主動觸發merge,以減少集羣的segment個數和清理已刪除或更新的文檔。
  4. 多副本機制
    另外ES有多副本機制,一個分片的主副分片不能分片在同一個節點上,進一步保證數據的可靠性。

2.4 部分更新

lucene僅支持對文檔的整體更新,ES爲了支持局部更新,在Lucene的Store索引中存儲了一個_source字段,該字段的key值是文檔ID, 內容是文檔的原文。當進行更新操作時先從_source中獲取原文,與更新部分合並後,再調用lucene API進行全量更新, 對於寫入了ES但是還沒有refresh的文檔,可以從translog中獲取。另外爲了防止讀取文檔過程後執行更新前有其他線程修改了文檔,ES增加了版本機制,當執行更新操作時發現當前文檔的版本與預期不符,則會重新獲取文檔再更新。

3. ES的寫入流程

ES的任意節點都可以作爲協調節點(coordinating node)接受請求,當協調節點接受到請求後進行一系列處理,然後通過_routing字段找到對應的primary shard,並將請求轉發給primary shard, primary shard完成寫入後,將寫入併發發送給各replica, raplica執行寫入操作後返回給primary shard, primary shard再將請求返回給協調節點。大致流程如下圖:

3.1 coordinating節點

ES中接收並轉發請求的節點稱爲coordinating節點,ES中所有節點都可以接受並轉發請求。當一個節點接受到寫請求或更新請求後,會執行如下操作:

  1. ingest pipeline
    查看該請求是否符合某個ingest pipeline的pattern, 如果符合則執行pipeline中的邏輯,一般是對文檔進行各種預處理,如格式調整,增加字段等。如果當前節點沒有ingest角色,則需要將請求轉發給有ingest角色的節點執行。
  2. 自動創建索引
    判斷索引是否存在,如果開啓了自動創建則自動創建,否則報錯
  3. 設置routing
    獲取請求URL或mapping中的_routing,如果沒有則使用_id, 如果沒有指定_id則ES會自動生成一個全局唯一ID。該_routing字段用於決定文檔分配在索引的哪個shard上。
  4. 構建BulkShardRequest
    由於Bulk Request中包含多種(Index/Update/Delete)請求,這些請求分別需要到不同的shard上執行,因此協調節點,會將請求按照shard分開,同一個shard上的請求聚合到一起,構建BulkShardRequest
  5. 將請求發送給primary shard
    因爲當前執行的是寫操作,因此只能在primary上完成,所以需要把請求路由到primary shard所在節點
  6. 等待primary shard返回

3.2 primary shard

Primary請求的入口是PrimaryOperationTransportHandler的MessageReceived, 當接收到請求時,執行的邏輯如下

  1. 判斷操作類型
    遍歷bulk請求中的各子請求,根據不同的操作類型跳轉到不同的處理邏輯
  2. 將update操作轉換爲Index和Delete操作
    獲取文檔的當前內容,與update內容合併生成新文檔,然後將update請求轉換成index請求,此處文檔設置一個version v1
  3. Parse Doc
    解析文檔的各字段,並添加如_uid等ES相關的一些系統字段
  4. 更新mapping
    對於新增字段會根據dynamic mapping或dynamic template生成對應的mapping,如果mapping中有dynamic mapping相關設置則按設置處理,如忽略或拋出異常
  5. 獲取sequence Id和Version
    從SequcenceNumberService獲取一個sequenceID和Version。SequcenID用於初始化LocalCheckPoint, verion是根據當前Versoin+1用於防止併發寫導致數據不一致。
  6. 寫入lucene
    這一步開始會對文檔uid加鎖,然後判斷uid對應的version v2和之前update轉換時的versoin v1是否一致,不一致則返回第二步重新執行。
    如果version一致,如果同id的doc已經存在,則調用lucene的updateDocument接口,如果是新文檔則調用lucene的addDoucument.
    這裏有個問題,如何保證Delete-Then-Add的原子性,ES是通過在Delete之前會加上已refresh鎖,禁止被refresh,只有等待Add完成後釋放了Refresh Lock, 這樣就保證了這個操作的原子性。
  7. 寫入translog
    寫入Lucene的Segment後,會以key value的形式寫Translog, Key是Id, Value是Doc的內容。當查詢的時候,如果請求的是GetDocById則可以直接根據_id從translog中獲取。滿足nosql場景的實時性。
  8. 重構bulk request
    因爲primary shard已經將update操作轉換爲index操作或delete操作,因此要對之前的bulkrequest進行調整,只包含index或delete操作,不需要再進行update的處理操作。
  9. flush translog
    默認情況下,translog要在此處落盤完成,如果對可靠性要求不高,可以設置translog異步,那麼translog的fsync將會異步執行,但是落盤前的數據有丟失風險。
  10. 發送請求給replicas
    將構造好的bulkrequest併發發送給各replicas,等待replica返回,這裏需要等待所有的replicas返回,響應請求給協調節點。如果某個shard執行失敗,則primary會給master發請求remove該shard。這裏會同時把sequenceID, primaryTerm, GlobalCheckPoint等傳遞給replica。
  11. 等待replica響應
    當所有的replica返回請求時,更細primary shard的LocalCheckPoint。

3.3 replica shard

Replica 請求的入口是在ReplicaOperationTransportHandler的messageReceived,當replica shard接收到請求時執行如下流程:

  1. 判斷操作類型
    replica收到的寫如請求只會有add和delete,因update在primary shard上已經轉換爲add或delete了。根據不同的操作類型執行對應的操作
  2. Parse Doc
  3. 更新mapping
  4. 獲取sequenceId和Version
    直接使用primary shard發送過來的請求中的內容即可
  5. 寫如lucene
  6. write Translog
  7. Flush translog

4 總結與分析

Elasticsearch建立在Lucene基礎之上,底層採用Lucene來實現文件的讀寫操作,實現了文檔的存儲和高效查詢。然後Lucene作爲一個搜索庫在應對海量數據的存儲上仍有一些不足之處。

Elasticsearch通過引入分片概念,成功地將lucene部署到分佈式系統中,增強了系統的可靠性和擴展性。

Elasticsearch通過定期refresh lucene in-momory-buffer中的數據,使得ES具有了近實時的寫入和查詢能力。

Elasticsearch通過引入translog,多副本,以及定期執行flush,merge等操作保證了數據可靠性和較高的存儲性能。

Elasticsearch通過存儲_source字段結合verison字段實現了文檔的局部更新,使得ES的使用方式更加靈活多樣。

Elasticsearch基於lucene,又不簡單地只是lucene,它完美地將lucene與分佈式系統結合,既利用了lucene的檢索能力,又具有了分佈式系統的衆多優點。

本文參考

歡迎關注公衆號Elastic慕容,和我一起進入Elastic的奇妙世界吧

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章