elasticsearch index、create和update的源碼分析

社區裏面有人問了以下一個問題:java

執行 bulk 索引文檔的時候,用 index 或者 create 類型而且自定義 doc id 的狀況下,是否會像 update 同樣每次都要去 get 一遍原始文檔? 好比下面的這條命令:git

POST _bulk

{ "index" : { "_index" : "test", "_type" : "type1", "_id" : "1" } }
{ "field1" : "value1" }
{ "create" : { "_index" : "test", "_type" : "type1", "_id" : "3" } }
{ "field1" : "value3" }

問題出現的緣由是他們在 bulk 測試的時候遇到了寫性能的問題,而正巧社區裏面前幾天有這麼一個相似的帖子,說的是 es 5.x 版本里面作 update 操做的性能問題。雖然和這個問題不徹底一致,但都涉及到 es 索引數據的部分。github

侯捷老師說:「源碼面前,了無祕密」,那咱們就來簡單看下 es 這部分的相關代碼,以便回答開篇提出的問題。app

準備工做

我是用 IntelliJ IDEA 來閱讀 elasticsearch 源碼的,操做也簡單。操做步驟以下:elasticsearch

  1. 下載 es 源碼,因爲 es 的commit信息比較多,能夠增長 --depth=1 只下載最近的commit,減小下載時間。ide

    git clone https://github.com/elastic/elasticsearch.git --depth=1
  2. 安裝 gradle,確保版本在 3.3 及以上,而後在源碼目錄下執行如下命令準備導入 IntelliJ IDEA 須要的文件源碼分析

    gradle idea
  3. 下載安裝 IntelliJ IDEA,確保版本爲 2017.2 及以上版本。安裝完成後,將 elasticsearch 以 gradle 形式導入便可。

你們能夠參考 elasticsearch 文檔說明Elasticsearch源碼分析—環境準備 這兩篇文章,細節我這裏就不贅述了。性能

另外我是分析的 5.5.0 分支,你們記得 checkout,防止行數對應不起來。另外因爲 es 代碼結構有些複雜,先不在這篇文章裏面梳理整個流程了,直接說核心代碼。測試

Index/Create 源碼分析

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

457      public IndexResult index(Index index) throws IOException

注意這裏的 index 中包含有要寫入的 doc, 簡單畫下該方法的執行流程圖,代碼這裏就不貼了,剛興趣的本身去看。

index 流程圖

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

558      private IndexingStrategy planIndexingAsPrimary(Index index) throws IOException {

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

  • optimizedAppendOnly
  • skipDueToVersionConflict
  • processNormally
  • overrideExistingAsIfNotThere
  • skipAsStale

不一樣的策略對應了不一樣的處理邏輯,前面3個是經常使用的,咱們來看下流程圖。

planIndexingAsPrimary 流程圖

這裏的第一步判斷 是不是自定義 doc id?這一步就是 es 對於日誌類非自定義 doc id的優化,感興趣的能夠本身去看下代碼,簡單講就是在非自定義 id 的狀況下,直接將文檔 add ,不然須要 update,而 update 比 add 成本高不少。

而第二個判斷 檢查版本號是否衝突? 涉及到是如何根據文檔版本號來確認文檔可寫入,代碼都在index.versionType().isVersionConflictForWrites方法裏,邏輯也比較簡單,不展開講了,感興趣的本身去看吧。

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

389    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, TTLFieldMapper.NAME, TimestampFieldMapper.NAME},
                true, request.version(), request.versionType(), FetchSourceContext.FETCH_SOURCE);
        return prepare(indexShard.shardId(), request, getResult, nowInMillis);
    }

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

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

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

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

這裏就接上開篇提到的社區問題中的源碼分析了。代碼就不展開講了,感興趣的本身去看吧。

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

所以,比較看重效率的業務,最好仍是不要用 update 這種操做,直接用上面的 index 會更好一些。

總結

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

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

最後,記住侯捷老師的話:

源碼面前,了無祕密!

相關文章
相關標籤/搜索