Nacos 客户端源码阅读
客户端是理解 Nacos 的最后一环:注册、发现、缓存、心跳、故障转移全部发生在客户端。本文拆解 NacosNamingService 及其核心组件的源码实现。
客户端整体结构
NacosNamingService(服务发现客户端入口)
├─ NamingProxy / NamingGrpcProxy 通信代理(1.x HTTP / 2.x gRPC)
├─ HostReactor 实例列表缓存 + 更新
├─ BeatReactor 心跳线程
├─ ServiceInfoHolder 服务信息缓存
└─ EventDispatcher 事件分发(监听器回调)
ConfigService(配置客户端)
├─ ClientWorker 长轮询
└─ LocalConfigInfoProcessor 本地缓存NacosNamingService:客户端门面
java
// com.alibaba.nacos.client.naming.NacosNamingService
public class NacosNamingService implements NamingService {
private NamingProxy serverProxy; // 通信代理
private HostReactor hostReactor; // 实例缓存
private BeatReactor beatReactor; // 心跳
private EventDispatcher eventDispatcher; // 事件分发
public NacosNamingService(Properties properties) {
// 1. 初始化 ServerListManager(服务器列表,支持域名)
// 2. 初始化 NamingProxy(gRPC/HTTP 通信)
// 3. 初始化 BeatReactor、HostReactor
init(properties);
}
@Override
public void registerInstance(String serviceName, String ip, int port) {
// 1. 构造 Instance
// 2. serverProxy.registerService()
// 3. beatReactor.addBeatInfo() ← 开始心跳
}
@Override
public List<Instance> getAllInstances(String serviceName, boolean subscribe) {
// 从 HostReactor 缓存取(subscribe=true 时注册监听)
return hostReactor.getAllInstances(serviceName, ...);
}
@Override
public void subscribe(String serviceName, EventListener listener) {
// 1. 注册监听器
// 2. 立即拉取一次实例列表(初始化缓存)
}
}初始化流程
new NacosNamingService(properties)
├─ 1. ServerListManager:解析 server 地址(多个/域名)
├─ 2. NamingGrpcProxy:建立 gRPC 连接
├─ 3. BeatReactor:心跳线程池
└─ 4. HostReactor:实例缓存 + 更新调度HostReactor:实例缓存与更新
双重缓存结构
java
// com.alibaba.nacos.client.naming.core.HostReactor
public class HostReactor {
private final Map<String, ServiceInfo> serviceInfoMap; // 缓存:service → 实例列表
private final ScheduledExecutorService executor; // 定时更新线程
// 推送更新
public void processServiceJSON(String json) {
ServiceInfo serviceInfo = deserialize(json);
ServiceInfo oldService = serviceInfoMap.get(serviceInfo.getKey());
// 版本比对,更新缓存 + 触发监听器
}
// 定时兜底拉取
public class UpdateTask implements Runnable {
@Override
public void run() {
// 定期向服务端拉取最新实例列表
// 与缓存比对,变化则更新 + 通知
}
}
}更新机制
| 机制 | 触发 | 作用 |
|---|---|---|
| 服务端推送 | gRPC 长连接收到变更 | 实时更新缓存 |
| 定时拉取 | UpdateTask(默认 10s) | 兜底,防止推送丢失 |
| 失败重试 | 拉取失败 | 保留旧缓存,重试 |
缓存失效与重试
推送/拉取异常
─▶ 保留旧缓存(服务仍可用)
─▶ 定时重试(指数退避)
─▶ 恢复后更新BeatReactor:心跳线程
java
// com.alibaba.nacos.client.naming.core.BeatReactor
public class BeatReactor {
private final ScheduledExecutorService executor; // 心跳线程池
private final Map<String, BeatInfo> dom2Beat; // 服务 → 心跳信息
public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
// 为每个服务启动心跳任务
executor.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
}
class BeatTask implements Runnable {
@Override
public void run() {
// 1. 发送心跳请求(默认 5s 周期)
// 2. 失败则重试(指数退避)
// 3. 成功则继续下一个周期
executor.schedule(this, beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
}
}
}心跳细节
- 默认周期 5s(
nacos.naming.heart.beat.interval) - 心跳中携带本地实例数据,服务端可借此校正
- 心跳失败指数退避,避免风暴
- 服务端返回状态异常时,客户端可主动重注册
故障转移机制
多级容错
正常:gRPC 长连接 + 推送
│
▼ 长连接断开
重连:自动重连(指数退避,默认 3s 起)
│
▼ 重连失败
切换节点:ServerListManager 切换到下一个节点
│
▼ 所有节点不可用
本地缓存:使用 ServiceInfoHolder 缓存的旧数据
│
▼ 本地缓存也没有
失败:返回异常/空列表客户端本地缓存
java
// 服务信息持久化到本地磁盘(failover 目录)
// nacos 客户端默认开启(-Dnacos.naming.cache.dir)
ServiceInfoHolder:
缓存路径:~/nacos/naming/{namespace}/{service}.json
启动时读取 → 服务端不可用时使用故障转移配置
| 配置 | 默认 | 说明 |
|---|---|---|
| nacos.naming.retry.time | 3s | 重连间隔 |
| nacos.naming.cache.dir | ~/nacos | 本地缓存目录 |
| nacos.naming.heart.beat.interval | 5s | 心跳周期 |
| nacos.naming.push.empty.protection | false | 空列表推送保护 |
gRPC 连接管理(2.x)
双连接设计
客户端(gRPC)
├─ 主连接(bi-directional stream) ← 请求/响应
└─ 复用连接(长连接池) ← 后续请求复用
│
▼
NacosServerGrpc(服务端)连接建立与重连
java
// GrpcClient(客户端连接抽象)
GrpcClient.connect()
├─ 1. 选择服务器(ServerListManager)
├─ 2. 建立 gRPC 双向流
├─ 3. 发送 ConnectionSetupRequest(含身份信息)
└─ 4. 断线自动重连(指数退避)重连后需重新注册 + 重新订阅(服务端状态可能已清),客户端会做恢复操作。
完整调用时序:服务发现
应用调用 namingService.getAllInstances("order-service")
│
├─ 1. HostReactor 查缓存
│ ├─ 命中 → 返回缓存列表
│ └─ 未命中 → 触发同步拉取(UpdateTask)
│
├─ 2. 注册订阅(subscribe)
│ ├─ 服务端推送变更 → processServiceJSON → 更新缓存
│ └─ 定时兜底拉取
│
└─ 3. 返回实例列表常见问题
- 客户端缓存多久更新? 推送秒级,兜底轮询默认 10s;两者保证不落后太久。
- 心跳失败会立刻掉线吗? 不会,指数退避重试;超过服务端超时(15s)才被判定不健康。
- Nacos 全挂服务还能调用吗? 能,本地磁盘缓存 + 内存缓存提供旧实例列表。
- 重连后为什么有时恢复慢? 重连成功还需重新注册/订阅,期间短暂使用旧缓存。