Debug说明
- 记录ES写入数据的整个Debug流程,具体实现细节没有做过多解释说明
- Debug的索引,设置1shard、0replicas,便于理清主线
- 先创建索引,其次再执行写入;因此这里Debug的过程暂时不涉及创建索引的操作
- ES version:5.6.16
- 一个ES节点(master and data)
Debug记录
## 创建索引
curl -XPUT localhost:9200/twitter -H "Content-Type:application/json" -d '{"settings":{"index":{"number_of_shards":1, "number_of_replicas":0}}}'
## 执行具体的写入操作
curl -XPOST localhost:9200/twitter/doc -H "Content-Type:application/json" -d '{"name":"tony","age":13}'
Elasticsearch接口主要以RestAPI的方式向外提供,上述我们通过curl的方式执行的写入操作即属于RestAPI;ES源码中所有Rest请求的父类均为BaseRestHandler,因此这里我们在如下的位置打上DEBUG断点开始调试:
通过curl发送写入请求,进入Debug流程;当前handleRequest函数主要是对请求参数以及请求体做一些操作,因此往下执行,进入action.accept(channel)方法中;此时执行流程进入到了RestIndexAction类的channel -> client.index(...)位置,如下:
Step into 上述client.index(...)方法中,此时流程进入到AbstractClient抽象类的index(...)方法内,如下:
接着Step into execute(...)方法内,此时流程进入到AbstractClient抽象类的execute(...)方法内,如下:
接着Step into doExecute(...)方法内,此时流程进入到NodeClient类的doExecute(...)方法内,如下:
接着Step into executeLocally(...)方法内,此时流程进入到NodeClient类的executeLocally(...)方法内,如下:
接着Step into 上述execute(...)方法内,这里可以看到ES将Rest类型的请求转换成了内部的Transport类型,此时执行流程进入到抽象类TransportAction的execute(...)方法内,如下:
接着Step into 上述图片中的execute(...)方法内,此时流程进入到TransportAction类的execute(...)方法内,如下:
接着Step into 请求过滤链中的proceed(...)方法,如下:
TransportAction类的proceed(...)方法,主要是以请求过滤链的方式对一些逻辑进行处理,而且此处proceed(...)方法后面也会再次进入;Step into this.action.doExecute(...)方法,如下:
此时执行流程进入到TransportSingleItemBullkWriteAction类的doExecute(...)方法内,Step into bulkAction.execute(...)方法,如下:
再次经过请求过滤链之后,流程开始进入到处理实体类TransportBulkAction类的doExecute(...)方法内,如下:
该函数主要对请求体中所涉及到的索引进行检测,如果不存在则会执行创建操作;如果索引已存在,则接着往下执行具体的写入操作。这个函数体逻辑还是值得深入研究的,包含的信息很多;这里暂时不对此做分析。由于这里的写入数据是在创建完成索引之后进行的;因此这里不需要当前请求再创建索引了;此时逻辑会进入无需创建索引的操作,如下:
接着Step into executeBulk(...)方法内,此时流程进入到TransportBulkAction类的executeBulk(...)方法,如下:
BulkOperation继承ES的AbstractRunnable抽象类,而AbstractRunnable类实现了Java的Runnable接口,层次关系为:
因此主要的执行逻辑会放在run()方法中,此处Step into BulkOperation类的run()方法内,如下:
接着Step into doRun()方法,如下:
此时,真实进入BulkOperation类的doRun()方法中,方法内做了大量的核心工作,比如maping操作、根据路由信息找出doc对应的shardID等,可以做深入研究;这里直接跳转到shardBulkAction对象的execute(...)方法处,如下:
接着Step into shardBulkAction的execute(...)方法内,执行经过如下:
接着Step into this.action.doExecute(...)方法内,此时执行流程进入TransportReplicationAction类的doExecute(...)方法内,如下:
接着Step into doRun()方法内,此时执行流程进入到TransportReplicationAction类的doRun()方法内,如下:
如果要写入的primary shard所在的节点是当前执行的节点,则执行performLocalAction(...)方法,否则执行performRemoteAction(...);由于当前Debug的ES节点集master与data于一身,因此这里会执行performLocalAction(...)方法,如下:
Step into performLocalAction(...)方法内,执行流程经过:
执行到这里,ES会将具体的写入请求操作发送到具体的data节点上去执行真实的写入,这里Step into sendRequest(...)方法内,执行流程经过:
写入流程执行到这里,算是进入了真正的写入逻辑了;primary shard的写入操作由primary.perform(request)完成,可以Debug进入查看,数据主要写入Lucene中;Step into primary.perform(request)继续跟踪可以完整了解到写入Lucene的整个逻辑,这里暂时不涉及Lucene的写入逻辑。执行完primaryShard的操作后接着需要获取当前最新的cluster state,最后会执行performOnReplicas(...)副本的写入操作。这也验证了ES文档中常说的先写primaryShard,后写replicaShard的顺序。由于这里没有副本写入,因此不会执行performOnReplicas(...)真实的操作。最后successfulShards.incrementAndGet()统计执行写入操作成功的shard数量。到这里整个写入的操作算是执行完毕了(由此处一直往下Step over,代码执行逻辑便会结束)。这就是整个写入的执行逻辑。
Debug小结
上述是从整体上梳理了ES执行写入操作的流程,从最开始的Debug,一直跟踪代码的执行流程,经过哪个类哪个方法,直到最后逻辑的完成。这一篇文档算是给想要研究ES写入流程源码操作的同学的一个宏观上的认知;其中一些实现细节并没有做深入的说明。ES的代码结构很复杂,写入流程也很复杂。在我们宏观上有了对ES写入流程的认知后,后续若要研究某些具体的问题,能够比较快速的深入到具体的类具体的方法内去做深入的研究。从上述的Debug整个流程来看,我们暂时可以学习到如下几点:
- ES所有Rest请求的Handler类均继承BaseRestHandler
- ES内部最终都会将用户执行的Rest请求转换成内部的Transport相关的请求处理
- ES内部抽象出了AbstractRunnable的抽象类,然后在run()方法内定义具体的执行流程与规范,比如doRun、onFailure、onAfter
- ES协调节点用于接受请求,然后根据路由信息最终要将请求发送到真实的节点上来做执行
- ES的写入确实是先执行primaryShard的操作,其次再执行replicaShard的写入操作
- ES的代码结构与逻辑都很复杂,所以我们在调试的过程中要使用一些技巧,方便我们尽快找到核心逻辑
- 比如这里当前用一个节点即作为master又作为data
- 为Debug的索引只创建一个primaryShard、0 replicaShard
- 当然这些只是为了在刚开始接触ES代码时,减少Debug的难度;研究真实的问题时,Debug也是很复杂的
由于自己当时对ES与Java了解不够深,所以整个写入流程的打通花费了自己很长的时间,在此做个文档一方面是记录下来,方便自己以后查看;另一方面也是希望帮到和我当初一样处境的同学,尽快走通流程,然后往下继续做ES源码的深入分析。后续会继续将自己学习ES相关的内容与大家一起分享,一起学习。
硬核文档推荐