Nacos 源码阅读 —— CP 一致性协议(JRaft)
Nacos 的持久实例与配置数据走 CP 强一致,底层是 JRaft(蚂蚁开源的 Raft 实现)。本文从源码拆解:JRaft 在 Nacos 中如何接入、Leader 选举如何发生、日志如何复制、节点变更如何处理。
为什么需要 CP
| 数据 | 后果 |
|---|---|
| 配置数据不一致 | 不同节点下发不同配置 → 线上事故 |
| 持久实例不一致 | 人工注册的固定节点状态错乱 |
所以这两类数据必须强一致:写入经过多数派确认,任何节点读到一致结果。
JRaft 与一致性接口
Nacos 通过 ConsistencyService 抽象接入 JRaft:
ConsistencyService(接口)
├─ put(key, value) 写操作
├─ get(key) 读操作
└─ remove(key) 删除操作
│
├─ AP 实现:DistroConsistencyServiceImpl(临时实例)
└─ CP 实现:PersistentConsistencyServiceDelegateImpl
└─ JRaftConsistencyServiceImpl(持久实例/配置)配置/持久实例的写入入口
java
// 注册表写入时判断实例类型
if (instance.isEphemeral()) {
// 临时实例 → Distro(AP)
consistencyService.put(key, instance);
} else {
// 持久实例 → JRaft(CP)
persistentConsistencyService.put(key, instance);
}JRaft 在 Nacos 中的初始化
java
// JRaftServer / JRaftConsistencyServiceImpl
public class JRaftConsistencyServiceImpl implements PersistentConsistencyService {
private RaftGroupService raftGroupService; // 组服务
private Node raftNode; // 本节点 Raft 角色
public void init() throws Exception {
// 1. 配置 Raft 组(members:集群节点列表)
RaftGroup raftGroup = RaftGroup.newInstance(raftGroupId, peers);
// 2. 创建 Raft 节点
Node node = RaftNode.newNode(raftGroup, configuration);
// 3. 注册状态机(日志应用到状态机的回调)
node.setRaftStateMachine(NacosRaftStateMachine());
// 4. 注册 Snapshot(快照,日志压缩)
node.registerSnapshotExecutor(...);
// 5. 启动
node.start();
}
}核心组件
| 组件 | 作用 |
|---|---|
| RaftGroupService | Raft 组管理(成员配置、组创建) |
| Node | 单节点角色状态机(Leader/Follower/Candidate) |
| RaftStateMachine | 日志应用到业务状态的回调 |
| SnapshotExecutor | 快照保存/恢复(压缩日志) |
Leader 选举
选举流程(Raft 标准流程)
所有节点角色 = Follower
│
├─ 选举超时(随机 150-300ms)未收到 Leader 心跳
│ └─ 变为 Candidate,发起 PreVote
│ └─ 请求其他节点投票(RequestVote)
│ ├─ 日志较新的节点才被投票
│ └─ 获得多数派投票 → 成为 Leader
│
└─ Leader 开始发送心跳(AppendEntries,空日志)
└─ 所有 Follower 重置选举超时JRaft 源码关键类
java
// com.alipay.sofa.jraft.core.NodeImpl
public class NodeImpl implements Node {
// 选举超时后触发
public void handleElectionTimeout() {
// 1. 角色 Follower → Candidate
changeToCandidate();
// 2. 发起 PreVote(预投票,避免网络分区下的选举抖动)
sendPreVote();
// 3. PreVote 通过 → 正式 RequestVote
...
}
}PreVote 的意义
网络分区时,少数派节点收不到 Leader 心跳,会频繁发起选举。PreVote 先确认"自己能否当选"(日志是否最新),避免无意义选举导致任期(term)不断增长。
日志复制
写入流程
客户端写入(配置/持久实例)
│
▼
Leader 收到 Propose
│
├─ 1. 追加日志到本地 Log(任期 + 索引 + 数据)
├─ 2. 并行向 Follower 发送 AppendEntries
│
├─ 3. 多数派确认(Quorum)后
│ └─ 日志 Commit(提交)
│ └─ 应用到状态机(RaftStateMachine)
│
└─ 4. 返回客户端成功Nacos 状态机应用
java
// NacosRaftStateMachine 的 onApply 回调
public void onApply(Iterator iter) {
while (iter.hasNext()) {
LogEntry log = iter.next();
ByteBuffer data = log.getData();
// 反序列化操作(put/remove)
Operation op = serializer.deserialize(data);
// 应用到业务存储:
// 配置 → 写入内存缓存 + MySQL
// 持久实例 → 写入注册表
onSuccess(op, ...);
iter.next();
}
}日志复制时序
Leader Follower A Follower B
│ Propose 日志 t1 │ │
├───────────────────▶│ │
├────────────────────┼───────────────▶│
│ 多数派已确认 │ │
│ Commit t1 │ │
│ 应用状态机 │ 应用状态机 │ 应用状态机
│ 返回客户端成功 │ │节点变更(Membership Change)
单步变更 vs 联合共识
JRaft 支持节点增删,变更流程:
1. Leader 提交新成员配置(配置日志)
2. 多数派确认配置日志
3. 新节点加入:日志追平后成为 Follower
4. 旧节点移除:不再参与投票Nacos 集群节点变化
nacos.conf 修改 members
│
▼
节点重启后感知新成员列表
│
▼
JRaft 通过配置变更协议完成成员调整生产变更节点:逐个操作,先加新节点,稳定后再下旧节点,避免同时变动导致选举失效。
读一致性
线性一致读 vs 允许陈旧读
JRaft 提供两种读:
| 读方式 | 一致性 | 实现 |
|---|---|---|
| ReadIndex | 线性一致 | Leader 确认自身任期后读日志状态 |
| 本地读 | 可能陈旧 | 直接读本地状态机(性能最好) |
Nacos 配置读取通常直接读本地缓存(允许极短暂陈旧),写入才走 CP 全链路。
与 AP(Distro)的边界
同一个注册表里:
├─ 临时实例(ephemeral=true) → Distro,AP
└─ 持久实例(ephemeral=false) → JRaft,CP
同一节点上两套协议共存,互不干扰这是 Nacos 混合一致性设计的源码级体现:ConsistencyService 按实例类型分派。
关键源码文件索引
| 组件 | 位置(nacos 仓库) |
|---|---|
| PersistentConsistencyService | naming/src/main/java/.../consistency/PersistentConsistencyService.java |
| JRaftConsistencyServiceImpl | naming/.../consistency/persistent/JRaftConsistencyServiceImpl.java |
| NacosRaftStateMachine | naming/.../consistency/persistent/NacosRaftStateMachine.java |
| NodeImpl(JRaft) | jraft-core/.../NodeImpl.java(依赖 jar) |
| RaftGroupService | naming/.../core/RaftGroupService.java |
常见问题
- JRaft 选举失败? 检查集群节点数是否 ≥3、成员配置是否正确、节点时钟是否同步。
- 为什么写配置要多数派确认? 保证任何时刻最多一个 Leader 提交,避免分区时两个节点同时"写成功"。
- 日志无限增长怎么办? JRaft 快照机制定期压缩旧日志,只保留状态机结果。
- 可以只部署 2 节点吗? 不建议,2 节点多数派仍要 2 票,单点故障即不可用;生产 ≥3 节点。