首先什么是ES?
Elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎。
为什么使用ES?
elasticsearch不是什么全新的技术,它是将全文检索、数据分析以及分布式技术合并在了一起,其中全文检索指的是Apache lucene,数据分析指的是ES自身的分析器(analyzer),分布式技术主要指的就是ES分片技术,下面基于上述三个模块介绍下ES.
lucene:
Lucene 是一种高性能、可伸缩的信息搜索(IR)库,采用了基于倒排表的设计原理,可以非常高效地实现文本查找,在底层采用了分段的存储模式,使它在读写时几乎完全避免了锁的出现,大大提升了读写性能。
核心模块:
Lucene 的写流程和读流程如下图所示:
Lucene 中的主要模块及模块说明如下:
analysis:主要负责词法分析及语言处理,也就是我们常说的分词,通过该模块可最终形成存储或者搜索的最小单元 Term。
index 模块:主要负责索引的创建工作。
store 模块:主要负责索引的读写,主要是对文件的一些操作,其主要目的是抽象出和平台文件系统无关的存储。
queryParser 模块:主要负责语法分析,把我们的查询语句生成 Lucene 底层可以识别的条件。
search 模块:主要负责对索引的搜索工作。
similarity 模块:主要负责相关性打分和排序的实现。
看下Lucene中的专业术语:
Term:是索引里最小的存储和查询单元,对于英文来说一般是指一个单词,对于中文来说一般是指一个分词后的词。
词典(Term Dictionary,也叫作字典):是 Term 的集合。词典的数据结构可以有很多种,每种都有自己的优缺点。比如:排序数组通过二分查找来检索数据:HashMap(哈希表)比排序数组的检索速度更快,但是会浪费存储空间。FST(finite-state transducer)有更高的数据压缩率和查询效率,因为词典是常驻内存的,而 FST 有很好的压缩率,所以 FST 在 Lucene 的最新版本中有非常多的使用场景,也是默认的词典数据结构。使用通过输入有序字符串构建最小有向无环图,可以达到3-20倍的压缩率。
倒排序(Posting List):一篇文章通常由多个词组成,倒排表记录的是某个词在哪些文章中出现过。
正向信息:原始的文档信息,可以用来做排序、聚合、展示等。
段(Segment):索引中最小的独立存储单元。一个索引文件由一个或者多个段组成。在 Luence 中的段有不变性,也就是说段一旦生成,在其上只能有读操作,不能有写操作。
Lucene正反向数据结构信息:
正向信息:
是为了更快的写入和数据的组织。正向信息的数据结构分为如下几层:
1、索引(Index):
一个索引对应于一个文件夹,相当于数据库中的数据库
index.search.slowlog.threshold.query.warn: 10s #超过10秒的query产生1个warn日志
index.search.slowlog.threshold.query.info: 5s #超过5秒的query产生1个info日志
index.search.slowlog.threshold.query.debug: 2s #超过2秒的query产生1个debug日志
index.search.slowlog.threshold.query.trace: 500ms #超过500毫秒的query产生1个trace日志
refresh_interval 代表buffer向cache中写入的间隔时间,写入到cache中就能查询到
curl -X PUT 'http://192.168.101.59:9<port>200/my_index?pretty' -H "Content-Type: application/json" -d '{"mappings": {"my_index_type": {"properties": {"pid": {"type": "keyword","index": "true"},"createtime": {"type": "date","format": "yyyy-MM-dd HH:mm:ss.SSS"},"context": {"type": "text","index": "false"}}}}}'
curl -X PUT 'http://192.168.101.59:9<port>200/my_index_name_v1?pretty' -d '{
"aliases": {
"my_index_name": {}
},
"settings": {
"index": {
"refresh_interval": "120s",
"number_of_shards" : "5",
"number_of_replicas" : "1",
"search.slowlog.threshold.query.warn": "5s",
"search.slowlog.threshold.query.info": "1s",
"search.slowlog.threshold.fetch.warn": "1s",
"search.slowlog.threshold.fetch.info": "800ms",
"indexing.slowlog.threshold.index.warn": "12s",
"indexing.slowlog.threshold.index.info": "5s"
}
},
"mappings": {
"my_type_name": {
"properties": {
"xxx_id": {
"type": "keyword"
},
"timestamp" : {
"type": "long"
},
"@timestamp" : {
"type": "date"
},
"xxx_status": {
"type": "integer"
},
"xxx_content": {
"type": "text"
}
}
}
}
}'
2、段(Segment):
一个索引包括多个段,每个段包括多个文档,且一个文档只能属于一个段,为了能够很快的插入数据,lucene在插入数据时,是不会将新的数据直接插入到旧的段中的(这也造就了段的只读性),而是写入新的段,然后定时会将段进行合并,但是段也一般不会合并到很大,如果很大的话数据的合并和检索都会变的很慢。
3、文档(Document)
文档是我们建索引的基本单位,不同的文档是保存在不同的段中的,一个段可以包含多篇文档。新添加的文档是单独保存在一个新生成的段中,随着段的合并,不同的文档合并到同一个段中。
4、域(Field):
一篇文档包含不同类型的信息,可以分开索引,比如标题,时间,正文,作者等,都可以保存在不同的域里。
5、词(Term):
词是比域更小的单位,比如一个域(正文)的信息是“今天喜洋洋团队冒烟”,可能会被分词为‘今天’、‘喜洋洋’、‘团队’、‘冒烟’,从而建立反向索引
反向索引
是为了更快的查询。
保存了词典到倒排表的映射:词(Term) –> 文档(Document)
词典中的 Term 指向的文档链表的集合,叫做倒排表。词典和倒排表是 Lucene 中很重要的两种数据结构,是实现快速检索的重要基石。
在高并发场景,es是怎么来提升读写性能的:
1、不需要锁,如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题,ES 用乐观并发控制(Optimistic Concurrency Control)来保证新版本的数据不会被旧版本的数据覆盖。
更新场景对于es来说,其实是删除和创建的操作,所以修改操作冲突也就是写冲突,先来看下es接口是怎么修改的,对于已存在的记录,PUT /website/blog/1?version=1,这个version就是一个标识,如果一旦这个标识在es中不是1,那么就表示修改失败,返回409,如果修改成功version会变成2。
ES为什么要这样做,首先不需要锁那么并发支持量就可以很大,而且一般修改场景也不是很多,此外最重要的是失败重试的成本并不是很高,比如一个实例化流程,一个步骤冲突了,回滚重试的成本比较高,这样加锁是比较好的选择,但是对应ES来说只是数据的插入,那么成本就没有那么高了。
乐观并发控制(OCC):是一种用来解决写-写冲突的无锁并发控制,认为事务间的竞争不激烈时,就先进行修改,在提交事务前检查数据有没有变化,如果没有就提交,如果有就放弃并重试。乐观并发控制类似于自选锁,适用于低数据竞争且写冲突比较少的环境。
2、一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。这个所说的不变性是指当你读到时,这里的数据就会是和最终磁盘上的数据是一致的,也就是当有数据修改后,该分片内存(文件系统中的数据就会是最新的,虽然这时的数据还没有到磁盘中)。
那数据写入的过程到底是怎么样的:
为了提升写索引速度,并且同时保证可靠性,Elasticsearch 在分段的基础上,增加了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行操作时均进行了日志记录。
1、一个文档被索引之后,就会被添加到内存中,并且 追加到了 translog,其中translog是被写入到文件系统(内存开辟的一段存储,程序挂了不会导致存储消失,只有整个系统宕机或者关机才会)中的
这里会先写入内存中而不是内存缓冲区中,但是追加到translog中的信息和内存缓冲区段的信息会定时5秒钟写入到磁盘,如果在这5秒内系统宕机会导致这5s的数据丢失。
2、分片每秒被刷新(refresh)一次:配置可以修改
当每秒钟刷新过后,内存中的文档数据会被刷新到内存的另外一块(内存缓冲区),这里会被写入指定的段中(一般是新的段),那之前内存里的数据也就被清空了,这是会产生一个提交点,这样,这次数据就可以被查询到,所有一般情况下插入的数据一秒钟后才能查询到
3、这个进程继续工作,更多的文档被添加到内存缓冲区和追加到事务日志
这样程序持续工作一段时间后,内存中的事务日志和内存缓冲区的数据越来越多
4、每隔一段时间--例如 translog 变得越来越大--索引被刷新(flush);一个新的 translog 被创建,并且一个全量提交被执行:
1、内存中的translog每 5s 刷新(fsync )一次磁盘,所以故障重启,可能会丢失 5s 的数据。fsync 操作同时会将内存中段的信息写入磁盘
2、translog 执行 flush 操作,默认 30 分钟一次,或者 translog 太大(超过512M) 也会执行,会将磁盘里面的数据进行合并重组。
上述的流程就保证了记录能够快速的写入,并且能确保正确性。如果数据在内存中还没有持久化到磁盘上时发生了类似断电等不可控情况,就可能丢失数据。
总结:
document写入时:
1、会将数据先写入到内存和translog中,这时translog是在内存中的
2、定时1秒钟,会执行一次刷新(Refresh),内存中的数据会被写入到文件缓存系统中,生成segment,这是还没生产commit point,但是数据能够被查询了,这样数据就会被查询到这也就是为什么ES写入的数据,1秒后才能被查询到
3、定时5秒钟,会执行一次translog的Fsync操作,这个操作会将translog日志写入到磁盘中,这就解释为什么系统宕机,会导致5秒钟内写入的数据丢失
4、当日志数据的大小超过 512MB 或者时间超过 30 分钟时,需要触发一次刷新(flush),该次刷新,会将文件系统中的数据Fsync到磁盘中,并且将旧的translog删除,而旧的translog里面的数据已经被刚才的Fsync操作写入到磁盘中,所以,如果宕机也不需要恢复旧的translog里面的数据,所以可以直接删除,而新生成的translog是在flush(比较耗时)的过程中产生的。同时生产commit point,生产一个.del文件,commit point都会维护一个.del文件(es删除数据本质是不属于物理删除),当es做删改操作时首先会在.del文件中声明某个document已经被删除,文件内记录了在某个segment内某个文档已经被删除,当查询请求过来时在segment中被删除的文件是能够查出来的,但是当返回结果时会根据commit point维护的那个.del文件把已经删除的文档过滤掉。
segment 合并:
由于每一次refresh都会产生一个segment并且打开它使得搜索可见,segment 数目太多会带来较大的麻烦。每一个 segment 都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个 segment ;所以 segment 越多,搜索也就越慢。在 ES 后台会有一个线程进行 segment 合并。合并的segment可以是磁盘上已经commit过的索引,也可以在内存中还未commit的segment。
流程如下:
1、merge进程会在后台选择一些小体积的segments,然后将其合并成一个更大的segment,这个过程不会打断当前的索引和搜索功能
2、新的segment会被flush到磁盘
3、然后会生成新的commit point文件,包含新的segment名称,并排除掉旧的segment和那些被合并过的小的segment
4、接着新的segment会被打开用于搜索
5、最后旧的segment会被删除掉
commit point记录提交点,这样一旦宕机,会去恢复commit point之后的数据
至此原来标记伪删除的document都会被清理掉,如果不加控制,合并一个大的segment会消耗比较多的io和cpu资源,同时也会搜索性能造成影响,所以默认情况下es已经对合并线程做了资源限额以便于它不会搜索性能造成太大影响。调用如下接口限制该线程的最大使用内存:
PUT /_cluster/settings {"persistent" : {"indices.store.throttle.max_bytes_per_sec" : "100mb"}}
分布式:
来看下ES的分布式集群:
conf/elasticsearch.yml:
node.master: true/false
node.data: true/false
当node.master为true时,其表示这个node是一个master的候选节点,可以参与选举。ES正常运行时只能有一个master(即leader),多于1个时会发生脑裂。当node.data为true时,这个节点作为一个数据节点,会存储分配在该node上的shard的数据并负责这些shard的写入、查询等。
此外,任何一个集群内的node都可以执行任何请求,其会负责将请求转发给对应的node进行处理,所以当node.master和node.data都为false时,这个节点可以作为一个类似proxy的节点,接受请求并进行转发、结果聚合等。
上图是一个ES集群的示意图,其中NodeA是当前集群的Master,NodeB和NodeC是Master的候选节点,其中NodeA和NodeB同时也是数据节点(DataNode),此外,NodeD是一个单纯的数据节点,Node_E是一个proxy节点。每个Node会跟其他所有Node建立连接。
节点发现
ZenDiscovery是ES自己实现的一套用于节点发现和选主等功能的模块,没有依赖Zookeeper等工具。
conf/elasticsearch.yml:
discovery.zen.ping.unicast.hosts: [1.1.1.1, 1.1.1.2, 1.1.1.3]
这个配置可以看作是,在本节点到每个hosts中的节点建立一条边,当整个集群所有的node形成一个联通图时,所有节点都可以知道集群中有哪些节点,不会形成孤岛
Master选举
上面提到,集群中可能会有多个master-eligible node,此时就要进行master选举,保证只有一个当选master。如果有多个node当选为master,则集群会出现脑裂,脑裂会破坏数据的一致性,导致集群行为不可控,产生各种非预期的影响。
为了避免产生脑裂,ES采用了常见的分布式系统思路,保证选举出的master被多数派(quorum)的master-eligible node认可,以此来保证只有一个master。这个quorum通过以下配置进行配置:
conf/elasticsearch.yml:
discovery.zen.minimum_master_nodes: 2
master选举谁发起,什么时候发起?
master选举当然是由master-eligible节点发起,当一个master-eligible节点发现满足以下条件时发起选举:
- 该master-eligible节点的当前状态不是master。
- 该master-eligible节点通过ZenDiscovery模块的ping操作询问其已知的集群其他节点,没有任何节点连接到master。
- 包括本节点在内,当前已有超过minimum_master_nodes个节点没有连接到master。
当需要选举master时,选举谁?
先根据节点的clusterStateVersion比较,clusterStateVersion越大,优先级越高。clusterStateVersion相同时,进入compareNodes,其内部按照节点的Id比较(Id为节点第一次启动时随机生成)。
总结一下:
- 当clusterStateVersion越大,优先级越高。这是为了保证新Master拥有最新的clusterState(即集群的meta),避免已经commit的meta变更丢失。因为Master当选后,就会以这个版本的clusterState为基础进行更新。(一个例外是集群全部重启,所有节点都没有meta,需要先选出一个master,然后master再通过持久化的数据进行meta恢复,再进行meta同步)。
- 当clusterStateVersion相同时,节点的Id越小,优先级越高。即总是倾向于选择Id小的Node,这个Id是节点第一次启动时生成的一个随机字符串。之所以这么设计,应该是为了让选举结果尽可能稳定,不要出现都想当master而选不出来的情况。
什么时候选举成功?
当一个master-eligible node(我们假设为Node_A)发起一次选举时,它会按照上述排序策略选出一个它认为的master。
- 假设Node_A选Node_B当Master:
Node_A会向Node_B发送join请求,那么此时:
(1) 如果Node_B已经成为Master,Node_B就会把Node_A加入到集群中,然后发布最新的cluster_state, 最新的cluster_state就会包含Node_A的信息。相当于一次正常情况的新节点加入。对于Node_A,等新的cluster_state发布到Node_A的时候,Node_A也就完成join了。
(2) 如果Node_B在竞选Master,那么Node_B会把这次join当作一张选票。对于这种情况,Node_A会等待一段时间,看Node_B是否能成为真正的Master,直到超时或者有别的Master选成功。
(3) 如果Node_B认为自己不是Master(现在不是,将来也选不上),那么Node_B会拒绝这次join。对于这种情况,Node_A会开启下一轮选举。
- 假设Node_A选自己当Master:
此时NodeA会等别的node来join,即等待别的node的选票,当收集到超过半数的选票时,认为自己成为master,然后变更cluster_state中的master node为自己,并向集群发布这一消息。
假如集群中有3个master-eligible node,分别为Node_A、 Node_B、 Node_C, 选举优先级也分别为Node_A、Node_B、Node_C。三个node都认为当前没有master,于是都各自发起选举,选举结果都为Node_A(因为选举时按照优先级排序,如上文所述)。于是Node_A开始等join(选票),Node_B、Node_C都向Node_A发送join,当Node_A接收到一次join时,加上它自己的一票,就获得了两票了(超过半数),于是Node_A成为Master。此时cluster_state(集群状态)中包含两个节点,当Node_A再收到另一个节点的join时,cluster_state包含全部三个节点。
选举怎么保证不脑裂?
基本原则还是多数派的策略,如果必须得到多数派的认可才能成为Master,那么显然不可能有两个Master都得到多数派的认可。
上述流程中,master候选人需要等待多数派节点进行join后才能真正成为master,就是为了保证这个master得到了多数派的认可。但是我这里想说的是,上述流程在绝大部份场景下没问题,听上去也非常合理,但是却是有bug的。
因为上述流程并没有限制在选举过程中,一个Node只能投一票,那么什么场景下会投两票呢?比如NodeB投NodeA一票,但是NodeA迟迟不成为Master,NodeB等不及了发起了下一轮选主,这时候发现集群里多了个Node0,Node0优先级比NodeA还高,那NodeB肯定就改投Node0了。假设Node0和NodeA都处在等选票的环节,那显然这时候NodeB其实发挥了两票的作用,而且投给了不同的人。
那么这种问题应该怎么解决呢,比如raft算法中就引入了选举周期(term)的概念,保证了每个选举周期中每个成员只能投一票,如果需要再投就会进入下一个选举周期,term+1。假如最后出现两个节点都认为自己是master,那么肯定有一个term要大于另一个的term,而且因为两个term都收集到了多数派的选票,所以多数节点的term是较大的那个,保证了term小的master不可能commit任何状态变更(commit需要多数派节点先持久化日志成功,由于有term检测,不可能达到多数派持久化条件)。这就保证了集群的状态变更总是一致的。
这里要表达的是,ES的ZenDiscovery模块与成熟的一致性方案相比,在某些特殊场景下存在缺陷,下一篇文章讲ES的meta变更流程时也会分析其他的ES无法满足一致性的场景。
1、ES怎么做分布式和分布式又是怎么做查询呢!
首先理解下分片的概念,分片其实就是将ES分为多个ES服务,每个服务对应一个完整的lucene服务,每个分片里包含的数据是不一样的,和我们微服务中的POD副本是不同的概念,也就是一个文档只能存在于某一个分片中
分片副本,这里的副本其实和微服务中的POD副本概念很像,对应也提供服务能力,但是副本只对外提供查询不提供创删改的能力
2、那具体设置多少分片呢
目前这个还没有定论,需要实际的经验,很多使用者认为多多益善,这样的话每个分片就可以分担压力,但是由于每个分片里面存储的数据不一致,如果查询相关度前1000的文档,那么每个分片都要返回相关度前1000的文档,再由协调节点服务进行排序筛选,所以会导致协调节点压力非常大,所以说也不是越多越好,建议合适的分片上多一点富余即可
ES所有文档中所说的节点其实对应于实际使用中微服务的副本。
每秒钟进行刷新(refresh)时,会将对应的文档信息通过推模式(push replication)push给副本,所以在refresh完后各个副本中的内存中都会有相应的segment文件并且写入副本。后续flush到磁盘中是每个副本自行执行,可能会导致每个副本中的磁盘中的数据组织不太一样,但是包括的内容是一样的,也就是查询物理哪个副本,查询到的信息都是一样的。
3、ES的读写流程如下:
ES的任意节点都可以作为协调节点(coordinating node)接受请求,当协调节点接受到请求后进行一系列处理,然后通过_routing字段找到对应的primary shard,并将请求转发给primary shard, primary shard完成写入后,将写入并发发送给各replica, raplica执行写入操作后返回给primary shard, primary shard再将请求返回给协调节点
对于协调节点:
1、ingest pipeline
查看该请求是否符合某个ingest pipeline的pattern, 如果符合则执行pipeline中的逻辑,一般是对文档进行各种预处理,如格式调整,增加字段等。如果当前节点没有ingest角色,则需要将请求转发给有ingest角色的节点执行。这个其实是对数据的预处理,很像django里面中间件,只不过这些不同的处理模块都是由不同的线程来处理的,比如将字符串的切割、字符串的替换等,例如:比如当你使用“长城在china”查询时,该模块会将字符串切割为“长城”“china”再将China替换为“中国”,进行分词查询
2、自动创建索引
判断索引是否存在,如果开启了自动创建则自动创建,否则报错,当我们做创建操作时,如果对应的index还未存在,那么就会先去创建该索引
3、设置routing
获取请求URL或mapping中的_routing,如果没有则使用_id, 如果没有指定_id则ES会自动生成一个全局唯一ID。该_routing字段用于决定文档分配在索引的哪个shard上。一般通过求余算法计算该文档存在那个分片上:shard_num = hash(\routing) % num_primary_shards
4、构建BulkShardRequest
由于Bulk Request中包含多种(Index/Update/Delete)请求,这些请求分别需要到不同的shard上执行,因此协调节点,会将请求按照shard分开,同一个shard上的请求聚合到一起,构建BulkShardRequest,这里举个例子:当我们查询“中国”相关的文档,默认先取前100,那么首先会将该词条发送给各个分片,每个分片先在本分片中找到打分在前100的文档,并将文档的id和score返回给协调节点,这样协调节点会根据各分片返回的信息进行排序筛选,如果有5个分片,那么就会收到5*100个文档信息,那么协调节点会取score值在前100个,并将前100个按分片归并,这里就形成了BulkShardRequest,然后调用不同的分片进行查询,最后再返回出去
4、将请求发送给primary shard
因为当前执行的是写操作,因此只能在primary上完成,所以需要把请求路由到primary shard所在节点
5、等待primary shard返回
对于主分片(primary shard):
Primary请求的入口是PrimaryOperationTransportHandler的MessageReceived, 当接收到请求时,执行的逻辑如下
1、判断操作类型
遍历bulk请求中的各子请求,根据不同的操作类型跳转到不同的处理逻辑
2、将update操作转换为Index和Delete操作
获取文档的当前内容,与update内容合并生成新文档,然后将update请求转换成index请求,此处文档设置一个version v1
3、Parse Doc
解析文档的各字段,并添加如_uid等ES相关的一些系统字段
4、更新mapping
对于新增字段会根据dynamic mapping或dynamic template生成对应的mapping,如果mapping中有dynamic mapping相关设置则按设置处理,如忽略或抛出异常
5、获取sequence Id和Version
从SequcenceNumberService获取一个sequenceID和Version。SequcenID用于初始化LocalCheckPoint, verion是根据当前Versoin+1用于防止并发写导致数据不一致。
6、写入lucene
这一步开始会对文档uid加锁,然后判断uid对应的version v2和之前update转换时的versoin v1是否一致,不一致则返回第二步重新执行。 如果version一致,如果同id的doc已经存在,则调用lucene的updateDocument接口,如果是新文档则调用lucene的addDoucument. 这里有个问题,如何保证Delete-Then-Add的原子性,ES是通过在Delete之前会加上已refresh锁,禁止被refresh,只有等待Add完成后释放了Refresh Lock, 这样就保证了这个操作的原子性。
7、写入translog
写入Lucene的Segment后,会以key value的形式写Translog, Key是Id, Value是Doc的内容。当查询的时候,如果请求的是GetDocById则可以直接根据_id从translog中获取。满足nosql场景的实时性。
8、重构bulk request
因为primary shard已经将update操作转换为index操作或delete操作,因此要对之前的bulkrequest进行调整,只包含index或delete操作,不需要再进行update的处理操作。
9、flush translog
默认情况下,translog要在此处落盘完成,如果对可靠性要求不高,可以设置translog异步,那么translog的fsync将会异步执行,但是落盘前的数据有丢失风险。
10、发送请求给replicas
将构造好的bulkrequest并发发送给各replicas,等待replica返回,这里需要等待所有的replicas返回,响应请求给协调节点。如果某个shard执行失败,则primary会给master发请求remove该shard。这里会同时把sequenceID, primaryTerm, GlobalCheckPoint等传递给replica。
11、等待replica响应
当所有的replica返回请求时,更细primary shard的LocalCheckPoint。
对于副分片(replica shard):
Replica 请求的入口是在ReplicaOperationTransportHandler的messageReceived,当replica shard接收到请求时执行如下流程:
1、判断操作类型
replica收到的写如请求只会有add和delete,因update在primary shard上已经转换为add或delete了。根据不同的操作类型执行对应的操作
2、Parse Doc
3、更新mapping
4、获取sequenceId和Version 直接使用primary shard发送过来的请求中的内容即可
5、写如lucene
6、write Translog
7、Flush translog