一、基本概念和理念
1、索引结构
ES是面向文档的。各种文本内容以文档的形式存储到ES中,一般使用JSON作为文档的序列化格式。
在存储结构上,由_index、_type和_id唯一标识一个文档。
_index指向一个或多个物理分片的逻辑命令空间。
_type类型用于区分同一个集合中的不同细分,在不同的细分中,数据的整体模式是相同或相似的,不适合完全不同的类型数据。
_id文档标记符由系统自动生成或使用者提供。
不应该将_index理解成RDBMS中的数据库,_type理解成表。在ES 6.x版本中,一个索引已经只允许存在一个_type,在未来的版本还会移除_type概念。
2、分片
在分布式系统中,单机无法存储规模巨大的数据,都需要依靠集群处理和存储这些数据,一般通过加机器的方式来提高系统的水平拓展能力。这就需要将数据切分成N个小块,然后把这些小块均匀分布到集群中的所有机器,然后通过某种路由策略找到某个数据块所在的位置。这就是“分片”。
将数据分片能提高水平拓展能力,但除了拓展能力,还要考虑系统的可用性,即集群中的某个节点挂了,不会影响整个集群的运行。所以在分布式存储中,会把数据复制成多个副本,放置到不同的机器上。不过引入了副本的概念,也会带来一致性的问题:部分副本写成功,部分写失败。
ES将分片分为主分片和副分片。写过程先写主分片,成功后再写副分片,恢复阶段以主分片为准。
一个ES的分片就是一个luncene的索引,它本身就是一个完整的搜索引擎,可以独立执行建立索引和搜索任务。每个luncene索引够若干个分段组成,每个分段就是一个倒排索引,ES每次“refresh”都会产生一个新的分段。在每个分段内部,文档的不同字段(Field)被单独建立索引。每个字段的值由若干个词(Term)组成。
ES index->ES shard(luncene index)->luncene segment->luncene field->lucene term。
搜索1个有着50个分片的索引和搜索50个只有单分片的索引,效果相同。
3、数据更新
段在生成之后就是不可变的,这样设计有许多好处:对文件的访问不需要加锁,读取索引时可以被文件系统缓存等。
段不可变的话,更新和删除是怎么做的?
更新和删除等操作是将数据标记为删除,记录到单独的位置,这种方式称为标记删除。因此删除部分数据不会释放空间。
4、近实时搜索
ES执行写操作,会将数据在内存中缓存,到达一定的时间间隔—默认1秒或者一定数据量,才会把这些数据写入磁盘,每次写入硬盘的这批数据就是一个分段(Segment)。
一般情况下,通过操作系统write接口写到磁盘的数据先到达系统缓存,write函数返回成功时,数据未必刷到磁盘。不过数据进入系统缓存的时候,文件已经能像其他文件一样被打开和读取。
ES利用了这种特性实现了近实时搜索。每秒产生一个新分段,将新分段写入文件系统缓存。
不过这种方式,存在丢失数据的风险。所以ES引入了translo机制,每次对ES进行操作时都会记录事务日志,当ES启动的时候,重放translog中所有在最后一次提交后发生的变更操作。
5、段合并
ES每秒都会产生一个段,但是分段数量太多会带来性能问题,每个段都会消耗文件句柄、内存;每个搜索请求都需要轮流检查每个段,然后把结果合并;段越多,搜索越慢。所以会通过一定的策略,将小段合并为大段。合并的过程中,标记为删除的数据不会被写入新的分段中,即标记删除的数据,只有到了段合并的时候,才会释放磁盘空间。
二、集群节点角色
主节点
主节点负责集群层面的相关操作,管理集群变更。
通过配置node.master:true,默认为true使节点具有被选举为主节点的资格。主节点是唯一的,从所有具有被选举资格的节点中选举出来。
为避免网络分区时出现多主的情况,配置discovery.zen.minimum_master_nodes原则上最小值应该为(master_eligible_nodes/2)+1
数据节点
负责保存数据、执行数据相关操作:CRUD、搜索、聚合等。通过配置node.data:true,默认为true。
预处理节点
预处理操作允许在索引文档前,即写入文档之前,通过事先定义好的一系列processors和pipeline,对数据进行转换、富化。通过配置node.ingest:true,默认为true。
协调节点
客户端请求可以发送到集群的任意节点,每个节点都知道任意文档所在的位置,然后转发这些请求,收集数据并返回给客户端,处理客户端请求的节点称为协调节点。
三、集群健康状态
Green,所有的主分片和副分片都正常运行。
Yellow,所有的主分片都正常运行,但不是所有的副分片都正常运行。
Red,有主分片没能正常运行。
四、集群状态
集群状态元数据是全局信息,包括内容路由信息、配置信息等,其中最重要的是内容路由信息,描述了“哪个分片位于哪个节点”。
集群状态由主节点负责维护,如果主节点从数据节点接收更新,则将这些更新广播到集群的其他节点,让每个节点上的集群状态保持最新。
五、集群扩容
当扩容集群、添加节点时,分片会均衡地分配到集群的各个节点,从而对索引和搜索过程进行负载均衡,这些都是系统自动完成。
分片分配过程中出了让节点间均匀存储,还要保证不把主副分片分配到一个节点上,避免单个节点故障引起数据丢失。
六、集群启动流程
选举主节点 -> 选举集群元信息 -> allocation过程 -> index recovery
1、选举主节点
算法
ES的选主算法是基于Bully算法的改进,Bully算法的主要思路是对节点ID排序,取ID值最大的节点最为Master,每个节点都运行这个流程。这种做法选举出来的节点不一定持有最新的元数据信息,所以在选举出Master之后,还需要从其他机器上把最新的元数据信息同步过来。
基于节点ID排序的简单选举算法有三个附加条件:
- 参选人数需要过半,到达quorum(过半数节点)后就选出临时的Master。因为可能因为网络原因,每个节点观察到的节点列表都不一样,选出来的结果不一致,所以通过这种方式选举出来的只是临时Master。
- 当某个节点成为临时Master,并且加入它的节点数过半,则可以确认它的Master身份,成为正式的Master。
- Master检测到节点离开时,必须判断当前节点数是否过半。如果达不到quorum,则放弃Master身份,重新加入集群,触发新的Master选举。目的是防止发生网络分区,然后原Master节点又是在少数的那一边,就可能产生双主——脑裂。
重要配置
ES集群并不知道自己一共有多少个节点,quorum(过半数节点)值从配置读取的discovery.zen.minimum_master_nodes——最小主节点数,这是防止脑裂、防止数据丢失的极其重要的参数。
该配置除了用于“多数”,还用于多处重要的判断,至少包含以下时机:
- 触发选主:进入选主的流程之前,参选的节点数需要达到法定人数。
- 决定Master:选出临时的Master之后,这个临时Master需要判断加入它的节点到达法定人数,才确认选主成功。
- gateway选举元信息:向有Master资格的节点发起请求,获取元数据,获取的相应数量必须达到法定人数,也就是参与元信息选举的节点数。
- Master发布集群状态:发布成功数量不许达到法定人数。
流程
先选举临时Master,如果本节点当选,则等待确立Master,如果其他节点当选,则尝试加入集群,然后启动节点失效探测器。
选举临时Master
- “ping”所有节点,获取节点列表,ping结果不包含本节点,把本节点单独添加到列表中。
- 构建两个列表:存储集群当前活跃Master的列表——activeMasters、存储Master候选者的列表——masterCandidates。
- 优先从activeMasters选择合适的节点作为Master,如果activeMasters为空,再从masterCandidates选择。
投票与得票
发送投票就是向目标节点发送加入集群的请求。得票就是申请加入该节点的请求数量。
确立Master
- 等待足够多的具备Master资格的节点加入本节点,完成选举。
- 超时(默认30秒)后没有满足投票法定人数,则选举失败,需要重新进行选举。
- 成功选举后,发布新的集群状态。
加入集群
- 不再接受其他节点的加入集群请求。
- 向Master节点发送加入集群请求,并等待回复。超时时间默认为1分钟,如果遇到异常默认超时3次。
- 最终当选的Master会发布集群状态,才确认其他节点的加入集群请求。其他非Master节点收到集群状态,会检查集群状态中的Master节点,如果为空或者不是选择的节点,则重新选举。
节点失效检测
选举成功后,节点需要开启失效检测器:
- 在Master节点,启动NodesFaultDeletection,简称NodeFD。定期探测加入集群的节点是否活跃。
- 在非Master节点,启动MasterFaultDelection,简称MasterFD。定期探测Master节点是否活跃。
两种探测器都是通过定期(默认1秒)发送ping请求探测节点是否正常的,当失败一定次数(默认为3次),或者收到来自底层连接模块的节点离线通知时,处理节点离开事件。
2、选举集群元信息
从上一小节,我们能知道,选举出来的主节点元数据信息不一定是最新的,所以当Master被选举出来后,第一件事情就是让所有节点把各自存储的元信息发给它,进行元信息选举,选举的过程中,不接受新节点的加入请求。
主节点根据版本号确定最新的元信息,然后再把这个信息广播出去,让所有节点的元信息都变成最新的。
为了集群的一致性,参与选举的元信息数量需要过半,主节点发布集群状态成功的规则也是等待发布成功的节点数过半。
元数据信息只包含两个级别:集群级和索引级。不包含哪个分片存在于哪个节点这种信息。
3、allocation过程
集群级和索引级的元数据选举完成后,就开始选举分片级元信息,构建内容路由表,这是在allocation模块完成的。
在初始阶段,所有的分片都处于未分配状态。ES通过分配过程决定哪个分片位于哪个节点上,构建路由表信息。
选主分片
假设现在要选分片A的主分片,主节点会询问集群中的所有节点,让大家点把A分片的元信息发给它,主节点收到所有请求的返回后,根据一定策略,从中选择一个作为主分片。
这种方式效率比较低,询问量=分片数*节点数,所以分片的数量不适合太多。
如何选择合适的分片作为主分片呢?
ES会给每个分片都设置一个UUID,然后在集群的元信息中记录哪些分片是最新的。选主分片的时候,就选择汇报中存在于“最新分片列表”的分片。
选副分片
在选主分片的时候,已经收集了分片的所有副本信息。如果汇总信息中不存在,则分配一个全新副本——例如副本数目前是3,但是汇总中只拿到2个。
创建全新副本的操作不是马上执行的,而是根据延迟配置项:index.unassigned.node_left.delayed_timeout。
4、index recovery
分片分配成功后进入recovery流程。主分片的recovery不会等待其副本分片分配成功才开始。它们是独立的流程,只是副分片的recovery需要等它的主分片恢复完毕。
主分片
在节点意外挂掉的时候,可能有一些数据没来得及刷盘,主分片的recovery,就是为了恢复这部分未刷盘的数据。
ES的写操作都会记录事务日志(translog),事务日志记录了相关的数据变更。因此将最后一次提交(Lucene的一次提交就是一次fsync刷盘过程)之后的事务日志进行重放,建立Luncene索引,这样就完成了主分片的recovery。
副分片
在节点意外挂掉的时候,可能主分片已经写完数据,但是副分片没来得及同步,主副分片的数据不一致。
副分片需要恢复成与主分片一致,同时,恢复期间允许新的索引操作。恢复的过程分为两阶段:
- 阶段1:在主节点上获取事务日志保留锁,获取到锁之后,事务日志就不会因为刷盘而被清空。接着调用Luncene接口把分片数据做个快照,把生成的快照传给副分片节点。快照传输完成后,副分片就可以开始处理读写请求了。
- 阶段2:对事务日志做快照,传输给副分片节点,副分片节点把从阶段1获取锁开始,到分片快照数据传输完毕这段时间的操作进行重放。
阶段1完成,副分片就开始接受新请求,但是阶段2的时候,还需要重放操作,这两者不会有冲突么?
不会,ES中的数据是有版本号的概念,只要根据版本号进行过滤,只有最新一次操作生效。
第一阶段需要完整传输整个分片的数据,数据量大,恢复会变得很漫长,能避免这种全量同步么?
能,ES每个写入成功的操作,都会分配一个序号——SequenceNumber,通过比较主副分配的差异范围,如果差异范围目前还在事务日志中保留着,则可以通过主分片的事务日志增量恢复。或者主副分片的syncid和文档数都相同,可以直接跳过阶段1。
七、PacificA算法
ES的数据副本模型基于主从模式,在实现上参考了PacificA算法,该算法有几个特点:
- 设计了一个通用的、抽象的框架,而不是具体的、特定的算法。模型的正确性很容易验证。
- 配置管理和数据副本分离,paxos负责管理配置,数据副本策略采取主从模式。
- 将错误检测和配置更新放在数据副本的交互里实现,去中心化。
PacificA算法涉及的几个术语如下:
- Relica Group:一个互为副本的数据集合称为副本组。其中只有一个副本是主数据(Primary),其他为从数据(Secondary)。
- Configuration:配置信息中描述了一个副本组都有哪些副本,Primary是谁,以及它们位于哪个节点。
- Configuration Version:配置信息的版本号,每次发生变更时递增。
- Serial Number:代表每个写操作的顺序,每次写操作时递增,简称SN。每个主副本维护自己的递增SN。
- Prepared List:写操作的准备序列。存储来自外部的请求列表,将请求按照SN排序,向列表中插入的序列号必须大于列表中最大的SN。每个副本上有自己的Prepared List。
- Committed List:写操作的提交序列。
设计前提与假设:
- 节点可以失效,对消息延迟的上限不做假设。
- 消息可以丢失、乱序,但不能被篡改,即不存在拜占庭问题。
- 网络分区可以发生,系统时钟可以不同步,但漂移是有限度的。
整个系统框架主要由两部分组成:存储管理和配置管理
- 存储管理:负责数据的读取和更新,使用多副本方式保证数据的可靠性和可用性。
- 配置管理:对配置信息进行管理,维护所有配置信息的一致性。
存储管理
多个副本中存在一个主副本和多个从副本。所有写操作都进入主副本,当主副本出现故障,系统会从其他从副本选择合适的副本作为新的主副本。
数据的写入流程:
- 写请求进入主副本节点,节点为该操作分配SN,使用该SN创建UpdateRequest结构。然后将该UpdateRequest插入自己的prepare list。
- 主副本节点将携带SN的UpdateRequest发送给从副本节点,从节点接收到后也插入到prepare list,完成后给主副本节点回复一个ACK。
- 一旦主副本节点收到所有从副本节点的ACK,确定该数据已经被正确写入所有的从副本,此时认为可以提交了,将此UpdateRequest放入committed list,committed list前移。
- 主副本节点回复客户端更新成功。对每个Prepare消息,主副本节点向从副本节点发送一个commit通知,告诉它们自己的committed point位置,从副本节点收到通知后根据指示移动committed point到相同位置。
本质上就是一个两阶段提交,committed_R<=committed_P<=prepared_R。
配置管理
全局的配置管理器负责管理所有副本组的配置。节点可以向管理器提出添加/移除副本的请求,每次请求都会附带当前的配置版本号,只有这个版本号和管理器记录的版本号一致,请求才会被执行。如果请求成功,则版本号会被更新。
错误检测
PacificA算法使用租约(lease)机制来解决网络分区的问题:
- 如果主副本节点在一定时间(lease period)内未收到从副本节点的租约回复,则主副本节点认为从副本节点异常,向配置管理器汇报,将该异常从副本从副本组中移除,同时自己降级,不再作为主副本节点,触发选的主副本选举。
- 如果从副本节点在一定时间(grace period)内未收到主副本节点的租约请求,则任务主副本节点异常,向配置管理器汇报,将主副本从副本组移除,同时将自己提升为主副本。如果多个从副本同时执行该操作,则先到先得。
只要不发生时钟漂移,确保grace period>=lease period,则租约机制能保证主副本节点比其他从副本节点先感知到租约的失效。同时任何一个从副本只有在它租约失效时,才会去争取当主副本,因此保证了新主副本产生前,旧的主副本已经降级,不会产生两个主副本。
八、数据副本模型
ES中的每个索引都会拆分多个分片,并且每个分片都有多个副本。这些副本称为replication group(副本组,与PacificA的副本组概念一致)。保持副本之间的同步,以及从中读取的过程称为数据副本模型。
写入模型
写入流程:
- 请求到达协调节点,协调节点根据当前集群状态,将请求路由到对应主分片所在节点。
- 操作在主分片本地执行。
- 操作成功执行后,并行转发操作到当前in-sync副本组的所有副本分片。
- 一旦所有的副分片成功执行操作并回复主分片,主分片会把请求执行成功的信息返回给协调节点,协调节点返回给客户端。
每个分片副本都会被分配一个ID,集群元数据中会维护一个最新的分片副本的ID,成为in-sync allocation IDS。只有ID在该集合里的副本分片才可能被选择为主分片。
主分片出问题怎么办?
主分片所在节点会通知Master主节点,Master主节点会把一个副分片提升为主分片。
主分片所在节点挂了怎么办?
Master主节点会监控集群节点的健康状态,做故障转移。
哪些副本分片能被提升为主分片?
主节点回维护一个包含最新数据的副本子集(in-sync副本集合),存储在集群状态中,只有在该子集中的副本分片才会被提升为主分片(可人工干预)。
主分片转发操作到副分片的时候,转发失败或者没收到回复,怎么办?
主分片会通知Master主节点,将它认为有问题的副分片从in-sync副本集合中移除,主节点移除后,主分片才会这次操作成功,主节点也会指导另一个节点重新建立副分片。
脏读
主分片是先写本地,再同步到副分片,副分片写成功,才回复客户端成功。但是主分片写入后,从主分片就已经能读取到刚写入的数据。
某个分片慢,可能降低索引速度
有一个分片写入特别慢,写入操作都需要等这个分片,就会导致整个写入操作慢。
读取模型
读取流程:
- 协调节点把请求转发到相关分片。
- 从各个分片的副本组中选择一个活跃副本,可以是主分片或副分片。
- 发送分片级的读请求到被选中的副本。
- 合并结果并给客户端返回相应。
当选择的活跃副本不能响应,怎么办?
协调节点会从副本组中选择另一个副本,将请求转发新的副本。
九、关闭流程
- 关闭快照和HTTPServer,不再响应用户REST请求
- 关闭集群拓扑管理,不再响应ping请求
- 关闭网络模块,让节点离线
- 执行各个插件的关闭流程
- 关闭IndicesService
节点关闭对写入过程的影响
在写数据的时候,会对Engine加写锁。IndicesService的doStop方法最终会调用Engine的flushAndClose方法,该方法也会对Engine加写锁。由于写入操作已经获取了Engine的写锁,此时尝试获取写锁会等待,直到写操作完成。
但是由于网络模块被关闭,客户端的连接会被断开,客户端作为失败处理,而ES服务端的写流程还是在继续,直到完成。
节点关闭对读过程的影响
读数据的时候,会对Engine加读锁。同样道理,执行Engine的flushAndClose方法会一直等待,直到读操作完成。但是客户端因为连接断开,判定为读失败。
N、主要内部模块
1、Cluster
2、allocation
3、Discovery
4、gateway
5、Indices
6、HTTP
7、Transport
8、Engine