ElasticStack系列之十四 & ElasticSearch5.x bulk update 中重復 id 性能驟降


  目前在絕對多數公司在使用 ElasticSearch 將其當做數據庫使用,將多個數據庫中的數據同步到 ElasticSearch 索引是非常常見的應用場景。那么自然而然就會涉及到數據頻繁的新增和更新,而官方的文檔並沒有對 update 的底層機制做特別說明,而當我們從 2.x 版本升級到 5.x 發現反而比之前的性能差很多,那這到底是怎么回事呢?

問題描述

  在 ElasticSearch5.x 里通過 bulk update 將數據從數據庫同步到 ElasticSearch,如果短時間更新的一批數據里存在相同的 id,例如一個 bulk update 里大量寫入下面類型的數據:

{id:1,name:aaa} 
{id:1,name:bbb}
{id:1,name:ccc}
{id:2,name:aaa}
{id:2,name:bbb}
{id:2,name:ccc}
.......

  則更新的速度會變的非常的慢。而在 ElasticSearch1.x 和 ElasticSearch2.x 里同樣的操作會快的多。

根源追溯

  update 操作時分為兩個步驟進行的,即先根據文檔 id 做一次 GET 操作,得到舊版本的文檔,然后在其基礎上做更新再寫回去,問題就出在這個 GET 上面。

  在 core/src/main/java/org/elasticsearch/index/engine/InternalEngine.java 這個類里,get 函數會根據一個 realtime 參數(默認是 true),決定如何拿到文檔。

public GetResult get(Get get, Function<String, Searcher> searcherFactory, LongConsumer onRefresh) throws EngineException {
        assert Objects.equals(get.uid().field(), uidField) : get.uid().field();
        try (ReleasableLock lock = readLock.acquire()) {
            ensureOpen();
            if (get.realtime()) {
                VersionValue versionValue = versionMap.getUnderLock(get.uid());
                if (versionValue != null) {
                    if (versionValue.isDelete()) {
                        return GetResult.NOT_EXISTS;
                    }
                    if (get.versionType().isVersionConflictForReads(versionValue.getVersion(), get.version())) {
                        throw new VersionConflictEngineException(shardId, get.type(), get.id(),
                            get.versionType().explainConflictForReads(versionValue.getVersion(), get.version()));
                    }
                    long time = System.nanoTime();
                    refresh("realtime_get");
                    onRefresh.accept(System.nanoTime() - time);
                }
            }
            // no version, get the version from the index, we know that we refresh on flush
            return getFromSearcher(get, searcherFactory);
        }

  可以看到 realtime 參數決定了 GET 到的數據的實時性。如果設置為 false,則直接從 searcher 里面拿,而 searcher 只能訪問 refresh 過的數據,意味着剛寫入的數據由於存在於 index writer buffer 里還未 refresh,暫時無法搜索到,所以這種方式拿到的數據是准實時的。而默認的 realtime=true,則決定了獲取到的數據必須是實時的,也就是說 index writer buffer 里的數據也要能被檢索到。從代碼里可以看到,其中存在一個 refresh("realtime_get"); 的函數調用,這個函數會檢查 GET 的 doc id 是否都是可以被搜索到的。如果已經寫入了但是無法搜索到,也就是剛剛寫入到 index writer buffer 里還未 refresh 這種情況,就會強制執行一次 refresh 操作,讓數據對 searcher 可見,保證最后的 getFromSearcher 方法調用拿的是完全實時的數據。

  實際上測試下來,也是這樣的,關閉自動刷新,寫入一條文檔,然后對該文檔 id 執行一次 GET 操作,就會看到有一個新的 segment 生成。

  查了下文檔,GET API 調用時,可以選擇實時或者非實時,只需要在 url 里帶上可選參數 realtime=[true/false]。

  然而不幸的是,UPDATE API 的文檔和源碼都沒有提供一個 “禁用” 實時性的參數。update 對 GET 的調用,傳入的 realtime 參數是寫死的即為 true,意味着 update 的時候,強制執行 realtime GET。

  至於為什么 update 一定要需要實時 GET,想一下其實也可以理解的。因為 update 允許對文檔進行局部更新,如果有兩個請求分別更新了同一個文檔的不同字段,可能先更新的數據還在 index writer buffer 里,沒來得及 refresh,因為對 searcher 不可見。如果不做refresh,那后面的更新還是在老版本上做的,前面的更新就會丟失掉。

  另外一個問題,為啥 ElasticSearch5.x 之前的版本沒有這個性能問題呢?

  看了 ElasticSearch2.4 的 GET 方法源碼,其沒有采用 refresh 的方式來保障數據的實時性,而是通過訪問 translog 來達到同樣的目的。ElasticSearch5.x 將機制從 translog 改為了 refresh,理由是之前 ElasticSearch 里有很多地方利用 translog 來位數數據的位置,使得很多操作變得很慢,去掉對 translog 的依賴可以全面提高性能。

  很遺憾,這個更改對於短時間反復大量更新相同的 doc id 的操作,會因為過於頻繁的強制 refresh,短時間生成很多小的 segment,繼而不斷觸發 segment 合並,產生性能損耗。官方認為,在提升大多數應用場景性能的前提下,對於這種較少見的場景下的性能損耗是值得付出的。所以建議從應用層面去解決該問題。

  因此,如果實際應用場景里遇到類似的數據更新問題,只能優化應用數據架構,在應用層面合並相同的 doc id 的數據更新后再寫入到 ElasticSearch 索引中;或者 只能使用 ElasticSearch2.x 這樣的老版本來間接的解決這類問題。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM