今天被人问到了ES相关的问题,没有说上来。ES有段时间没用了,很多细节都忘了,幸好之前有看过一些源码,这里就整理下,写篇博客记录一下。首先大致说下写索引的大概流程:

    primary分片是写索引的主入口,由它负责验证输入正确性,并负责将操作复制到其他副本。首先是根据document id, route到primary shard上执行。复制的时候不需要复制完全部的replica,主节点会维护一个维护一个需要复制的列表,复制到这些shard上(parallel),然后回复client。

    再单个节点上写数据的时候,会同时写索引数据和translog文件(类似HBase的WAL日志),达到一定的阈值再刷写到磁盘上。

      

写入流程主要分成两部分:

  1. 客户端包装输入数据,发送给服务端
  2. 服务端在各个shard上处理数据

 

客户端处理:

首先创建一个bulk请求:

es使用curl插入数据 es数据写入流程_服务端

接着定义了一个回调对象,用于存放返回结果,调用bulk.execute()方法,将请求发往服务端

es使用curl插入数据 es数据写入流程_服务端_02

真正执行的是里面的client.execute(action, beforeExecute(request), listener),nodesService 代表的是这个客户端所连接的集群的信息

es使用curl插入数据 es数据写入流程_客户端_03

通过nodes.get(index % nodes.size()) 来负载均衡,随机选取一个node,这里的index是个AtomicInteger,每次自增1来随机选取node

es使用curl插入数据 es数据写入流程_服务端_04

然后回调到了上面,这样就获取到了要把请求发往那个node…真啰嗦

es使用curl插入数据 es数据写入流程_服务端_05

到此 客户端的发送就结束了。总结下就是客户端封装输入的数据,随机选取一个节点,然后发送请求。

 

服务端处理:

接下来就看下服务端这边是如何的进行的。服务端的入口是这个channelHandler,就是netty处理请求的地方

es使用curl插入数据 es数据写入流程_数据_06

服务端会检查索引是否存在,不存在的话自动建索引:

es使用curl插入数据 es数据写入流程_客户端_07

接着来到了获取shardId的地方:

es使用curl插入数据 es数据写入流程_服务端_08

内部的详细流程为:

  1. indexMetaData(clusterState, index) 获取到插入的这个索引的详细信息
  2. id = x 这个就是你输入的ID
  3. routing 这个没有设置过,那就是为null

即随机获取到一个shardId ,将文档索引到这个shard上

然后开始执行写主分片和备分片,执行这两部调用的还是TransportAction类中的execute():

es使用curl插入数据 es数据写入流程_es使用curl插入数据_09

接着到了这TransportReplicationAction.java,根据shardId和该node上记录的ShardRouting信息找到这个分片所对应的主节点,然后发往主节点上执行

es使用curl插入数据 es数据写入流程_服务端_10

然后跳到了这里,因为我这个是单机,所以直接在本地执行的,否则执行下面那句,发到远程主分片所在的节点上执行

es使用curl插入数据 es数据写入流程_服务端_11

接着就真正的在primary上开始执行了, sendLocalRequest(xxx)

es使用curl插入数据 es数据写入流程_数据_12

跳进去看下,是对接受到的信息,创建一个异步的Primary Action来执行, run()

es使用curl插入数据 es数据写入流程_服务端_13

执行的时候要加锁:indexShard.acquirePrimaryOperationLock(onAcquired, executor);

 

如果检测到有副本的话,在执行主分片之前,会先把副本上要执行的操作创建好,然后再execute()

es使用curl插入数据 es数据写入流程_客户端_14

 

Primary上执行:

先是做了一些写之前的准备工作,主要看下有没有动态字段(有的话需要更新mapping),最后再真正执行Index:

es使用curl插入数据 es数据写入流程_服务端_15

对索引的操作都是封装在Engine对象中执行的:

es使用curl插入数据 es数据写入流程_服务端_16

接着执行indexResult = indexIntoLucene(index, plan),就是将封装好的这个Index对象写成Lucene文件,同时追加到Translog里面:

写成Lucene数据并不是立即刷新的,先写Lucene个人看到的一种说法是建索引的时候比较复杂,容易出错,避免写大量无用的记录到translog里面。

es使用curl插入数据 es数据写入流程_es使用curl插入数据_17

看下这个写Lucene的方法:

先要判断文档是否存在,存在的话就是更新,否则就是插入(VersionMap里面的检查逻辑暂时还没有看明白…)

es使用curl插入数据 es数据写入流程_es使用curl插入数据_18

执行完毕之后,会返回一个location ,代表了translog的位置

es使用curl插入数据 es数据写入流程_es使用curl插入数据_19

结合这个返回的location,同时也构造了另外一个和refresh有关的请求,并执行                     

es使用curl插入数据 es数据写入流程_数据_20

 

从代码中看到,默认情况下在 maybeFlush()方法中会检查translog的大小,translog太大(超过512m), 会刷新translog文件到磁盘

es使用curl插入数据 es数据写入流程_es使用curl插入数据_21

每隔一秒钟(这也是ES称为近实时的原因)会被写入新的segment file,同时进入os cache,此时index segment file被打开并供search使用。然后buffer被清空,重复上述操作。

    当translog大小或者时间间隔达到一定程度的时候,commit操作发生,将数据真正flush到磁盘上(包括当前buffer中的数据),同时也会并写入os cache,打开供使用。现有的translog也会被清空,创建一个新的translog。

 

 

接着回来写replica,和写primary的流程是一样的,因为掉的是同样的方法。因为ES天生是个分布式的系统,所以相当于是每次执行操作,都要发送一次请求:

es使用curl插入数据 es数据写入流程_客户端_22

 

至此,整个流程就执行结束了!

整个流程如下图所示:

es使用curl插入数据 es数据写入流程_es使用curl插入数据_23

总结下就是先写主分片,再写副本。写主分片和副本的流程是一样的。

写的时候,索引文件和日志文件是同时写的,索引文件(segment)默认每个1s生成一个,从内存缓存中写到os cache中,然后重新open并更新commit point使得文件可以被搜索。然后当日志文件达到一定大小或者时间时,讲segment文件和日志文件都刷写到磁盘上进行持久化!

 

两种异常情况处理:

1.主节点失效

       该主节点存在的node会向master发送信息,默认等待一分钟后,master会将其他节点提升为主节点,然后请求会转发到新的主节点. 注意,这个时候可能是由于网络或者GC的问题导致主节点失联了,此时主节点还不知道它已经不是主节点了,这个时候它仍然会将请求转到打其他的replica,当它收到来自其他replica reject时,才会意识到知己已经降级了,这些操作随后会route到new primary.

 

2.备节点失效

       主节点会向master报个这个node失联了,然后master会将这个node从in-sync replica set中移除,然后master会在其他node上重新启动一个shard。

 

 

 

 

 

参考:

       (ES写入图解)

       https://www.jianshu.com/p/7fae8e9fa7d1(ES写入简要流程描述)