目录
以下是 Nacos 1.x 在 服务发现 和 配置管理 两个核心功能中,客户端和服务端的数据存储与读取设计解析:
1. 服务发现(Naming Service)
服务端存储设计
- 数据分片与存储
-
- 临时实例:存储在内存(
ConcurrentHashMap
),使用 Distro 协议异步分片复制。 - 持久实例:存储在磁盘(内嵌
Derby
或外置MySQL
),通过定期健康检查维护状态。 - 元数据:服务名、实例 IP、端口、健康状态等。
- 临时实例:存储在内存(
- Distro 协议分片机制
// Distro 分片逻辑(简化)
public class DistroDataStorage {
// Key: serviceName + clusterName
private Map<String, Service> serviceMap = new ConcurrentHashMap<>();
// 哈希分片,确定当前节点是否负责该服务
public boolean isResponsible(String serviceName) {
int hash = HashUtil.murmur2(serviceName);
return hash % nodeList.size() == currentIndex;
}
}
- 数据同步流程
客户端存储设计
- 本地缓存
-
- 客户端(如微服务)缓存服务列表(
ServiceInfo
对象),通过 UDP 推送 或 定时拉取 更新。 - 缓存文件路径:
${user.home}/nacos/naming/${namespace}/cache/${serviceName}
。
- 客户端(如微服务)缓存服务列表(
- 心跳维持
// 客户端心跳线程(简化)
public class BeatReactor {
public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
executor.scheduleAtFixedRate(() -> {
// 发送心跳到Nacos Server
serverProxy.sendBeat(beatInfo);
}, 0, beatInfo.getPeriod(), TimeUnit.SECONDS);
}
}
数据读取流程
2. 配置管理(Config Service)
服务端存储设计
- 数据持久化
-
- 配置数据存储在 本地文件系统 或 MySQL(集群模式)。
- 文件路径:
${nacos.home}/data/config-data/${group}/${dataId}
。
- Raft 协议强一致
-
- 所有写操作通过 Raft Leader 同步到多数节点后才返回成功。
- 日志结构示例:
raft_log/
log_001.meta # 日志元数据
log_001.data # 日志条目(配置内容、操作类型)
- 多版本与快照
-
- 每个配置变更生成新版本(
version
),旧版本保留用于回滚。 - 定期生成快照(Snapshot)加速恢复。
- 每个配置变更生成新版本(
客户端存储设计
- 本地缓存
-
- 配置内容缓存到本地文件(如
application.properties
),路径:${user.home}/nacos/config/${namespace}/${group}/${dataId}
。 - 缓存降级:当服务端不可用时,客户端使用本地缓存继续运行。
- 配置内容缓存到本地文件(如
- 监听机制
// 客户端配置监听(简化)
public class ClientWorker {
public void addListener(String dataId, Listener listener) {
// 长轮询检查配置变更
executor.execute(() -> {
while (true) {
String newConfig = checkConfigUpdate(dataId);
if (newConfig != null) {
listener.receiveConfigInfo(newConfig);
}
Thread.sleep(3000); // 默认长轮询间隔
}
});
}
}
数据读取流程
关键设计对比
维度 | 服务发现(Distro) | 配置管理(Raft) |
一致性模型 | 最终一致性(AP) | 强一致性(CP) |
存储介质 | 内存(临时实例)+ 磁盘(持久实例) | 磁盘(文件或DB) |
客户端缓存 | 服务列表缓存 + 定时拉取 | 本地配置文件 + 长轮询监听 |
数据分片 | 哈希分片(Distro协议) | 无分片(全量复制) |
故障恢复 | 自动重新分片(节点宕机时) | Raft日志重放 + 快照恢复 |
典型操作延迟 | 低(内存操作+异步复制) | 较高(需多数节点同步) |
设计深潜:配置管理的 Raft 日志复制
// Raft 日志复制核心逻辑(简化)
public class RaftCore {
public void publishConfig(String dataId, String content) {
// 1. Leader生成日志条目
LogEntry logEntry = new LogEntry(term, index, dataId, content);
logRepository.add(logEntry);
// 2. 同步到Followers
for (Follower follower : followers) {
sendLogEntry(follower, logEntry);
}
// 3. 等待多数节点ACK
if (ackCount > majority) {
applyLogEntry(logEntry); // 提交到状态机
writeToDisk(dataId, content); // 持久化到磁盘
}
}
}
常见问题处理
- 服务发现缓存不一致
-
- 解决:客户端强制刷新缓存(
namingService.getInstances(serviceName, true)
)。
- 解决:客户端强制刷新缓存(
- 配置更新未触发监听
-
- 检查:确认客户端监听的
dataId
和group
是否与服务端一致。
- 检查:确认客户端监听的
- Raft 日志堆积导致写入慢
-
- 优化:调整
nacos.core.protocol.raft.snapshot_interval
(默认12小时),增加快照频率。
- 优化:调整
总结
- 服务发现:轻量级内存操作 + 异步分片复制,适合高可用但容忍短暂不一致的场景。
- 配置管理:强一致磁盘存储 + Raft同步,确保配置变更的准确性和可靠性。
- 客户端设计:通过本地缓存和监听机制,平衡实时性与容灾能力。