社区里面有人问了以下一个问题: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 操做,即获取完整的文档信息。
最后,记住侯捷老师的话:
源码面前,了无秘密!