ElasticSearch集群
因为ElasticSearch是应对海量数据的检索使用,所以ES一定是以集群为基础进行访问的,上文中我们引申提到过分片和副本的概念,其实也对应于Kafka的分区和副本的概念。本节详细聊聊ElasticSearch的集群
集群术语
在ElasticSearch集群中有如下的前置概念术语需要理解,分别是集群、节点、分片和复制:
- 集群 cluster:一个集群由一个或多个节点组织在一起,它们共同持有整个的数据,并一起提供索引和搜索功能。一个集群由一个唯一的名字标识,节点只能通过指定某个集群的名字加入集群,也即cluster.name必须一致,通过discovery.zen.ping.unicast.hosts可以相互关联起来,discovery.zen.ping.unicast.hosts 列表中的IP列表称为种子节点,整个集群提供索引和搜索的服务。
- 节点 node:一个节点是集群中的一个服务器,作为集群的一部分,它存储数据,参与集群的索引和搜索功能,节点分为三类:主节点、数据节点\候选主节点、协调节点,当然并不意味着一个节点只能是一种角色
- 分片 shard:一个索引可以存储超出单个结点硬件限制的大量数据。比如一个具有10亿文档的索引占据1TB的磁盘空间,而任一节点都没有这样大的磁盘空间或单个节点处理搜索请求响应太慢。为此Elasticsearch提供了将索引划分成多份的能力,这些份就叫做分片。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上,允许水平分割/扩展内容容量,在分片之上进行分布式的、并行的操作来提高性能/吞吐量,提供了高可扩展及高并发能力。
- 复制 replica:分片故障时故障转移机制非常必要。Elasticsearch允许创建分片的一份或多份拷贝,这些拷贝叫做复制分片。在分片/节点失败的情况下,提供了高可用性。因为这个原因,注意到复制分片从不与原/主要(original/primary)分片置于同一节点上是非常重要的,同时复制分片还能提提高并发量。所以复制分片的作用是高可用\高并发,副本越多消耗越大,也越保险,集群的可用性就越高,但是由于每个分片物理上相当于一个Lucene的索引文件,会占用一定的文件句柄、内存及CPU,并且分片间的数据同步也会占用一定的网络带宽,所以索引的分片数和副本数也不是越多越好
其实在创建好的索引mapping信息中我们也能get到这一信息,接下来配置一个集群并创建索引来对照下我们上边提到的各种概念。
集群配置
了解了集群的基本术语后,我们亲手构建一个集群,如果一个集群中有三个节点,那么这三个节点可以通过以下方式配置来相互发现:修改每个节点config\elasticsearch.yml
配置文件如下:
#节点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"] #跨域访问配置 http.cors.enabled: true http.cors.allow-origin: "*" #指定主节点 cluster.initial_master_nodes: ["node-1"]
#节点2的配置信息: #集群名称,保证唯一 cluster.name: elasticsearch-tml #节点名称,必须不一样 node.name: node-2 #必须为本机的ip地址 network.host: 127.0.0.1 #服务端口号,在同一机器下必须不一样 http.port: 9201 #集群间通信端口号,在同一机器下必须不一样 transport.tcp.port: 9301 #设置集群自动发现机器ip集合 discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300","127.0.0.1:9301","127.0.0.1:9302"] #跨域访问配置 http.cors.enabled: true http.cors.allow-origin: "*"
#节点3的配置信息: #集群名称,保证唯一 cluster.name: elasticsearch-tml #节点名称,必须不一样 node.name: node-3 #必须为本机的ip地址 network.host: 127.0.0.1 #服务端口号,在同一机器下必须不一样 http.port: 9202 #集群间通信端口号,在同一机器下必须不一样 transport.tcp.port: 9302 #设置集群自动发现机器ip集合 discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300","127.0.0.1:9301","127.0.0.1:9302"] #跨域访问配置 http.cors.enabled: true http.cors.allow-origin: "*"
配置集群后启动并添加上小节设置的5个分片1个复制的tml-userinfo
索引后,集群状态如下图所示:
我们我们来查看索引的信息,可以看到如下内容:
```java { "version":9, "mapping_version":2, "settings_version":1, "aliases_version":1, "routing_num_shards":640, "state":"open", "settings":{ "index":{ "routing":{ "allocation":{ "include":{ "_tier_preference":"data_content" } } }, "number_of_shards":"5", "provided_name":"tml-userinfo", "creation_date":"1610800394680", "number_of_replicas":"1", "uuid":"MbmG0jnUQ22UOfM9zhJ3kg", "version":{ "created":"7100299" } } }, "mappings":{ "_doc":{ "properties":{ "describe":{ "analyzer":"standard", "store":true, "type":"text" }, "id":{ "store":true, "type":"long" }, "title":{ "analyzer":"standard", "store":true, "type":"text" }, "content":{ "analyzer":"standard", "store":true, "type":"text" } } } }, "aliases":[ ], "primary_terms":{ "0":1, "1":1, "2":1, "3":1, "4":1 }, "in_sync_allocations":{ "0":[ "dFqgw2xGR2K2_2N5H1mg0Q", "zi7v_U9LQNCzyj6mqygowA" ], "1":[ "c36TL2OFTOWmxhs4uuVIug", "storqrL-TKCRYyneXTp5wQ" ], "2":[ "6o_AFiphQYi_IysAN6Dmnw", "veZZlgZ5Q2ihu2ALlxA2Hg" ], "3":[ "l5L-1ZNBQ5mv0JNBh9RKMw", "A8oYRyK0Qu-oL0SAndIhVA" ], "4":[ "YnNkHjeWQTSOjQFJ5MGxjw", "An8hAJP7R1Wo4uQ7IVFZJQ" ] }, "rollover_info":{ }, "system":false }
节点分析
上文提到,节点分为3类,每个节点既可以是候选主节点也可以是数据节点,通过在配置文件…/config/elasticsearch.yml中设置即可,默认都为true:
node.master: true //是否候选主节点 node.data: true //是否数据节点
那么这三类节点各代表什么意义呢?
- 数据节点(data)【物理配置】:负责数据的存储和相关的操作,例如对数据进行增、删、改、查和聚合等操作,所以数据节点(data节点)对机器配置要求比较高,对CPU、内存和I/O的消耗很大。通常随着集群的扩大,需要增加更多的数据节点来提高性能和可用性。可以存放数据的节点
- 候选主节点(master-eligible node)【物理配置】可以被选举为主节点(master节点),集群中只有候选主节点才有选举权和被选举权,其他节点不参与选举的工作。可以参与主节点竞选的节点
- 主节点(master)【动态概念】:负责创建索引、删除索引、跟踪哪些节点是集群的一部分,并决定哪些分片分配给相关的节点、追踪集群中节点的状态等,稳定的主节点对集群的健康是非常重要的。管理整个集群中的节点、索引等的节点,master
- 协调节点(proxy)【动态概念】:虽然对节点做了角色区分,但是用户的请求可以发往任何一个节点,并由该节点负责分发请求、收集结果等操作,而不需要主节点转发,这种节点可称之为协调节点,协调节点是不需要指定和配置的,集群中的任何节点都可以充当协调节点的角色,接收用户请求并反馈结果给用户的节点
一般而言,一个节点既可以是候选主节点也可以是数据节点,但是由于数据节点对CPU、内存核I/0消耗都很大,所以如果某个节点既是数据节点又是主节点,那么可能会对主节点产生影响从而对整个集群的状态产生影响,会导致我们接下来分析的脑裂现象
NodeA是当前集群的Master,NodeB和NodeC是Master的候选节点,其中NodeA和NodeB同时也是数据节点(DataNode),此外,NodeD是一个单纯的数据节点,Node_E是一个双非节点(既非Data也非Master候选),但还是可以充当proxy节点。每个Node会跟其他所有Node建立连接,形成一张网状图
发现机制
每个节点是如何连接到一起并构建成集群的呢,这个就是由集群的发现机制来实现的,上文中我们提到两个参数,在节点配置信息上必须具备:
#集群名称,保证唯一 cluster.name: elasticsearch-tml #设置集群自动发现机器ip集合 discovery.zen.ping.unicast.hosts: ["127.0.0.1:9300","127.0.0.1:9301","127.0.0.1:9302"]
每个节点配置相同的 cluster.name 即可加入集群,那么ES内部是如何通过一个相同的设置cluster.name 就能将不同的节点连接到同一个集群的?
ES的内部使用了Zen Discovery——Elasticsearch的内置默认发现模块(发现模块的职责是发现集群中的节点以及选举master节点),发现规则为单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。如果集群的节点运行在不同的机器上,使用单播,可以为 Elasticsearch 提供它应该去尝试连接的节点列表。 模拟发现按照如下步骤:
- 每个节点的配置文件都维护一个初始节点主机列表,单播列表不需要包含集群中的所有节点, 它只是需要足够的节点,当一个新节点联系上其中一个并且说上话就可以了,
discovery.zen.ping.unicast.hosts: ["host1", "host2:port"]
- 节点使用发现机制通过Ping的方式查找其他节点,节点启动后先 ping,
如果discovery.zen.ping.unicast.hosts
有设置,则 ping 设置中的 host ,否则尝试 ping localhost 的几个端口 - 节点检测cluster.name是否一致,如果一致,就联系上了这个集群,之后会联系这个集群的master,来通知集群它已经准备好加入到集群中了
当一个节点联系到单播列表中的成员时,它就会得到整个集群所有节点的状态,然后它会联系 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
- 当前master eligible节点与其它的节点通信无法发现master,我联系不上master
- 集群中无法连接到master的master eligible节点数量已达到
discovery.zen.minimum_master_nodes
所设定的值,超过一半的兄弟们联系不上master
即当一个master-eligible节点发现包括自己在内的多数派的master-eligible节点认为集群没有master时,就可以发起master选举。
选举规则
选举规则由两个参数决定,一个是clusterStateVersion,一个是节点的ID,按照如下步骤进行选举,即每个候选主节点
- clusterStateVersion越大,优先级越高。这是为了保证新Master拥有最新的clusterState(即集群的meta),避免已经commit的meta变更丢失。因为Master当选后,就会以这个版本的clusterState为基础进行更新。候选主节点寻找clusterStateVersion比自己高的master eligible的节点,向其发送选票
- 当clusterStateVersion相同时,节点的Id越小,优先级越高。即总是倾向于选择Id小的Node,这个Id是节点第一次启动时生成的一个随机字符串。之所以这么设计,应该是为了让选举结果尽可能稳定,不要出现都想当master而选不出来的情况。如果clusterStatrVersion一样,则计算自己能找到的master eligible节点(包括自己)中节点id最小的一个节点,向该节点发送选举投票
- 如果一个节点收到足够多的投票(即 minimum_master_nodes 的设置),并且它也向自己投票了,那么该节点成为master开始发布集群状态
选举时分两种情况,一种是当前候选主节点选自己当Master,另一种是当前候选主节点选别的节点当Master,当一个master-eligible node(我们假设为Node_A)发起一次选举时,它会按照上述排序策略选举
当前候选主节点选自己(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,这样做既能防止脑裂现象的发生,也能最大限度地提升集群的高可用性,因为只要不少于discovery.zen.munimum_master_nodes个候选节点存活,选举工作就能正常进行。当小于这个值的时候,无法触发选举行为,集群无法使用,不会造成分片混乱的情况。
当然这里只讨论当前master连接不上重新发起选举的情况,其实在选举过程中也存在重复投票的问题,不做深入讨论。
ElasticSearch工作流程
主分片和副本分片是如何同步的?创建索引的流程是什么样的?ES如何将索引数据分配到不同的分片上的?本节讨论以上的问题来详细解读当一个索引相关请求进入ElasticSearch集群时,经历了哪些流程。
下图描述了3个节点的集群,共拥有12个分片,包括4个主分片(S0、S1、S2、S3)和8个复制分片(R0、R1、R2、R3),每个主分片对应两个副本分片,节点1是主节点(Master节点)负责整个集群状态
路由规则
需要注意,写索引是只能写在主分片上,然后同步到副本分片。这里有4个主分片,一条数据ES是根据什么规则写到特定分片上的呢?这个过程是根据下面这个公式决定的:
shard = hash(routing) % number_of_primary_shards
以上公式中涉及注意事项如下如下:
- routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值【例如租户ID】。 routing 通过 hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到余数 。这个在 0 到
number_of_primary_shards-1
之间的余数,即文档所在分片位置 - 如果是自定义的routing,在查询时,一定要指定routing进行查询,否则是查询不到文档的。这并不是局限性,恰恰相反,指定routing的查询,性能上会好很多,因为指定_routing意味着直接去存储数据的shard上搜索,而不会搜索所有shard
公式解释了为什么要在创建索引的时候就确定好主分片的数量并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了
写操作流程
每个节点都有处理读写请求的能力。在一个写请求被发送到某个节点后,该节点即为上文提到的协调节点,协调节点会根据路由公式计算出需要写到哪个分片上,再将请求转发到该分片的主分片节点上。假如此时数据通过路由计算公式取余后得到的值是 shard = hash(routing) % 4 = 0,则具体流程如下:
- 客户端向E-Node2节点(协调节点)发送写请求,通过路由计算公式得到值为0,则当前数据应被写到主分片S0上。
- E-Node2节点将请求转发到S0主分片所在的节点E-Node3,E-Node3接受请求并写入到磁盘S0。
- 并发将数据复制到两个副本分片R0上,其中通过乐观并发控制数据的冲突。
- 一旦所有的副本分片都报告成功,则节点ES3将向协调节点报告成功,协调节点向客户端报告成功。
下图为流程说明图,后续各操作流程类似,不多余画图:
读操作流程
同写操作一样,GET某一条数据的流程也会计算路由,当写入了某个document后,这个document会自动给你分配一个全局唯一的id,doc id,同时也是根据doc id进行hash路由到对应的primary shard上去:
- 客户端发送请求到E-Node2节点,该节点成为协调节点
- 协调节点对document进行路由,路由规则同上,将请求转发到路由主分片E-Node3节点,此时会使用round-robin【这个轮询算法在我的Kafka相关blog介绍过】随机轮询算法,在E-Node3_S0以及其所有replica【E-Node1_R0、E-Node2_R0】中随机选择一个,让读请求负载均衡
- 接收请求的节点【E-Node3_S0、E-Node1_R0、E-Node2_R0中随机的一个】返回document给coordinate node【E-Node2】
- 协调节点向客户端报告成功,返回document给客户端
以上就是通过一个具体的文档Id读数据的流程,当然其实ES我们更多用到的是它的搜索。