Nacos 源码阅读 —— AP 一致性协议(Distro)
临时实例的注册表走 AP 最终一致,协议是 Nacos 自研的 Distro。Distro 没有 Leader,每个节点平等自治,通过"任务引擎 + 反熵"收敛数据。本文从源码拆解其设计。
为什么注册中心要 AP
服务注册数据的特性:
- 写入频繁(每个实例注册 + 心跳)
- 允许短暂不一致(消费者路由到旧实例影响很小)
- 必须高可用(注册中心挂了对服务是灾难)
所以临时实例用 AP:任何节点可写、异步同步、最终一致,节点故障不影响服务可用。
Distro 协议核心思想
无 Leader 的对称架构:
每个节点都:
├─ 可接受写请求(就近写入)
├─ 本地维护完整注册表副本
├─ 定时把自己的数据同步给其他节点
└─ 定时从其他节点拉取差异(反熵)关键特性
| 特性 | 说明 |
|---|---|
| 无 Leader | 无单点,任何节点可服务 |
| 就近写入 | 客户端连哪个节点就写哪个节点 |
| 异步同步 | 数据写入本地后,后台任务推送给其他节点 |
| 反熵收敛 | 周期拉取对账,修正丢失/错误数据 |
源码组件
DistroConsistencyServiceImpl(核心)
├─ DistroTaskEngine 任务引擎(同步任务调度)
│ ├─ DistroSyncTask 推任务(本节点数据 → 其他节点)
│ └─ DistroVerifyTask 拉任务(校验/反熵)
├─ DistroProtocol 协议入口
├─ DistroHttpRegistry / gRPC 传输
└─ ServiceDataStorage 数据存储(注册表)
NamingProxy / DistroDataProcessor(接收端)
└─ 处理其他节点发来的数据写入流程
客户端注册临时实例
│
▼
NamingService → ConsistencyService.put(Distro 实现)
│
├─ 1. 写入本节点注册表(内存)
├─ 2. 发布本机事件(触发本地变更通知)
└─ 3. 提交 Distro 同步任务(异步)
└─ 任务引擎:把变更推送给其他节点任务引擎
java
// DistroTaskEngine:定时任务调度
public class DistroTaskEngine {
private final ScheduledExecutorService executor;
public void submitSyncTask(DistroKey key, DataOperation op) {
// 对每个远端节点生成同步任务
List<Member> members = memberManager.allMembersWithoutSelf();
for (Member member : members) {
DistroSyncTask task = new DistroSyncTask(member, key, op);
executor.submit(task);
}
}
}同步是异步批量的:不阻塞注册请求,由后台线程周期冲刷。
数据同步:推与拉
推(DistroSyncTask)
本节点数据变更(注册/注销/心跳状态)
│
▼
对每个其他节点生成同步任务
│
├─ 打包数据(变更 key + 内容)
├─ 通过 gRPC/HTTP 发送
└─ 目标节点处理(DistroDataProcessor)
└─ 更新自己的注册表副本拉(DistroVerifyTask / 反熵)
定时(默认 5s)对每个远端节点:
├─ 发送本节点全部数据的摘要(key 列表)
├─ 对方返回缺失/不一致的 key
└─ 拉取差异数据补齐反熵的意义
推可能失败(网络抖动、节点短暂不可用),反熵保证最终收敛:只要节点间最终连通,数据必然一致。
推失败 → 暂时不一致
↓
反熵周期 → 发现差异 → 补齐 → 收敛一致健康状态与数据生命周期
心跳与状态机
临时实例
├─ 心跳正常 → healthy=true
├─ 15s 无心跳 → healthy=false(保留)
└─ 30s 无心跳 → 本地删除 + 同步删除到其他节点节点故障恢复
某节点宕机
│
├─ 其他节点心跳超时标记不健康
├─ 故障节点数据仍在其内存(宕机丢失?)
│
└─ 恢复时:
├─ 启动后向集群其他节点拉取注册表(反熵)
└─ 补齐缺失数据 → 恢复服务AP 的代价就在这:节点宕机期间其内存数据不可用,但消费者连到其他节点仍可发现大部分实例。结合客户端本地缓存,注册中心整体故障时服务仍可用。
Distro vs JRaft(CP)对比
| 维度 | Distro(AP) | JRaft(CP) |
|---|---|---|
| 一致性 | 最终一致 | 强一致 |
| Leader | 无 | 有(选举产生) |
| 写入 | 任意节点 | 仅 Leader(转发) |
| 同步 | 异步任务 + 反熵 | 日志复制(多数派) |
| 存储 | 内存 | 持久化(MySQL) |
| 适用 | 临时实例 | 持久实例 / 配置 |
| 故障代价 | 可能短暂不一致 | 可能短暂不可用 |
客户端视角的 AP 体验
消费者
├─ 订阅服务
├─ 任一节点返回实例列表(该节点本地副本)
├─ 不同节点可能返回略有差异的列表(短暂)
└─ 负载均衡选择实例,天然容忍偏差生产注意点
- 网络分区:分区期间两侧各自写,恢复后反熵合并,可能丢"最后写入"(后写覆盖先写)
- 时钟一致性:心跳过期判定依赖时间,时钟偏移导致误判健康
- 批量注册:服务扩容瞬间同步任务变多,注意任务队列容量
- 频繁变更:频繁注册/注销会产生大量同步任务,控制发布节奏
关键源码文件索引
| 组件 | 位置(nacos 仓库) |
|---|---|
| DistroConsistencyServiceImpl | naming/.../consistency/ephemeral/distro/DistroConsistencyServiceImpl.java |
| DistroTaskEngine | naming/.../consistency/ephemeral/distro/v2/DistroTaskEngine.java |
| DistroProtocol | naming/.../consistency/ephemeral/distro/v2/DistroProtocol.java |
| DistroDataProcessor | naming/.../consistency/ephemeral/distro/DistroDataProcessor.java |
| DistroSyncTask / DistroVerifyTask | naming/.../consistency/ephemeral/distro/v2/task/ |
常见问题
- 为什么注册数据偶尔读不到最新? AP 是最终一致,刚写入某节点未同步完,其他节点返回旧列表;正常现象。
- 反熵多久跑一次? 默认约 5s 一轮(
NACOS_DISTRO_VERIFY_INTERVAL),可配置。 - 节点恢复后数据从哪来? 通过反熵从其他节点拉取补齐;保证集群整体数据完整。
- Distro 会丢数据吗? 网络分区时可能覆盖丢失(后写覆盖),这是 AP 的固有取舍;客户端本地缓存能缓解影响。