浅谈Elasticsearch和实际应用
1. Elasticsearch 基本概念和原理
1.1 简介
- Elasticsearch 是一个分布式、高扩展、高实时的数据搜索与数据分析引擎。它能很方便的使大量数据具有搜索、分析和探索的能力。充分利用Elasticsearch的水平伸缩性,能使数据在生产环境变得更有价值。
1.2 有关概念
cluster
:代表一个集群,集群中有多个节点,其中有一个为主节点,这个主节点是可以通过选举产生的,主从节点是对于集群内部来说的。es的一个概念就是去中心化,字面上理解就是无中心节点,这是对于集群外部来说的,因为从外部来看es集群,在逻辑上是个整体,你与任何一个节点的通信和与整个es集群通信是等价的。shards
:代表索引分片,es可以把一个完整的索引分成多个分片,这样的好处是可以把一个大的索引拆分成多个,分布到不同的节点上。构成分布式搜索。分片的数量只能在索引创建前指定,并且索引创建后不能更改。replicas
:代表索引副本,es可以设置多个索引的副本,副本的作用一是提高系统的容错性,当某个节点某个分片损坏或丢失时可以从副本中恢复。二是提高es的查询效率,es会自动对搜索请求进行负载均衡。recovery
:代表数据恢复或叫数据重新分布,es在有节点加入或退出时会根据机器的负载对索引分片进行重新分配,挂掉的节点重新启动时也会进行数据恢复。gateway
:代表es索引快照的存储方式,es默认是先把索引存放到内存中,当内存满了时再持久化到本地硬盘。gateway对索引快照进行存储,当这个es集群关闭再重新启动时就会从gateway中读取索引备份数据。transport
:代表es内部节点或集群与客户端的交互方式,默认内部是使用tcp协议进行交互,同时它支持http协议(json格式)、thrift、servlet、memcached、zeroMQ等的传输协议(通过插件方式集成)。
1.2.1 索引结构
- ES是面向文档的。各种文本内容以文档的形式存储到ES中,一般使用
JSON
作为文档的序列化格式。文档可以有很多字段,在创建索引的时候,我们需要描述文档中每个字段的数据类型,并且可能需要指定不同的分析器。 - 在存储结构上,由
_index、_type(已被淘汰)
和_id
作为唯一标识,标识一个文档。_index
指向一个或多个物理分片的逻辑命名空间;_type
类型用于区分同一个集合中的不同细分。一个index
下可以有多个不同的_type
。_id
就是文档的标记符,可以由系统自动生成或使用者自己提供。
注:1) 由于不同的_type
下的字段不能冲突,删除整个_type
也不会释放空间,所以在实际应用中,对需要多个_type
的需求是,我们应该再单独建一个索引,而不是在一个索引下使用多个_type
。删除过期老化的数据,最好是以_index
为单位,而不是_type、_id
。
2) 正是由于_type
在实际应用中容易引起概念混淆,以及没什么实际意义。所以在ES 6.x版本中,一个_index
只允许存在一个,并且在7.x版本以上,完全删除了_type
的概念。
1.2.2 分片(shard)
- 把一个大的索引拆分成多个,分布到不同的节点上,构成分布式搜索。数据分片可以提高水平扩展能力,将数据分成若干小块分配到各个机器上,然后通过某种路由策略找到某个数据块所在的分片位置。
- ES将数据副本分为主分片和副分片。主分片数据作为权威数据,写过程中先写主分片,成功后再写副分片,恢复阶段以主分片为准。
- 分片是底层的基本读写单元,分片的目的是分割巨大索引,让读写可以并行操作,有多台机器共同完成。分片是数据的容器,文档保存在分片内,不会跨分片存储。分片又被分配到集群内部的各个节点里。当集群规模扩大或者缩小时,ES会自动在各个节点中迁移分片,使数据仍然均匀分布在集群里。(问题:当集群规模缩小时,分片迁移时,es是如何提供读写服务的?)
- 主分片数量:先根据硬件情况定好单个分片容量,然后根据业务场景预估数据量和增长量,再除以单个分片容量。当分片数不够用时,可以考虑新建索引。搜索1个有50个分片的索引与搜索50个每个只有一个分片的索引是完全等价的。
- ES的更新、删除等操作实际上是将数据标记为删除,记录到单独的位置,这种标记删除的方式并不会释放磁盘空间,只会在段合并的时候才会真正的从磁盘上删除。
- 不要向单个索引里持续写数据。因为这样会使它的分片巨大无比,巨大的索引会在数据老化后难以删除。以
_id
为单位删除文档时不会立刻释放空间,删除的doc
只在Lucene
分段合并时才会真正从磁盘中删除。即使人工触发分段合并,仍然会引起较高的I/O
压力,并且可能因为分段巨大导致在合并中磁盘空间不足。因此,对于在实际开发中,对于这种场景的需求我们可以创建一个索引别名来关联这些索引,对于删除比较频繁的索引,可以直接删除整个索引。这样可以防止资源被占用。
1.2.3 分段和段合并
- 在写操作中,一般会先在内存中缓冲一段数据,再将这些数据写入磁盘,每次写入磁盘的这批数据称为一个分段。可以通过手动调用
flush
,或者操作系统通过一定策略将系统缓存刷到磁盘。这样可以大幅提升写的效率。 - 正因如此ES实现的是近实时搜索。由于系统先缓冲一段数据才写,并且新段不会立即刷入磁盘。这两个过程中如果出现意外情况(主机断电),则会存在数据丢失的风险。ES对此的做法是记录事务日志,每次对ES进行操作时均记录事务日志,当ES启动的时候,重放
translog
中所有在最后一次提交后发生的变更操作。 - 在ES中,每秒清空一次写缓冲,将这些数据写入文件,这个过程叫
refresh
,每次refresh
会创建一个新的Lucene
段。但是段越多,搜索会变的越慢。ES会通过一定的策略将较小的段合并为大的段,常用的方案是选择大小相似的分段进行合并。在段合并的过程中,标记为删除的数据不会写入新的分段中。在合并结束后,旧的分段数据会被删除,标记删除的数据才会从磁盘删除。
注:1)新段的产生需要一定的磁盘空间,我们要保证系统又足够的剩余可用空间。如果空间不足,不再产生新的分段时,则无法对标记删除的数据进行真正的物理删除。
2)ES的refresh
调用时Lucene
的flush
;ES的flush
调用的是Lucene
的commit
。
1.3 集群
- 分布式系统的集群方式可以分为主从模式和无主模式。ES使用的是主从模式,优点是可以简化系统设计,部分操作仅由Master执行,并负责维护集群元信息。缺点是Master节点存在单点故障,需要解决灾备问题,并且集群规模会受限于Master节点的管理能力。
1.3.1 集群节点角色
1.3.1.1 主节点(Master node)
- 主节点负责集群层面的相关操作,管理集群变更。通过配置
node.master: true(默认)
使节点具有被选举为Master的资格。主节点是全局唯一的,将从有资格称为Master的节点中进行选举。 - 主节点也可以作为数据节点,但尽可能做少量的工作,因此尽量分离主节点和数据节点,创建独立节点的配置:
node.master: true
node.data: false
- 为了防止数据丢失,每个主节点应该知道有资格称为主节点的数量,默认值为1。为避免网络分区时出现多主的情况,配置
discovery.zen.minimum_master_nodes
原则上最小值应该是:
(master_eligible_nodes/2) + 1
1.3.1.2 数据节点(Data node)
- 负责保存数据、执行数据相关操作:CRUD、搜索、聚合等。一般情况下,数据读写流程只和数据节点交互,不会和主节点打交道。
- 通过配置
node.data: true(默认)
来使一个节点成为数据节点。
1.3.1.3 预处理节点(Ingest node)
- 预处理操作允许在索引文档之前,即写入数据之前,通过事先定义好的一系列的处理器(process)和管道(pipeline),对数据进行某种转换,富化。
- 默认情况下,在所有的节点上启用
ingest
,如果想在某个节点上禁用ingest
,则可以添加配置node.ingest: false
。
1.3.1.4 协调节点(Coordinating node)
- 客户端请求可以发送到集群的任何节点,每个节点都知道任意文档所处在的位置,它会转发请求到所在的数据节点上,收集数据并返回给客户端,处理客户端请求的节点称为协调节点。
- 协调节点将请求转发给保存数据的数据节点。每个数据节点在本地执行请求,并将结果返回协调节点。协调节点收集完数据后,将每个数据节点的结果合并为单个全局结果。
1.3.2 集群中的索引的健康状态
-
Green
:所有主分片和副分片都正常执行。 -
Yellow
:所有的主分片都正常运行,但不是所有的副分片都正常运行。这意味着存在单点故障。 -
Red
:有主分片没能正常运行。此时,ES集群会拒绝对相关的索引的搜索,聚合等服务。
1.3.3 集群扩容
- 当扩容集群,增加节点时,分片会均衡的分配到集群的各个节点上,从而对索引和搜索过程进行负载均衡,这些都是系统自动完成的。分片副本实现了数据冗余,从而防止硬件故障导致的数据丢失的问题。
下面几张图展示了集群只有一个节点,到变成两个节点、三个节点时的shard迁移过程示例。
- 起初,在NODE1上有三个主分片,没有副分片。
- 添加第二个节点后,副分片被分配到NODE2。
- 添加第三个节点后,索引的六个分片被平均分配到集群的三个节点上。
分片分配过程中除了让节点间均匀存储,还要保证不把主分片和副分片分配到同一节点上,避免单个节点故障引起数据丢失。
1.3.4 集群启动流程
- 集群启动流程,期间经历选举主节点、主分片、数据恢复等重要阶段。整体流程如下图所示:
1.3.4.1 选举主节点
- 参选人数需要过半,达到 quorum(多数)后就选出了临时的主。 例如:
集群有5台主机,节点ID分别是1、2、3、4、5。当产生网络分区或节点启动速度差异较大时,节点1看到的节点列表是1、2、3、4,选出4;节点2看到的节点列表是2、3、4、5,选出5。结果就不一致了,由此产生下面的第二条限制。
- 得票数需过半。 某节点被选为主节点,必须判断加入它的节点数过半,才确认Master身份。
- 当探测到节点离开事件时,必须判断当前节点数是否过半(防止出现脑裂)。 如果达不到 quorum,则放弃Master身份,重新加入集群。例如:
假设5台机器组成的集群产生网络分区,2台 一组,3台一组,产生分区前,Master位于2台中的一个,此时3台一 组的节点会重新并成功选取Master,产生双主,俗称脑裂。
注:quorum
值是从配置中读取的,discovery.zen.minimum_master_nodes
。为了避免脑裂,最小值应该是有Master
资格的节点数n/2 + 1
。
选举主节点整体流程图如下:
activeMasters
列表:存储集群当前活跃的Master列表。将每个节点所认为的当前Master节点加入activeMaster列表中(不包括本节点)。masterCanadidates
列表:存储master候选者列表。去掉不具备Master资格的节点,添加到这个列表中。
节点失效检测
节点失效检测会监控节点是否离线,然后处理其中的异常。失效检测是选主流程之后不可或缺的步骤,不执行失效检测可能会产生脑裂。为此我们需要启动两种是失效探测器:
- 在Master节点:启动
NodesFaultDetection
,简称NodesFD
。定期探测加入集群的节点是否活跃。 - 在非Master节点:启动
MasterFaultDetection
,简称MasterFD
。定期探测Master节点是否活跃。
NodesFaultDetection
和MasterFaultDetection
都是通过定期(默认1s)发送的ping请求探测节点是否正常的,当失败达到一定次数(默认为3次),或者收到来自底层连接模块的节点离线通知时,开始处理节点离开事件。
主节点在探测到节点离线的事件处理中,如果发现当前集群节点数量不足法定人数,则放弃Master身份,从而避免产生双主。
1.3.4.2 选举集群元信息
- 集群元信息的选举保包括两个级别:集群级和索引级。
- 集群状态元数据是全局信息,元数据包括内容路由信息、配置信息等。集群状态由主节点负责维护,如果主节点从数据节点接收更新,则将这些更新广播到集群的其他节点,让每个节点上的集群状态保持最新。
- 选出来的Master和集群元信息是没有关系的。因此它的第一个任务就是选举元信息,让各节点把各自存储的元信息发过来,根据版本号确定最新的元信息,然后把这个信息广播下去,这样集群的所有节点都有了最新的元信息。
- 为了集群一致性,参与选举的元信息数量需要过半,Master发布集群状态成功的规则也是等待发布成功的节点数过半。
- 在选举集群元信息时,不接受新节点的加入请求。
- 集群元信息选举完毕后,Master发布首次集群状态,然后开始选举shard级元信息。
1.3.4.3 allocation(分片分配)过程
- ES在这个过程中决定哪个分片位于哪个节点,重构内容路由表。
- 选举主分片
- ES 5.x以下的版本,通过对比shard级元信息的版本号来决定。如果只有一个shard信息汇报上来,则它一定会被选举为主分片,但也许数据不是最新的,版本号比它大的那个shard所在节点还没启动。
- ES 5.x以上的版本,给每一个shard都设置一个UUID,然后再集群级的元信息中记录哪个shard是最新的,因为ES是先写主分片,再由主分片节点转发请求去写副分片,所以主分片所在的节点肯定是最新的。如果它转发失败了,则要求Master删除那个节点。也就是说:主分片选举过程是通过集群级元信息中记录的“最新主分片的列表”来确定主分片的。
- 如果集群禁止分配分片,即集群设置:
"cluster.routing.allocation.enable": "none"
集群仍会强制分配主分片。因此,集群重启后状态为Yellow,而不是Red。
- 选举副分片
- 主分片选举完成后,从上一个过程汇总的shard信息中选择一个副本作为副分片。如果这个汇总信息不存在,则会分配一个全新副本
allocation过程中允许新启动的节点加入集群。
1.3.4.4 index recovery(索引恢复)
- 分片分配成功后就进入
recovery
流程。主分片的恢复不会等待其副分片分配成功才开始recovery
,而副分片的恢复需要主分片recovery
成功后才开始。 - 对于主分片来说,可能有一些数据还没来及刷盘;对于副分片来说,一时没刷盘,二是主分片写完了,副分片还没来得及写,主副分片数据不一致。
- 主分片 recovery
- 由于每次写操作都会记录事务日志(translog),事务日志中记 录了哪种操作,以及相关的数据。因此将最后一次提交(Lucene的 一次提交就是一次fsync刷盘的过程)之后的translog中进行重放, 建立Lucene索引,如此完成主分片的recovery。
- 副分片 recovery
副分片需要恢复成与主分片一致,同时,recovery期间允许新的索引操作。目前分成两个阶段执行:
-
phase1
:在主分片所在节点,获取translog保留锁,从获取保留锁开始,会保留translog不受其刷盘清空的影响。然后调用Lucene 接口把shard做快照,这是已经刷磁盘中的分片数据。把这些shard数据复制到副本节点。在phase1
完毕前,会向副分片节点发送告知对方启动engine,在phase2
开始之前,副分片就可以正常处理写请求了。 -
phase2
:对translog做快照,这个快照里包含从phase1
开始,到执行translog快照期间的新增索引。将这些translog发送到副分片所在节点进行重放。
由于需要从主分片拉取全量的数据,所以第一阶段耗时比较漫长。在ES 6.x以上,对phase1
做了优化:标记每个操作。在正常的写操作中,每次写入成功的操作都分配一个序号,通过对比序号就可以计算出差异范围;在实现方式上,添加了全局检查点(global checkpoint)和本地检查点(local checkpoint),主分片维护global checkpoint,代表所有分片都已经写入这个序号的位置,local checkpoint代表当前已写入成功的最新位置,恢复时通过对比这两个序列号,计算出缺失的数据范围,然后通过translog重放这部分数据,同时translog会为此保留更长的时间。
1.4 数据模型
1.4.1 PacificA 算法
PacificA是一个读写都满足强一致性的算法,它把数据的一致性与配置的一致性分开,使用额外的一致性组件(Configuration Manager)维护配置的一致性,**在数据的可用副本数少于半数时,仍可以写入新数据并保证强一致性。**该算法涉及的几个术语如下:
-
Replica Group
:一个互为副本的数据集合称为副本组。其中只有一个副本是主数据(Primary),其它为从数据(Secondary)。 -
Configuration
:配置信息中心描述了一个副本组都有哪些副本,Primary是哪个,以及它们位于哪个节点。 -
Configuration Version
:配置信息的版本号,每次发生变更时递增。 -
Serial Number
:代表每个写操作的顺序,每次写操作时递增,简称SN。每个主副本维护自己的递增SN。 -
Prepared List
:写操作的准备序列。存储来自外部请求的列表,将请求按照SN排序,向列表中插入的序列号必须大于列表中最大的SN。每个副本上都有自己的Prepared List
。 -
Committed List
:写操作的提交序列。
整个系统框架主要由两部分组成:存储管理和配置管理。
- 存储管理:负责数据的读取和更新,使用多副本方式保证数据的可靠性和可用性。
- 配置管理:对配置信息进行管理,维护所有配置信息的一致性。
1.4.1.1 数据副本策略
分片副本使用主从模式。多个副本中存在一个主副本Primary和多个从副本Secondary。所有的数据写入操作都进入主副本,当主副本出现故障无法访问时,集群会从其它从副本中选择合适的副本作为新的主副本。
数据写入的流程如下:
- 写请求进入主副本节点,节点为该操作分配SN,使用该SN创建UpdateRequest结构。然后将该它插入自己的prepare list。
- 主副本节点将携带SN的UpdateRequest发往从副本节 点,从节点收到后同样插入prepare list,完成后给主副本节点回复一个ACK。
- 一旦主副本节点收到所有从副本节点的响应,确定该数据已 经被正确写入所有的从副本节点,此时认为可以提交了,将此 UpdateRequest放入committed list,committed list向前移动。
- 主副本节点回复客户端更新成功完成。对每一个Prepare消息,主副本节点向从副本节点发送一个commit通知,告诉它们自己的 committed point位置, 从副本节点收到通知后根据指示移动自己的committed point到相同的位置。
- 因为主副本只有在所有从副本将请求添加进prepared list之后才可以通过移动committed point的方式将该请求插入committed list中,因此主副本的committed list是任何一个从副本的prepared list的子集。例如,从副本prepared list中的SN为[1,2,3,4],主副本committed list中的SN一定不会大于4。
- 同时,因为一个从副本只有在主副本将一个请求添加进committed list后才会把同样的请求添加进committed list中,因此一个从副本上的committed list是主副本上committed list的子集。
1.4.1.2 配置管理
全局的配置管理负责管理所有副本组的配置。节点可以向管理器提出添加/移除副本的请求,每次请求都需要附带当前配置的版本号。只有这个版本号和管理器记录的版本号一致,该请求才会被执行。如果请求成功,则这个新配置会被赋予新的版本号。
1.4.1.3 错误检测
分布式系统经常存在网络分区、节点离线等异常。全局的配置管理器维护权威配置信息,但其他各节点上的配置信息不一定同步,我们必须处理旧的主副本和新的主副本同时存在的情况——旧的主副本可能没有意识到重新分配了一个新的主副本,从而违反了强一致性。
PacificA使用了租约(lease)机制来解决这个问题。 主副本定期向其他从副本获取租约。这个过程中可能产生两种情况:
- 如果主副本节点在一定时间内(lease period)未收到从副本节点的租约回复,则主副本节点认为从副本节点异常,向配置管理器汇报,将该异常的从副本从副本组中移除。同时,它也将自己降级,不再作为主副本节点。
- 如果从副本节点在一定时间内(grace period)未收到主副本节点的租约请求,则认为主副本异常,向配置管理器汇报,将主副本从副本组中移除,同时将自己提升为新的主。如果存在多个从副本,则哪个从副本先执行成功,哪个从副本就被提升为新主。
只要grace period ≥ lease period,则租约机制就可以保证主副本会比任意副本先感知到租约失效,同时任意一个从副本只有在它租约失效时才会争取去当新的主副本。因此保证了新主副本产生之前,旧的主分片已经降级,不会产生两个主副本。从而保证了强一致性。
1.4.2 ES的数据副本模型
- ES中的每个索引都会被拆分为多个分片,并且每个分片都有多个副本。这些副本称为replication group(副本组,与PacificA中的副本组概念一致),并且在删除或添加文档的时候,各个副本必须同步。否则,从不同副本中读取的数据会不一致。我们把保持分片副本之间的同步,以及从中读取的过程称为数据副本模型(data replication model)。
- ES的数据副本模型基于主备模式(primary-backup model),主分片是所有索引操作的入口,它负责验证索引操作是否有效。一旦主分片接受一个索引操作,主分片的副分片也会接受该操作。
1.4.2.1 基本写入模型
- 每个索引操作首先会使用routing参数解析到副本组,通常基于文档ID。一旦确定副本组,就会内部转发该操作到分片组的主分中。 主分片负责验证操作和转发它到其他副分片。ES维护一个可以接收该操作的分片的副本列表。这个列表叫作同步副本列表(in-sync copies),并由Master节点维护。这个分片副本列表中的分片,都会保证已成功处理所有的索引和删除操作,并给用户返回ACK。主分片负责维护不变性(各个副本保持一致),因此必须复制这些操作到这个列表中的每个副本。
写入过程的基本流程如下:
- 请求到达协调节点,协调节点先验证操作,如果有错就拒绝该操作。然后根据当前集群状态,请求被路由到主分片所在节点。
- 该操作在主分片上本地执行。例如,索引、更新或删除文档。这也会验证字段的内容,如果未通过就拒绝操作(例如,字段串的长度超出Lucene定义的长度)。
- 操作成功执行后,转发该操作到当前in-sync 副本组的所有副分片。如果有多个副分片,则会并行转发。
- 一旦所有的副分片成功执行操作并回复主分片,主分片会把请求执行成功的信息返回给协调节点,协调节点返回给客户端。
1.4.2.2 写故障处理
写入期间可能会发生很多错误——硬盘损坏、节点离线或者某些配置错误,这些错误都可能导致无法在副分片上执行某个操作,虽然 这比较少见,但是主分片必须汇报这些错误信息。
- 对于主分片自身错误的情况,它所在的节点会发送一个消息到Master节点。这个索引操作会等待(默认为最多一分钟)Master节点 提升一个副分片为主分片。这个操作会被转发给新的主分片。这通常发生在主分片所在的节点离线的时候。
- 在主分片上执行的操作成功后,该主分片必须处理在副分片上潜在发生的错误。
- 在副分片上执行操作时发生的错误。
- 因为网络阻塞,导致主分片无法转发操作到副分片,或者副分片无法返回结果给主分片。
这些错误都会导致相同的结果:in-sync copies中的一个分片丢失一个即将要向用户确认的操作。为了避免出现不一致,主分片会发送一条消息到Master节点,要求它把有问题的分片从in-sync copies中移除。**一旦Master确认移除了该分片,主分片就会确认这次操作。**注意,Master也会指导另一个节点建立一个新的分片副本,以便把系统恢复成健康状态。
- 操作过程中,主分片被降级的情况。
在转发请求到副分片时,主分片会使用副分片来验证它是否仍是一个活跃的主分片。如果主分片因为网络原因(或很长时间的GC)被隔离,则在它意识到被降级之前可能会继续处理传入的索引操作。来自陈旧的主分片的操作将会被副分片拒绝。当它接收来自副分片的拒绝其请求的响应时,它将会访问一下主节点,然后就会知道自己已被替换。最后将操作路由到新的主分片。
1.4.2.3 基本读取模型
- 通过ID读取是非常轻量级的操作,而一个巨大的复杂的聚合查询请求需要消耗大量CPU和内存资源。主从模式的一个好处是保证所有的分片副本都是一致的(正在执行的操作例外)。因此,单个in-sync中的某个副本也可以提供服务。当一个读请求被协调节点接收,这个节点负责转发它到其他涉及相关分片的节点,并整理响应结果发送给客户端。接收用户请求的这个节点称为协调节点。
基本流程如下:
- 把读请求转发到相关分片。注意,因为大多数搜索都会发送到一个或多个索引,通常需要从多个分片中读取,每个分片都保存这些数据的一部分。
- 从副本组中选择一个相关分片的活跃副本。**它可以是主分片或副分片。**默认情况下,ES会简单地循环遍历这些分片。
- 发送分片级的读请求到被选中的副本。
- 合并结果并给客户端返回响应。注意,针对通过ID查找的get请求,会跳过这个步骤,因为只有一个相关的分片。
1.4.2.4 读故障处理
当分片不能响应一个读请求时,协调节点会从副本组中选择另一个副本,将请求转发给它。没有可用的分片副本会导致重复的错误。 在某些情况下,例如,_search
,ES 倾向于尽早响应,即使只有部分结果,也不等待问题被解决(可以在响应结果的_shards
字段中检查本次结果是完整的还是部分的)。
1.4.3 Allocation IDs
- ES从5.x版本开始引入Allocation IDs的概念,用于主分片选举策略。每个分片有自己的唯一的Allocation ID,同时集群元信息中有一个列表,记录了哪些分片拥有最新数据。
- ES通过在集群中保留多个数据副本的方式提供故障转移功能,当出现网络分区或节点挂掉时,更改操作可能无法在所有副本上完成, 此时我们希望把写失败的副本标记出来。
- ES的数据副本模型会假定其中一个数据副本为权威副本,称之为主分片。所有的索引操作写主分片,完成后,主分片所在节点会负责 把更改转发到活跃的备份副本,称之为副分片。如果当前主分片临时或永久地变为不可用状态,则另一个分片副本将被提升为主分片。因为主分片是权威的数据副本,因此在这个模型中,只把含有最新数据的分片作为主分片是至关重要的。如果将一个旧数据的分片作为主分片,则它将作为最终副本,从而导致这个副本之后的数据将会丢弃。 下面我们介绍如何追踪到那个可以安全地被选为主分片的副本,也称之为同步(in-sync)分片副本。
总结:Allocation IDs的出现,目的是为了保证升级为主分片的副本分片的数据是最新的那一个。
1.4.3.1 安全地分配主分片
- **分片分配就是决定哪个节点应该存储一个活跃分片的过程。**分片决策过程在主节点完成,并记录在集群状态中,该数据结构还包含了 其他元数据,如索引设置及映射。
- 分配决策包含两部分:
- 哪个分片应该分配到哪个节点。
- 哪个分片作为主分片,哪些作为副分片。
- 主节点广播集群状态到集群的所有节点。这样每个节点都有了集群状态,它们就可以实现对请求的智能路由。因为每个节点都知道主副分片分配到了哪里。
- 每个节点都会通过检查集群状态来判断某个分片是否可用。
- 如果一个分片被指定为主分片,则这个节点只需要加载本地分片副本,使之可以用于搜索即可。
- 如果一个分片被分配为副分片,则节点首先需要从主分片所在节点复制差异数据。
- 当集群中可用副分片不足时
index.number_of_replicas
,主节点也可以将副分片分配到不含任何此分片副本的节点,从而指示这些节点创建主分片的完整副本。
- Allocation IDs由主节点在分片分配时指定。主节点负责追踪包含最新数据副本的子集。这些副本集合称为同步分片标识(in-sync allocation IDs),存储于集群状态中。集群状态存在于集群的主节点和所有数据节点。 它确保集群中有共同的理解,即哪些分片副本被认为是同步的(in-sync),隐式地将那些不在同步集合中的分片副本标记为陈旧(stale)。也就是说:Allocation IDs存储在shard级元信息中,每个shard 都有自己唯一的Allocation ID,同时集群级元信息中记录了一个被认为是最新shard的Allocation ID集 合 , 这个集合称为in-sync allocation IDs。
- 当分配主分片时,主节点检查磁盘中存储的 Allocation ID 是否会在集群状态的in-sync allocations IDs集合中出现,只有在这个集合中找到了,此分片才有可能被选为主分片。如果活跃副本中的主分片挂了,则in-sync集合中的活跃分片会被提升为主分片,确保集群的写入可用性不变。
1.4.3.2 将分配标记为陈旧
- 处理写请求过程中,当网络产生分区、节点故障或者部分节点未启动,主分片本地执行完写操作,转发到副分片时,转发操作可能在一个或多个副分片上没能执行成功,这意味着主分片中含有一些没有传播到所有分片的数据,如果这些副分片仍然被认为是同步的,那么即使它们遗漏了一些变化,它们也可能稍后被选为主分片,结果丢失数据。
- 解决这种问题有两种方法:
- 让写请求失败,已经写的做回滚处理。
- 确保差异的(divergent)分片不再被视为同步。
- ES在这种情况下选择了写入可用性:主分片所在节点命令主节点将差异分片的Allocation IDs从同步集合(in-sync set)中删除。然 后,主分片所在节点等待主节点删除成功的确认消息,这个确认消息意味着集群一致层(consensus layer)已成功更新,之后才向客户端确认写请求。这样确保只有包含了所有已确认写入的分片副本才会被主节点选为主分片。
1.4.3.3 不会丢失全部
- 发生严重灾难时,集群中可能会出现只有陈旧副本可用的情况。ES不会把这些分片自动分配为主分片,集群将持续保持Red状态。
- 如果所有in-sync副本都消失了,则集群仍有可能使用陈旧副本进行恢复,但这需要管理员手工干预。
-
reroute API
提供了一个子命令allocate_stale_primary,用于将 一个陈旧的分片分配为主分片。使用此命令意味着丢失给定分片副本中缺少的数据。如果同步分片副本只是暂时不可用,则使用此命令意味着会丢失同步分片副本中最近更新的数据。 - 应该把它看作使集群至少运行一些数据的最后一种措施。
- 在所有分片副本都不存在的情况下,还可以强制ES使用空分片副本分配主分片,这意味着丢失与该分片相关联的所有先前数据。
1.4.4 Sequence IDs
- ES从6.0版本开始引入了Sequence IDs概念,使用唯一的ID来标记每个写操作。通过这个ID我们有了索引操作的总排序。
- 写操作先到达主分片,主分片写完后转发到副分片,在转发到副分片前,增加一个计数器,为每个操作分配一个序列号是很简单的。但是,由于节点离线随时可能发生,例如,网络分区等,主分片可能被其他副分片取代,仅仅由主分片分配一个序列号无法保证全局 唯一性和单调性。因此,我们把当前主分片做一个标记,放到每个操作中,这就是Primary Terms。这样,来自旧的主分片的迟到的操作就可以被检测到然后拒绝。
1.4.4.1 Primary Terms和Sequence Numbers
- 添加Primary Terms的目的:
- 能够区分新旧两种主分片。
- 整个集群对新旧主分片要达成一致。
- Primary Terms由主节点分配,当一个主分片被提升时,Primary Terms递增。然后持久化到集群状态中,从而表示集群主分片所处的一个版本。有了 Primary Terms,操作历史中的任何冲突都可以通过查看操作的 Primary Terms来解决。新的Terms优先于旧的Terms,拒绝过时的操作,避免混乱的情况。
- 一旦我们有了Primary Terms的保护,就可以添加一个简单的计数器,给每个操作分配一个Sequence Numbers(序列号)。Sequence Numbers使我们能够理解发生在主分片节点上的索引操作的特定顺序。例如这样一个response:
{
"_index": "foo",
"_type": "doc",
"_id": "M1DVXDASDWQ52132ASDA",
"_version": 1,
"_shard": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 19,
"_primary_term": 1
}
为了实现将操作排序,我们比较两个操作c1和c2时,如果c1 < c2,那就意味着:
c1.seq# < c2.seq# 或者 ( c1.seq# == c2.seq# and c1.term# < c2.term# )
总结:
- Primary Terms由主节点分配给每个主分片,每次主分片发生变化时递增 。
- Sequence Numbers标记发生在某个分片上的写操作。由主分片分配,只对写操作分配。注:当主分片离线,副分片被提升为新的主分片时,对于后续写操作,序列号接着继续递增,不会被重置。
1.4.4.2 本地及全局检查点
当同时为每秒成百上千的事件做索引时,比较数百万个操作的历史是不切实际的。存储成本非常昂贵,直接进行比较的计算工作量太大。为了解决这个问题,ES维护了一个名为**全局检查点(global checkpoint)**的安全标记。
- 全局检查点是所有活跃分片历史都已对齐的序列号,换句话说,所有低于全局检查点的操作都保证已被所有活跃的分片处理完毕。
- 当主分片失效时,我们只需要比较新主分片与其他副分片之间的最后一个全局检查点之后的操作即可。
- 当旧主分片恢复时,我们使用它知道的全局检查点,与新主分片进行比较。这样,我们只有小部分操作需要比较,不用比较全部。
- 主分片负责推进全局检查点,它通过跟踪在副分片上完成的操作来实现。一旦它检测到所有副分片已经超出给定序列号,它将相应地更新全局检查点。
- 副分片不会跟踪所有操作,而是维护一个类似全局检查点局部变量,称为**本地检查点。**本地检查点也是一个序列号,所有序列号低于它的操作都已在该分片上处理完毕。当副分片确认一个写操作到主分片节点时,它们也会更新本地检查点。
- 使用本地检查点,主分片节点能够更新全局检查点,然后在下一次索引操作时将其发送到所有分片副本。全局检查点和本地检查点在内存中维护,但也会保存在每个Lucene提交的元数据中。
下面演示在写入过程中,全局/本地检查点的更新情况。
- 某索引有1个主分片,2个副分片,初始状态没有数据,全局检查点和本地检查点都在0的位置:
- 主分片写入一条数据成功后,本地检查点向前推进。
- 主分片将写请求转发到副分片,副分片本地处理成功后,将本地检查点向前推进。
- 主分片收到所有副分片都处理成功的消息,根据汇报的各副本上的本地检查点更新全局检查点。
- 在下一次索引操作时,主分片节点将全局检查点发送给所有分片副本。
1.4.4.3 用于快速恢复
- 当ES恢复一个分片时,需要保证恢复之后与主分片一致。对于冷 数据来说,synced flush可以快速验证副分片与主分片是否相同,但 对于热数据来说,恢复过程需要从主分片复制整个Lucene分段,如果分段很大,则是非常耗时的操作。
- 通过对比全局检查点和本地检查点,可以计算出需要同步的数据量,大大的缩小了恢复数据的数量和时间。
1.4.5 _version
- 每个文档都有一个版本号(
_version
),当文档被修改时版本号递增。ES 使用这个_version
来确保变更以正确顺序执行。如果旧版本的文档在新版本之后到达,则它可以被简单地忽略。 - 版本号由主分片生成,在将请求转发给副本片时将携带此版本号。
- 版本号的另一个作用是实现乐观锁,如同其他数据库的乐观锁一 样。我们在写请求中指定文档的版本号,如果文档的当前版本与请求 中指定的版本号不同,则请求会失败。
1.5 写流程
在ES中,写入单个文档的请求称为Index请求,批量写入的请求称为Bulk请求。**写单个和多个文档使用相同的处理逻辑,请求被统一封装为BulkRequest。**在ES中,对文档的操作有下面几种类型:
enum OpType {
INDEX(0),
CREATE(1),
UPDATE(2),
DELETE(3);
}
- INDEX:向索引中
put
一个文档的操作称为索引一个文档。 此处索引为动词。 - CREATE:put 请求可以通过 op_type 参数设置操作类型为 create,在这种操作下,如果文档已存在,则请求将失败。
- UPDATE:默认情况下,
put
一个文档时,如果文档已存在, 则更新它。 - DELETE:删除文档。
1.5.1 Index/Bulk 基本流程
写单个文档的流程如下图所示:
- 客户端向 NODE1 发送写请求。
- NODE1 使用文档ID来确定文档属于分片0,通过集群状态中的内容路由信息表信息获知分片0的主分片位于NODE3,因此请求被转发到NODE3上。
- NODE3 上的主分片执行写操作。如果写入成功,则它将请求并行转发到 NODE1 和 NODE2 的副分片上,等待返回结果。当所有的副分片都报告成功后,NODE3 将向协调节点报告成功,协调节点再向客户端报告成功。
在5.x版本以后,写一致性策略由wait_for_active_shards
参数控制。默认情况下,在执行写入操作之前,只要主分片处于活跃状态就可以执行写入操作。wait_for_active_shards
用于指定在开始执行写入操作前需要等待的活跃分片数量。默认该值为1,即主分片活跃既可以开始进行写操作。(疑问:ES的写操作时强一致性还是弱一致性?)
1.5.2 Index/Bulk 详细流程
以不同角色节点执行的任务整理流程图如下:
1.5.2.1 协调节点流程
- 参数检查:如果有问题,就直接拒绝。
- 处理pipeline请求
- 如果Index或Bulk请求中指定了pipeline参数,则先使用相应的pipeline进行处理。如果本节点不具备预处理资格,则将请求随机转 发到其他具备预处理资格的节点。
- 自动创建索引
- 如果配置为允许自动创建索引(默认允许),则计算请求中涉及 的索引,可能有多个,其中有哪些索引是不存在的,然后创建它。如 果部分索引创建失败,则涉及创建失败索引的请求被标记为失败。其他索引正常执行写流程。
- 对请求的预先处理
- 对请求的预先处理只是检查参数、 自动生成id、处理routing等。
- 由于上一步可能有创建索引操作,所以在此先获取最新集群状态信息。然后遍历所有请求,从集群状态中获取对应索引的元信息,检 查mapping、routing、id等信息。如果id不存在,则生成一个UUID作为文档id。
- 检查集群状态
- 协调节点在开始处理时会先检测集群状态,若集群异常则取消写入。
- 内容路由,构建基于shard的请求
- 将用户的 bulkRequest 重新组织为基于 shard 的请求列表。基于shard的请求结构如下:
Map<ShardId, List<BulkItemRequest>> requestByShard = new HashMap<>();
根据路由算法计算某文档属于哪个分片。遍历所有的用户请求, 重新封装后添加到上述map结构。也就是说,ES会在这里合并请求,将写往同一个主分片的请求合并为1个。
- 路由算法
- 路由算法就是根据routing和文档id计算目标shardId的过程。 一般情况下,路由计算方式为下面的公式:
shard_num = hash(_routing) % num_primary_shards
ES使用随机id和Hash算法来确保**文档均匀地分配给分片。**当使用自定义id或routing时, id或routing值可能不够随机,造成数据倾斜,部分分片过大 。 在这种情况下,可以使用index.routing_partition_size
配置来减少倾斜的风险。routing_partition_size
越大,数据的分布越均匀。 在设置了index.routing_partition_size
的情况下,计算公式为:
shard_num = (hash(_routing) + hash(_id) % routing_partition_size) % num_primary_shards
注: index.routing_partition_size取值应具有大于1且小于index.number_of_shards的值。
总结:路由算法的目的:要均匀的分配数据到各个分片上,防止某个分片存储的数据太大,导致数据倾斜。
- 转发请求并等待响应
- 主要是根据集群状态中的内容路由表确定主分片所在节点,转发请求并等待响应。
- 如果某个shard的响应中部分doc写失败了,则将异常信息填充到Response中,整体请求做成功处理。待收到所有响应后(无论成功还是失败的),回复给客户端。
- 转发前先获取最新集群状态,根据集群状态中的内容路由表找到目的shard所在的主分片,如果主分片不在本机,则转发到相应的节 点,否则在本地执行。
1.5.2.2 主分片节点流程
主分片所在节点负责在本地写主分片,写成功后,转发写副本片请求,等待响应,回复协调节点。
- 检查请求: 主分片所在节点收到协调节点发来的请求后也是先做了校验工作,主要检测要写的是否是主分片,AllocationId是否符合预期,索引是否处于关闭状态等。
- 是否延迟执行: 如果需要延迟则放入队列,否则继续下面的流程。
- 判断主分片是否已经发生迁移:如果已经发生迁移,则转发请求到迁移的节点。
- 检测写一致性: 在开始写之前,检测本次写操作涉及的shard,活跃shard数量是否足够,不足则不执行写入。默认为1,只要主分片可用就执行写入。
- 写Lucene和事务日志:
- 在写入Lucene之前,先生成Sequence Number和Version。SN每次递增1,Version根据当前doc的最大版本加1。
- 索引过程为先写Lucene,后写translog。 因为Lucene写入时对数据有检查,写操作可能会失败。如果先写translog,写入Lucene时失败,则还需要对translog进行回滚处理。
- flush translog: 根据配置的translog flush策略进行刷盘控制,定时或立即刷盘。
- 写副分片
- 现在已经为要写的副本shard准备了一个列表,循环处理每个shard,跳过unassigned状态的shard,向目标节点发送请求,等待响应。这个过程是异步并行的。
- 转发请求时会将SequenceID、PrimaryTerm、GlobalCheckPoint、version等传递给副分片。
- 在等待Response的过程中,本节点发出了多少个Request,就要等待多少个Response。无论这些Response是成功的还是失败的,直 到超时。
- 收集到全部的Response后,执行
finish()
。给协调节点返回消息,告知其哪些成功、哪些失败了。
- 处理副分片写失败情况
- 主分片所在节点将发送一个shardFailed请求给Master。然后Master会更新集群状态,在新的集群状态中,这个shard将:
- 从in_sync_allocations列表中删除;
- 在routing_table的shard列表中将state由
STARTED
更改为UNASSIGNED
; - 添加到routingNodes的unassignedShards列表。
1.5.2.3 副分片节点流程
- 执行与主分片基本相同的写doc过程,写完毕后回复主分片节点。
1.5.3 I/O 异常处理
- 在一个shard上执行的一些操作可能会产生I/O异常之类的情况。一个shard上的CRUD等操作在ES里由一个Engine对象封装,在Engine处理过程中,部分操作产生的部分异常ES会认为有必要关闭此Engine,上报Master。
- 收到节点的SHARD_FAILED_ACTION_NAME消息后,Master通过reroute将失败的shard通过reroute迁移到新的节点,并更新集群状态。
异常流程总结:
- 如果请求在协调节点的路由阶段失败,则会等待集群状态更新,拿到更新后,进行重试,如果再次失败,则仍旧等集群状态更新,直到超时1分钟为止。超时后仍失败则进行整体请求失败处理。
- 在主分片写入过程中,写入是阻塞的。只有写入成功,才会发起写副本请求。如果主shard写失败,则整个请求被认为处理失败。 如果有部分副本写失败,则整个请求被认为处理成功。
- 无论主分片还是副分片,当写一个doc失败时,集群不会重试,而是关闭本地shard,然后向Master汇报。
1.5.4 系统特性
ES本身也是一个分布式存储系统,如同其他分布式系统一样,我们经常关注的一些特性如下。
- **数据可靠性:**通过分片副本和事务日志机制保障数据安全。
- **服务可用性:**在可用性和一致性的取舍方面,默认情况下ES更倾向于可用性,只要主分片可用即可执行写入操作。
- **一致性:**弱一致性。只要主分片写成功,数据就可能被读取。因此读取操作在主分片和副分片上可能会得到不同结果。
- 原子性:索引的读写、别名更新是原子操作,不会出现中间状态。但bulk不是原子操作,不能用来实现事务。
- **扩展性:**主副分片都可以承担读请求,分担系统负载。
1.6 GET流程
搜索和读取文档都属于读操作,可以从主分片或副分片中读取数据。流程如下:
- 客户端向NODE1发送读请求。
- NODE1使用文档ID来确定文档属于分片0,通过集群状态中的内容路由表信息获知分片0有三个副本数据,位于所有的三个节点中,此时它可以将请求发送到任意节点,这里它将请求转发到 NODE2。
- NODE2将文档返回给NODE1,NODE1将文档返回给客户端。
NODE1作为协调节点,会将客户端请求轮询发送到集群的所有副本来实现负载均衡。在读取时,文档可能已经存在于主分片上,但还没有复制到副分片。在这种情况下,读请求命中副分片时可能会报告文档不存在,但是命中主分片可能成功返回文档。一旦请求成功返回给客户端,则意味着文档在主分片和副分片都是可用的。
1.6.1 GET详细分析
GET/MGET
流程涉及两个节点:协调节点和数据节点,流程如下:
1.6.1.1 协调节点
- 内容路由。 获取集群状态、节点列表等信息。计算出目标shardId,也就是文档落在了哪个分片上。
- 转发请求。 如果目标shardId在本地,则直接读取数据。如果不是,则目标节点转发请求。
- 如果转发到目标节点的请求失败,会进行重试。(重试时会选择下一个目标节点)
1.6.1.2 数据节点
- 读取及过滤。 会先检查是否需要refresh,然后再读取数据。
1.7 Search 流程
GET操作只能对单个文档进行处理,由_index、_type
和_id
来确定唯一文档。但搜索需要一种更复杂的模型,因为不知道查询会命中哪些文档。 找到匹配文档仅仅完成了搜索流程的一半,因为多分片中的结果必须组合成单个排序列表。集群的任意节点都可以接收搜索请求,接收客户端请求的节点称为协调节点。在协调节点,搜索任务被执行成一个两阶段过程,即query then fetch。真正执行搜索任务的节点称为数据节点。 需要两个阶段才能完成搜索的原因是,在查询的时候不知道文档位于哪个分片,因此索引的所有分片(某个副本)都要参与搜索,然后协调节点将结果合并,再根据文档ID获取文档内容。
- query:检索出匹配条件的相关文档。
- fetch:去文档所在的位置获取文档内容。
1.7.1索引和搜索
ES中的数据可以分为两类:精确值和全文。
- 精确值,比如日期和用户id、IP地址等。
- 全文,指文本内容,比如一条日志,或者邮件的内容。 这两种类型的数据在查询时是不同的:对精确值的比较是二进制的,查询要么匹配,要么不匹配;全文内容的查询只能找到结果是看起来像你要查询的东西,因此把查询结果按相似度排序,评分越高,相似度越大。ES尽可能的返回可能命中的数据。
1.7.2 数据建立索引和执行搜索的原理如下图
如果是全文数据,则对文本内容进行分析,这项工作在 ES 中由分析器实现。分析器实现如下功能:
- 字符过滤器。主要是对字符串进行预处理,例如,去掉HTML, 将&转换成and等。
- 分词器(Tokenizer)。将字符串分割为单个词条,例如,根据 空格和标点符号分割,输出的词条称为词元(Token)。
- Token过滤器。根据停止词(Stop word)删除词元,例如,and、the等无用词,或者根据同义词表增加词条,例如,jump和 leap。
- 语言处理。对上一步得到的Token做一些和语言相关的处理,例如,转为小写,以及将单词转换为词根的形式。语言处理组件输出的 结果称为词(Term)。 分析完毕后,将分析器输出的词(Term)传递给索引组件,生成倒排和正排索引,再存储到文件系统中。
1.7.3 分布式搜索过程
1.7.3.1 Query过程
在初始查询阶段,查询会广播到索引中每一个分片副本(主分片或副分片)。每个分片在本地执行搜索并构建一个匹配文档的优先队列。 优先队列是一个存有topN匹配文档的有序列表。优先队列大小为 分页参数from + size。
- 客户端发送search请求到NODE3。
- NODE3将查询请求转发到索引的每个主分片或副分片中。
- 每个分片在本地执行查询,并使用本地的Term/Document Frequency信息进行打分,添加结果到大小为from + size的本地有序优先队列中。
- 每个分片返回各自优先队列中所有文档的ID和排序值给协调节点,协调节点合并这些值到自己的优先队列中,产生一个全局排序后的列表。
协调节点广播查询请求到所有相关分片时,可以是主分片或副分片,协调节点将在之后的请求中轮询所有的分片副本来分摊负载。
查询阶段并不会对搜索请求的内容进行解析,无论搜索什么内容,只看本次搜索需要命中哪些shard,然后针对每个特定shard选择 一个副本,转发搜索请求。
1.7.3.2 Fetch过程
- 协调节点向相关NODE发送GET请求。
- 分片所在节点向协调节点返回数据。
- 协调节点等待所有文档被取得,然后返回给客户端。
每次分页的请求都是一次重新搜索的过程,而不是从第一次搜索 的结果中获取。看上去不太符合常规的做法,事实上互联网的搜索引擎都是重新执行了搜索过程:人们基本只看前几页,很少深度分页;重新执行一次搜索很快;如果缓存第一次搜索结果等待翻页命中,则这种缓存的代价较大,意义却不大,因此不如重新执行一次搜索。
搜索需要遍历分片所有的Lucene分段,因此合并Lucene分段对搜索性能有好处。
1.8 索引恢复
索引恢复(indices.recovery)是ES数据恢复过程。待恢复的数据是客户端写入成功,但未执行刷盘(flush)的Lucene分段。例如,当节点异常重启时,写入磁盘的数据先到文件系统的缓冲,未必来得及刷盘,如果不通过某种方式将未刷盘的数据找回来,则会丢失一些数据,这是保持数据完整性的体现;另一方面,由于写入操作在 多个分片副本上没有来得及全部执行,副分片需要同步成和主分片完全一致,这是数据副本一致性的体现。根据数据分片性质,索引恢复过程可分为主分片恢复流程和副分片恢复流程。
- 主分片从translog中自我恢复,尚未执行flush到磁盘的Lucene分段可以从translog中重建;
- 副分片需要从主分片中拉取Lucene分段和translog进行恢复。 但是有机会跳过拉取Lucene分段的过程。
1.8.1 主分片恢复流程
- INIT 阶段
一个分片的恢复流程中,从开始执行恢复的那一刻起,被标记为INIT
阶段。**注:在判断此分片属于哪种恢复类型之前就被设置为INIT
阶段。**开始阶段主要是一些验证工作,例如,校验当前分片是否为主分片,分片状态是否异常 等。
- INDEX阶段
本阶段从Lucene读取最后一次提交的分段信息,获取其中的版本号,更新当前索引版本。
- VERIFY_INDEX阶段
本阶段的作用是验证当前分片是否损坏。
- TRANSLOG阶段
一个Lucene索引由许多分段组成,每次搜索时遍历所有分段。内部维护了一个称为提交点的信息,其描述了当前Lucene索引都包括哪些分段,这些分段已经被fsync系统调用,从操作系统的cache刷入磁盘。每次提交操作都会将分段刷入磁盘实现持久化。
本阶段需要重放事务日志中尚未刷入磁盘的信息,因此,根据最后一次提交的信息做快照,来确定事务日志中哪些数据需要重放。重放完毕后将新生成的Lucene数据刷入磁盘。事务日志重放完毕后,进入下一阶段。
- FINALIZE阶段
本阶段执行刷新(refresh)操作,将缓冲的数据写入文件,但不刷盘,数据在操作系统的cache中。至此,主分片恢复完毕,对恢复结果进行处理。
- 如果恢复成功, 向Master发送恢复成功的请求。
- 如果恢复失败,向Master发送恢复失败的请求。
1.8.2 副分片恢复流程
副分片恢复的核心思想是从主分片拉取Lucene分段和translog进行恢复。按数据传递的方向,主分片节点称为Source,副分片节点称为Target。
拉取主分片的translog:在副分片恢复期间允许新的写操作,从复制Lucene分段的那一刻开始,所恢复的副分片数据不包括新增的内容,而这些内容存在于主分片的translog中,因此副分片需要从主分片节点拉取translog进行重放,以获取新增内容。
- 阶段1:在主分片所在节点,获取translog保留锁,从获取保留锁开始,会保留translog不受其刷盘清空的影响。然后调用Lucene接口把shard做快照,快照含有shard中已经刷到磁盘的文件引用,把这些shard数据复制到副本节点。
- 阶段2:对translog做快照,这个快照里包含从phase1开始,到执行translog快照期间的新增索引。将这些translog发送到副分片所在节点进行重放。
由于阶段1需要通过网络复制大量数据,过程非常漫长,在ES6.x中,有两个机会可以跳过阶段1:
- 如果可以基于恢复请求中的SequenceNumber进行恢复, 则跳过阶段1。
- 如果主副两分片有相同的syncid且doc数相同,则跳过阶段1。
1.8.2.1 synced flush机制
synced flush本质上是一次普通的flush操作,只是在Lucene的commit过程中多写了一个syncid。
- 为了解决副分片恢复过程第一阶段时间太漫长而引入了synced flush,默认情况下5分钟没有写入操作的索引被标记为inactive,执行synced flush,生成一个唯一的syncid,写入分片的所有副本中。 这个syncid是分片级,意味着拥有相同syncid的分片具有相同的 Lucene索引。
synced flush期间不能有新写入的内容,**如果syncflush执行期间收到写请求,则ES选择了写入可用性:让synced flush失败,让写操作成功。**在没有执行flush的情况下已有syncid不会失效。
在某个分片上执行普通flush操作会删除已有syncid。因此,synced flush操作是一个不可靠操作,只适用于冷索引。
1.8.2.2 副分片节点处理过程
副分片恢复的VERIFY_INDEX、TRANSLOG、FINALIZE三个阶段由主分片节点发送的RPC调用触发,如下图所示。
- INIT 阶段
与主分片恢复的INIT阶段类似,恢复任务开始时被设置为INIT阶段。构建准备发往主分片的StartRecoveryRequest请求,请求中包括将本次要恢复的shard相关信息,如shardid、metadataSnapshot等。metadataSnapshot中包含syncid。
- INDEX 阶段
INDEX阶段负责将主分片的Lucene数据复制到副分片节点。这个阶段可能会耗时很长
- VERIFY_INDEX 阶段
副分片的索引验证过程与主分片相同,是否进行验证取决于配置。默认为不执行索引验证。
- TRANSLOG 阶段
将主分片的translog数据复制到副分片节点进行重放。
- FINALIZE 阶段
主分片节点执行完 phase2,调用finalizeRecovery,向副分片节点发送请求。副分片节点对此请求的处理为先更新全局检查点,然后执行 与主分片相同的清理操作。
- DONE 阶段
副分片节点等待INDEX阶段执行完成后,进入DONE阶段。与主分片的postRecovery处理过程相同,包括对恢复成功或失败的处理,也 和主分片的处理过程相同。
1.8.2.3 主分片节点处理过程
副分片恢复的INDEX阶段向主分片节点发送数据的恢复请求,主分片对此请求的处理过程是副分片恢复的核心流程。核心流程如下图: