ElasticSearch Bulk 源码解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 对于RPC类的调用,我会在后文简单提及,只是endpoint不一样,内部处理逻辑还是一样的。这篇只会讲IndexRequest,其他如DeleteRequest,UpdateRequest之类的,我们暂时不涉及。
本来应该先有这篇文章,后有 如何提高ElasticSearch 索引速度才对。不过当时觉得后面一篇文章会更有实际意义一些,所以先写了后面那篇文章。结果现在这篇文章晚了20多天。
前言
读这篇文章前,建议先看看ElasticSearch Rest/RPC 接口解析,有利于你把握ElasticSearch接受处理请求的脉络。对于RPC类的调用,我会在后文简单提及,只是endpoint不一样,内部处理逻辑还是一样的。这篇只会讲IndexRequest,其他如DeleteRequest,UpdateRequest之类的,我们暂时不涉及。

类处理路径

RestBulkAction -> 
            TransportBulkAction -> 
                       TransportShardBulkAction
AI 代码解读
其中TransportShardBulkAction比较特殊,有个继承结构:
   TransportShardBulkAction < TransportReplicationAction < TransportAction
AI 代码解读
主入口是TransportAction,具体的业务逻辑实现分布到子类(TransportReplicationAction)和孙子类(TransportShardBulkAction)里了。
另外,我们也会提及org.elasticsearch.index.engine.Engine相关的东西,从而让大家清楚的了解ES是如何和Lucene关联上的。

RestBulkAction

入口自然是org.elasticsearch.rest.action.bulk.RestBulkAction,一个请求会构建一个BulkRequest对象,BulkRequest.add方法会解析你提交的文本。对于类型为index或者create的(还记得bulk提交的文本格式是啥样子的么?),都会被构建出IndexRequest对象,这些解析后的对象会被放到BulkRequest对象的属性requests里。当然如果是update,delete等则会构建出其他对象,但都会放到requests里。
public class BulkRequest extends ActionRequest<BulkRequest> implements CompositeIndicesRequest {
    //这个就是前面提到的requests
    final List<ActionRequest> requests = new ArrayList<>();  

//这个复杂的方法就是通过http请求参数解析出
//IndexRequest,DeleteRequest,UpdateRequest等然后放到requests里
public BulkRequest add(BytesReference data, 
@Nullable String defaultIndex, 
@Nullable String defaultType, 
@Nullable String defaultRouting, 
@Nullable String[] defaultFields, 
@Nullable Object payload, boolean allowExplicitIndex) throws Exception {
        XContent xContent = XContentFactory.xContent(data);
        int line = 0;
        int from = 0;
        int length = data.length();
        byte marker = xContent.streamSeparator();
        while (true) {
AI 代码解读
接着通过NodeClient将请求发送到TransportBulkAction类(回忆下之前文章里提到的映射关系,譬如  TransportAction,两层映射关系解析  )。对应的方法如下:
//这里的client其实是NodeClient
client.bulk(bulkRequest, new RestBuilderListener<BulkResponse>(channel) {
TransportBulkAction
AI 代码解读
看这个类的签名:
public class TransportBulkAction extends HandledTransportAction<BulkRequest, BulkResponse> {
AI 代码解读
实现了HandledTransportAction,说明这个类同时也是RPC接口的逻辑处理类。如果你点进HandledTransportAction就能看到ES里经典的messageReceived方法了。这个是题外话
该类对应的入口是:
protected void doExecute(final BulkRequest bulkRequest, final ActionListener<BulkResponse> listener) {
AI 代码解读
这里的bulkRequest 就是前面RestBulkAction组装好的。该方法第一步是判断是不是需要自动建索引,如果索引不存在,就自动创建了。
接着通过executeBulk方法进入原来的流程。在该方法中,对bulkRequest.requests 进行了两次for循环。
第一次判定如果是IndexRequest就调用IndexRequest.process方法,主要是为了解析出timestamp,routing,id,parent 等字段。
第二次是为了对数据进行分拣。大致是为了形成这么一种结构:
//这里的BulkItemRequest来源于 IndexRequest等
Map[ShardId, List[BulkItemRequest]]
AI 代码解读
接着对新形成的这个结构(ShardId -> List[BulkItemRequest])做循环,也就是针对每个ShardId里的数据进行统一处理。有了ShardId,bulkRequest,List[BulkItemRequest]等信息后,统一封装成BulkShardRequest。从名字看就很好理解,就是对属于同一ShardId的数据构建一个新的类似BulkRequest的对象。
接着就到TransportShardBulkAction,TransportReplicationAction,TransportAction 三代人出场了:
//这里的shardBulkAction 是TransportShardBulkAction
shardBulkAction.execute(bulkShardRequest, new ActionListener<BulkShardResponse>() {
TransportReplicationAction/TransportShardBulkAction
TransportAction是一个通用的主类,具体逻辑还是其子类来实现。虽然前面提到shardBulkAction是TransportShardBulkAction,但其实流程逻辑还是TransportReplicationAction来完成的。入口在该类的doExecute方法:
@Override
    protected void doExecute(Request request, ActionListener<Response> listener) {
        new PrimaryPhase(request, listener).run();
    }
AI 代码解读
我们知道在ES里有主从分片的概念,所以一条数据被索引后需要经过两个阶段:
  1. 将数据写入Primary(主分片)
  2. 将数据写入Replication(从分片)
至于为什么不直接从Primary进行复制,而是将数据分别写入到Primary和Replication我觉得主要考虑如果一旦Primary是损坏的,不至于影响到Replication(考虑下,如果Primary是损坏的文件,然后所有的Replication如果是直接复制过来,就都坏了)。
又扯远了。我们看到doExecute 首先是进入PrimaryPhase阶段,也就是写主分片。

Primary Phase

在PrimaryPhase.doRun方法里,你会看到两行代码
final ShardIterator shardIt = shards(observer.observedState(), internalRequest);
final ShardRouting primary = resolvePrimary(shardIt);
AI 代码解读
其中这个ShardIterator是类似 shardId->ShardGroup 的结构。不管这个shardId是什么,它一定是个Replication或者Primary的shardId, ShardGroup 就是Replication和Primary的集合。resolvePrimary方法则是遍历这个集合,然后找出Primary的过程。
知道Primary后就可以判断是转发到别的Node或者直接在本Node处理了:
routeRequestOrPerformLocally(primary, shardIt);
AI 代码解读
如果Primary就在本节点,直接就处理了:
//我去掉了一些无关代码哈
if (primary.currentNodeId().equals(observer.observedState().nodes().localNodeId())) {
                try {
                    threadPool.executor(executor).execute(new AbstractRunnable() {
                         @Override
                        protected void doRun() throws Exception {
                            performOnPrimary(primary, shardsIt);
                        }
            }
AI 代码解读
这里用上了线程池。前面对每个shardId对应的数据集合做处理,其实是顺序循环执行的,这里实现了将数据处理异步化。
在performOnPrimary方法中,BulkShardRequest被转化成了PrimaryOperationRequest,理由也很简单,更加specific了,因为就是针对主分片的Request。接着进入shardOperationOnPrimary 方法,该方法是在孙子类TransportShardBulkAction类里实现的。
protected Tuple<BulkShardResponse, BulkShardRequest> shardOperationOnPrimary(
ClusterState clusterState, 
PrimaryOperationRequest shardRequest) {
AI 代码解读
到该方法,有两个比较重要的概念会出现:
//伟大的版本号,实现了对并发修改的支持
long[] preVersions = new long[request.items().length];
VersionType[] preVersionTypes = new VersionType[request.items().length];
//事物日志,为Shard Recovery以及
//避免过多的Index Commit做出突出贡献,
//同时也是是实现了GetById的实时性
Translog.Location location = null;
AI 代码解读
上面两个概念成就了ES从一个简单的全文检索引擎到类No-SQL的转型(好吧,我好像又扯远了)
接着就是for循环了:
//这里的request是BulkShardRequest
//对应的items则是BulkItemRequest集合
for (int requestIndex = 0;
 requestIndex < request.items().length; 
requestIndex++) {
AI 代码解读
循环会根据BulkItemRequest的不同类型而有了分支。其实就是
IndexRequest,DeleteRequest,UpdateRequest,我们这里依然只讨论IndexRequest。如果发现BulkItemRequest是IndexRequest,进行如下操作:
WriteResult<IndexResponse> result = shardIndexOperation(request, 
indexRequest, 
clusterState, 
indexShard, 
true);
AI 代码解读
shardIndexOperation里嵌套的核心方法是executeIndexRequestOnPrimary,该方法第一步是获取到Operation对象,
Engine.IndexingOperation operation = prepareIndexOperationOnPrimary(shardRequest, request, indexShard);
AI 代码解读
Engine对象是比较底层的一个对象了,是对Lucene的IndexWriter,Searcher之类的封装。这里的Engine.IndexingOperation对应的是Create或者Index类。你可以把这两个类理解为待索引的Document,只是还带上了动作。
第二步是判断索引的Mapping是不是要动态更新,如果是,则更新。
第三步执行实际的建索引操作:
final boolean created = operation.execute(indexShard);
AI 代码解读

operation.execute 额外引出的话题

我们会暂时深入到operate.execute方法里,但这个不是主线,看完后记得回到上面那行代码上。
刚才我们说了operation可能是Create或者Index,我们会以Create为主线进行分析。所谓Create和Index,你可以理解为一个待索引的Document,只是带上动作的语义。
上面对应的execute 方法签名是:
@Overridepublic boolean execute(IndexShard shard) {     shard.create(this);   
 return true;
}
AI 代码解读
我们看到这里是反向调用indexShard对象的create方法来进行索引的创建。我们来看看IndexShard的create方法:
//我依然做了删减,体现一些核心代码
public void create(Engine.Create create) {        
        engine().create(create);
    }
AI 代码解读
engine()方法返回的是InternalEngine实例,InternalEngine .innerCreate方法执行到构建索引的操作。这个方法值得分析一下,所以我就贴了一坨的代码。
private void innerCreate(Create create) throws IOException {
        if (engineConfig.isOptimizeAutoGenerateId() && create.autoGeneratedId() && !create.canHaveDuplicates()) {
            // We don't need to lock because this ID cannot be concurrently updated:
            innerCreateNoLock(create, Versions.NOT_FOUND, null);
        } else {
            synchronized (dirtyLock(create.uid())) {
                final long currentVersion;
                final VersionValue versionValue;
                versionValue = versionMap.getUnderLock(create.uid().bytes());
                if (versionValue == null) {
                    currentVersion = loadCurrentVersionFromIndex(create.uid());
                } else {
                    if (engineConfig.isEnableGcDeletes() && versionValue.delete() && (engineConfig.getThreadPool().estimatedTimeInMillis() - versionValue.time()) > engineConfig.getGcDeletesInMillis()) {
                        currentVersion = Versions.NOT_FOUND; // deleted, and GC
                    } else {
                        currentVersion = versionValue.version();
                    }
                }
                innerCreateNoLock(create, currentVersion, versionValue);
            }
        }
    }
AI 代码解读
首先,如果满足如下三个条件就无需进行版本检查:
  1. index.optimize_auto_generated_id 被设置为true(默认是false,话说注释上说是默认是true,但是我看着觉得像是false)
  2. id设置为自动生成(没有人工设置id)
  3. create.canHaveDuplicates == false ,该参数一般是false
提这个是主要为了说明,譬如一般的运维日志啥的,就不要自己生成ID了,采用自动生成的ID,可以跳过版本检查,从而提高入库的效率。
第二个指的说的是,如果对应文档在缓存中没有找到(versionMap),那么就会由如下的代码执行实际磁盘查询操作:
currentVersion = loadCurrentVersionFromIndex(create.uid());
AI 代码解读
通过对比create对象里的版本号和从索引文件里加载的版本号 ,最终决定是进行update还是create操作。
在innerCreateNoLock 方法里,你会看到熟悉的Lucene操作,譬如:
indexWriter.addDocument(index.docs().get(0));
//或者
indexWriter.updateDocument(index.uid(), index.docs().get(0));
AI 代码解读
现在回到TransportShardBulkAction的主线上。执行完下面的代码后:
final boolean created = operation.execute(indexShard);
AI 代码解读
就能获得对应文档的版本等信息,这些信息会更新对应的IndexRequest等对象。
到目前为止,Primay Phase 完成,接着开始Replication Phase
replicationPhase = new ReplicationPhase(shardsIt, 
primaryResponse.v2(), 
primaryResponse.v1(), 
observer, 
primary, 
internalRequest, 
listener, 
indexShardReference);
finishAndMoveToReplication(replicationPhase);
AI 代码解读
最后一行代码会启动replicationPhase阶段。

Replication Phase

Replication Phase 流程大致和Primary Phase 相同,就不做过详细的解决,我这里简单提及一下。
ReplicationPhase的doRun方法是入口,核心方法是performOnReplica,如果发现Replication  shardId所属的节点就是自己的话,异步执行shardOperationOnReplica,大体逻辑如下:
threadPool.executor(executor).execute(new AbstractRunnable() {
                        @Override
                        protected void doRun() {
                            try {
                                shardOperationOnReplica(shard.shardId(), replicaRequest);
                                onReplicaSuccess();
                            } catch (Throwable e) {
                                onReplicaFailure(nodeId, e);
                                failReplicaIfNeeded(shard.index(), shard.id(), e);
                            }
                        }
AI 代码解读
在Replication阶段,shardOperationOnReplica 该方法完成了索引内容解析,mapping动态新增,最后进入索引(和就是前面提到的operation.execute)等动作,所以还是比Primary 阶段更紧凑些。
另外,在Primary Phase 和 Replication Phase, 一个BulkShardRequest 处理完成后(也就是一个Shard 对应的数据集合)才会刷写Translog日志。所以如果发生数据丢失,则可能是多条数据。

总结

这篇文章以流程分析为主,很多细节我们依然没有讲解详细,比如Translog和Version。这些争取能够在后续文章中进一步阐述。另外错误之处在所难免,请大家在评论处提出。
相关实践学习
使用阿里云Elasticsearch体验信息检索加速
通过创建登录阿里云Elasticsearch集群,使用DataWorks将MySQL数据同步至Elasticsearch,体验多条件检索效果,简单展示数据同步和信息检索加速的过程和操作。
ElasticSearch 入门精讲
ElasticSearch是一个开源的、基于Lucene的、分布式、高扩展、高实时的搜索与数据分析引擎。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr(也是基于Lucene)。 ElasticSearch的实现原理主要分为以下几个步骤: 用户将数据提交到Elastic Search 数据库中 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据 当用户搜索数据时候,再根据权重将结果排名、打分 将返回结果呈现给用户 Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。
目录
打赏
0
0
0
0
110
分享
相关文章
ElasticSearch基础概念解析
以上就是ElasticSearch的基础概念。理解了这些概念,你就可以更好地使用ElasticSearch,像使用超级放大镜一样,在数据海洋中找到你需要的珍珠。
109 71
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
101 29
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
从入门到精通:H5游戏源码开发技术全解析与未来趋势洞察
H5游戏凭借其跨平台、易传播和开发成本低的优势,近年来发展迅猛。接下来,让我们深入了解 H5 游戏源码开发的技术教程以及未来的发展趋势。
分片上传技术全解析:原理、优势与应用(含简单实现源码)
分片上传通过将大文件分割成多个小的片段或块,然后并行或顺序地上传这些片段,从而提高上传效率和可靠性,特别适用于大文件的上传场景,尤其是在网络环境不佳时,分片上传能有效提高上传体验。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
在线教育网课系统源码开发指南:功能设计与技术实现深度解析
在线教育网课系统是近年来发展迅猛的教育形式的核心载体,具备用户管理、课程管理、教学互动、学习评估等功能。本文从功能和技术两方面解析其源码开发,涵盖前端(HTML5、CSS3、JavaScript等)、后端(Java、Python等)、流媒体及云计算技术,并强调安全性、稳定性和用户体验的重要性。
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
mindspeed-llm源码解析(一)preprocess_data
mindspeed-llm是昇腾模型套件代码仓,原来叫"modelLink"。这篇文章带大家阅读一下数据处理脚本preprocess_data.py(基于1.0.0分支),数据处理是模型训练的第一步,经常会用到。
102 0

热门文章

最新文章

推荐镜像

更多