分布式系统中间件整理

整理多种中间件,横向对比分布式相关的特性,关注高可用、负载均衡、故障恢复等不同中间件的实现方式。主要包含Redis、Kafka、RabbitMQ、RocketMQ、Elasticsearch、MongoDB。

Redis

当前版本6.0.5(2020-6-28)
集群架构中,Redis支持主从,哨兵,以及最新的无主集群模式(Redis-Cluster)

集群架构(主从模式+哨兵)

Redis主节点正常启动,从节点使用slaveof命令设定为从节点。
哨兵节点最好设置为集群,这样能够在单个哨兵节点网络故障时不会出现误判,多数哨兵节点共同判断节点失效可信度更高。当主服务器失效的时候,见识这个主服务器的所有Sentinel就会基于彼此共有的信息选出一个Sentinel,并从现有的从服务器当中选出一个新的主服务器。当被选中的从服务器转换为主服务器之后,那个被选中的Sentinel就会让剩余的其他服务器去复制这个新的主服务器(默认Sentinel会一个一个迁移从服务器,可以通过配置选项进行修改)
S_DOWN:subjectively down,直接翻译的为"主观"失效,即当前sentinel实例认为某个redis服务为"不可用"状态.
O_DOWN:objectively down,直接翻译为"客观"失效,即多个sentinel实例都认为master处于"SDOWN"状态,那么此时master将处于ODOWN,ODOWN可以简单理解为master已经被集群确定为"不可用",将会开启failover.

选主算法

一、使用如下条件筛选备选node:
1、slave节点状态处于S_DOWN,O_DOWN,DISCONNECTED的除外
2、最近一次ping应答时间不超过5倍ping的间隔(假如ping的间隔为1秒,则最近一次应答延迟不应超过5秒,redis sentinel默认为1秒)
3、info_refresh应答不超过3倍info_refresh的间隔(原理同2,redis sentinel默认为10秒)
4、slave节点与master节点失去联系的时间不能超过( (now - master->s_down_since_time) + (master->down_after_period * 10))。总体意思是说,slave节点与master同步太不及时的(比如新启动的节点),不应该参与被选举。
5、Slave priority不等于0(这个是在配置文件中指定,默认配置为100)。

二、从备选node中,按照如下顺序选择新的master
1、较低的slave_priority(这个是在配置文件中指定,默认配置为100)
2、较大的replication offset(每个slave在与master同步后offset自动增加)
3、较小的runid(每个redis实例,都会有一个runid,通常是一个40位的随机字符串,在redis启动时设置,重复概率非常小)
4、如果以上条件都不足以区别出唯一的节点,则会看哪个slave节点处理之前master发送的command多,就选谁。

集群架构(Redis-Cluster)

在3.x提出cluster集群模式。Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。
key 分布模式,key空间分布被划分为16384个slot,所以一个集群,主节点的个数最大为16384(一般建议master最大节点数为1000)
Cluster bus,每个节点有一个额外的TCP端口,这个端口用来和其他节点交换信息。这个端口一般是在与客户端链接端口上面加10000,比如客户端端口为6379,那么cluster bus的端口为16379.
cluster 拓扑,Redis cluster 是一个网状的,每一个节点通过tcp与其他每个节点连接。假如n个节点的集群,每个节点有n-1个出的链接,n-1个进的链接。这些链接会一直存活。假如一个节点发送了一个ping,很就没收到pong,但还没到时间把这个节点设为 unreachable,就会通过重连刷新链接。
Nodes handshake,如果一个节点发送MEET信息(METT 类似ping,但是强迫接受者,把它作为集群一员)。一个节点发送MEET信息,只有管理员通过命令行,运行如下命令CLUSTER MEET ip port。如果这个节点已经被一个节点信任,那么也会被其他节点信任。比如A 知道B,B知道C,B会发送gossip信息给A关于C的信息。A就会认为C是集群一员,并与其建立连接。
失败检测,集群失效检测就是,当某个master或者slave不能被大多数nodes可达时,用于故障迁移并将合适的slave提升为master。当slave提升未能有效实施时,集群将处于error状态且停止接收Client端查询。
集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此交换各个节点状态信息,检测各个节点状态:在线状态、疑似下线状态PFAIL、已下线状态FAIL。当主节点A通过消息得知主节点B认为主节点D进入了疑似下线(PFAIL)状态时,主节点A会在自己的clusterState.nodes字典中找到主节点D所对应的clusterNode结构,并将主节点B的下线报告(failure report)添加到clusterNode结构的fail_reports链表中
如果集群里面,半数以上的主节点都将主节点D报告为疑似下线,那么主节点D将被标记为已下线(FAIL)状态,将主节点D标记为已下线的节点会向集群广播主节点D的FAIL消息,
所有收到FAIL消息的节点都会立即更新nodes里面主节点D状态标记为已下线。
将node标记为FAIL需要满足以下两个条件:
1.有半数以上的主节点将node标记为PFAIL状态。
2.当前节点也将node标记为PFAIL状态。

选主算法

选新主的过程基于Raft协议选举方式来实现的
1)当从节点发现自己的主节点进行已下线状态时,从节点会广播一条
CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息,并且具有投票权的主节点向这个从节点投票
2)如果一个主节点具有投票权,并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条,CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点
3)每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持
4)如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于集群N/2+1张支持票时,这个从节点就成为新的主节点
5)如果在一个配置纪元没有从能够收集到足够的支持票数,那么集群进入一个新的配置纪元,并再次进行选主,直到选出新的主节点为止

负载均衡

客户端请求获得指定键的值时只需要向任意集群中的节点发送命令,该节点会判断这个键是否在本节点中,在的话直接获取返回,如果不在则返回给客户端一个重定向命令,并附带存储此键对应的节点的ip、端口、槽位。客户端重新发送请求即可。也可以配置令节点自动转发请求到指定节点处理再返回给客户端。当然这样在一定程度上会影响性能,可以由客户端配置缓存插槽和节点的信息来解决。

其他

Codis是豌豆荚团队开源的使用Go语言编写的Redis分布式解决方案,它是作为中间件,以代理的身份接收请求,底层再将请求转发到指定的节点中,Codis的优势在于可以不停机动态增加或者删除节点,并提供了图像化的管理界面!

Kafka

当前版本2.5.0(2020-6-28)
KafkaController作为Kafka集群控制管理模块。由于Zookeeper上保存了Kafka机器的元数据信息,因为KafkaController通过3在不同目录注册不同的回调函数来达到监测集群状态的目的,以及响应集群状态的变化

集群架构

集群中主要的模式是Partition-Replica,Kafka的核心概念在于 每个Topic下分为多个Partition,每个Partition都有一个或多个Replica。其中一个Replica为Leader,其他都为Follower,Leader处理Partition的所有读写请求,Follower定期同步Leader上的数据。

选主算法

Leader和Follower的选举是基于Zookeeper实现的,尝试在Zookeeper的相同路径上创建瞬时节点(Ephemeral Node),只有一个KafkaController会创建成功。其中负责状态管理的类为ZookeeperLeaderElector。

Topic分区的Leader Replica在不同场景下的选举策略是不一样的,不同选举策略都基础PartitionLeaderSelector。其根据Topic、Partition、当前Leader、当前的ISR选举出新的Leader,新的ISR和新的AR(在线状态),共有5种不同的策略:

  • NoOpLeaderSelector:默认的选举策略
  • ReassignedPartitionLeaderSelector:当分区AR重新分配时使用的策略
  • PreferredReplicaPartitionLeaderSelector:集群内部自动平衡负载或者用户触发手动平衡负载时使用的策略
    随着Topic的新建删除以及Broker Server的上下线,原本Topic分区的Leader Replica在集群中的分布越来越不均匀。 auto.leader.rebalance.enable为true,则会自动触发分区的Leader Replica选举,或者管理员下发分区Leader Replica选举指令。这会在Zookeeper的 /admin/preferred_replica_election指定具体的Topic和分区,此时Leader状态的KafkaController监测到这个路径的数据变化就会触发相应的回调函数,促使对应的Topic分区发生Leader Replica的选举。
  • OfflinePartitionLeaderSelector:分区状态从OfflinePartition或者NewPartition切换为OnlinePartition时使用的策略
  1. 筛选出在线的ISR和AR
  2. 优先从在线的ISR中选择,如果列表不为空则选择列表中的第一个,选举结束
  3. 在线ISR为空,根据 unclean.leader.election.enable 决定是否从在线的AR中选举Leader,如果允许,则选择AR列表中的第一个,结束选举,如果AR列表为空选举失败。
  • ControllerShutdownLeaderSelector:Leader状态的KafkaController处理其他Broker Server下线导致分区的Leader Replica发生切换时使用的策略。
  1. 筛选出在线的ISR
  2. 剔除离线的ISR形成新的ISR列表
  3. 如果新的ISR列表不为空,则选举第一个Replica作为新的Leader,否则选举失败

负载均衡

消费的时候,只会从leader去读,但是只有当一个消息已经被所有follower都同步成功并返回ack的时候,这个消息才能够被消费者读到。所以可以说读取是没有负载均衡地。复制集仅为了高可用。
Partition的AR列表的第一个Replica称为“Preferred Replica”,并均匀分布在整个Kafka集群中。由于每个Partition只有Leader Replica对外提供读写服务,并且Partition创建的时候默认的Leader Replica位于Preferred Replica之上,此时Kafka集群的负载是均衡的,如果Kafka集群长时间运行,Broker Server中途由于异常而发生重启,此时Partition的Leader Replica会发生迁移,这样会导致其Partition的Leader Replica在集群中不再均衡了。
kafka-reassign-partitions.sh提供来重新分配分区副本的能力。该工具可以促进Kafka集群的负载均衡。因为Follower Replica需要从Leader Replica Fetch数据以保持与与Leader Replica同步,仅保持Leader Replica分布的平衡对整个集群的负载均衡时不够的。另外当Kafka集群扩容后,该工具可以将已有Topic的Partition迁移到新加入的Broker上。

RabbitMQ

当前版本3.8.5(2020-6-28)
RabbitMQ客户端中与事务机制相关的方法有三个:channel.txSelect(设置为事务模式)、channel.txCommit(提交事务)和channel.txRollback(回滚事务)。事务会很大影响RabbitMQ的消息吞吐量。
轻量级的方式:发送方确认机制。生产者将信道设置成confirm(确认)模式,一旦信道进入confirm模式,所有该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配队列之后,RabbitMQ就发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),这就使得生产者知晓消息已经正确的到达了目的地。如果消息和队列是可持久化的,那么确认消息就会在消息写入磁盘后发出。RabbitMQ回传给生产者的确认消息中的deliveryTag包含了待确认消息的序号,此外RabbitMQ也可以设置channel.basicAck方法中的multiple参数,表示这个序号之前的所有消息都已经得到了处理。

集群架构

RabbitMQ集群中的所有节点都会备份所有的元数据信息,包括:

  • 队列元数据:队列的名称以及属性
  • 交换器:交换器的名称以及属性
  • 绑定关系元数据:交换器与队列或者交换器与交换器之间的绑定关系
  • vhost元数据:为vhost内的队列、交换器和绑定命名空间及安全属性
    每个virtual host本质上都是一个RabbitMQ Server,拥有它自己的queue,exchagne,和bings rule等等。这保证了你可以在多个不同的application中使用RabbitMQ。

在RabbitMQ集群中创建队列,集群只会在单个节点而不是在所有节点上创建队列的进程并包含完整的队列(元数据、状态、内容)。这样只有队列的宿主节点(所有者)节点知道队列的所有信息,所有其他非所有者只知道队列的元数据和指向该队列存在的那个节点的指针。节点崩溃时,该节点的队列进程和关联的绑定都会消失,附加在那些队列上的消费者也会丢失其所订阅的信息,并且任何匹配该队列绑定信息的新消息也会丢失。
不同于队列那样有自己的进程,交换器实际上只是一个名称和绑定列表。当消息发布到交换器时,实际上是由所连接的信道将消息上的路由键同交换器的绑定列表进行比较,然后再路由消息。当创建一个新的交换器时,RabbitMQ所要做的就是将绑定列表添加到集群中的所有节点上。这样每个节点上的每个信道都可以访问到新的交换器。

多机多节点是指每台物理机器都安装了RabbitMQ,应当只在局域网内使用,广域网应当使用Federation或者Shovel。
命令行主要使用 rabbitmqctl join_cluster {nodename} 加入集群节点
rabbitmqctl forget_cluester_node {nodename}
RabbitMQ要求集群中至少有一个磁盘节点,其他节点可以是内存节点。当节点加入或者离开集群的时候,它们必须将变更通知到至少一个磁盘节点。如果唯一一个磁盘节点崩溃,集群可以继续收发消息,但是不能执行创建队列、交换器、绑定关系、用户,以及更改权限、添加和删除集群节点的操作。所以集群应该保障有两个或者多个磁盘节点的存在。

Federation(联邦交换器)

Federation插件可以让多个交换器或者多个队列进行联邦,一个联邦交换器(federated exchange)或者一个联邦队列(federated queue)接收上游(upstream)的消息,这里的上游时指位于其他Broker上的交换器或者队列联邦交换器能够将原本发送给上游交换器(upstream exchange)的消息路由到本地的某个队列中;联邦队列则允许一个本地消费者接收到来自上游队列(upstream queue)的消息。

Shovel

与Federation具备的数据转发功能类似,Shovel能够可靠、持续的从一个Broker的队列拉取数据并转发只另一个Broker的交换器。实际上是基于AMQP协议的转发器。
Shovel可以部署在源端也可以部署在目的端。有两种方式可以部署Shovel:静态方式(static)和动态方式(dynamic)。静态方式是指在RabbitMQ.config配置文件中设置,动态方式只指通过Runtime Parameter设置。

当集群消息堆积严重时,可以通过Shovel将队列中的消息移交给另一个集群,这是一备一的情况。如果需要一备多,可以采用镜像队列或者引入Federation。

Shovel工作在Federation的更低一层。监狱Federation从一个交换器中转发消息到另一个交换器(如果有必要可以确认消息是否被转发),Shovel只是简单地从某个Broker上的队列消费消息,然后转发消息到另一个Broker上的交换器而已。Shovel也可以再一台单独的服务器上去转发消息,例如将一个队列中的数据移动到另一个队列中。

镜像队列

引入镜像队列(Mirror Queue)的机制,可以将队列镜像到集群中的其他Broker节点之上,如果集群中的一个节点失效了,队列能自动地切换到镜像的另一个节点上以保证服务的可用性。每一个配置镜像的队列都包含一个主节点(master)和若干个从节点(slave),如果master失效,slave加入时间最长的会提升为master。发送到镜像队列的所有消息会被同时发往master和其他所有的slave。除了发送消息(Basic.Publish)外所有动作都只会想master发送,然后由master将命令执行的结果广播给各个slave。
消费者与slave建立连接消费时实质上都是从master上获取消息,只不过看似从slave上消费而已。例如消费者与slave建立了TCP连接后执行Basic.Get操作,由slave转发给master,再由master准备好数据返回给slave,投递给消费者。这里的master和slave针对队列而言,队列可以均匀地散落在集群的各个Broker节点中以达到负载均衡地目的,真正的负载还是针对实际的物理机器而言,而不是内存中驻留的队列进程。

网络分区

网络分区的恢复
首先选一个最信任的partition,Mnesia使用该partition中的状态,其他partitions中发生的变化都将丢失。
停止其他partitions中的所有nodes,之后重启这些nodes。当这些nodes重新加入cluster后将从信任的partition恢复状态。
最后还需重启信任的partition中的所有nodes以清除network partition的警告信息

Rabbitmq自动处理网络分区的3种模式
RabbitMQ提供了3种自动处理network partitions的方式:默认为ignore模式,也即需要手工处理
pause-minority mode:暂停少数模式;
pause-if-all-down mode:暂停-如果全部停止模式
autoheal mode:自动愈合模式
pause-minority mode:暂停少数模式
在pause-minority模式下,察觉其他nodes down掉后,RabbitMQ将自动暂停认为自己是少数派的 nodes(例如小于或等于总nodes数的一半),network partition一旦发生,“少数派”的nodes将立刻暂停,直至partition结束后重新恢复。这可以保证在network partition发生时,至多只有一个partition中的nodes继续运行。(牺牲可用性保证一致性)
若所有分区的nodes个数都小于总nodes个数一半,则意味着所有分区的nodes都会认为自己是少数派,即所有nodes都将暂停
pause-if-all-down mode:暂停-如果全部停止模式
http://www.rabbitmq.com/partitions.html
autoheal模式
在autoheal模式下一旦发生了partition,RabbitMQ将自动确定一个优胜partition,然后重启所有不在优胜partition中的nodes。
获胜的partition为拥有最多客户端连接的partition(若连接相同则为节点最多的partition)。
关于自动处理partitions的设置在配置文件的cluster_partition_handling参数中进行。
各自的适用场景
network partitions自动处理并不能保证cluster不出任何问题。
一般来说可作如下选择:
ignore:若网络非常可靠。所有nodes在同一机架,通过交换机连接,该交换机也是通往外部网络的出口。在cluster的某一部分故障时不希望其余部分受影响。或者cluster只有两个node。
pause_minority:网络较不可靠。cluster处于EC2的3个AZ中,假定每次至多只有其中一个AZ故障,想要剩余的AZ继续提供服务而故障的AZ中的nodes在AZ恢复后重新自动加入到cluster。
autoheal:网络很不可靠。与数据完整性相比更关注服务的持续性。cluster只有两个node。

RocketMQ

当前版本4.7.0 release(2020-6-28)
RocketMQ是一个消息与流处理平台,具有低延时、高性能、高可靠、万亿级消息储存以及可扩展性灵活等特性。主要由4个核心部分组成:name servers, brokers, producers and consumers,每一部分都能集群部署避免单点故障。

集群架构

Name Server

Name Server是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。提供轻量级的服务发现和路由。 每个 NameServer 记录完整的路由信息,提供等效的读写服务,并支持快速存储扩展。

Broker

Broker 通过提供轻量级的 Topic 和 Queue 机制来处理消息存储,同时支持推(push)和拉(pull)模式以及主从结构的容错机制。
Broker部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的Broker Name,不同的Broker Id来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。
每个Broker与Name Server集群中的所有节点建立长连接,定时(每隔30s)注册Topic信息到所有Name Server。Name Server定时(每隔10s)扫描所有存活broker的连接,如果Name Server超过2分钟没有收到心跳,则Name Server断开与Broker的连接。

Producer

Producer生产者,产生消息的实例,拥有相同 Producer Group 的 Producer 组成一个集群。
Producer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
Producer每隔30s(由ClientConfig的pollNameServerInterval)从Name server获取所有topic队列的最新情况,这意味着如果Broker不可用,Producer最多30s能够感知,在此期间内发往Broker的所有消息都会失败。
Producer每隔30s(由ClientConfig中heartbeatBrokerInterval决定)向所有关联的broker发送心跳,Broker每隔10s中扫描所有存活的连接,如果Broker在2分钟内没有收到心跳数据,则关闭与Producer的连接。

Consumer

Consumer消费者,接收消息进行消费的实例,拥有相同 Consumer Group 的
Consumer 组成一个集群。
Consumer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定。
Consumer每隔30s从Name server获取topic的最新队列情况,这意味着Broker不可用时,Consumer最多最需要30s才能感知。
Consumer每隔30s(由ClientConfig中heartbeatBrokerInterval决定)向所有关联的broker发送心跳,Broker每隔10s扫描所有存活的连接,若某个连接2分钟内没有发送心跳数据,则关闭连接;并向该Consumer Group的所有Consumer发出通知,Group内的Consumer重新分配队列,然后继续消费。
当Consumer得到master宕机通知后,转向slave消费,slave不能保证master的消息100%都同步过来了,因此会有少量的消息丢失。但是一旦master恢复,未同步过去的消息会被最终消费掉。

Elasticsearch

当前版本7.8.0(2020-6-28)

集群架构

绿色——最健康的状态,代表所有的主分片和副本分片都可用;
黄色——所有的主分片可用,但是部分副本分片不可用;
红色——部分主分片不可用。此时执行查询部分数据仍然可以查到

  1. 启动过程:
    es使用发现(discovery)模块(根据配置文件中的集群名称)对网络进行多播,找到拥有相同集群名称的其他节点,由一个节点被选为主节点,负责集群状态以及集群拓扑变化时做出反应,分发索引分片至集群相应节点。对等架构决定了主(管理)节点并不重要。
    管理节点读取集群状态信息,有必要会进行恢复(recovery)处理。会简称由哪些索引分片,决定哪些分片作为主分片,之后集群进入黄色状态。集群可以查询,但是吞吐量以及其他情况未知(主分片就绪副本未分配)。接下来会寻找冗余的分片作为副本,如果某个主分片副本数量过少,管理节点将决定基于某个主分片创建分片和副本,一切顺利则进入绿色状态。
  2. 故障检测
    管理节点会监控所有可用节点,如果任何节点再预定义的超时时间内不响应,则认为该节点已经断开并启动错误处理过程。意味着可能要在集群 - 分片直接重新平衡,选择在新的主节点等。
  3. 与Elasticsearch通信
    每个es功能模块都有一个API,主API是基于Restful。也可以使用Java API进行节点间通信。
    索引数据API:可以通过REST API,也可以通过bulk API或者 UDP bulk API一次发送多个文档至集群。建索引操作只会发生在主分片上,而不是副本。如果索引请求发送到副本节点,会被转发到争取的主分片节点,然后该节点会把索引请求群发给所有副本,等他们响应(比如达到规定数目的副本都完成了更新时)结束索引过程。
    查询数据API:使用查询DSL可以做下面这些事情
  • 使用各种查询类型,包括简单词项查询、短语查询、布尔查询、模糊查询、区间查询、通配符查询、空间查询、以及具备人类可读的打分控制功能的函数查询等
  • 组合简单查询构建复杂查询
  • 文档过滤,在不影响评分的前提下抛弃那些不满足特定查询条件的文档
  • 查找与特定文档相似的文档
  • 查找特定短语的查询建议和拼写检查
  • 使用切面构建动态导航和计算各种统计量
  • 使用预搜索(prospective search)和查找与指定文档匹配的query集合

查询不是简单单步骤的操作,一般分为两个阶段:分散阶段(scatter phase)和合并阶段(gather phase)。在分散阶段将查询分发到包含相关文档的多个分片中执行查询,合并阶段从众多分片中收集返回结果,对它们进行合并、排序、进行后续处理并返回客户端。

节点和分片(物理设计)

默认情况下每个索引由5个主要分片组成,每个主要分片有一个副本。一个分片是一个目录中的文件,Lucene用这些文件存储索引数据,分片也是Elasticsearch将数据从一个节点迁移到另一个节点的最小单位。
多个节点使用同样的集群名称(cluster.name)启动,可以连接集群中的任一节点访问完整数据集。

默认情况下,索引一篇文档时系统首先根据文档ID的散列值选择一个主分片,并将文档发送到该主分片,然后发送给该主分片的所有副本分片进行索引。搜索时就可以在主副分片之间进行负载均衡。
索引的每篇文档都有一个ID,ID经过了散列处理。索引的每个分片有一个散列的取值范围。索引的文档会分发到散列范围包含该文档ID散列值的分片。散列的ID被称为路由值(routing value),将文档分配到某个分配的过程称为路由(routing)。

一个分片是Lucene的索引:一个包含倒排索引的文件目录。

选主算法

Elasticsearch可以使用广播(broadcast)或者单播(unicast)来发现另一个节点。发现节点后会确认主节点,负责管理集群的状态(当前的设置和集群中分片、索引以及节点的状态)。会建立内部ping机制来确保每个节点在集群中保持活跃和健康,被成为错误识别。discovery.zen.minimum_master_nodes最好是节点数除以2加1,能够防止脑裂。
Bully算法
Leader选举的基本算法之一。 它假定所有节点都有一个惟一的ID,该ID对节点进行排序。 任何时候的当前Leader都是参与集群的最高id节点。 该算法的优点是易于实现,但是,当拥有最大 id 的节点处于不稳定状态的场景下会有问题,例如 Master 负载过重而假死,集群拥有第二大id 的节点被选为 新主,这时原来的 Master 恢复,再次被选为新主,然后又假死…
elasticsearch 通过推迟选举直到当前的 Master 失效来解决上述问题,但是容易产生脑裂,再通过 法定得票人数过半 解决脑裂

选主流程
只有一个 Leader将当前版本的全局集群状态推送到每个节点。 ZenDiscovery(默认)过程就是这样的:
每个节点计算最低的已知节点ID,并向该节点发送领导投票
如果一个节点收到足够多的票数,并且该节点也为自己投票,那么它将扮演领导者的角色,开始发布集群状态。
所有节点都会参数选举,并参与投票,但是,只有有资格成为 master 的节点的投票才有效.
有多少选票赢得选举的定义就是所谓的法定人数。 在弹性搜索中,法定大小是一个可配置的参数。 (一般配置成:可以成为master节点数n/2+1)

MongoDB

当前版本4.2.8(2020-6-28)

集群架构

默认数据目录是/data/db,它负责存储所有的MongoDB的数据文件。在MongoDB内部每个数据库都包含一个.ns文件(保存了每张表和每个索引的命名空间元数据)和一些数据文件(foo.0 foo.1 foo.2 递增)。
MongoDB内部有预分配空间的机制,每个预分配的文件都用0填充,这样就能始终保持额外的空间和空余的数据文件。随着表中数据增加,数据文件每新分配一次,大小就会是上一个数据文件的2倍,最大2G。

Replica Sets 复制集

MongoDB支持在多个机器中通过异步复制达到故障转移和实现冗余。多机器中同一时刻只有一台用于写操作。担当Primary角色的机器能把读操作分发给slave。
高可用分为两种:

  • Master-Slave主从复制
    只需要在某一个服务启动时加上 -master,另一个服务加上 -slave与-source参数。不推荐
  • Replica Sets 复制集
    1.6版本推出了新功能复制集,比之前的replication强大,增加来故障自动切换和自动修复成员节点,各个DB之间数据完全一致。

需要创建主从key文件,用于标识集群的私钥完整路径,各个实例的key file内部不一致程序不能正常使用。

Replica Set通过一个日志来存储写操作,这个日志就是oplog。oplog.rs时一个固定长度的capped collection,存在local数据库中。Oplog 其实就像 MySQL 的 Binlog 一样,记录着主节点上执行的每一个操作,而 Secondary 通过复制 Oplog 并应用的方式来进行数据同步。
选择同步源节点
Replica Sets中的节点从距离它“最近”的节点同步数据,这个“最近”是通过ping的时间来判断的。在节点之间的心跳检测中,会记录ping某个节点和收到响应的时间,通过这个时间的长短,来确定距离的远近,时间越长视为距离越远。知道了和节点之间的距离健康度来确定同步的源节点。
当我们在MongoDB中执行一个写操作时,默认情况下,写操作指令发送后,就认为写操作执行成功了。为了保证系统可用性和数据安全性,我们可以更改配置,当写操作在n个节点(n包括primary,如果n=1,那就是在primary执行成功后返回)都执行成功后,才返回成功。
我们可以通过运行db.adminCommand({replSetGetStatus:1})命令来查看当前的节点状况,在secondary上运行这个命令的时候,能够看到syncingTo这个字段,这个字段的值就表示secondary节点同步数据的源节点。有2个secondary或者更多,链式同步

Replica Sets角色

  • Primary:主服务器,只有一组,处理客户端的请求,一般是读写
  • Secondary:从服务器,有多组,保存主服务器的数据副本,主服务器出问题时其中一个从服务器可提升为新主服务器,可提供只读服务,可设置为允许客户端读,但默认情况下Secondary不允许读,需要设置slaveok参数。
  • Hidden:一般只用于备份节点,不处理客户端的读请求
  • Secondary-Only:不能成为 primary 节点,只能作为 secondary 副本节点,防止一些性能不高的节点成为主节点
  • Delayed:slaveDelay 来设置,为不处理客户端请求,一般需要隐藏
  • Non-Voting:没有选举权的 secondary 节点,纯粹的备份数据节点。
  • Arbiter:仲裁节点,不存数据,只参与选举,可用可不用。通常在拥有偶数个节点的复制集中添加(且仅能添加)一个Arbiter,这样可以使一次选举中达到大多数(majority)而避免选举分裂(split vote)。

选主算法

pv0: 基于priority 和 optime 选举新主,依赖clock synchronization。
有选举权的节点,每一轮选举最多投一票,在30s内,不能重复投票。

pv1:基于Raft协议,每个成员都有 对候选主列表成员投赞成或者反对票,不是单方面否决选举,没有节点投反对票,且获得赞成票数超过有权投票节点总数的1/2,则能成为Primary。否则进入下一轮选举。
因使用了Raft协议,加快 back-to-back选主,减少整个选举新主所需花费的总时间,相应的会增加WriteConcern(w:1)rollback的可能性。
Raft将时间分为多个term,term以连续的整数来标识,每个term以一次election开始,如果有server被选为leader,则该term的剩余时间该server都是leader。

3.2之前(只支持pv0 协议),>= 3.2开始:支持pv0 协议、pv1协议,默认是pv1协议。

Sharding 分片

将海量的数据水平扩展的数据库集群系统,数据分表存储在sharding的各个节点上。
MongoDB的数据分块称为chunk,每个chunk都是collection中一段连续的数据记录,通常最大尺寸是200MB,超出则生成新的数据块
一个MongoDB Sharding Cluster,需要三种角色:

  • Shard Server
    存储实际数据的分片,每个Shard可以是一个mongod实例,也可以是一组mongd实例构成的Replica Set。为了实现每个Shard内部的auto-failover,MongoDB官方建议每个Shard为一组Replica Set。
  • Config Server
    为了将一个特定的collection存储在多个shard中,需要为该collection指定一个shard key。shard key可以决定记录归属于那个chunk。Config Server就是用来存储所有shard节点的配置信息,每个chunk的shard key范围、chunk在各shard的分布情况、该集群中所有的DB和collection的sharding配置信息。
  • Route Process
    这是一个前端路由,客户端由此接入,询问Config Servers需要到哪个Shard上查询或保存记录,再连接相应的Shard操作,最后将结果返回给客户端。客户端值需要将原本发给mongod的查询更新发给RoutingKey Process,而不关心操作记录存储在哪个Shard上。

整合 Replica Sets+Sharding

MongoDB Auto-Sharding 解决来海量存储和动态扩容的问题,“Replica Sets+Sharding”的方案:

  • Shard:使用Replica Sets,确保每个数据节点都具有备份、自动容错转移、自动恢复能力
  • Config:使用3个配置服务器,确保元数据完整性
  • Route:使用3个路由进程,实现负载平衡,提高客户端接入性能。

选主算法

Consensus一致性这个概念,它是指多个服务器在状态达成一致,但是在一个分布式系统中,因为各种意外可能,有的服务器可能会崩溃或变得不可靠,它就不能和其他服务器达成一致状态。这样就需要一种Consensus协议,一致性协议是为了确保容错性,也就是即使系统中有一两个服务器当机,也不会影响其处理过程。
  为了以容错方式达成一致,我们不可能要求所有服务器100%都达成一致状态,只要超过半数的大多数服务器达成一致就可以了,假设有N台服务器,N/2 +1 就超过半数,代表大多数了。
  Paxos和Raft都是为了实现Consensus一致性这个目标,这个过程如同选举一样,参选者需要说服大多数选民(服务器)投票给他,一旦选定后就跟随其操作。Paxos和Raft的区别在于选举的具体过程不同。

Paxos算法原理

Paxos算法是什么?
Paxos算法运行在允许宕机故障的异步系统中,不要求可靠的消息传递,可容忍消息丢失、延迟、乱序以及重复。它利用大多数 (Majority) 机制保证了2F+1的容错能力,即2F+1个节点的系统最多允许F个节点同时出现故障。
一个或多个提议进程 (Proposer) 可以发起提案 (Proposal),Paxos算法使所有提案中的某一个提案,在所有进程中达成一致。系统中的多数派同时认可该提案,即达成了一致。最多只针对一个确定的提案达成一致。

Paxos算法角色

Proposer: 提出提案 (Proposal)。Proposal信息包括提案编号 (Proposal ID) 和提议的值 (Value)。
Acceptor:参与决策,回应Proposers的提案。收到Proposal后可以接受提案,若Proposal获得多数Acceptors的接受,则称该Proposal被批准。
Learner:不参与决策,从Proposers/Acceptors学习最新达成一致的提案(Value)

Raft算法

Raft将系统中的角色分为领导者(Leader)、跟从者(Follower)和候选人(Candidate):
Leader:接受客户端请求,并向Follower同步请求日志,当日志同步到大多数节点上后告诉Follower提交日志。
Follower:接受并持久化Leader同步的日志,在Leader告之日志可以提交之后,提交日志。
Candidate:Leader选举过程中的临时角色。

Raft算法原理

Raft 使用心跳(heartbeat)触发Leader选举。当服务器启动时,初始化为Follower。Leader向所有Followers周期性发送heartbeat。如果Follower在选举超时时间内没有收到Leader的heartbeat,就会等待一段随机的时间后发起一次Leader选举。
Follower将其当前term加一然后转换为Candidate。它首先给自己投票并且给集群中的其他服务器发送 RequestVote RPC (RPC细节参见八、Raft算法总结)。结果有以下三种情况:

  • 赢得了多数的选票,成功选举为Leader;
  • 收到了Leader的消息,表示有其它服务器已经抢先当选了Leader;
  • 没有服务器赢得多数的选票,Leader选举失败,等待选举时间超时后发起下一次选举。

Nocos

Nacos中的服务注册数据被设计为五层结构,包括Namespace、Group、Service、Cluster、Instance。
NACOS选举机制的底层原理是RAFT共识算法,NACOS没有依赖诸如zookeeper之类的第三方库,而是自实现了一套RAFT算法。
相较于大名鼎鼎的Paxos算法,RAFT算法最突出的优势就是易于理解,学习起来很轻松。
在RAFT算法领域中,有三种基本的状态(角色):follower、candidate、leader。
处于follower状态的server不会发起任何的request,只是被动的响应leader和candidate。
处于leader状态的server会主动的发送心跳包给各个follower,并且接收client所有的request。
而candidate是一种过渡状态,只有整个cluster在进行新的选举的时候,才会出现此种状态的server。

注册中心选型

C是所有节点在同一时间看到的数据是一致的;
A的定义是所有请求都会收到响应。

1 eureka AP
eureka 保证了可用性,实现最终一致性。
Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册或时如果发现连接失败,则会自动切换至其它节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性),其中说明了,eureka是不满足强一致性,但还是会保证最终一致性
2 zookeeper CP
zookeeper在选举leader时,会停止服务,直到选举成功之后才会再次对外提供服务,这个时候就说明了服务不可用,但是在选举成功之后,因为一主多从的结构,zookeeper在这时还是一个高可用注册中心,只是在优先保证一致性的前提下,zookeeper才会顾及到可用性
选型依据:
在粗粒度分布式锁,分布式选主,主备高可用切换等不需要高 TPS 支持的场景下有不可替代的作用,而这些需求往往多集中在大数据、离线任务等相关的业务领域,因为大数据领域,讲究分割数据集,并且大部分时间分任务多进程 / 线程并行处理这些数据集,但是总是有一些点上需要将这些任务和进程统一协调,这时候就是 ZooKeeper 发挥巨大作用的用武之地。
但是在交易场景交易链路上,在主业务数据存取,大规模服务发现、大规模健康监测等方面有天然的短板,应该竭力避免在这些场景下引入 ZooKeeper,在阿里巴巴的生产实践中,应用对 ZooKeeper 申请使用的时候要进行严格的场景、容量、SLA 需求的评估。
所以可以使用 ZooKeeper,但是大数据请向左,而交易则向右,分布式协调向左,服务发现向右。

配置中心过程

简单总结一下刚刚分析的整个过程。

  • 客户端发起长轮训请求,
  • 服务端收到请求以后,先比较服务端缓存中的数据是否相同,如果不通,则直接返回
  • 如果相同,则通过schedule延迟29.5s之后再执行比较
  • 为了保证当服务端在29.5s之内发生数据变化能够及时通知给客户端,服务端采用事件订阅的方式来监听服务端本地数据变化的事件,一旦收到事件,则触发DataChangeTask的通知,并且遍历allStubs队列中的ClientLongPolling,把结果写回到客户端,就完成了一次数据的推送
  • 如果 DataChangeTask 任务完成了数据的 “推送” 之后,ClientLongPolling 中的调度任务又开始执行了怎么办呢?很简单,只要在进行 “推送” 操作之前,先将原来等待执行的调度任务取消掉就可以了,这样就防止了推送操作写完响应数据之后,调度任务又去写响应数据,这时肯定会报错的。所以,在ClientLongPolling方法中,最开始的一个步骤就是删除订阅事件

所以总的来说,Nacos采用推+拉的形式,来解决最开始关于长轮训时间间隔的问题。当然,30s这个时间是可以设置的,而之所以定30s,应该是一个经验值。

服务如何发现

服务注册到注册中心后,服务的消费者就可以进行服务发现的流程了,消费者可以直接向注册中心发送获取某个服务实例的请求,这种情况下注册中心将返回所有可用的服务实例给消费者,但是一般不推荐这种情况。另一种方法就是服务的消费者向注册中心订阅某个服务,并提交一个监听器,当注册中心中服务发生变更时,监听器会收到通知,这时消费者更新本地的服务实例列表,以保证所有的服务均是可用的。
Nacos 客户端进行服务注册有两个部分组成,一个是将服务信息注册到服务端,另一个是像服务端发送心跳包,这两个操作都是通过 NamingProxy 和服务端进行数据交互的。
Nacos 客户端进行服务订阅时也有两部分组成,一个是不断从服务端查询可用服务实例的定时任务,另一个是不断从已变服务队列中取出服务并通知 EventListener 持有者的定时任务。

HostReactor#getServiceInfo
维护了一个serviceInfoMap,顾名思义,维护了serverList的信息,key值是serverName,value是ServiceInfo;类中还有一个定时任务ScheduledExecutorService
getServiceInfo()方法主要逻辑是:
1、先从已经存在的serviceInfoMap中通过serverName获取一个ServiceInfo,如果已经有了,需要再判断,另一个updatingMap是否存在这个key,如果存在,在wait 5秒,这个时间是写死的。在返回ServiceInfo前,调用scheduleUpdateIfAbsent()方法更新。
2、如果上面第一步serviceInfoMap不存在,则将传来的参数(erviceName, clusters, env)构建一个ServiceInfo,同时维护到serviceInfoMap和updatingMap中,同时根据allIPs这个参数的不同(我断点时为false)调用不同的接口去Nacos服务端拉取数据,通过方法updateService4AllIPNow 和 updateServiceNow,最后与上一步一样,调用scheduleUpdateIfAbsent方法。
3、scheduleUpdateIfAbsent方法,维护另一个map --futureMap

1.客户端组装自己的服务信息,然后向服务端发起注册请求
2.服务端接收到注册请求后,将服务信息加入到本地缓存,并且加入定时,不断的检测服务是否健康,更新或删除注册的服务
3.客户端会在启动时,spring创建feign bean的时候会去从Nacos服务端获取service信息,并且加入定时,不断的拉取feign对应模块的服务信息
4.客户端在通过feign调用的时候,会通过负载均衡算法,从本地缓存中选择一个服务进行调用
5.服务端接收到发现请求时,会根据条件从服务端本地缓存中获取对应的实例,封装好返回给客户端

分布式系统之Quorum机制

  1. Quorum介绍
    1.1 Write all read one(WARO)
    WARO的意思是:在更新时写所有副本,只有在所有副本中更新成功才算成功 ,保证了所有副本中数据的一致性,读取时可以读任意副本数据。
    1.2 Quorum机制
    对WARO条件进行松弛,对读写服务可用性做折中。
    1.3 增强Quorum机制(读取最新成功提交数据)
    为了能够保证系统的强一致性,系统应该返回最新成功提交数据,需要对Quorum机制进行条件增强。
  2. 基于Quorum机制选择primary
    当Quorum机制与Primary-Secondary协议结合时,可以通过读取primary的方式读取最新成功提交的数据。
    在primary-secondary协议中,primary负责进行更新同步操作。在primary-secondary协议中引入Quorum机制,primary成功更新W个节点(含本身)后向用户返回成功。
posted @ 2020-08-10 09:54  陈晨_软件五千言  阅读(834)  评论(0编辑  收藏  举报