分布式一致性协议实现原理
为什么需要一致性
- 数据不能存在单个节点(主机)上,否则可能出现单点故障。
- 多个节点(主机)需要保证具有相同的数据。
- 一致性算法就是为了解决上面两个问题。
一致性算法的定义
一致性就是数据保持一致,在分布式系统中,可以理解为多个节点中数据的值是一致的。
一致性的分类
- 强一致性
- 说明:保证系统改变提交以后立即改变集群的状态。
- 模型:
- Paxos
- Raft(muti-paxos)
- ZAB(muti-paxos)
- 弱一致性
- 说明:也叫最终一致性,系统不保证改变提交以后立即改变集群的状态,但是随着时间的推移最终状态是一致的。
- 模型:
- DNS系统
- Gossip协议
一致性算法实现举例
- Google的Chubby分布式锁服务,采用了Paxos算法
- etcd分布式键值数据库,采用了Raft算法
- ZooKeeper分布式应用协调服务,Chubby的开源实现,采用ZAB算法
Paxos算法
- 概念介绍
- Proposal提案,即分布式系统的修改请求,可以表示为[提案编号N,提案内容value]
- Client用户,类似社会民众,负责提出建议
- Propser议员,类似基层人大代表,负责帮Client上交提案
- Acceptor投票者,类似全国人大代表,负责为提案投票,不同意比自己以前接收过的提案编号要小的提案,其他提案都同意,例如A以前给N号提案表决过,那么再收到小于等于N号的提案时就直接拒绝了
- Learner提案接受者,类似记录被通过提案的记录员,负责记录提案
一、什么是Paxos
Paxos是一种算法,用于在一组通过异步网络通信的分布式设备之间达成共识。一个或多个客户端向Paxos提出一个值,当大多数(一半以上)运行Paxos的设备同意其中一个提议的value时,我们就达成了共识(Consensus)。Paxos被广泛使用,因为它是第一个被严格证明为正确的共识算法。
二、Paxos的基本角色
1)提议者Proposer:从客户端接收请求(值),然后尝试让接受者接收提议的这些值。
2)接受者:Acceptor:接收提议者提出的特定值,并且让当前提议者知道本设备此前已经接受了什么值。一次接受者的回应代表了针对特定提议值的一次投票。
3)学习者Learner:接收共识的结果。
三、Paxos的基本流程
Paxos是一个2阶段协议,意味着提议者需要和接受者交互两次。
从上层来看:
阶段一:提议者向网络中每一个接受者确认它们是否已经接收过提议,如果没有,则提议一个值。
阶段二:如果一半以上接受者都同意这个值,那么则达成共识。
- Basic Paxos算法
- 步骤
- Propser准备一个N号提案
- Propser询问Acceptor中的多数派是否接收过N号的提案,如果都没有进入下一步,否则本提案不被考虑
- Acceptor开始表决,Acceptor无条件同意从未接收过的N号提案,达到多数派同意后,进入下一步
- Learner记录提案
Basic Paxos算法
- 节点故障
- 若Proposer故障,没关系,再从集群中选出Proposer即可
- 若Acceptor故障,表决时能达到多数派也没问题
- 潜在问题-活锁
- 假设系统有多个Proposer,他们不断向Acceptor发出提案,还没等到上一个提案达到多数派下一个提案又来了,就会导致Acceptor放弃当前提案转向处理下一个提案,于是所有提案都别想通过了。
- Multi Paxos算法
- 根据Basic Paxos的改进:整个系统只有一个Proposer,称之为Leader。
- 步骤
- 若集群中没有Leader,则在集群中选出一个节点并声明它为第M任Leader。
- 集群的Acceptor只表决最新的Leader发出的最新的提案
- 其他步骤和Basic Paxos相同
Multi Paxos算法
- 算法优化
Multi Paxos角色过多,对于计算机集群而言,可以将Proposer、Acceptor和Learner三者身份集中在一个节点上,此时只需要从集群中选出Proposer,其他节点都是Acceptor和Learner,这就是接下来要讨论的Raft算法
Raft算法
- 说明:Paxos算法不容易实现,Raft算法是对Paxos算法的简化和改进
- 概念介绍
- Leader总统节点,负责发出提案
- Follower追随者节点,负责同意Leader发出的提案
- Candidate候选人,负责争夺Leader
Raft算法中的角色
- 步骤:Raft算法将一致性问题分解为两个的子问题,Leader选举和状态复制
- Leader选举
1.每个Follower都持有一个定时器
2.当定时器时间到了而集群中仍然没有Leader,Follower将声明自己是Candidate并参与Leader选举,同时将消息发给其他节点来争取他们的投票,若其他节点长时间没有响应Candidate将重新发送选举信息
3. 集群中其他节点将给Candidate投票
4. 获得多数派支持的Candidate将成为第M任Leader(M任是最新的任期)
5. 在任期内的Leader会不断发送心跳给其他节点证明自己还活着,其他节点受到心跳以后就清空自己的计时器并回复Leader的心跳。这个机制保证其他节点不会在Leader任期内参加Leader选举。
6. 当Leader节点出现故障而导致Leader失联,没有接收到心跳的Follower节点将准备成为Candidate进入下一轮Leader选举
7. 若出现两个Candidate同时选举并获得了相同的票数,那么这两个Candidate将随机推迟一段时间后再向其他节点发出投票请求,这保证了再次发送投票请求以后不冲突
- 状态复制
- Leader负责接收来自Client的提案请求(红色提案表示未确认)
2. 提案内容将包含在Leader发出的下一个心跳中
3. Follower接收到心跳以后回复Leader的心跳
4. Leader接收到多数派Follower的回复以后确认提案并写入自己的存储空间中并回复Client
5. Leader通知Follower节点确认提案并写入自己的存储空间,随后所有的节点都拥有相同的数据
6. 若集群中出现网络异常,导致集群被分割,将出现多个Leader
7. 被分割出的非多数派集群将无法达到共识,即脑裂,如图中的A、B节点将无法确认提案
8. 当集群再次连通时,将只听从最新任期Leader的指挥,旧Leader将退化为Follower,如图中B节点的Leader(任期1)需要听从D节点的Leader(任期2)的指挥,此时集群重新达到一致性状态
ZAB算法
- 说明:ZAB也是对Multi Paxos算法的改进,大部分和raft相同
- 和raft算法的主要区别:
- 对于Leader的任期,raft叫做term,而ZAB叫做epoch
- 在状态复制的过程中,raft的心跳从Leader向Follower发送,而ZAB则相反
Zab 借鉴了 Paxos 算法,是特别为 Zookeeper 设计的支持崩溃恢复的原子广播协议。基于该协议,Zookeeper 设计为只有一台客户端(Leader)负责处理外部的写事务请求,然后Leader 客户端将数据同步到其他 Follower 节点。即 Zookeeper 只有一个 Leader 可以发起提
案。
Zab 协议内容
Zab 协议包括两种基本的模式:消息广播、崩溃恢复。
消息广播
崩溃恢复——异常假设
崩溃恢复——Leader选举
崩溃恢复——数据恢复
Gossip算法
- 说明:Gossip算法每个节点都是对等的,即没有角色之分。Gossip算法中的每个节点都会将数据改动告诉其他节点(类似传八卦)。有话说得好:"最多通过六个人你就能认识全世界任何一个陌生人",因此数据改动的消息很快就会传遍整个集群。
- 步骤:
gossip 是一种弱一致算法,也就是最终一致性算法。
特点:
1,去中心化,集群中各个节点都是对等的。
2,无法保证在某个时刻所有节点状态一致。
3,比较适合小数据量的同步。失败检测、路由同步、Pub/Sub、动态负载均衡
应用:redis 的 sentinel 的同步。 Cassandra集群。
例子:有3个节点A,B,C。对任何一个节点A,以固定频率或一定的概率,将自己的数据及版本号发送到其他节点B,C。对于B,C,在接收到数据后会跟自己的数据进行对比,将新数据保存下来,将A没有的数据发送给A,A可以在接收到数据后给B,C响应。
经过多次交互,最终达到一致状态。
主要针对数据比较稳定的场景,如果数据变化比较频繁,会对网络带宽、CPU资源造成很大的负载。
Gossip 协议最终目的是将数据分发到网络中的每一个节点。根据不同的具体应用场景,网络中两个节点之间存在三种通信方式:推送模式、拉取模式、Push/Pull。
Push: 节点 A 将数据 (key,value,version) 及对应的版本号推送给 B 节点,B 节点更新 A 中比自己新的数据
Pull:A 仅将数据 key, version 推送给 B,B 将本地比 A 新的数据(Key, value, version)推送给 A,A 更新本地
Push/Pull:与 Pull 类似,只是多了一步,A 再将本地比 B 新的数据推送给 B,B 则更新本地
如果把两个节点数据同步一次定义为一个周期,则在一个周期内,Push 需通信 1 次,Pull 需 2 次,Push/Pull 则需 3 次。虽然消息数增加了,但从效果上来讲,Push/Pull 最好,理论上一个周期内可以使两个节点完全一致。直观上,Push/Pull 的收敛速度也是最快的。
————————————————
- 集群启动,如下图所示(这里设置集群有20个节点)
2. 某节点收到数据改动,并将改动传播给其他4个节点,传播路径表示为较粗的4条线
3. 收到数据改动的节点重复上面的过程直到所有的节点都被感染
今天来讲一下 Reids Cluster 的 Gossip 协议和集群操作,文章的思维导图如下所示。
集群模式和 Gossip 简介
对于数据存储领域,当数据量或者请求流量大到一定程度后,就必然会引入分布式。比如 Redis,虽然其单机性能十分优秀,但是因为下列原因时,也不得不引入集群。
单机无法保证高可用,需要引入多实例来提供高可用性单机能够提供高达 8W 左右的QPS,再高的QPS则需要引入多实例单机能够支持的数据量有限,处理更多的数据需要引入多实例;单机所处理的网络流量已经超过服务器的网卡的上限值,需要引入多实例来分流。有集群,集群往往需要维护一定的元数据,比如实例的ip地址,缓存分片的 slots 信息等,所以需要一套分布式机制来维护元数据的一致性。这类机制一般有两个模式:分散式和集中式
分散式机制将元数据存储在部分或者所有节点上,不同节点之间进行不断的通信来维护元数据的变更和一致性。Redis Cluster,Consul 等都是该模式。
而集中式是将集群元数据集中存储在外部节点或者中间件上,比如 zookeeper。旧版本的 kafka 和 storm 等都是使用该模式。
两种模式各有优劣,具体如下表所示:
分散式的元数据模式有多种可选的算法进行元数据的同步,比如说 Paxos、Raft 和 Gossip。Paxos 和 Raft 等都需要全部节点或者大多数节点(超过一半)正常运行,整个集群才能稳定运行,而 Gossip 则不需要半数以上的节点运行。
Gossip 协议,顾名思义,就像流言蜚语一样,利用一种随机、带有传染性的方式,将信息传播到整个网络中,并在一定时间内,使得系统内的所有节点数据一致。对你来说,掌握这个协议不仅能很好地理解这种最常用的,实现最终一致性的算法,也能在后续工作中得心应手地实现数据的最终一致性。
Gossip 协议又称 epidemic 协议(epidemic protocol),是基于流行病传播方式的节点或者进程之间信息交换的协议,在P2P网络和分布式系统中应用广泛,它的方法论也特别简单:
在一个处于有界网络的集群里,如果每个节点都随机与其他节点交换特定信息,经过足够长的时间后,集群各个节点对该份信息的认知终将收敛到一致。
这里的“特定信息”一般就是指集群状态、各节点的状态以及其他元数据等。Gossip协议是完全符合 BASE 原则,可以用在任何要求最终一致性的领域,比如分布式存储和注册中心。另外,它可以很方便地实现弹性集群,允许节点随时上下线,提供快捷的失败检测和动态负载均衡等。
此外,Gossip 协议的最大的好处是,即使集群节点的数量增加,每个节点的负载也不会增加很多,几乎是恒定的。这就允许 Redis Cluster 或者 Consul 集群管理的节点规模能横向扩展到数千个。
Redis Cluster 的 Gossip 通信机制
Redis Cluster 是在 3.0 版本引入集群功能。为了让让集群中的每个实例都知道其他所有实例的状态信息,Redis 集群规定各个实例之间按照 Gossip 协议来通信传递信息。
上图展示了主从架构的 Redis Cluster 示意图,其中实线表示节点间的主从复制关系,而虚线表示各个节点之间的 Gossip 通信。
Redis Cluster 中的每个节点都维护一份自己视角下的当前整个集群的状态,主要包括:
当前集群状态集群中各节点所负责的 slots信息,及其migrate状态集群中各节点的master-slave状态集群中各节点的存活状态及怀疑Fail状态也就是说上面的信息,就是集群中Node相互八卦传播流言蜚语的内容主题,而且比较全面,既有自己的更有别人的,这么一来大家都相互传,最终信息就全面而且一致了。
Redis Cluster 的节点之间会相互发送多种消息,较为重要的如下所示:
MEET:通过「cluster meet ip port」命令,已有集群的节点会向新的节点发送邀请,加入现有集群,然后新节点就会开始与其他节点进行通信;PING:节点按照配置的时间间隔向集群中其他节点发送 ping 消息,消息中带有自己的状态,还有自己维护的集群元数据,和部分其他节点的元数据;PONG: 节点用于回应 PING 和 MEET 的消息,结构和 PING 消息类似,也包含自己的状态和其他信息,也可以用于信息广播和更新;FAIL: 节点 PING 不通某节点后,会向集群所有节点广播该节点挂掉的消息。其他节点收到消息后标记已下线。Redis 的源码中 cluster.h 文件定义了全部的消息类型,代码为 redis 4.0版本。
通过上述这些消息,集群中的每一个实例都能获得其它所有实例的状态信息。这样一来,即使有新节点加入、节点故障、Slot 变更等事件发生,实例间也可以通过 PING、PONG 消息的传递,完成集群状态在每个实例上的同步。下面,我们依次来看看几种常见的场景。
定时 PING/PONG 消息
Redis Cluster 中的节点都会定时地向其他节点发送 PING 消息,来交换各个节点状态信息,检查各个节点状态,包括在线状态、疑似下线状态 PFAIL 和已下线状态 FAIL。
Redis 集群的定时 PING/PONG 的工作原理可以概括成两点:
一是,每个实例之间会按照一定的频率,从集群中随机挑选一些实例,把 PING 消息发送给挑选出来的实例,用来检测这些实例是否在线,并交换彼此的状态信息。PING 消息中封装了发送消息的实例自身的状态信息、部分其它实例的状态信息,以及 Slot 映射表。二是,一个实例在接收到 PING 消息后,会给发送 PING 消息的实例,发送一个 PONG 消息。PONG 消息包含的内容和 PING 消息一样。下图显示了两个实例间进行 PING、PONG 消息传递的情况,其中实例一为发送节点,实例二是接收节点
新节点上线
Redis Cluster 加入新节点时,客户端需要执行 CLUSTER MEET 命令,如下图所示。
节点一在执行 CLUSTER MEET 命令时会首先为新节点创建一个 clusterNode 数据,并将其添加到自己维护的 clusterState 的 nodes 字典中。有关 clusterState 和 clusterNode 关系,我们在最后一节会有详尽的示意图和源码来讲解。
然后节点一会根据据 CLUSTER MEET 命令中的 IP 地址和端口号,向新节点发送一条 MEET 消息。新节点接收到节点一发送的MEET消息后,新节点也会为节点一创建一个 clusterNode 结构,并将该结构添加到自己维护的 clusterState 的 nodes 字典中。
接着,新节点向节点一返回一条PONG消息。节点一接收到节点B返回的PONG消息后,得知新节点已经成功的接收了自己发送的MEET消息。
最后,节点一还会向新节点发送一条 PING 消息。新节点接收到该条 PING 消息后,可以知道节点A已经成功的接收到了自己返回的P ONG消息,从而完成了新节点接入的握手操作。
MEET 操作成功之后,节点一会通过稍早时讲的定时 PING 机制将新节点的信息发送给集群中的其他节点,让其他节点也与新节点进行握手,最终,经过一段时间后,新节点会被集群中的所有节点认识。
节点疑似下线和真正下线
Redis Cluster 中的节点会定期检查已经发送 PING 消息的接收方节点是否在规定时间 ( cluster-node-timeout ) 内返回了 PONG 消息,如果没有则会将其标记为疑似下线状态,也就是 PFAIL 状态,如下图所示。
然后,节点一会通过 PING 消息,将节点二处于疑似下线状态的信息传递给其他节点,例如节点三。节点三接收到节点一的 PING 消息得知节点二进入 PFAIL 状态后,会在自己维护的 clusterState 的 nodes 字典中找到节点二所对应的 clusterNode 结构,并将主节点一的下线报告添加到 clusterNode 结构的 fail_reports 链表中。
随着时间的推移,如果节点十 (举个例子) 也因为 PONG 超时而认为节点二疑似下线了,并且发现自己维护的节点二的 clusterNode 的 fail_reports 中有半数以上的主节点数量的未过时的将节点二标记为 PFAIL 状态报告日志,那么节点十将会把节点二将被标记为已下线 FAIL 状态,并且节点十会立刻向集群其他节点广播主节点二已经下线的 FAIL 消息,所有收到 FAIL 消息的节点都会立即将节点二状态标记为已下线。如下图所示。
需要注意的是,报告疑似下线记录是由时效性的,如果超过 cluster-node-timeout *2 的时间,这个报告就会被忽略掉,让节点二又恢复成正常状态。
Redis Cluster 通信源码实现
综上,我们了解了 Redis Cluster 在定时 PING/PONG、新节点上线、节点疑似下线和真正下线等环节的原理和操作流程,下面我们来真正看一下 Redis 在这些环节的源码实现和具体操作。
Cluster中的每个节点都维护一份在自己看来当前整个集群的状态,主要包括:
- 当前集群状态
- 集群中各节点所负责的slots信息,及其migrate状态
- 集群中各节点的master-slave状态
- 集群中各节点的存活状态及不可达投票
Redis 集群是去中心化的,彼此之间状态同步靠 gossip 协议通信,集群的消息有以下几种类型:
- Meet 通过「cluster meet ip port」命令,已有集群的节点会向新的节点发送邀请,加入现有集群。
- Ping 节点每秒会向集群中其他节点发送 ping 消息,消息中带有自己已知的两个节点的地址、槽、状态信息、最后一次通信时间等。
- Pong 节点收到 ping 消息后会回复 pong 消息,消息中同样带有自己已知的两个节点信息。
- Fail 节点 ping 不通某节点后,会向集群所有节点广播该节点挂掉的消息。其他节点收到消息后标记已下线。