ElasticStack系列之十六 & ElasticSearch5.x index/create 和 update 源碼分析
開篇
在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步:
- 嘗試從 versionMap 中讀取待寫入文檔的 version,也即從內存中讀取。versionMap 會暫存還沒有 commit 到磁盤的文檔版本信息。
- 如果第 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 步最終會調用 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 源碼分析