1. 程式人生 > >ElasticStack系列之十六 & ElasticSearch5.x index/create 和 update 源碼分析

ElasticStack系列之十六 & ElasticSearch5.x index/create 和 update 源碼分析

elastics quest 流程圖 刪除 context pre brush 什麽 log

開篇

  在ElasticSearch 系列十四中提到的問題即 ElasticStack系列之十四 & ElasticSearch5.x bulk update 中重復 id 性能驟降,繼續這個問題再繼續查看更加多的源代碼,看看底層在執行 index、create 和 update 操作到底有什麽不同,有什麽可以使得我們使用性能更加好的。

準備

  使用 Intellij IDEA 來閱讀 ElasticSearch 源碼,操作相對來說比較簡單。具體操作步驟如下:

1. 下載 ElasticSearch 源碼

git clone https://github.com/elastic/elasticsearch.git 

2. 下載安裝 gradle,確保版本在 3.3 及以上,我電腦是 macOS,自動下載最新版本為:4.2.1

brew update && brew install gradle

3. 進入到 ElasticSearch 源碼目錄,在該目錄下執行以下命令準備導入 IntelliJ IDEA 需要的文件

gradle idea

 註意:

  1. 運行這個命令需要下載很多東西,有時候可能因為某一個包卡住,不要緊張,退出重新運行該命令,多嘗試幾次就好了。

  2. 各個版本代碼上有一定的差別,但是核心代碼整體是不會有大改動的,我查看的源碼版本為:5.5.0

Index/Create 源碼分析

  es index 和 create 最終都會調用 org/elasticsearch/index/engine/InternalEngine.java 中下面的方法:

  public IndexResult index(Index index) throws IOException

  註意這裏的 index 中包含有要寫入的 doc, 簡單畫下該方法的執行流程圖,代碼這裏就不貼了

  技術分享

  請結合上面的流程圖來看相應的代碼,整個邏輯應該還是很清晰的,接下來我們看 planIndexingAsPrimary 的邏輯。

  private IndexingStrategy planIndexingAsPrimary(Index index) throws IOException

 這個方法最終返回一個 IndexingStrategy,即一個索引的策略,總共有如下幾個策略:

  • optimizedAppendOnly
  • skipDueToVersionConflict
  • processNormally
  • overrideExistingAsIfNotThere
  • skipAsStale

  不同的策略對應了不同的處理邏輯,前面3個是常用的,我們來看下流程圖。

  技術分享

  這裏的第一步判斷:是否是自定義 doc id? 這一步就是 es 對於日誌類非自定義 doc id 的優化,感興趣的可以自己去看下代碼,簡單講就是在非自定義 id 的情況下,直接將文檔 add ,否則需要 update,而 update 比 add 成本高很多。
  而第二個判斷:檢查版本號是否沖突? 涉及到是如何根據 文檔版本號(doc version) 來確認文檔可寫入,代碼都在 index.versionType().isVersionConflictForWrites 方法裏,邏輯也比較簡單,不展開講了,感興趣的自己去看吧。

  上面的流程圖也比較清晰地列出了策略選擇的邏輯,除去 optimizedAppendOnly 策略,其他都需要根據待寫入文檔的版本號來做出決策。接下來我們就看下獲取文檔版本號的方法。

  private VersionValue resolveDocVersion(final Operation op) throws IOException

該方法邏輯比較簡單,主要分為2步:

  1. 嘗試從 versionMap 中讀取待寫入文檔的 version,也即從內存中讀取。versionMap 會暫存還沒有 commit 到磁盤的文檔版本信息。
  2. 如果第 1 步中沒有讀到,則從 index 中讀取,也即從文件中讀取。

  看到這裏,開篇問題便有了答案。es 在 index 或者 create 的時候並不會 get 整個文檔,而是只會獲取文檔的版本號做對比,而這個開銷不會很大。

Update 源碼分析

  es update 的核心代碼在 org/elasticsearch/action/update/UpdateHelper.java 中,具體方法如下:

public Result prepare(UpdateRequest request, IndexShard indexShard, LongSupplier nowInMillis) {
        final GetResult getResult = indexShard.getService().get(request.type(), request.id(),
                new String[]{RoutingFieldMapper.NAME, ParentFieldMapper.NAME},
                true, request.version(), request.versionType(), FetchSourceContext.FETCH_SOURCE);
        return prepare(indexShard.shardId(), request, getResult, nowInMillis);
}

 代碼邏輯很清晰,分兩步走:

  1. 獲取待更新文檔的數據
  2. 執行更新文檔的操作

  第 1 步最終會調用 InternalEngine 中的 get 方法,如下:

  public GetResult get(Get get, Function<String, Searcher> searcherFactory, LongConsumer onRefresh) throws EngineExceptio

  update 操作需要先獲取原始文檔的原因也很簡單,因為這裏是允許用戶做部分更新的,而 es 底層每次更新時要求必須是完整的文檔(因為 lucene 的更新實際是刪除老文檔,新增新文檔),如果不拿到原始數據的話,就不能組裝出更新後的完整文檔了。

  因此,比較看重效率的業務,最好還是不要用 update 這種操作,直接用上面的 index 會更好一些。

總結

  本文通過源碼分析的方式解決了開篇提到的問題,答案簡單總結在下面。

  es 在 index 和 create 操作的時候,如果沒有自定義 doc id,那麽會使用 append 優化模式,否則會獲取待寫入文檔的版本號(doc version),進行版本檢查後再決定是否寫入 lucene。所以這裏不會去做一個 get 操作,即獲取完整的文檔信息。

ElasticStack系列之十六 & ElasticSearch5.x index/create 和 update 源碼分析