在之前的blog里简单搭建了一个ES的集群【ElasticSearch从入门到放弃系列 五】ElasticSearch分布式集群搭建,在那篇blog里我详细介绍了分布式集群的一些相关概念,以及快速搭建了一个集群,并在集群上进行了索引创建,但仅仅是从使用层面上进行了操作,这篇blog我们来探讨如下几个问题:节点之间是如何发现的?节点有哪几类?主节点是如何选举的?如何防止脑裂?
概念回顾
首先回顾下ES的集群的概念,主要就是集群\节点,分片\复制,我之前在讨论这些概念的时候没有对其中的重点信息抽象和挖掘,仅做了概念的罗列:
级别 | 概念 |
集群 cluster | 一个集群就是由一个或多个节点组织在一起,它们共同持有整个的数据,并一起提供索引和搜索功能。一个集群由一个唯一的名字标识,这个名字默认就是“elasticsearch”。这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群 |
节点 node | 一个节点是集群中的一个服务器,作为集群的一部分,它存储数据,参与集群的索引和搜索功能。和集群类似,一个节点也是由一个名字来标识的,在这个管理过程中,会去确定网络中的哪些服务器对应于Elasticsearch集群中的哪些节点。一个节点可以通过配置集群名称的方式来加入一个指定的集群。默认情况下,每个节点都会被安排加入到一个叫做“elasticsearch”的集群中,这意味着,如果你在你的网络中启动了若干个节点,并假定它们能够相互发现彼此,它们将会自动地形成并加入到一个叫做“elasticsearch”的集群中。在一个集群里,只要你想,可以拥有任意多个节点。而且,如果当前你的网络中没有运行任何Elasticsearch节点,这时启动一个节点,会默认创建并加入一个叫做“elasticsearch”的集群 |
分片 | 一个索引可以存储超出单个结点硬件限制的大量数据。比如,一个具有10亿文档的索引占据1TB的磁盘空间,而任一节点都没有这样大的磁盘空间;或者单个节点处理搜索请求,响应太慢。为了解决这个问题,Elasticsearch提供了将索引划分成多份的能力,这些份就叫做分片。当你创建一个索引的时候,你可以指定你想要的分片的数量。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上。分片很重要:允许你水平分割/扩展你的内容容量,允许你在分片之上进行分布式的、并行的操作,进而提高性能/吞吐量至于一个分片怎样分布,它的文档怎样聚合回搜索请求,是完全由Elasticsearch管理的,对于作为用户的你来说,这些都是透明的 |
复制 | 在一个网络/云的环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了,这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。为此目的,Elasticsearch允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片,或者直接叫复制。复制之所以重要: 在分片/节点失败的情况下,提供了高可用性。因为这个原因,注意到复制分片从不与原/主要(original/primary)分片置于同一节点上是非常重要的。扩展你的搜索量/吞吐量,因为搜索可以在所有的复制上并行运行。总之,每个索引可以被分成多个分片。一个索引也可以被复制0次(意思是没有复制)或多次。一旦复制了,每个索引就有了主分片(作为复制源的原来的分片)和复制分片(主分片的拷贝)之别。分片和复制的数量可以在索引创建的时候指定。在索引创建之后,你可以在任何时候动态地改变复制的数量,但是你事后不能改变分片的数量。默认情况下,Elasticsearch中的每个索引被分片5个主分片和1个复制,这意味着,如果你的集群中至少有两个节点,你的索引将会有5个主分片和另外5个复制分片(1个完全拷贝),这样的话每个索引总共就有10个分片 |
这几个概念各有要点:
- 集群:ES各节点想组成一个集群有两个必要条件:cluster.name 必须一致,通过discovery.zen.ping.unicast.hosts可以相互关联起来,discovery.zen.ping.unicast.hosts 列表中的IP列表称为种子节点,整个集群提供索引和搜索的服务。
- 节点:经过正确的配置,节点之间可以互相发现;节点分为三类:主节点、数据节点\候选主节点、协调节点,当然并不意味着一个节点只能是一种角色,这个接下来详细分析。
- 分片:每个分片是个功能完善的索引,只是数据不同,分片相当于增加水平扩展能力,高性能\搞吞吐
- 复制:副本是为了提高集群的稳定性和提高并发量,让分片高可用。高可用\高并发
总结一下:
- 节点的各种类型,角色不同,在流程中发挥不同的作用。
- 将数据分片是为了提高可处理数据的容量和易于进行水平扩展,为分片做副本是为了提高集群的稳定性和提高并发量。
- 副本是乘法,越多消耗越大,但也越保险。分片是除法,分片越多,单分片数据就越少也越分散。
- 副本越多,集群的可用性就越高,但是由于每个分片都相当于一个Lucene的索引文件,会占用一定的文件句柄、内存及CPU,并且分片间的数据同步也会占用一定的网络带宽,所以索引的分片数和副本数也不是越多越好
简单回顾了概念后,我们进入本篇blog的要点部分,应对我们开篇讨论的几个问题。
节点分析
每个节点既可以是候选主节点也可以是数据节点,通过在配置文件…/config/elasticsearch.yml中设置即可,默认都为true
node.master: true //是否候选主节点 node.data: true //是否数据节点
- 数据节点(data)【物理配置】:负责数据的存储和相关的操作,例如对数据进行增、删、改、查和聚合等操作,所以数据节点(data节点)对机器配置要求比较高,对CPU、内存和I/O的消耗很大。通常随着集群的扩大,需要增加更多的数据节点来提高性能和可用性。
- 候选主节点(master-eligible node)【物理配置】可以被选举为主节点(master节点),集群中只有候选主节点才有选举权和被选举权,其他节点不参与选举的工作。
- 主节点(master)【动态概念】:负责创建索引、删除索引、跟踪哪些节点是群集的一部分,并决定哪些分片分配给相关的节点、追踪集群中节点的状态等,稳定的主节点对集群的健康是非常重要的。
- 协调节点(proxy)【动态概念】:虽然对节点做了角色区分,但是用户的请求可以发往任何一个节点,并由该节点负责分发请求、收集结果等操作,而不需要主节点转发,这种节点可称之为协调节点,协调节点是不需要指定和配置的,集群中的任何节点都可以充当协调节点的角色
一般而言,一个节点既可以是候选主节点也可以是数据节点,但是由于数据节点对CPU、内存核I/0消耗都很大,所以如果某个节点既是数据节点又是主节点,那么可能会对主节点产生影响从而对整个集群的状态产生影响,会导致我们接下来分析的脑裂现象。
图片来源: https://blog.csdn.net/ailiandeziwei/article/details/87856210
以上是一个示意图,其中NodeA是当前集群的Master,NodeB和NodeC是Master的候选节点,其中NodeA和NodeB同时也是数据节点(DataNode),此外,NodeD是一个单纯的数据节点,Node_E是一个双非节点(既非Data也非Master候选),但还是可以充当proxy节点。每个Node会跟其他所有Node建立连接,形成一张网状图。
发现机制
还记得我在之前的那篇blog搭建集群时使用的配置文件,其中有两个比较重要的一定要填写正确的参数:
#节点1的配置信息: #集群名称,保证唯一 cluster.name: elasticsearch-tml #节点名称,必须不一样 node.name: node-1 #必须为本机的ip地址 network.host: 127.0.0.1 #服务端口号,在同一机器下必须不一样 http.port: 9200 #集群间通信端口号,在同一机器下必须不一样 transport.tcp.port: 9300 #设置集群自动发现机器ip集合 discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300","127.0.0.1:9301","127.0.0.1:9302"]
cluster.name和discovery.zen.ping.unicast.hosts。每个节点配置相同的 cluster.name 即可加入集群,默认值为 “elasticsearch”。确保不同的环境中使用不同的集群名称,否则最终会导致节点加入错误的集群。一个Elasticsearch服务启动实例就是一个节点(Node)。节点通过node.name来设置节点名称,如果不设置则在启动时给节点分配一个随机通用唯一标识符作为名称。
那么ES内部是如何通过一个相同的设置cluster.name 就能将不同的节点连接到同一个集群的?ES的内部使用了Zen Discovery——Elasticsearch的内置默认发现模块(发现模块的职责是发现集群中的节点以及选举master节点),发现规则为单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。如果集群的节点运行在不同的机器上,使用单播,可以为 Elasticsearch 提供它应该去尝试连接的节点列表。 当一个节点联系到单播列表中的成员时,它就会得到整个集群所有节点的状态,然后它会联系 master 节点,并加入集群发现规则,模拟发现按照如下步骤:
- 每个节点的配置文件都维护一个初始节点主机列表,单播列表不需要包含集群中的所有节点, 它只是需要足够的节点,当一个新节点联系上其中一个并且说上话就可以了,
discovery.zen.ping.unicast.hosts: ["host1", "host2:port"]
- 节点使用发现机制通过Ping的方式查找其他节点,节点启动后先 ping
,如果discovery.zen.ping.unicast.hosts
有设置,则 ping 设置中的 host ,否则尝试 ping localhost 的几个端口 - 节点检测cluster.name是否一致,如果一致,就联系上了这个集群,之后会联系这个集群的master,来通知集群它已经准备好加入到集群中了
通过这种方式节点就能发现集群,进而之后加入集群成为集群的一部分了。
选举机制
集群中可能会有多个master-eligible node,此时就要进行master选举,保证只有一个当选master。如果有多个node当选为master,则集群会出现脑裂,脑裂会破坏数据的一致性,导致集群行为不可控,产生各种非预期的影响。为了避免产生脑裂,ES采用了常见的分布式系统思路,保证选举出的master被多数派(quorum)的master-eligible node认可,以此来保证只有一个master。这个quorum通过discovery.zen.minimum_master_nodes
进行配置,要求可用节点必须大于 quorum (这个属性一般设置为 eligibleNodesNum / 2 + 1),才能对外提供服务。
选举发起条件
master选举由master-eligible节点发起,当一个master-eligible节点发现满足以下条件时发起选举:
- 当前master eligible节点不是master
- 当前master eligible节点与其它的节点通信无法发现master
- 集群中无法连接到master的master eligible节点数量已达到
discovery.zen.minimum_master_nodes
所设定的值
即当一个master-eligible节点发现包括自己在内的多数派的master-eligible节点认为集群没有master时,就可以发起master选举。
选举规则
选举规则由两个参数决定,一个是clusterStateVersion,一个是节点的ID,按照如下步骤进行选举,即每个候选主节点
- 当clusterStateVersion越大,优先级越高。这是为了保证新Master拥有最新的clusterState(即集群的meta),避免已经commit的meta变更丢失。因为Master当选后,就会以这个版本的clusterState为基础进行更新。(一个例外是集群全部重启,所有节点都没有meta,需要先选出一个master,然后master再通过持久化的数据进行meta恢复,再进行meta同步)。候选主节点寻找clusterStateVersion比自己高的master eligible的节点,向其发送选票
- 当clusterStateVersion相同时,节点的Id越小,优先级越高。即总是倾向于选择Id小的Node,这个Id是节点第一次启动时生成的一个随机字符串。之所以这么设计,应该是为了让选举结果尽可能稳定,不要出现都想当master而选不出来的情况。如果clusterStatrVersion一样,则计算自己能找到的master eligible节点(包括自己)中节点id最小的一个节点,向该节点发送选举投票
- 如果一个节点收到足够多的投票(即 minimum_master_nodes 的设置),并且它也向自己投票了,那么该节点成为master开始发布集群状态
选举时分两种情况,一种是当前候选主节点选自己当Master,另一种是当前候选主节点选别的节点当Master,当一个master-eligible node(我们假设为Node_A)发起一次选举时,它会按照上述排序策略选出一个它认为的master
当前候选主节点选自己(Node_A)当Master
NodeA会等别的node来join,即等待别的node的选票,当收集到超过半数的选票时,认为自己成为master,然后变更cluster_state中的master node为自己,并向集群发布这一消息
当前候选主节点选别的节点(Node_B)当Master
- 如果Node_B已经成为Master,Node_B就会把Node_A加入到集群中,然后发布最新的cluster_state, 最新的cluster_state就会包含Node_A的信息。相当于一次正常情况的新节点加入。对于Node_A,等新的cluster_state发布到Node_A的时候,Node_A也就完成join了。
- 如果Node_B在竞选Master,并且选了自己为Master,那么Node_B会把这次join当作一张选票。对于这种情况,Node_A会等待一段时间,看Node_B是否能成为真正的Master,直到超时或者有别的Master选成功。
- 如果Node_B认为自己不是Master(现在不是,将来也选不上),那么Node_B会拒绝这次join。对于这种情况,Node_A会开启下一轮选举。
以上就是整个选举的流程,如果没有特殊情况是一定能选举出一个master的。
脑裂现象
选举时当出现多个master竞争时,主分片和副本的识别也发生了分歧,对一些分歧中的分片标识为了坏片,更新的时候造成数据混乱或其它非预期结果,也就是我们上文提到的脑裂。其实按照如上的选举规则,能选举出一个确定的master是一定的,就算clusterStateVersion一样,也不可能有两个节点id一致,总会有大有小,按照此规则,所有节点其实是能达成共识的。“脑裂”问题可能有以下几个原因造成:
- 网络问题:集群间的网络延迟导致一些节点访问不到master,认为master挂掉了从而选举出新的master,并对master上的分片和副本标红,分配新的主分片
- 节点负载:主节点的角色既为master又为data,访问量较大时可能会导致ES停止响应(假死状态)造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。
- 内存回收:主节点的角色既为master又为data,当data节点上的ES进程占用的内存较大,引发JVM的大规模内存回收,造成ES进程失去响应。
为了避免脑裂现象的发生,我们可以从原因着手通过以下几个方面来做出优化措施:
- 适当调大响应时间,减少误判:通过参数
discovery.zen.ping_timeout
设置节点状态的响应时间,默认为3s,可以适当调大,如果master在该响应时间的范围内没有做出响应应答,判断该节点已经挂掉了。调大参数(如6s,discovery.zen.ping_timeout:6),可适当减少误判。 - 选举触发:我们需要在候选集群中的节点的配置文件中设置参数
discovery.zen.munimum_master_nodes
的值,这个参数表示在选举主节点时需要参与选举的候选主节点的节点数,默认值是1,官方建议取值(master_eligibel_nodes/2) + 1,其中master_eligibel_nodes为候选主节点的个数。这样做既能防止脑裂现象的发生,也能最大限度地提升集群的高可用性,因为只要不少于discovery.zen.munimum_master_nodes个候选节点存活,选举工作就能正常进行。当小于这个值的时候,无法触发选举行为,集群无法使用,不会造成分片混乱的情况。 - 角色分离:即是上面我们提到的候选主节点和数据节点进行角色分离,这样可以减轻主节点的负担,防止主节点的假死状态发生,减少对主节点“已死”的误判。
当然这里只讨论当前master连接不上重新发起选举的情况,其实在选举过程中也存在重复投票的问题。这里不做深入讨论。
重要参数
ElasticSearch运行时会启动两个探测进程。一个进程用于从主节点向集群中其它节点发送ping请求来检测节点是否正常可用。另一个进程的工作反过来了,其它的节点向主节点发送ping请求来验证主节点是否正常且忠于职守。以下为节点进行发现和选举时的一些重要参数:
- discovery.zen.ping.unicast.hosts :通过单播方式发现其他节点时,单播发送请求的主机列表
- discovery.zen.master_election.ignore_non_master_pings :集群在选举master时,不考虑非master-eligible的选票。
同时还有防止集群脑裂的参数:
- discovery.zen.ping.minimum_master_nodes : 完成选举所需要master-eligible节点数量;防止集群脑裂,在生产环境需要配置;此项设置还会影响到集群状态的update,当主节点收到来自其他节点的状态更新数据,更新数据是否接受前,必须由大于此设置的master-eligible数量承认,才说明此集群的更新成功,才会同步到其他所有的节点。 eligibleNodesNum / 2 + 1
- discovery.zen.ping_timeout:节点状态的响应时间,默认为3s,可以适当调大。
在发生问题或者在正式配置集群时如果机器允许尽量data、master节点分离,如果条件有限,也可以调整如上的一些参数来避免故障的发生。