社區裏面有人問了以下一個問題: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
下載 es 源碼,因爲 es 的commit信息比較多,能夠增長 --depth=1
只下載最近的commit,減小下載時間。ide
git clone https://github.com/elastic/elasticsearch.git --depth=1
安裝 gradle,確保版本在 3.3 及以上,而後在源碼目錄下執行如下命令準備導入 IntelliJ IDEA
須要的文件源碼分析
gradle idea
2017.2
及以上版本。安裝完成後,將 elasticsearch 以 gradle 形式導入便可。你們能夠參考 elasticsearch 文檔說明 和 Elasticsearch源碼分析—環境準備 這兩篇文章,細節我這裏就不贅述了。性能
另外我是分析的 5.5.0 分支,你們記得 checkout,防止行數對應不起來。另外因爲 es 代碼結構有些複雜,先不在這篇文章裏面梳理整個流程了,直接說核心代碼。測試
es index 和 create 最終都會調用 org/elasticsearch/index/engine/InternalEngine.java
中下面的方法:gradle
457 public IndexResult index(Index index) throws IOException
注意這裏的 index 中包含有要寫入的 doc, 簡單畫下該方法的執行流程圖,代碼這裏就不貼了,剛興趣的本身去看。
請結合上面的流程圖來看相應的代碼,整個邏輯應該仍是很清晰的,接下來咱們看 planIndexingAsPrimary
的邏輯。
558 private IndexingStrategy planIndexingAsPrimary(Index index) throws IOException {
這個方法最終返回一個 IndexingStrategy,即一個索引的策略,總共有以下幾個策略:
不一樣的策略對應了不一樣的處理邏輯,前面3個是經常使用的,咱們來看下流程圖。
這裏的第一步判斷 是不是自定義 doc id?
這一步就是 es 對於日誌類非自定義 doc id的優化,感興趣的能夠本身去看下代碼,簡單講就是在非自定義 id 的狀況下,直接將文檔 add ,不然須要 update,而 update 比 add 成本高不少。
而第二個判斷 檢查版本號是否衝突?
涉及到是如何根據文檔版本號來確認文檔可寫入,代碼都在index.versionType().isVersionConflictForWrites
方法裏,邏輯也比較簡單,不展開講了,感興趣的本身去看吧。
上面的流程圖也比較清晰地列出了策略選擇的邏輯,除去 optimizedAppendOnly 策略,其餘都須要根據待寫入文檔的版本號來作出決策。接下來咱們就看下獲取文檔版本號的方法。
389 private VersionValue resolveDocVersion(final Operation op) throws IOException {
該方法邏輯比較簡單,主要分爲2步:
看到這裏,開篇問題便有了答案。es 在 index 或者 create 的時候並不會 get 整個文檔,而是隻會獲取文檔的版本號作對比,而這個開銷不會很大。
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 步最終會調用 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 操做,即獲取完整的文檔信息。
最後,記住侯捷老師的話:
源碼面前,了無祕密!