一、CAP理论
1、概述
CAP 定理指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance),三者不可兼得。
- 一致性(C):分布式系统中多个主机之间是否能够保持数据一致的特性。即当系统数据发生更新操作后,各个主机中的数据仍然处于一致的状态。
- 可用性(A):系统提供的服务必须一直处于可用的状态。即对于用户的每一个请求,系统总是可以在有限的时间内对用户做出响应。
- 分区容错性(P):分布式系统在遇到任何网络分区故障时,仍能够保证对外提供满足一致性和可用性的服务。
2、CAP定理
CAP 定理:对于分布式系统,网络环境相对是不可控的,出现网络分区是不可避免的,因此系统必须具备分区容错性。但系统不能同时保证一致性与可用性。即要么 CP,要么 AP。
3、 CAP 的应用
CAP的应用 | 特点 |
---|---|
Zookeeper | Zookeeper 遵循的是 CP 模式,即保证一致性,但牺牲可用性。 |
Consul | Consul 遵循的是 CP 模式,即保证了一致性,但牺牲了可用性。 |
Redis | Redis 遵循的是 AP 模式,即保证了可用性,但牺牲了一致性。 |
Eureka | Eureka 遵循的是 AP 模式,即保证了可用性,但牺牲了一致性。 |
Nacos | Nacos 在做注册中心时,默认是 AP 的。但其也支持 CP 模式,但需要用户提交请求进行转换。 |
Zookeeper CP 模式:
当 Leader 节点中的数据发生了变化后,在 Follower 还没有同步完成之前,整个 Zookeeper集群是不对外提供服务的。如果此时有客户端来访问数据,则客户端会因访问超时而发生重试。由于 Leader 的选举非常快,所以这种重试对于用户来说几乎是感知不到的。所以说,Zookeeper 保证了一致性,但牺牲了可用性。
4、BASE 理论
BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的简写。
- 基本可用:指分布式系统在出现不可预知故障的时候,允许损失部分可用性。
- 软状态:指允许系统数据存在的中间状态,并认为该中间状态的存在不会影响系统的整体可用性。 即允许系统主机间进行数据同步的过程存在一定延时。软状态,其实就是一种灰度状态,过渡状态。
- 最终一致性:强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。 因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要保证系统数据的实时一致性。
- 实时一致性:要求数据内容一旦发生更新,客户端立刻可以访问到最新的数据。 所以,在集群环境下该特性是无法实现的,只存在于单机环境中。
- 最终一致性:数据内容发生更新后,经过一小段时间后,客户端可以访问到最新的数据。
- 强一致性(严格一致性):要求客户端访问到的一定是更新过的新数据
- 弱一致性:允许客户端从集群不同节点访问到的数据是不一致的
BASE 是对 CAP 中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于 CAP 定理逐步演化而来的。
BASE 理论的核心思想:即使无法做到强一致性,但每个系统都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
5、复制状态机
复制状态机(Replicated State Machine,RSM)是分布式系统中实现容错和高可用性的核心技术,其核心思想是通过多个节点(副本)维护相同的状态,并在这些副本之间保持一致的操作顺序。通常是通过复制日志来实现。
该模型可以描述为:多个节点上,从相同的初始状态开始,执行相同的一串命令,产生相同的最终状态。
复制状态机的逻辑图:
在上图中,服务器中的一致性模块(Consensus Modle)接受来自客户端的指令,并写入到自己的日志中,然后通过一致性模块和其他服务器交互,确保每一条日志都能以相同顺序写入到其他服务器的日志中,即便服务器宕机了一段时间。一旦日志命令都被正确的复制,每一台服务器就会顺序的处理命令,并向客户端返回结果。
二、Paxos 算法
Paxos算法的目的是为了解决分布式环境下一致性的问题
多个节点并发操作数据,如何保证在读写过程中数据的一致性,并且解决方案要能适应分布式环境下的不可靠性。
1、Paxos两个组件
- Proposer:提议发起者,处理客户端请求,将客户端的请求发送到集群中,以便决定这个值是否可以被批准
- Acceptor:提议批准者,负责处理接收到的提议,他们的回复就是一次投票。会存储一些状态来决定是否接收一个值
2、Paxos 两个原则
- 安全原则:保证不能做错的事
- 针对某个实例的表决只能有一个值被批准,不能出现一个被批准的值被另一个值覆盖的情况
- 每个节点只能学习到已经被批准的值,不能学习没有被批准的值
- 存活原则:只要有多数服务器存活并且彼此间可以通信,最终都要做到的下列事情
- 最终会批准某个被提议的值
- 一个值被批准了,其他服务器最终会学习到这个值
三、Raft 算法
1、Raft 概述
Raft 算法是一种用于管理分布式系统中复制日志的一致性算法。它通过选举一个领导者(Leader),并让领导者负责管理和协调日志复制,以确保所有节点的数据一致性。
Raft 算法通过领导者选举和日志复制两大核心机制实现分布式系统的数据一致性。其数据处理流程严格遵循状态机复制模型,以下是详细步骤解析:
- 客户端向集群发送写请求(例如:“账户A转账100元”)。
- 若客户端连接的是Follower,Follower返回当前Leader的地址(通过LeaderID字段)。客户端将请求重定向到Leader节点。
- Leader 处理请求
- Leader将客户端请求封装为日志条目(Log Entry),包含:
- Term:当前Leader的任期号(用于检测日志冲突)
- Command:客户端请求的具体操作(如转账指令)
- 将日志条目追加到本地日志(未提交状态)
- Leader 通过 RPC 方式 将 AppendEntries 广播给所有Followers
- Leader将客户端请求封装为日志条目(Log Entry),包含:
- Follower 收到AppendEntries后,会按照以下流程进行处理:
- 任期检查:
- 若请求中的Term < Follower当前Term,拒绝并返回自己的Term。
- 若请求中的Term > Follower当前Term,Follower更新自身Term,若是 Candidate 则转为Follower 状态。
- 日志连续性验证:
- 检查本地日志是否存在索引为PrevLogIndex且任期为PrevLogTerm的条目。
- 若不存在,返回失败,Leader 需回退PrevLogIndex重试(直到找到一致点)。
- 日志追加:
- 删除本地日志中从PrevLogIndex+1开始的所有冲突条目。
- 追加请求中的新条目Entries到本地日志。
- 提交更新:
- 若LeaderCommit > Follower当前提交索引,Followers将本地日志中所有 ≤LeaderCommit 的条目提交到状态机执行。状态机执行后更新系统状态(如账户余额)。
- 任期检查:
- Leader 收到大多数(n/2+1)Follower的成功响应。
- Leader 将其标记为已提交(Committed)
- Leader 更新LeaderCommit索引,并通过下一次AppendEntries通知Followers。
- Leader 执行状态机操作(如修改账户余额)
- 响应客户端
- 成功响应:Leader在日志提交后,返回执行结果给客户端(如“转账成功”)。
- 超时重试:若客户端未收到响应(如网络问题),重复发送请求,Leader通过日志去重保证幂等性。
Raft 将一致性问题分解为了三个子问题:
- Leader Election(领导者选举):当已有的 Leader 故障时必须选出一个新的 Leader。
- Log replication(日志复制):Leader接受来自客户端的命令,记录为日志,并复制给集群中的其他服务器,并强制其他节点的日志与leader保持一致。
- safet(安全措施):通过一些措施确保系统的安全性,如确保所有状态机按照相同顺序执行相同命令的措施。
2、Leader Election
Raft协议中,任一节点时刻处于三个状态中:Leader、Follower、Candidate。
- 所有节点初始状态都是 Follower
- 超时时间内没有收到 Leader 的心跳,则转换为 Candidate 发起选举
- Candidate 收到大多数节点的选票则转换为 Leader
- Candidate 发现 Leader 或者收到更高任期的请求则转换为 Follower
- Leader 在收到更高任期的请求后转换为 Follower
2.1 选举过程
Leader 会不停的给 Follower 发心跳消息,表明自己的存活状态。在一段时间内如果没有收到来自Leader的心跳,follower 就会认为 Leader 挂了,将主动发起选举。
步骤如下:
- 增加节点本地的 current term ,切换到Candidate状态
- 投自己一票
- 并行给其他节点发送 RequestVote RPCs
- 等待其他节点的回复
在这个过程中,根据来自其他节点的消息,可能出现三种结果:
- 收到majority(大多数)的投票(含自己的一票),则赢得选举,成为Leader
- 接收到别的 candidate 发来的新 leader 通知,比较发现新 leader 的 term 并不比自己的 term小,则转变为 follower
- 一段时间内没有收到majority投票,则保持candidate状态,重新发出选举
2.2 选举时机
在很多时候,当 Leader 真的挂了,Follower 几乎同时会感知到,所以它们几乎同时会变为 candidate 发起新的选举。此时就可能会出现较多 candidate 票数相同的情况,即无法选举出 Leader。
为了防止这种情况的发生,Raft 算法其采用了 randomized election timeouts
策略来解决这个问题。其会为这些 Follower 随机分配一个选举发起时间 election timeout,这个 timeout 在 150-300ms 范围内。只有到达了 election timeout 时间的 Follower 才能转变为 candidate,否则等待。那么 election timeout 较小的 Follower 则会转变为 candidate 然后先发起选举,一般情况下其会优先获取到过半选票成为新的 leader。
同时,leader-based 共识算法中,节点的数目都是奇数个,尽量保证majority的出现。
2.3 投票规则
- 在任一任期内,单个节点最多只能投一票
- 候选人知道的信息不能比自己的少
- 哪个候选节点先发消息就给谁投票
2.4 结果分析
结果一:赢得了选举之后,新的leader会立刻给所有节点发消息,广而告之,避免其余节点触发新的选举
结果二:
- 例如有节点A、B、C, A、B同时触发选举。
- A和B同时向其他节点发送 RequestVite [A->B,A->C],[B->A,B->C]。
- 假设C先收到A的请求,C给A投了一票,当C收到B的请求的时候,因为已经给A投过票了,因此就不会给B投票。
- 同时,A和B不会给对方投票。最终,A获得自己和C的投票一共2票胜出,成功当选Leader。
- A胜出后,给B和C发送心跳
- B节点发现A的Term不低于自己,知道已经有Leader产生了,于是切换为Follower
结果三:
- 例如有节点A、B、C、D,A、B同时触发选举
- 如果C、D各投A、B一票的化,就会出现A、B Split Vote(平票)
- 直到超时重新发起选举
2.5 Term 任期
哪个节点做 Leader 是大家投票选举出来的,每个 Leader 工作一段时间,然后选出新的 Leader 继续负责。每一届新的履职期称之为一届任期 (Term)。
在Raft算法中,一个节点的term(任期)会在以下几种情况下变动:
- 节点从Follower转变为Candidate
当一个Follower在选举超时时间内没有收到来自当前Leader的心跳消息时,它会认为当前没有有效的Leader。
随后,该Follower会增加自己的term号(即任期号加1),并转变为Candidate状态,开始发起新一轮的Leader选举。 - Candidate在选举过程中接收到更大的term
在选举过程中,Candidate可能会向其他节点发送RequestVote RPC请求投票。
如果Candidate收到了来自其他节点的包含更大term的ResponseVote RPC响应,或者收到了新的Leader的心跳消息(该心跳消息中的term大于Candidate当前的term),那么Candidate会更新自己的term为收到的较大值,并转变为Follower状态。 - Leader发现自己的term过期
Leader在正常运行过程中会不断发送心跳消息给Follower以维持其Leader地位。
如果Leader发现自己的term已经过期(例如,收到了包含更大term的请求或心跳消息),它会立即转变为Follower状态,并更新自己的term为较大的值。 - 节点重新加入集群时同步term
如果一个节点因为网络故障或其他原因暂时离开了集群,当它重新加入时,它会尝试与集群中的其他节点同步状态。
在同步过程中,该节点可能会发现其他节点的term比自己大,因此它会更新自己的term以与其他节点保持一致。
3、Log Replication
当 Leader 产生之后,系统就可以处理客户端发来的请求了。客户端的一切请求都发送到Leader,由Leader来调度和处理这些请求,并且将请求和执行顺序告知Follower,保证与Follower的状态一致性。
在raft中,leader将客户端请求(command)封装到一个个log entry,将这些log entries复制(replicate)到所有follower节点,然后大家按相同顺序应用(apply)log entry中的command,达到状态的一致。
3.1 AppendEntries
AppendEntries 的作用:
- 日志复制:将新日志条目从Leader同步到Followers。
- 心跳机制:空内容的AppendEntries(不含日志条目)用于维持Leader活跃状态。
- 一致性检查:通过前一条日志的元数据(索引和任期)确保日志连续性。
AppendEntries 通常包含以下参数:
type AppendEntriesRequest struct {
Term int // Leader的当前任期号。防止过期Leader发送无效请求。若Follower的任期更大,则拒绝请求并通知Leader更新
LeaderID int // Leader的ID。Followers据此识别Leader,用于客户端请求重定向或网络分区恢复后快速同步。
PrevLogIndex int // 前一条日志的索引。Follower需检查本地日志在PrevLogIndex处是否存在日志,且任期是否匹配PrevLogTerm。若不匹配,说明日志不连续,触发冲突解决。
PrevLogTerm int // 前一条日志的任期号。与PrevLogIndex配合,确保Leader和Follower的日志在指定位置完全一致。
Entries []LogEntry // 需要复制的日志条目列表(若为空则为心跳)
LeaderCommit int // Leader已提交的最高日志索引。告知Followers可以安全提交到该索引之前的日志(提交后状态机执行操作)。
}
3.2 LogEntry
LogEntry 通常包含:
type LogEntry struct {
Term int // 生成该条目的Leader任期号。用于检测日志冲突(例如,不同任期的Leader可能对同一索引位置写入不同命令)。
Command interface{} // 状态机执行的指令。
}
4、Safety
在分布式系统的算法和设计中,safety和liveness是2个非常重要的属性:
- safety : something “bad” will never happen
- liveness : something “good” will must happen (but we don’t know when)
在任何系统模型下,都需要满足safety属性,即在任何情况下,系统都不能出现不可逆的错误,也不能向客户端返回错误的内容。比如,raft保证被复制到大多数节点的日志不会被回滚,那么就是safety属性。而raft最终会让所有节点状态一致,这属于liveness属性。
Raft 的安全机制通过强 Leader 模型、多数派原则和日志连续性验证,构建了一个闭环的容错系统。
Election Safety: at most one leader can be elected in a given term.
Leader Append-Only: a leader never overwrites or deletes entries in its log, it only appends new entries.
Log Matching: if two logs contain an entry with the same index and term, then the logs are identical in all entries up through the given index.
Leader Completeness: if a log entry is committed in a given term, then that entry will be present in the logs of the leaders for all higher-numbered terms.
State Machine Safety: if a server has applied a log entry at a given index to its state machine, no other server will ever apply a different log entry for the same index.
4.1 Election Safety(选举安全)
核心目标:同一任期内至多只能有一个 Leader,避免脑裂(Split Brain)。
实现机制:
- 任期递增(Term):每个节点的 Term 单调递增,且节点只能投票给 Term ≥ 当前 Term 的候选者。
- 一票一投:每个节点在单个 Term 内只能投出一票(先到先得)。
- 多数派原则:候选者需获得集群多数节点的投票才能成为 Leader。
意义:防止多个 Leader 同时存在导致日志冲突和状态不一致。
例如,在 5 节点集群中,最多只有 1 个候选者能获得至少 3 票成为 Leader。
4.2 Leader Append-Only(Leader 仅追加日志)
核心目标:Leader 只能追加新日志,不能修改或删除已有日志。
实现机制:
- Leader 处理客户端请求时,始终将新日志追加到本地日志末尾。
- 日志冲突时,强制 Follower 回退到与 Leader 一致的日志位置(通过 AppendEntries 的 PrevLogIndex 和 PrevLogTerm 检查)。
意义:防止 Leader 因覆盖或删除日志导致已提交的操作丢失(如已完成的转账被撤销)。
例如,若 Leader 本地日志索引 5 的条目为 Term=3, Command=X,则后续日志只能从索引 6 开始追加。
4.3 Log Matching(日志匹配)
核心目标:若两个节点在相同索引位置有相同 Term 的日志,则两者在该索引之前的所有日志完全一致。
实现机制:
- 每次 AppendEntries RPC 携带前一条日志的 (PrevLogIndex, PrevLogTerm),Followers 需验证本地日志是否匹配。
- 若匹配失败,Leader 回退 PrevLogIndex 并重试,直到找到一致点后强制覆盖冲突日志。
意义:确保所有节点日志在一致点后的内容完全相同,避免出现“部分日志一致、部分不一致”的中间状态。
例如,若 Leader 和 Follower 在索引 4 处 Term 均为 2,则索引 1~4 的日志必然完全一致。
4.4 Leader Completeness(Leader 完整性)
核心目标:已提交的日志条目一定存在于所有更高 Term 的 Leader 日志中。
实现机制:
- 选举限制:候选者发起投票时,需携带本地最新日志的 (Index, Term)。投票节点只会支持日志至少与自己一样新的候选者(通过 RequestVote RPC 的日志比对)。
- 若日志 A 的 Term > 日志 B 的 Term,或 Term 相同但 A 的 Index ≥ B 的 Index,则认为 A 比 B 新。
意义:防止新 Leader 缺失已提交的日志条目,保证已提交操作持久化。
例如,若日志索引 5 在 Term 2 被提交,则 Term 3 的 Leader 一定包含该日志。
4.5 State Machine Safety(状态机安全)
核心目标:若某个节点将某索引位置的日志应用到状态机,则其他节点不可能在该索引应用不同的日志。
实现机制:
- 提交规则:Leader 只能提交当前 Term 的日志(或间接提交更早 Term 的日志),且需确保日志已复制到多数节点。
- 顺序应用:所有节点按日志索引顺序依次将已提交日志应用到状态机。
意义:避免不同节点在同一索引位置应用不同命令,导致状态机状态分歧。
例如,索引 5 的日志若被某节点执行为 Command=X,则其他节点在索引 5 处也必须执行 Command=X。
4.6 安全机制的协同作用
Raft 通过上述机制形成闭环保护:
- 选举安全确保同一 Term 内唯一 Leader。
- Leader Append-Only 和 Log Matching 保证日志传播的一致性。
- Leader Completeness 确保新 Leader 包含所有已提交日志。
- State Machine Safety 最终确保状态机的一致性。
4.7 异常场景下的安全保障
场景 1:旧 Leader 网络隔离后恢复
- 旧 Leader 因 Term 过低无法提交新日志,最终被新 Leader 的日志覆盖。
- 完整性机制确保新 Leader 的日志包含所有已提交条目。
场景 2:Follower 日志与 Leader 冲突
- Leader 通过 PrevLogIndex 和 PrevLogTerm 定位冲突点,强制 Follower 回退并同步日志。
- 日志匹配机制确保冲突点后的日志与 Leader 完全一致。
场景 3:部分节点宕机后重启
- 重启节点通过 AppendEntries 追赶日志,Leader Append-Only 和 日志匹配机制保证其日志与 Leader 一致。
5、日志压缩
Raft 通过 快照(Snapshot)机制实现日志压缩。
5.1 为什么需要日志压缩?
- 存储压力:长时间运行的系统日志会无限增长,占用大量磁盘空间。
- 恢复效率:新节点加入或落后节点追赶时,全量日志传输耗时过长。
- 性能优化:状态机直接加载快照比回放所有历史日志更快。
5.2 快照的核心设计
(1) 快照内容
- 状态机数据:某一时刻系统状态的完整拷贝(如数据库的键值对)。
- 元数据:
- last_included_index:快照对应的最后一条日志的索引。
- last_included_term:该日志的任期号。
- 集群配置信息(成员变更记录)。
(2) 触发条件
- 日志大小阈值:当日志条目数量超过预设值(如 10,000 条)时触发。
- 定期生成:按固定时间间隔(如每小时)生成快照。
- 管理命令:手动触发快照(用于维护或调试)。
5.3 快照工作流程
(1) 生成快照(Leader/Follower)
- 锁定状态机:短暂暂停处理新请求,确保状态一致性。
- 序列化状态:将当前状态机状态序列化为快照文件。
- 保留元数据:记录 last_included_index 和 last_included_term。
- 删除旧日志:丢弃所有索引 ≤ last_included_index 的日志条目。
(2) 传播快照(Leader → Follower)
当 Follower 日志落后过多(所需日志已被快照覆盖)时:
- Leader 发送 InstallSnapshot RPC,包含快照数据和元数据。
- Follower 接收后:
- 丢弃旧日志:删除所有索引 ≤ last_included_index 的日志。
- 加载快照:将快照数据反序列化到本地状态机。
- 更新日志:保留索引 > last_included_index 的日志。
5.4 关键机制与优化
(1) 增量快照
- 问题:生成完整快照可能阻塞系统或消耗大量 I/O。
- 优化:部分系统(如 TiKV)使用增量快照,仅记录状态变化部分,降低生成开销。
(2) 快照与日志协同
- 一致性保障:快照的 last_included_index 和 last_included_term 必须与日志严格对应,避免状态机与日志冲突。
- 日志保留策略:即使生成快照,Leader 可能保留部分近期日志以加速同步。
(3) 网络传输优化
- 分块传输:将大快照拆分为多个块(Chunk),避免一次性传输占用过多带宽。
- 流量控制:动态调整传输速率,避免影响正常日志复制。
5.5 安全性与异常处理
(1) 快照冲突
- 场景:Leader 发送的快照元数据与 Follower 现有日志冲突。
- 处理:Follower 必须验证 last_included_term 和 last_included_index 是否匹配本地日志,若不匹配则拒绝并请求更新数据。
(2) 快照损坏
- 校验和:快照数据包含校验码(如 CRC32),接收方验证完整性。
- 重传机制:若传输失败,Follower 可请求重新发送缺失的快照块。
(3) 领导者变更
- 新 Leader 的职责:新 Leader 必须保留足够日志,确保能向其他节点提供所需的快照或日志条目。
5.6 示例场景
假设一个 3 节点集群:
- Leader 生成快照:
- 当前日志索引 1000,生成快照 S1(last_included_index=1000)。
- 删除索引 ≤1000 的日志。
- Follower 落后:
- Follower A 的日志仅到索引 800,无法通过普通 AppendEntries 同步。
- 快照同步:
- Leader 向 A 发送 InstallSnapshot,A 加载快照后直接恢复至索引 1000 的状态。
- A 后续仅需同步索引 1001 后的新日志。
5.7 对比其他日志压缩方案
方案 | 原理 | 适用场景 | 缺点 |
---|---|---|---|
Raft | 快照 | 定期全量状态转储 | 通用分布式系统 |
Log Cleaning | 合并或删除冗余日志条目 | 日志结构存储(如 LSM-Tree) | 实现复杂,需处理条目依赖关系 |
分段日志 | 将日志分为多个文件,定期归档 | 高吞吐写入场景 | 恢复时需合并多个文件,效率较低 |
Raft 的日志压缩算法通过快照机制有效平衡了存储效率与一致性要求:
- 存储优化:定期清理旧日志,避免无限增长。
- 快速恢复:新节点通过快照快速同步,减少网络传输。
- 安全保证:快照与日志元数据严格绑定,确保状态机一致性。
实际系统中(如 etcd、Consul),快照通常结合压缩策略(如每 10,000 条日志触发)和分块传输,以降低性能影响。
6、应用场景
Raft算法可以应用于各种需要分布式一致性的场景,如:
- 分布式数据库:构建高可用、高性能的分布式数据库,保持多个副本之间的一致性和可靠性。
- 分布式锁:实现分布式的锁机制,确保多个节点之间的锁状态保持一致。
- 分布式缓存:构建分布式的缓存系统,实现缓存的一致性和高可用性。
- 分布式事务:实现分布式事务管理器,确保多个事务操作在分布式系统中的一致性和可靠性。
7、特点与优势
- 易于理解:相对于其他一致性算法(如Paxos),Raft算法通过逻辑分离和明确的角色划分,使得其更易于理解和实现。
- 高可用性和强一致性:Raft算法能够快速选出新的领导者,并保证系统的高可用性。同时,通过严格的日志匹配和日志提交机制,确保了系统的强一致性。
- 广泛应用:Raft算法广泛应用于需要高可用性和高可靠性的分布式系统中,如分布式数据库、分布式文件系统和分布式协调服务等。