系列导读
前四篇我们分别剖析了 Nacos 的全局架构、服务发现的心跳与摘除、配置中心的长轮询与灰度发布、以及支撑服务发现 AP 模型的 Distro 协议。如果说 Distro 是 Nacos “放飞自我”追求极致可用性的右脑,那 Raft 就是它“一丝不苟”守护数据绝对准确的左脑。今天,我们不仅会讲清楚 Raft 在 Nacos 中的落地,还会直面三个生产中最让人困惑的疑问:Leader 宕机后客户端到底读谁的数据?发布配置时会不会出现不同客户端读到新旧两个版本?11 台机器脑裂成 5 台和 6 台时,真没办法保证一致吗? 读完这一篇,你会对配置中心的强一致性有刻进骨子里的理解。


一、为什么配置中心必须用 Raft?

配置中心存储的是“数据库连接串”、“限流阈值”、“开关配置”等直接影响业务行为的关键数据。一个错误的读写可能导致:

  • 发布成功但部分节点没收到,线上新旧配置并存。

  • 节点崩溃后配置丢失,重启后回退到旧版。

  • 网络分区期间出现“脑裂”,两个客户端读到两个值。

这些场景在金融、电商等系统是不可接受的。所以 Nacos 在设计配置中心时,毫不犹豫地选择了 CP 模型:宁可短暂牺牲可用性,也要保证数据绝对一致。

在分布式一致性协议的选择上,Lamport 的 Paxos 理论完备但极其难懂,而 Ongaro 的 Raft 协议 通过明确的角色划分(Leader、Follower、Candidate)和模块化解耦(选举、日志复制、安全)将一致性变得易于工程实现。Nacos 没有重复造轮子,而是直接集成了蚂蚁金服开源的、经过大规模生产验证的 SOFAJRaft 库。


二、Raft 协议的三种角色与状态轮转

在深入代码之前,必须先把 Raft 的三张“脸”认清楚。

2.1 三种角色

  • Leader(领导者):集群唯一(正常情况),处理所有写请求。负责将日志复制到 Follower,并提交。定期发送心跳(AppendEntries)维持权威。

  • Follower(跟随者):被动接收 Leader 的心跳和日志复制。不主动发起请求,只响应 Leader 和 Candidate 的 RPC。如果一段时间没收到心跳,会变成 Candidate。

  • Candidate(候选人):是 Follower 到 Leader 的“竞选状态”,是临时状态。发起投票请求(RequestVote),争取成为 Leader。获得超过半数选票则晋升为 Leader;若选举超时没结果,term+1 后重选。

在 Nacos 中,配置中心底层使用 SOFAJRaft,节点状态完全遵循 Raft 原始定义。你可以通过日志中的 [JRaft] 关键字看到类似 Became Leader at term 6 的状态变化。

2.2 状态轮转(Leader 宕机全过程)

假设集群有 A(Leader)、B(Follower)、C(Follower)三节点。

  1. A 宕机,心跳停止。

  2. B 和 C 各自的随机选举超时(150ms~300ms)触发,谁先超时谁变成 Candidate,并将自己的 term 加 1(比如从 5 变成 6)。

  3. Candidate 给自己投一票,然后向其他节点发送 RequestVote RPC,携带自己的 term 和最新日志 index。

  4. 投票规则:每个节点同一 term 只投一票,且 Candidate 的日志必须至少和自己一样新。

  5. 假设 B 先得到自己 + C 的 2 票,超过 3 节点集群的半票,B 立刻变为 Leader,并发送心跳给 C。C 自动退回为 Follower。

  6. 若 A 恢复,发现心跳来自 B 且 term=6 > 自己的 term=5,A 立即降级为 Follower,接受日志复制。集群恢复。

整个过程通常在 1 秒以内 完成。


三、Leader 宕机时,客户端请求谁的数据?

这是生产中最直接的问题。答案分  和 

3.1 写请求(发布配置)

  • 只能由 Leader 处理。如果客户端向 Follower 发起写,Follower 会拒绝并返回“Not Leader”,同时告知当前 Leader 地址(如果有)。SDK 会自动重试。

  • 在选举窗口(<1s)内,写入暂时失败,SDK 重试直到成功。这就是 CP 模式下牺牲的可用性。

3.2 读请求(获取配置)

Follower 完全可以处理读请求,但有严格的安全机制防止读到脏数据。

Raft 要求 线性一致读,具体通过 SOFAJRaft 的 ReadIndex 机制实现:

  1. Follower 收到读请求后,不直接读本地状态机,而是先向 Leader 发送一次轻量心跳确认,获取当前集群的 最新 committedIndex

  2. Follower 等待自己的状态机应用到至少该 Index。

  3. 应用完成后,从状态机读取数据返回给客户端。

这样确保 任何读请求拿到的都是集群已提交的最新值,与 Leader 一致。即使 Leader 刚刚提交、尚未通知所有 Follower,Follower 通过 ReadIndex 也能立即同步到最新 commit 点,彻底消灭了“已提交但未应用”的时间窗口。

Nacos 的配置读取实际行为

  • 默认开启线性一致读,保证强一致。

  • 同时 Nacos 服务端有内存缓存 CacheItem。若线性一致读暂时不可用(比如集群正在选举,ReadIndex 无法完成),Nacos 可退回本地缓存读,返回最后一次更新值。由于配置变更频率低,这种秒级滞后对绝大多数业务可接受,且绝不会返回未提交的脏数据。如果你要求严格 CP,可通过参数强制必须线性一致读。


四、深入解答:发布过程中会读到两个版本吗?

读者提问:
Leader 写数据,先发数据给半数 Follower 并确认,此时 Leader 提交并应用新版本 V2。但 Leader 要在下一次心跳才通知其余 Follower 该日志已提交。如果此时客户端请求一个还没收到心跳的 Follower,它会不会返回旧版本 V1?造成不同客户端读到两个版本?

答案:在 Raft 线性一致读的保护下,不会发生。

时间线还原:

  1. Leader 将日志复制到 Follower A(加上自己共两票,3 节点集群过半),Leader 提交并应用 V2。

  2. Follower B 此时尚未收到提交消息,其状态机仍是 V1。

  3. 客户端向 Follower B 发起读请求。

如果没有线性一致读,Follower B 直接返回本地状态机,确实会得到 V1。但 Raft 要求 Follower 必须先通过 ReadIndex 与 Leader 确认最新 commit index。流程变为:

  • Follower B 向 Leader 请求最新 committedIndex。

  • Leader 返回最新的 commit index(对应 V2)。

  • Follower B 发现自己的状态机还没应用到该 index,等待应用完成(V2 已在日志中,只是未提交,此时立即应用)。

  • 应用完成后返回 V2。

结果:客户端读到的永远是 V2,不可能出现新旧两个版本。整个确认过程只需一次轻量 RPC,开销极低。


五、极端脑裂:11 台机器分成 5 台和 6 台,数据会错吗?

读者提问:
如果脑裂,11 台机器分成 5 台(集群 A)和 6 台(集群 B)。假设集群 A 拥有更新的数据,集群 B 拥有旧的数据,客户端请求了集群 B 就拿不到最新数据了,这个问题是不是没法解决?

答案:你描述的场景在 Raft 协议下不可能发生,因为“5 台拥有更新数据”违背了 Raft 的多数派提交规则。 而真正的脑裂场景,Raft 早已给出了安全解法。

5.1 为什么 5 台不可能有“更新”的已提交数据?

Raft 的核心铁律:任何日志条目必须被超过半数节点确认才算提交。11 个节点,过半数是 6。因此:

  • 一条已经提交的配置(即对客户端返回成功的配置),必然存在于至少 6 台机器的磁盘上。

  • 当网络分裂成 5 台和 6 台两组时,拥有 6 台的那一组必然包含了所有已提交日志。5 台组最多只能有一些未提交的日志(比如原 Leader 只复制到 5 台就宕机),但这些未提交日志永远不会被当成“最新数据”暴露给客户端。

所以,你的假设“5 台集群有更新的数据”只能指未提交的脏日志,而 Raft 的“已提交”语义自动把这种可能性排除了。

5.2 真实脑裂时,客户端实际行为

场景:11 台分裂为少数派(5 台)和多数派(6 台)。

  • 多数派:能选举出 Leader(6 票过半数),继续承接读写,数据为最新已提交。

  • 少数派:无法选出 Leader(得不到 6 票),所有节点要么是 Follower 要么是 Candidate,无法提交任何新日志

客户端请求

  • 写请求打到少数派:节点拒绝,返回“Not Leader”或超时。SDK 重试到多数派,写入成功。

  • 读请求打到少数派:

    • 若强制线性一致读:节点发现无法联系 Leader 完成 ReadIndex,拒绝读取(返回错误或超时)。客户端重试到多数派,读到最新值。

    • 若允许缓存读退化:节点返回自己最后一次提交的值(V_old),这不是脏数据,而是真实的最后一次全局提交。虽然相比多数派可能滞后,但这是 CP 系统在分区时牺牲一致性换取部分可用性的可配置行为。

结论:绝不会出现两个客户端同时读到不同且矛盾的已提交值。 少数派要么闭嘴(拒绝服务),要么只复述旧闻(返回旧已提交值)。这并非 Raft 无能,而是 CAP 定理的物理边界:网络分区时,不可能同时保证一致性和全可用。Raft 通过“多数派拥有全部真理”这一设计,将脑裂风险转化为一个干净的数学选择。


六、SOFAJRaft:Nacos 使用的 Raft 引擎

回到实现。Nacos 从 1.4.0 版本起引入蚂蚁金服的 SOFAJRaft 作为配置中心的 CP 引擎。核心组件位于 config 模块的 com.alibaba.nacos.config.server.service.raft 包:

  • RaftCore:Raft 核心调度器,负责启动 JRaft 节点、管理生命周期。

  • RaftStore:日志存储接口,默认实现为本地文件(基于 RocksDB 或 SegmentLog)。

  • RaftNetwork:网络通信实现,基于 gRPC。

  • ConfigSM:状态机实现,负责将提交的日志应用到 Nacos 的配置存储中(写入数据库 + 更新缓存 + 发布事件)。


七、一次配置发布的 Raft 全程走读(带读一致性视角)

我们以“配置发布”为例,看看 Raft 如何保障从写入到读取全程一致。

  1. 控制台发起变更 → ConfigController.publishConfig()

  2. 委托 Raft → RaftConsistencyServiceImpl.put(dataId, group, tenant, content)

  3. Leader 提案:若当前节点不是 Leader,JRaft 自动转发到 Leader。Leader 将“配置变更”包装为日志条目。

  4. 日志复制:Leader 并行向所有 Follower 发送 AppendEntries RPC。

  5. 过半确认:当 Leader 确认超过半数节点(如 3 节点集群中 2 台)已持久化该日志后,标记日志为“已提交”。

  6. 状态机应用:JRaft 回调 ConfigSM.onApply(iter)

    public void onApply(Iterator iter) {
        while (iter.hasNext()) {
            ByteBuffer data = iter.next().getData();
            ConfigChangePacket packet = (ConfigChangePacket) Serializer.deserialize(data);
            configPersistService.updateOrInsert(packet);  // 持久化到DB
            configCacheService.updateOrInsert(packet);    // 更新内存缓存
            NotifyCenter.publishEvent(new ConfigDataChangeEvent(...)); // 通知客户端
            iter.next();
        }
    }
  7. 返回成功:Leader 向控制台返回“发布成功”。

  8. 读请求处理:任意节点收到读请求后,通过 ReadIndex 与 Leader 同步 commit 点,确保状态机包含该日志后返回最新配置。不会出现旧值。

即使 Leader 在第 7 步后立刻宕机,新 Leader 的日志里也必然包含这条已提交日志,配置永不丢失。


八、Raft 与 Distro 如何和平共处?

同一个 Nacos 进程内,Distro 和 Raft 并行运行却互不干扰:

  • 数据完全隔离:服务实例数据走 Distro,配置数据走 Raft。

  • 线程模型分开:Distro 使用自己的线程池,Raft 使用 JRaft 内部线程模型。

  • 故障域独立:即使 Raft 集群正在选主,Distro 照常处理心跳和注册。

这使得 Nacos 在整体上既拥有服务发现的高可用,又有配置管理的强一致。


九、总结与下篇预告

本篇核心知识点回顾:

  • Raft 三种角色(Leader/Follower/Candidate)及选举轮转,Leader 宕机 1 秒内自愈。

  • Leader 宕机时,写必须 Leader,读通过线性一致读(ReadIndex)保证 Follower 返回最新已提交值。

  • 线性一致读消灭了“已提交但未通知”的时间窗口,杜绝新旧版本共存。

  • 脑裂时,多数派拥有全部已提交数据,少数派无法服务或只返回旧已提交值,绝不会出现脏读或矛盾版本。

  • Nacos 集成 SOFAJRaft,配置发布全程走 Raft:Leader 提案 → 过半确认 → 状态机持久化 + 通知。

  • Raft 与 Distro 独立运行,共同构成 Nacos 的 AP+CP 双引擎。

理解了配置中心的强一致性及其边界,你已经看懂了 Nacos 两大核心引擎的另一半。接下来的问题是:如何将 Nacos 集群部署到生产环境,做到跨机房高可用、异地多活? 下一篇《第 6 篇:Nacos 集群与高可用:跨机房、多数据中心》将给你一套完整的运维指南。


本系列持续更新,从原理剖析到源码实战。如果本文对你有帮助,请点赞、收藏,你的支持是我最大的动力!有问题欢迎在评论区讨论。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐