Nacos 2.X配置中心源码深度剖析:从客户端到服务端的完整设计
一、Nacos配置中心整体架构概览
1.1 核心架构设计
Nacos配置中心采用经典的客户端-服务器架构,支持配置的动态推送、版本管理、权限控制等高级功能。其整体架构如下图所示:
text
客户端层(Config Client)
↓
服务发现与负载均衡
↓
Nacos配置服务器集群(Config Server)
↓
配置持久化层(MySQL/本地文件)
1.2 核心模块职责
-
Config Client:负责与配置服务器通信,获取配置、注册监听器、处理配置变更
-
Config Server:处理配置的存储、发布、推送和版本管理
-
持久化层:配置数据的持久化存储,支持MySQL和本地文件
-
集群通信:基于gRPC的集群节点间数据同步与事件通知
二、配置中心客户端源码深度分析
2.1 ConfigService核心接口
ConfigService是配置中心客户端的核心接口,定义了配置操作的所有基本功能:
java
// ConfigService接口核心方法
public interface ConfigService {
// 获取配置
String getConfig(String dataId, String group, long timeoutMs) throws NacosException;
// 获取配置并注册监听器
String getConfigAndSignListener(String dataId, String group, long timeoutMs, Listener listener);
// 注册配置变更监听器
void addListener(String dataId, String group, Listener listener);
// 发布配置
boolean publishConfig(String dataId, String group, String content);
boolean publishConfig(String dataId, String group, String content, String type);
// 删除配置
boolean removeConfig(String dataId, String group);
// 移除监听器
void removeListener(String dataId, String group, Listener listener);
// 获取服务器状态
String getServerStatus();
// 关闭客户端
void shutdown();
}
2.2 客户端使用示例
java
public class ConfigServerDemo {
public static void main(String[] args) throws NacosException, InterruptedException {
String serverAddr = "localhost";
String dataId = "nacos-config-demo.yaml";
String group = "DEFAULT_GROUP";
// 1. 创建配置服务实例
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
ConfigService configService = NacosFactory.createConfigService(properties);
// 2. 获取配置
String content = configService.getConfig(dataId, group, 5000);
System.out.println(content);
// 3. 注册监听器
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
System.out.println("===recieve:" + configInfo);
}
@Override
public Executor getExecutor() {
return null;
}
});
// 4. 发布配置(示例)
boolean isPublishOk = configService.publishConfig(dataId, group, "content");
System.out.println(isPublishOk);
// 发送properties格式配置
configService.publishConfig(dataId, group, "common.age=30", ConfigType.PROPERTIES.getType());
Thread.sleep(3000);
content = configService.getConfig(dataId, group, 5000);
System.out.println(content);
}
}
2.3 获取配置流程分析
getConfig方法是客户端获取配置的核心方法,其执行流程如下:
java
// NacosConfigService.getConfig()方法核心逻辑
public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
// 1. 参数校验和默认值处理
group = null2defaultGroup(group);
ParamUtils.checkKeyParam(dataId, group);
// 2. 从本地快照文件获取配置
ConfigResponse configResponse = getConfigInner(tenant, dataId, group, timeoutMs);
// 3. 如果本地没有,从服务器获取
if (configResponse == null || configResponse.getContent() == null) {
// 通过gRPC从远端拉取配置
configResponse = worker.getServerConfig(dataId, group, tenant, timeoutMs, true);
// 4. 保存到本地快照
if (configResponse != null && configResponse.getContent() != null) {
cacheData.setContent(configResponse.getContent());
cacheData.setMd5(configResponse.getMd5());
}
}
return configResponse == null ? null : configResponse.getContent();
}
关键点解析:
-
本地优先策略:首先尝试从本地快照文件读取,减少网络请求
-
快照机制:将配置缓存在本地磁盘,即使Nacos服务器不可用,也能读取最近一次的有效配置
-
gRPC通信:使用gRPC协议与服务器通信,支持长连接和双向流
2.4 监听器注册与配置变更通知
2.4.1 监听器注册机制
Nacos提供两种方式注册监听器:
-
ConfigService#addListener():直接注册监听器 -
ConfigService#getConfigAndSignListener():获取配置同时注册监听器
两者内部均调用ClientWorker类的addCacheDataIfAbsent方法:
java
// ClientWorker.addCacheDataIfAbsent核心逻辑
public void addCacheDataIfAbsent(String dataId, String group, String tenant) {
String key = GroupKey.getKeyTenant(dataId, group, tenant);
// 1. 从缓存map中获取或创建CacheData
CacheData cacheData = cacheMap.get(key);
if (cacheData == null) {
cacheData = new CacheData(configFilterChainManager, agent.getName(), dataId, group, tenant);
// 2. 添加到缓存map
CacheData lastCacheData = cacheMap.putIfAbsent(key, cacheData);
if (lastCacheData != null) {
cacheData = lastCacheData;
}
}
return cacheData;
}
2.4.2 CacheData核心结构
CacheData是维护配置项和其下注册的所有监听器的核心类:
java
public class CacheData {
// 配置标识信息
private final String dataId;
private final String group;
private final String tenant;
// 配置内容
private volatile String content;
private volatile String md5;
// 监听器列表
private final CopyOnWriteArrayList<ManagerListenerWrap> listeners;
// 任务执行器
private final ExecutorService executor;
// 配置类型
private String type;
// 是否正在执行监听器回调
private volatile boolean isInitializing = true;
// 核心方法:检查配置变更并通知监听器
public void checkListenerMd5() {
for (ManagerListenerWrap wrap : listeners) {
if (!md5.equals(wrap.lastCallMd5)) {
// 触发监听器回调
safeNotifyListener(dataId, group, content, type, md5, wrap);
wrap.lastCallMd5 = md5;
}
}
}
}
2.4.3 配置变更通知流程
配置变更通知的完整流程如下:
text
1. 服务器检测到配置变更 2. 通过gRPC长连接推送变更事件到客户端 3. 客户端收到事件,触发CacheData.checkListenerMd5() 4. 比较新旧配置的MD5值 5. 如果MD5不同,执行监听器的receiveConfigInfo方法 6. 更新本地快照文件
关键优化:
-
MD5比较:通过比较配置内容的MD5值判断是否变更,避免不必要的监听器触发
-
CopyOnWriteArrayList:监听器列表使用线程安全的CopyOnWriteArrayList,支持并发修改
-
异步通知:监听器回调可以通过自定义Executor异步执行,避免阻塞主线程
三、配置中心服务端源码深度分析
3.1 配置dump机制
3.1.1 DumpService初始化
服务端启动时,DumpService的init方法会被调用,负责从数据库加载配置到本地:
java
// DumpService.init方法核心逻辑
public void init() throws Throwable {
// 1. 读取心跳文件,获取最后心跳时间
Timestamp heartheatLastStamp = getLastHeartHeatTime();
// 2. 判断是全量dump还是增量dump
if (heartheatLastStamp == null
|| System.currentTimeMillis() - heartheatLastStamp.getTime() > DUMP_ALL_INTERVAL) {
// 全量dump
dumpAll();
} else {
// 增量dump(最近6小时内的变更)
dumpChange(heartheatLastStamp);
}
// 3. 启动定时dump任务
startDumpTask();
}
3.1.2 全量dump流程
java
private void dumpAll() throws IOException {
// 1. 清空磁盘缓存
clearDiskCache();
// 2. 分页从数据库加载配置
int pageNo = 0;
int pageSize = 1000;
while (true) {
List<ConfigInfo> configList = configInfoMapper.findAll(pageNo, pageSize);
if (configList.isEmpty()) {
break;
}
// 3. 写入磁盘和内存缓存
for (ConfigInfo config : configList) {
// 写入本地文件
dumpToDisk(config);
// 更新内存缓存
updateMemoryCache(config);
}
pageNo++;
}
}
3.1.3 增量dump流程
java
private void dumpChange(Timestamp lastStamp) {
// 1. 查询最近6小时内的变更配置
List<ConfigInfo> changedConfigs = configInfoMapper.findChangeConfig(lastStamp);
// 2. 刷新内存和文件
for (ConfigInfo config : changedConfigs) {
// 更新或删除配置
if (config.isDeleted()) {
removeFromCache(config);
removeFromDisk(config);
} else {
dumpToDisk(config);
updateMemoryCache(config);
}
}
// 3. 全量比对数据库,确保一致性
verifyAllConfigs();
}
增量dump的优势:
-
减少IO操作:只处理变更的配置,降低数据库和磁盘压力
-
快速恢复:服务重启后能快速加载最近配置
-
数据一致性:通过全量比对确保内存缓存与数据库一致
3.2 配置发布流程
3.2.1 配置发布入口
配置发布的核心代码位于ConfigController#publishConfig:
java
@PostMapping
@Secured(action = ActionTypes.WRITE, parser = ConfigResourceParser.class)
public Boolean publishConfig(HttpServletRequest request, HttpServletResponse response,
@RequestParam("dataId") String dataId,
@RequestParam("group") String group,
@RequestParam(value = "tenant", required = false, defaultValue = StringUtils.EMPTY) String tenant,
@RequestParam(value = "content") String content,
@RequestParam(value = "tag", required = false) String tag,
@RequestParam(value = "appName", required = false) String appName,
@RequestParam(value = "src_user", required = false) String srcUser,
@RequestParam(value = "config_tags", required = false) String configTags,
@RequestParam(value = "desc", required = false) String desc,
@RequestParam(value = "use", required = false) String use,
@RequestParam(value = "effect", required = false) String effect,
@RequestParam(value = "type", required = false) String type,
@RequestParam(value = "schema", required = false) String schema) throws NacosException {
// 1. 参数校验
ParamUtils.checkParam(dataId, group, content, type);
// 2. 持久化到数据库
ConfigInfo configInfo = new ConfigInfo(dataId, group, tenant, appName, content);
persistService.insertOrUpdate(configInfo);
// 3. 发布配置变更事件
ConfigDataChangeEvent event = ConfigDataChangeEvent.builder()
.dataId(dataId)
.group(group)
.tenant(tenant)
.content(content)
.build();
// 4. 通过gRPC通知集群所有节点
NotifyCenter.publishEvent(event);
// 5. 记录操作日志
recordOperationLog(request, dataId, group, tenant, content);
return true;
}
3.2.2 集群同步机制
在集群部署模式下,配置发布后的同步流程如下:
text
1. 客户端请求到达任意一台Nacos服务器(假设为Server A) 2. Server A将配置写入MySQL数据库 3. Server A发布ConfigDataChangeEvent事件 4. 事件通过gRPC通知集群所有节点(包括Server A自身) 5. 每个节点收到事件后: a. 更新内存缓存 b. 更新本地磁盘文件 c. 通知连接的客户端配置变更
关键设计:
-
最终一致性:集群节点间通过事件驱动实现最终一致性
-
去中心化同步:每个节点都可以独立处理客户端请求,通过事件同步状态
-
故障容错:即使部分节点故障,其他节点仍能正常服务
3.2.3 事件发布与处理
java
// NotifyCenter事件发布核心逻辑
public static void publishEvent(Event event) {
// 1. 获取事件发布器
EventPublisher publisher = getPublisher(event.getClass());
// 2. 发布事件
if (publisher != null) {
publisher.publish(event);
}
}
// gRPC事件处理器
public class GrpcClusterClient implements EventPublisher {
@Override
public void publish(Event event) {
// 转换为gRPC消息
ClusterRequest request = convertToRequest(event);
// 发送到所有集群节点
for (Member member : clusterMembers) {
if (!member.isSelf()) { // 不发送给自己(已处理)
try {
grpcClientProxy.sendRequest(member.getAddress(), request);
} catch (Exception e) {
log.error("Send cluster event failed, member: {}", member, e);
}
}
}
}
}
四、客户端缓存与快照机制
4.1 本地快照设计
客户端将配置缓存在本地磁盘,结构如下:
text
${user.home}/nacos/config/
├── fixed-${serverAddr}/ # 固定服务器地址的配置
│ ├── ${namespace}/ # 命名空间
│ │ ├── ${group}/ # 分组
│ │ │ ├── ${dataId} # 配置文件
│ │ │ └── ${dataId}.md5 # MD5校验文件
│ │ └── snapshot-control/ # 快照控制文件
└── snapshot/ # 旧版本兼容目录
4.2 快照加载策略
java
public class LocalConfigInfoProcessor {
// 获取本地快照配置
public static String getSnapshot(String name, String dataId, String group, String tenant) {
// 1. 构建快照文件路径
String snapshotFile = getSnapshotFile(name, dataId, group, tenant);
// 2. 读取文件内容
if (snapshotFile != null && new File(snapshotFile).exists()) {
try {
return readFile(snapshotFile);
} catch (IOException e) {
log.error("read snapshot error, file: {}", snapshotFile, e);
}
}
return null;
}
// 保存快照到本地
public static void saveSnapshot(String name, String dataId, String group,
String tenant, String config) {
// 1. 构建快照文件路径
String snapshotFile = getSnapshotFile(name, dataId, group, tenant);
// 2. 写入文件
if (snapshotFile != null) {
try {
FileUtils.writeStringToFile(new File(snapshotFile), config, "UTF-8");
// 3. 保存MD5
String md5File = snapshotFile + ".md5";
String md5 = MD5Utils.md5Hex(config, "UTF-8");
FileUtils.writeStringToFile(new File(md5File), md5, "UTF-8");
} catch (IOException e) {
log.error("save snapshot error, file: {}", snapshotFile, e);
}
}
}
}
4.3 缓存一致性保证
客户端通过以下机制保证缓存一致性:
-
MD5校验:每次配置变更都更新MD5值,通过比较MD5判断配置是否变更
-
版本控制:配置变更时版本号递增,客户端记录最后拉取的版本号
-
失败重试:网络异常或服务器不可用时,客户端使用本地快照,并定期重试
-
一致性哈希:客户端缓存与服务器配置通过MD5建立映射关系
五、性能优化与监控
5.1 客户端性能优化
5.1.1 批量监听器通知
java
// ClientWorker.checkUpdateDataIds()方法
private void checkUpdateDataIds() {
List<CacheData> updateList = new ArrayList<>();
// 1. 收集需要更新的CacheData
for (CacheData cacheData : cacheMap.values()) {
if (cacheData.isUseLocalConfigInfo() ||
cacheData.getTaskId() == null ||
!cacheData.isInitializing()) {
continue;
}
// 检查配置是否变更
if (checkUpdateConfig(cacheData)) {
updateList.add(cacheData);
}
}
// 2. 批量通知监听器
if (!updateList.isEmpty()) {
notifyListeners(updateList);
}
}
5.1.2 连接池管理
Nacos客户端使用gRPC连接池管理服务器连接:
java
public class GrpcClient {
private final Map<String, ManagedChannel> channelMap = new ConcurrentHashMap<>();
private final Map<String, ConfigRpcClient> clientMap = new ConcurrentHashMap<>();
// 获取或创建连接
public ConfigRpcClient getOrCreateRpcClient(String serverIp, int serverPort) {
String serverAddress = serverIp + ":" + serverPort;
return clientMap.computeIfAbsent(serverAddress, address -> {
// 创建gRPC Channel
ManagedChannel channel = ManagedChannelBuilder.forAddress(serverIp, serverPort)
.usePlaintext()
.keepAliveTime(5, TimeUnit.SECONDS) // 心跳保活
.keepAliveTimeout(10, TimeUnit.SECONDS)
.idleTimeout(30, TimeUnit.SECONDS) // 空闲超时
.build();
channelMap.put(address, channel);
// 创建gRPC客户端
return new ConfigRpcClient(channel);
});
}
}
5.2 服务端性能优化
5.2.1 内存缓存设计
java
public class ConfigCacheService {
// 使用ConcurrentHashMap存储配置缓存
private static final ConcurrentHashMap<String, CacheItem> CACHE =
new ConcurrentHashMap<>(1024);
// 缓存项结构
private static class CacheItem {
// 配置内容
private volatile String content;
// MD5值
private volatile String md5;
// 最后更新时间
private volatile long lastModified;
// 读写锁
private final ReadWriteLock lock = new ReentrantReadWriteLock();
// 获取配置(读锁)
public String getContent() {
lock.readLock().lock();
try {
return content;
} finally {
lock.readLock().unlock();
}
}
// 更新配置(写锁)
public void update(String newContent) {
lock.writeLock().lock();
try {
this.content = newContent;
this.md5 = MD5Utils.md5Hex(newContent, "UTF-8");
this.lastModified = System.currentTimeMillis();
} finally {
lock.writeLock().unlock();
}
}
}
}
5.2.2 异步化处理
java
// 异步事件处理
@Component
public class AsyncNotifyService {
private final ExecutorService executor = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * 2,
new ThreadFactoryBuilder()
.setNameFormat("nacos-config-notify-%d")
.build()
);
// 异步通知客户端
public void asyncNotifyClients(ConfigDataChangeEvent event) {
executor.submit(() -> {
// 1. 查找订阅该配置的客户端
List<Connection> connections = findSubscribedConnections(event);
// 2. 批量通知
for (Connection connection : connections) {
try {
notifyClient(connection, event);
} catch (Exception e) {
log.error("Notify client failed, connectionId: {}",
connection.getConnectionId(), e);
}
}
});
}
}
5.3 监控与指标
Nacos配置中心提供了丰富的监控指标:
5.3.1 客户端监控指标
java
public class ConfigMetrics {
// 配置获取成功率
private static final Meter getConfigSuccessMeter =
Metrics.newMeter("nacos.config.get.success", "requests", TimeUnit.SECONDS);
// 配置获取失败率
private static final Meter getConfigFailMeter =
Metrics.newMeter("nacos.config.get.fail", "requests", TimeUnit.SECONDS);
// 配置变更推送延迟
private static final Histogram configPushLatency =
Metrics.newHistogram("nacos.config.push.latency");
// 记录配置获取成功
public static void recordGetConfigSuccess() {
getConfigSuccessMeter.mark();
}
// 记录配置获取失败
public static void recordGetConfigFail() {
getConfigFailMeter.mark();
}
}
5.3.2 服务端监控指标
java
public class ServerConfigMetrics {
// 配置发布QPS
private static final Meter publishQps =
Metrics.newMeter("nacos.config.publish.qps", "requests", TimeUnit.SECONDS);
// 内存缓存命中率
private static final Counter cacheHitCounter = Metrics.newCounter("nacos.config.cache.hit");
private static final Counter cacheMissCounter = Metrics.newCounter("nacos.config.cache.miss");
// 数据库查询耗时
private static final Timer dbQueryTimer = Metrics.newTimer("nacos.config.db.query");
}
六、总结与最佳实践
6.1 核心设计思想总结
-
分层缓存设计:
-
客户端本地快照 → 服务端内存缓存 → 数据库持久化
-
每层缓存失效时自动降级到下一层
-
-
事件驱动架构:
-
配置变更通过事件通知机制传播
-
解耦各个处理环节,提高系统可扩展性
-
-
最终一致性模型:
-
集群节点间异步同步配置变更
-
通过版本号和MD5校验保证数据正确性
-
-
故障容错设计:
-
客户端本地快照保证服务器不可用时的基本功能
-
集群节点故障自动隔离与恢复
-
6.2 性能调优建议
-
客户端调优:
yaml
# application.yml配置 spring: cloud: nacos: config: # 快照文件路径 snapshot-path: ${user.home}/nacos/config # 长轮询超时时间 config-long-poll-timeout: 30000 # 最大重试次数 max-retry: 3 # 连接超时时间 config-retry-time: 2000 -
服务端调优:
properties
# application.properties配置 # 内存缓存大小 nacos.config.cache.max-size=100000 # 数据库连接池大小 spring.datasource.hikari.maximum-pool-size=20 # 异步处理线程数 nacos.config.async.notify-threads=20
-
集群部署建议:
-
推荐3节点或5节点集群,保证选举和容错能力
-
使用独立的MySQL集群作为持久化存储
-
配置合理的JVM参数,避免频繁Full GC
-
6.3 故障排查指南
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 配置不生效 | 1. 客户端本地缓存未更新 2. 监听器注册失败 3. 网络分区 |
1. 检查客户端快照文件 2. 验证监听器注册日志 3. 检查网络连接 |
| 配置推送延迟 | 1. 服务端处理队列堆积 2. 网络延迟高 3. 客户端处理慢 |
1. 监控服务端队列长度 2. 检查网络质量 3. 优化客户端处理逻辑 |
| 内存持续增长 | 1. 缓存未及时清理 2. 监听器泄漏 3. 连接未释放 |
1. 检查缓存清理策略 2. 监控监听器数量 3. 检查连接池配置 |
6.4 未来演进方向
-
多级缓存优化:引入Redis等分布式缓存,减少数据库压力
-
配置分片存储:超大配置的分片存储与合并处理
-
智能推送优化:基于配置变更影响的智能推送策略
-
安全性增强:更完善的配置加密与权限控制
更多推荐



所有评论(0)