Nacos源码解析:Distro协议如何撑起AP注册中心
一个常见的生产场景
假设你有一套 Nacos 集群,三个节点部署在北京、上海、广州。某个时刻,网络抖动导致上海节点和另外两个节点失联了。这时候会发生什么?
如果 Nacos 用的是 Raft(CP 模型),上海节点会发现选不出 Leader,整个集群可能拒绝服务,注册在里面的服务全部不可用。但实际用 Nacos 的人都知道——Nacos 默认不会这么干。上海节点依然能接受服务注册,本地服务依然能发现实例。这就是 Distro 协议在起作用。
Distro 是 Nacos 自研的 AP 型一致性协议,专门为"服务注册发现"这个场景设计。它和 Raft、Paxos 走的是完全不同的路子:不追求强一致,追求的是每个节点都能独立处理写请求,最终数据收敛。
Distro 的整体架构
先看一张简化后的数据流图:
┌─────────────────────────────────────────┐
│ Nacos Cluster │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ │ Node A │ │ Node B │ │ Node C │
│ │ │ │ │ │ │
│ │ 责任分区 │ │ 责任分区 │ │ 责任分区 │
│ │ [S1,S2] │ │ [S3,S4] │ │ [S5,S6] │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ └──── 异步同步 ─┴──────────────┘
│ │
└─────────────────────────────────────────┘
核心思想只有两条:
第一,每个节点负责一部分服务的写入。 服务按名字哈希,分配到某个节点作为"责任节点"。写请求发到哪个节点,如果这个节点不是责任节点,它会转发给责任节点。
第二,责任节点异步把数据同步给其他节点。 不等别人确认,自己先返回成功。
这就解释了前面那个场景:上海节点失联了,它负责的服务照常读写,只是数据暂时不会同步到其他节点。等网络恢复,数据再追上来。
核心类与调用链路
从服务注册的入口开始追。HTTP 请求打到 InstanceController,最终落到 DistroConsistencyServiceImpl:
// DistroConsistencyServiceImpl 核心结构
public class DistroConsistencyServiceImpl implements ConsistencyService {
// 每个节点自己维护的数据副本
private final Map<String, Datum> dataMap = new ConcurrentHashMap<>();
// 待同步的任务队列(按责任节点分组)
private final Map<String, BlockingQueue<String>> taskMap =
new ConcurrentHashMap<>();
// 负责数据同步的执行器
private TaskScheduler notifier;
public void put(String key, Record value) throws NacosException {
// 1. 先写本地
onPut(key, value);
// 2. 加入同步队列,异步发给责任节点
notifier.addTask(key, DataOperation.CHANGE);
}
}
调用链路可以概括成:
InstanceController.register()
└─> DistroConsistencyServiceImpl.put(key, instance)
├─> onPut() // 写本地 dataMap + 通知本地监听器
└─> notifier.addTask(key, CHANGE)
└─> DistroConsistencyServiceImpl.sync()
└─> DistroMapper.mapSrv(key) // 找到责任节点
└─> NamingProxy.syncData() // HTTP POST 到责任节点
这里有三个关键点。
onPut 先写本地再通知监听器。 本地服务发现能立刻感知到新实例,不用等同步完成。这是 AP 的直接体现。
DistroMapper.mapSrv() 决定数据归属。 它把服务名做哈希,映射到集群节点列表的某个位置。所有节点用同一份节点列表和同一个哈希算法,算出来的责任节点必然一致。这是无中心化分片的关键。
NamingProxy.syncData() 是 HTTP 调用,不是 RPC。 用 HTTP 的好处是简单、跨版本兼容,坏处是慢。所以同步是异步批量做的,不阻塞主流程。
同步与校验:数据怎么收敛
异步同步最大的问题是可能丢数据。比如责任节点收到同步请求前宕机了,或者网络分区导致部分节点没收到。Distro 用了两个机制兜底。
第一个是 sync 任务重试。 DistroConsistencyServiceImpl 里的 TaskScheduler 会把失败的任务重新入队,按固定间隔重试。
第二个是定时全量校验。 每个节点会定期向其他节点发起 checksum 请求,比较数据的校验和。不一致就触发全量同步。
// 校验逻辑简化示意
public void run() {
// 计算本地所有数据的 checksum
Map<String, String> checksumMap = getChecksum();
// 发给其他节点比对
for (String server : otherServers) {
Map<String, String> remoteChecksum =
NamingProxy.getChecksum(server, checksumMap);
// 找出不一致的 key,触发同步
for (Map.Entry<String, String> entry : remoteChecksum.entrySet()) {
if (!checksumMap.get(entry.getKey()).equals(entry.getValue())) {
syncData(entry.getKey());
}
}
}
}
校验和比对的设计很讨巧:不传全量数据,只传每个服务的哈希值。数据量大时也能快速定位差异。踩坑点在于 checksum 的计算频率——太频繁会给集群带来额外压力,太稀疏则数据不一致的窗口会拉长。Nacos 默认是 5 秒一次,生产环境需要根据集群规模调整。
为什么不用 Raft
这是理解 Distro 的关键。Raft 要求多数派确认,写延迟至少是一个 RTT。服务注册发现场景下,实例数量动辄上千,心跳上报每秒都在发生。如果每次注册都要多数派确认,集群吞吐会成为瓶颈。
Distro 的选择是:接受短暂的数据不一致,换取写入的高可用和低延迟。 服务发现有天然的容错性——某个消费者晚几秒看到新实例,通常不影响业务;但如果注册中心整体不可写,影响的是所有服务。
这是一种典型的"场景驱动设计":不是 Distro 比 Raft 更好,而是它更适合注册中心这个场景。
代价也很明确:Distro 不保证读到最新数据。极端情况下,两个节点可能对同一个服务返回不同的实例列表。Nacos 1.x 里这个问题更明显,2.x 通过 gRPC 长连接和更好的同步机制有所改善,但 AP 的本质没变。
下一步可以做什么
如果想深入理解 Distro,建议从 DistroConsistencyServiceImpl 的 sync() 方法入手,配合 DistroMapper 的哈希逻辑一起看。重点观察两件事:责任节点是怎么算出来的,以及同步失败后的重试策略。把这两块搞明白,Distro 的整体设计就清楚了。
本文关键词:Nacos、Distro协议、AP模型、服务注册、数据同步