在分布式系统中,数据存储有两种,一种是共用一个持久化数据库,这种自带一致性;一种是数据在内存中,需要自己用一定方案保证集群中多节点数据一致性。 下面讨论的针对的是内存中数据在集群中的同步。
1. CAP 理论
CAP定理主要关注的是分布式系统中的数据一致性、可用性和分区容错性,特别是在分布式集群内部的数据管理和通信。它不直接涉及客户端和服务端的交互,而是关于在分布式环境中的多个节点如何协同工作以提供服务。
在分布式系统中,无法同时保证以下三个特性:
Consistency(一致性):在所有节点看到的数据是一致的,即读操作总是返回最近写入的值,无论读请求发送到哪个节点。
Availability(可用性):每个非故障节点对读写请求总是能做出响应,且不保证返回最新数据。
Partition Tolerance(分区容错性):系统能够容忍网络分区(部分节点间的通信中断)而继续运行。
因为必须保证高可用,所以分区容错性P是必须要保证的,也就是必须做多节点部署。在网络分区的情况下,无法同时保证一致性与可用性。这是因为当网络分区发生时,节点之间无法通信,系统必须做出选择:
如果优先保证一致性,那么在无法确认所有节点状态一致之前,系统可能会拒绝某些请求,以防止数据冲突,这样就牺牲了可用性。
如果优先保证可用性,系统将继续接受和处理请求,但可能会返回旧的数据或者部分节点的数据,从而牺牲了一致性。
因此,在设计分布式系统时,通常需要在CAP三者之间进行权衡,根据业务场景和需求选择合适的折衷方案。许多现代分布式系统倾向于牺牲一致性(或弱化一致性模型,如最终一致性)以保证可用性和分区容错性。
2. 一致性
1. 为什么需要一致性
- 数据不能存在单个节点(主机)上,否则可能出现单点故障。
- 多个节点(主机)需要保证具有相同的数据。
2. 分类
强一致性: 保证系统改变提交以后立即改变集群的状态,模型:Paxos、Raft(muti-paxos)、ZAB(muti-paxos)。muti-paxos 是一种解决分布式一致性问题的基础算法,而 Multi-Paxos 则是用于处理多轮提案(propose)的 Paxos 实现,使得系统能够处理连续的、多阶段的决策过程。
弱一致性: 也叫最终一致性,系统不保证改变提交以后立即改变集群的状态,但是随着时间的推移最终状态是一致的。模型:DNS系统、Gossip协议
从单主和多主的角度对协议进行分类。
- 多主协议:即整个集群中不只存在一个主节点,从多个主节点出发传输数据,传输顺序具有随机性,因而数据的有序性无法得到保证,只保证最终数据的一致性。比如著名的gossip、pow 协议。
- 单主协议:即整个分布式集群中只存在一个主节点,主节点发出数据,传输给其余从节点,能保证数据传输的有序性。采用这个思想的主要有2PC, Paxos, Raft等。
3. 实例
- Google的Chubby分布式锁服务,采用了Paxos算法
- etcd分布式键值数据库,采用了Raft算法
- ZooKeeper分布式应用协调服务,Chubby的开源实现,采用ZAB算法
- Gossip: Redis Cluster,Apache Cassandra,Apache Mesos
3. 多主协议
多主协议只保证最终一致性,允许多个节点并发写,能够显著提升系统性能。由于多主协议一般提供的都是最终一致性,所以常用在对数据一致性要求不高的场景中。
1. gossip
1. 简介
Gossip protocol 也叫 Epidemic Protocol (流行病协议)。原本用于分布式数据库中节点同步数据使用,后被广泛用于数据库复制、信息扩散、集群成员身份确认、故障探测等。
简单的理解就是集群以扩散的方式传播消息,每个节点收到信息之后随机选择其他几个节点进行发送。
例子:
Goosip 协议的信息传播和扩散通常需要由种子节点发起。整个传播过程可能需要一定的时间,由于不能保证某个时刻所有节点都收到消息,但是理论上最终所有节点都会收到消息,因此它是一个最终一致性协议。
Gossip协议是一个多主协议,所有写操作可以由不同节点发起,并且同步给其他副本。Gossip内组成的网络节点都是对等节点,是非结构化网络。
2. 消息传播和通信
Gossip 协议的消息传播方式有两种:
- Anti-Entropy(反熵传播):是以固定的概率传播所有的数据。所有参与节点只有两种状态:Suspective(病原)、Infective(感染)。这种节点状态又叫做simple epidemics(SI model)。过程是种子节点会把所有的数据都跟其他节点共享,以便消除节点之间数据的任何不一致,它可以保证最终、完全的一致。缺点是消息数量非常庞大,且无限制;通常只用于新加入节点的数据初始化。
- Rumor-Mongering(谣言传播):是以固定的概率仅传播新到达的数据。所有参与节点有三种状态:Suspective(病原)、Infective(感染)、Removed(愈除)。这种节点状态又叫做complex epidemics(SIR model)。过程是消息只包含最新 update,谣言消息在某个时间点之后会被标记为 removed,并且不再被传播。缺点是系统有一定的概率会不一致,通常用于节点间数据增量同步。
Gossip 协议最终目的是将数据分发到网络中的每一个节点。根据不同的具体应用场景,网络中两个节点之间存在三种通信方式:
- 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 的收敛速度也是最快的。
3. 确保数据同步到所有节点以及处理重复接收消息的两个关键机制:
到达所有节点:
重复接收:
4. 优劣
它具备以下优势:
- 可扩展性:允许节点的任意增加和减少,新增节点的状态最终会与其他节点一致。
- 容错性:任意节点的宕机和重启都不会影响 Gossip 消息的传播,具有天然的分布式系统容错特性。
- 去中心化:无需中心节点,所有节点都是对等的,任意节点无需知道整个网络状况,只要网络连通,任意节点可把消息散播到全网。
同样也存在以下缺点:
- 消息延迟:节点随机向少数几个节点发送消息,消息最终是通过多个轮次的散播而到达全网;不可避免的造成消息延迟。
- 消息冗余:节点定期随机选择周围节点发送消息,而收到消息的节点也会重复该步骤;不可避免的引起同一节点消息多次接收,增加消息处理压力。
- 拜占庭问题:如果有一个恶意传播消息的节点,Gossip协议的分布式系统就会出问题。
5. 适用场景
适合于AP场景的数据一致性处理,常见应用有:P2P网络通信、Apache Cassandra、Redis Cluster、Consul。
实际就是基于算力来决定,有可能出现算力一致。
4. 单主协议
一种常见的用于保证数据一致性的方式。在这种模式下,只有一个节点担任主节点的角色,负责处理所有的写操作,并同步这些更改到其他从节点。这种方式通常用于需要强一致性的场景,例如数据库复制或者分布式键值存储。常见的有:Paxos、Raft(muti-paxos)、ZAB(muti-paxos)
1. Paxos:
Paxos 是由 Leslie Lamport 提出的一种分布式一致性算法。它允许一组节点(称为接受者或服务器)在面临网络延迟和故障的情况下就提案达成一致。
Paxos 的核心思想是通过多轮提议和投票来确保只有一个提案被接受。它分为提议阶段、准备阶段和提交阶段,通过多数派原则保证决策的正确性。
角色:(一个进程可以充当多个角色)
proposer: 向集群提出议案,在发生冲突时候起到冲突调节作用
acceptor: 对提议投票 只有在达到多数派时提议才会被最终接受
leader: 提议接受者, 对集群一致性没什么影响,单纯的记录人员
proposal:proposer 提出的议案或建议, 编号n 或者内容value。
算法目标:每个proposer、acceptor、leadner 都认为同一个proposal 中的value 被选中
两大阶段:
- prepare 阶段
- Prepare(N) 请求,proposer 提出一个proposal,编号为N(编号应该是递增的,大于等于之前所有已经提出的编号),向所有的Acceptor广播。(这里只有编号没有内容)
- promise(n,value): 如果N 大于该acceptor 此前接受的所有提案编号就接受(并承诺不在接受比N小的提议), 否则就拒绝。 如果该acceptor 已经存在相同的甜就返回这个提案的编号和内容(返回空值就表示接收)
- accept 阶段
- Accept(N,value) 请求:proposer 收到多数派(多于一半的acceptor返回的promise), 如果存在编号大的议案就进行更新value, 范泽返回本次议案的value。(收到议案的n 与提出的编号N 无关)
- accepted(N)返回: 如果在此期间没有任何编号大于N的提案,就接收提案内容,否则拒绝
- 当proposer 收到超过半数的acceptor 的返回值后,达成共识
例子:
过程图如下:
解释:
存在的问题以及解决方案:
2. Raft协议
思想: 集群有一个主节点,所有的写请求打到主节点。主节点然后同步阻塞将数据同步给其他节点,如果半数以上成功,就代表写入成功; 否则写入失败会抛出非法状态异常,可能就需要排查集群是否正常。也就是集群有一半的机器存活,集群就能正常工作。
核心逻辑:
- 领导选举:
(1). Raft 系统中的节点在任何时候都处于三种状态之一:领导者(Leader-处理操作请求,自己处理后同步其他follower 节点)、跟随者(Follower-请求的被动更新着,接收leader 的更新请求)或候选者(Candidate - follower 一定时间内没收到leader 的心跳认为leader 故障,会把自己升为候选者,随机时间到期后自己发起投票给自己投一票)。
(2). 当系统启动或领导者失效时,节点会转换为候选者状态并发起选举,通过请求投票(RequestVote) RPC 向其他节点寻求支持。
(3). 基于任期(Term)的概念,每个选举都有一个独一无二的任期号,确保过时的投票无效,防止选票分裂。
(4). 获得大多数节点支持的候选者将成为新的领导者,负责处理客户端请求和管理日志复制
简单理解:
- 每个节点有一个随机等待时间,到达之后判断是否需要发起集群选主; 有一个term 数值,用于判断当前的选举版本。当集群暂未选出主节点或者没收到主节点心跳,把自己的角色设为CANDIDATE(只要是候选者在未选出主的情况下就会一直向其他节点拉票)、随机等待时间到期之后给自己投一票(term + 1),然后向兄弟节点收集投票结果,假设B先到期
- 兄弟节点A收到B的投票请求后和自己的term 值(term = 0)比较,如果B小于等于A的值,那A投给A自己; 否则,A把自己的角色改为FOLLOWER 从节点,然后把自己的票数term 设为和B节点的一样、重置自己的等待时间. (这里A的voteFor为B,A同时把自己的term 也改为1; 同理,其他节点也一样,都是voteFor = B, term = 1)
- B每次收集完结果之后,判断票数最多(根据voteFor 判断B最多)且超过半数(不包含自己的选举结果),那么就可以设为leader 主节点(将自己设为leader), 这是B已经知道自己是主节点
- 主节点每次心跳会将自己的信息同步给兄弟节点(只有主节点会发心跳给其他节点,其他节点记录主节点等信息-com.alibaba.nacos.naming.consistency.persistent.raft.RaftCore.HeartBeat#sendBeat)。
总数: 票数是根据voteFor 汇总得到的,需要大于一半以上(n/2 + 1); 判断是否给其他节点投票是根据term 判断的(你大于我的term我投给你)
- 数据同步
(1). 所有的写请求会到达leader 节点(如果请求打到非leader 节点,会转发给leader 节点)
(2). leader 同步将数据发给其他兄弟节点,超过半数回复正常才会认为写入成功; 否则会抛出非法状态异常,这时候可能需要排查 集群状态 - 几个核心问题:
参考: nacos raft 实现,源码: com.alibaba.nacos.naming.consistency.persistent.raft.RaftCore
3. ZAB 协议
ZAB 是 Apache ZooKeeper 项目使用的一种一致性算法,它结合了Paxos和Multi-Paxos的思想,用于实现原子广播(Atomic Broadcast)。
ZAB 专注于提供高可用性和一致性保证,特别是为分布式协调服务如Zookeeper设计的。
ZAB 包括了领导者选举和原子广播两个主要阶段。在选举阶段,一个节点成为领导者,其他节点成为跟随者。一旦选举完成,领导者将负责处理所有事务并广播给跟随者。
与 Raft 类似,ZAB 也依赖于领导者来保证一致性,但它的设计更侧重于满足分布式协调服务的需求,如保证数据的最终一致性。
4. Raft、zab 区别
设计目标:Raft协议的设计目标是易于理解和实现,而ZAB协议则是为ZooKeeper这样的系统设计的,注重高可用性和高性能。
日志和提案标识:Raft使用术语(Term)和索引(Index)来唯一标识每个提案,而ZAB使用Epoch(时代)和计数器(Count)的组合。
选举逻辑:在选举过程中,Raft的follower简单地根据term投票,而ZAB的follower在投票给一个leader之前,必须确保自己的日志与leader的日志一致。
日志匹配:在ZAB中,follower必须与leader的日志达到同步才能成为follower,而在Raft中,follower通常只需确认term即可。
心跳机制:在Raft和ZAB中,心跳都是从leader到follower,以检测节点的存活状态。然而,ZAB的心跳还用于同步和确认事务。
数据流向:在Raft中,数据流仅从leader到follower,确保所有节点都有相同的日志状态。ZAB也有类似的机制,但可能更关注确保领导者和跟随者之间的事务同步。
一致性模型:Raft使用一种称为“日志复制”的模型,确保了线性一致性。ZAB也实现了类似的一致性模型,但更侧重于快速恢复和高可用性。
应用范围:Raft被广泛应用于各种分布式系统,而ZAB是ZooKeeper专有的协议。
5. 无主协议
无主协议,也可以理解为多主协议。只是不是属于通用协议。节点都是同角色的兄弟节点。
1. Eureka 集群
Eureka Server之间通过自我复制机制来实现数据同步。
当一个新的Eureka Server加入集群时,它会从现有的Eureka Server那里拉取服务注册表信息,也就是进行全量同步。当节点收到新的
写入请求时,会通过http 接口调用同步到其他兄弟节点。 内嵌Jersey(一种http 框架) http 接口。 读取的时候客户端随机选择节点进行获
取,每个节点存的都是全量数据。
2. nacos 集群 - DISTRO 协议
nacos 有AP\CP, 默认是AP,也就是默认走DISTRO 协议,可以通过配置设置为走Raft 协议。
自研的一致性协议,主要用于在集群节点之间实现数据的快速同步和一致性保证。 思想是实现AP,节点分数据处理,也就是一个节点处理部分数据。 处理完之后通过异步任务,同步到其他节点,趋向于最终一致性。
思路:读请求所有节点都可以处理;写请求是根据service_name 或者 实例信息(ip:port) 进行分片,分到对应的节点进行处理,节点处理完异步通知其他节点进行同步。(目的就是将写入的请求分片分到不同的节点,然后处理完成后同步给集群其他节点;该方式避免同一节点处理大量写入请求造成服务阻塞。)
大致过程:
- 初始化: 节点启动时初始化distro 相关组件,包含数据同步处理器以及服务监听器。
- 全量同步: 新加入集群的节点会请求全量数据,其他节点发送全量数据; 新节点收到全量数据后,进行校验和处理
- 增量同步:全量同步完成后,后续的同步是基于增量同步,只同步新产生的数据。 每个节点定期向其他节点发送增量请求,同步到本地进行处理
- 心跳与确认:节点之前定时发送心跳,以检测对方的存活状态
【当你用心写完每一篇博客之后,你会发现它比你用代码实现功能更有成就感!】