Nacos 配置中心原理#
候选人小孙在面试某互联网公司时,面试官看了他的项目经验"负责配置中心迁移",问道:
"Nacos 配置变更的时候,是推送还是拉取?"
小孙说:"推送的吧,我记得有个 watcher。"
面试官追问:"那为什么还有长轮询?"
小孙说:"呃...我不太确定..."
面试官追问:"Nacos 的命名空间和分组有什么区别?你们怎么隔离不同环境的配置?"
小孙支支吾吾。
【面试官心理】 这道题我用来区分"用过"和"理解过"Nacos 的候选人。Nacos 的长轮询机制是面试高频点,很多候选人说"推送"但说不出"服务端推送的是通知还是数据"。能讲清楚"MD5 比对 + 长轮询"组合拳的,基本都有源码阅读经验。
#一、Nacos 配置核心模型 🔴
#1.1 三元组:dataId + group + namespace
// Nacos 配置的定位通过三个维度确定:
// 1. dataId:配置文件的名称(相当于文件名)
// 2. group:配置分组(默认 DEFAULT_GROUP,相当于文件夹)
// 3. namespace:命名空间(相当于顶级目录)
// 三者的层级关系:
// namespace > group > dataId
// 典型场景:
// namespace: dev / test / prod
// group: order-service / user-service / payment-service
// dataId: application.yaml / db.properties / redis.properties
// 获取配置:
ConfigService configService = NacosFactory.createConfigService(serverAddr);
String content = configService.getConfig(dataId, group, timeout);
// 示例:
String dbConfig = configService.getConfig(
"db.properties", // dataId
"order-service", // group
3000 // timeout
);
// 等价于:
// namespace=prod 的 prod/order-service/db.properties#1.2 命名空间隔离:不同环境
// 命名空间用于隔离不同环境
// dev / test / uat / prod
// 客户端配置:
// application.yml
spring:
cloud:
nacos:
config:
namespace: prod # 指定命名空间
group: DEFAULT_GROUP # 指定分组
data-id: application.yaml # dataId(支持逗号分隔配置多个)
file-extension: yaml # 配置文件格式
// 或者通过 JVM 参数:
// -D nacos.config.namespace=prod命名空间隔离是环境隔离的关键。如果 namespace 配置错误,本地开发环境可能读到测试环境的配置,或者测试环境读到生产环境的配置。生产事故中,环境配置混淆是最常见的翻车原因之一。
#1.3 分组隔离:不同服务或模块
// 分组用于在同一个 namespace 下,进一步隔离配置
// 默认分组:DEFAULT_GROUP
// 自定义分组:按业务线、按服务名、按环境维度
// 场景:同一个 namespace 下,有多个微服务的配置
// namespace = prod
// group = order-service -> order 服务的配置
// group = user-service -> user 服务的配置
// group = payment-service -> payment 服务的配置
// dataId = jdbc.yaml -> 3 份 jdbc.yaml,分别属于3个分组
// 获取特定分组的配置:
ConfigService service = NacosFactory.createConfigService("localhost:8848");
String jdbcConfig = service.getConfig("jdbc.yaml", "order-service", 3000);
// prod/order-service/jdbc.yaml#1.4 共享配置与扩展配置
// 场景:多个微服务需要共享同一份配置(比如数据库连接池配置)
// 但又想保持各自的服务级配置
// 共享配置(shared-dataids):所有服务共享
// 扩展配置(ext-config):按需引入
spring:
cloud:
nacos:
config:
# 共享配置列表(按声明顺序,后面的覆盖前面的)
shared-dataids: |
jdbc.properties
redis.properties
# 共享配置可以自动刷新
refreshable-dataids: |
jdbc.properties
# 扩展配置
ext-config:
- data-id: db.properties
group: COMMON_GROUP
refresh: true#二、长轮询推送机制 🔴
#2.1 为什么需要长轮询
// 短轮询(Short Polling):客户端每隔几秒轮询一次
// 问题:延迟高(最多等待一个轮询周期)、服务端压力大
// 长连接推送(Push):服务端维护每个客户端的长连接
// 问题:Nacos 是无状态服务,维护百万级长连接成本高
// 需要引入 WebSocket 网关,架构复杂度增加
// Nacos 的方案:HTTP 长轮询(Long Polling)
// 核心思路:
// 1. 客户端发起 HTTP 长轮询请求,指定超时时间(默认 30 秒)
// 2. 服务端收到请求后,检查配置是否变化
// - 如果没变化:hold 住请求,直到超时或配置变化
// - 如果有变化:立即返回变更通知
// 3. 客户端收到返回后,立即发起下一次长轮询
// 4. 整个过程循环往复,实现"准推送"体验#2.2 客户端长轮询源码
// Nacos 客户端的核心轮询逻辑
// 类名:NacosContextRefresher 或 ConfigHttpServlet
public class LongPolling {
private ExecutorService executor = Executors.newSingleThreadExecutor();
public void startLongPolling() {
executor.submit(() -> {
while (true) {
doLongPolling();
}
});
}
private void doLongPolling() {
// 关键参数:
// - longPollTimeout: 30000 毫秒(30秒)
// - taskExecutor: 异步执行,不阻塞
// 发起长轮询请求
// GET /v1/cs/configs?dataId=xxx&group=xxx&contentMD5=xxx&timeout=30000
// MD5 比对:如果服务端配置的 MD5 和客户端传入的 MD5 相同
// 说明配置没变化,服务端会 hold 30 秒后返回空
// 如果 MD5 不同,说明配置变化了,服务端立即返回变更通知
}
}
// 客户端收到变更通知后的处理:
// 1. 重新拉取最新配置
// 2. 更新本地缓存的 MD5
// 3. 触发所有监听器的回调
// 4. 立即发起下一次长轮询(不等上一个请求的响应)#2.3 服务端的 MD5 比对与变更检测
// Nacos 服务端的关键逻辑
// 类名:ConfigController 或 ConfigServletInner
public class ConfigController {
// 长轮询接口
@GetMapping("/listener")
public String listener(
@RequestParam("dataId") String dataId,
@RequestParam("group") String group,
@RequestParam("contentMD5") String contentMD5,
@RequestParam("timeout") long timeout) {
// 1. 获取配置的当前 MD5
String currentMD5 = getConfigMD5(dataId, group);
// 2. MD5 相同 -> 配置没变,hold 住请求
if (currentMD5.equals(contentMD5)) {
// 等待配置变更或超时
// 使用 CompletableFuture 实现异步 wait
// 默认 wait 29 秒,留 1 秒给响应
return waitForConfigChange(dataId, group, timeout - 1000);
}
// 3. MD5 不同 -> 配置变了,立即返回变更通知
// 返回值:变更的 dataId 和 group
return formatChangeResult(dataId, group);
}
}
// 变更通知的返回格式:
// [{"dataId":"db.properties","group":"order-service","changeInfo":"..."}]
// 客户端收到后,解析返回,立即拉取最新配置#2.4 MD5 比对的精妙设计
// MD5 比对的好处:
// 1. 节省带宽:配置没变化时,服务端只需返回一个固定字符串(不是完整配置)
// 2. 精确判断:即使配置值变了但大小没变,也能检测到
// 3. 性能优化:服务端不需要比对每个配置项的详细内容
// 典型流程:
// 1. 客户端首次启动:拉取完整配置,记录 MD5
// 2. 后续轮询:传入上次获取的 MD5
// 3. 服务端比对:
// - MD5 相同 -> 返回空(无变化)
// - MD5 不同 -> 返回变更通知
// 4. 客户端收到通知 -> 立即拉取最新配置 -> 更新本地 MD5Nacos 的长轮询设计非常精妙:服务端不需要主动推送数据,也不需要维护大量长连接。只要HTTP请求能被hold住,就能实现秒级的变更通知。MD5比对让服务端只需返回"是否有变化"这个布尔值,而不是完整的配置内容。
#三、配置监听机制 🔴
#3.1 Listener 接口
// Nacos 提供了配置监听接口
// 当配置变更时,自动触发监听器回调
// 方式1:直接使用 Listener 接口
ConfigService service = NacosFactory.createConfigService(serverAddr);
service.addListener("db.properties", "order-service", new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
// 配置变更时回调
// configInfo 是最新的配置内容
System.out.println("配置变更: " + configInfo);
// 重新加载配置
reloadDatabaseConfig(configInfo);
}
@Override
public Executor getExecutor() {
// 回调使用的线程池(可选)
return null; // 使用默认线程池
}
});
// 方式2:Spring Cloud 集成(Nacos Starter 自动处理)
// 只需在 application.yml 配置,无需手动写监听代码
// Spring Boot 会自动创建 ConfigService 和 Listener
// 配置变更时自动刷新 @Value 注入的字段#3.2 监听器的工作原理
// 监听器的内部实现:
// 1. 客户端在本地维护一个 listenerMap
// 2. listenerMap 的结构:Map<dataId + group, List<Listener>>
// 3. 长轮询收到变更通知后,遍历对应的 Listener 列表
// 4. 逐个调用 receiveConfigInfo() 方法
// 关键代码(简化版):
public class ConfigServiceImpl implements ConfigService {
// 本地监听器注册表
private final Map<String, CopyOnWriteArrayList<Listener>> listenerMap =
new ConcurrentHashMap<>();
@Override
public void addListener(String dataId, String group, Listener listener) {
String key = dataId + "+" + group;
listenerMap.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>())
.add(listener);
}
// 长轮询线程收到变更通知
private void onConfigChanged(String dataId, String group, String configInfo) {
String key = dataId + "+" + group;
CopyOnWriteArrayList<Listener> listeners = listenerMap.get(key);
if (listeners != null) {
for (Listener listener : listeners) {
// 在独立线程池中执行,不阻塞长轮询线程
executor.execute(() ->
listener.receiveConfigInfo(configInfo)
);
}
}
}
}#3.3 Spring Boot 集成示例
// application.yml
spring:
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
namespace: dev
group: DEFAULT_GROUP
file-extension: yaml
refresh-enabled: true # 开启自动刷新
// 方式1:@Value 注入(需要加 @RefreshScope)
@RefreshScope
@RestController
public class OrderController {
@Value("${order.max-retry:3}") // 支持默认值
private int maxRetry;
@GetMapping("/order/retry")
public int getMaxRetry() {
return maxRetry; // Nacos 配置变更后,这个值会自动刷新
}
}
// 方式2:@ConfigurationProperties(自动刷新)
@Data
@Component
@ConfigurationProperties(prefix = "order")
public class OrderProperties {
private int maxRetry;
private int timeout;
// Nacos 配置变更后,Bean 的属性会自动更新
}
// 方式3:手动刷新(复杂的初始化逻辑)
@RestController
public class ManualRefreshController {
@NacosConfigurationListener
public void onConfigChange(NacosConfigEvent event) {
if ("db.properties".equals(event.getDataId())) {
// 重新初始化数据库连接池
reinitDataSource(event.getContent());
}
}
}
@RefreshScope 虽然能自动刷新配置,但它本身有代价:每次刷新都会创建新的 Bean 实例。如果 Bean 中有昂贵的初始化逻辑(比如建立连接池),频繁的配置刷新反而会导致性能问题。建议对配置变化做过滤,只处理真正需要变化的配置。
#四、多配置文件管理 🟡
#4.1 为什么需要多配置文件
// 微服务的配置通常分散在多个文件中:
// 1. application.yaml:应用主配置
// 2. db.properties:数据库配置
// 3. redis.properties:缓存配置
// 4. rpc.yaml:RPC 调用配置
// 5. feature-flags.yaml:功能开关
// Nacos 的优势:
// 1. 多个配置文件可以归到不同的 group
// 2. 不同团队维护各自负责的配置
// 3. 配置变更时可以精确控制影响范围#4.2 配置优先级
// Nacos 配置的优先级(高到低):
// 1. 通过 ext-config[-n] 引入的配置(后声明的优先)
// 2. shared-dataids 中的共享配置
// 3. 主配置 data-id(application.yaml)
// 完整优先级(由高到低):
// 1. 通过 JVM 参数传入 -D spring.cloud.nacos.configOverride=xxx
// 2. 通过 bootstrap.yml 扩展配置(ext-config[-n])
// 3. 共享配置(shared-dataids)
// 4. 主配置(主 dataId)
// 5. 本地配置文件(本地优先级最低)
// 实际例子:
// spring.cloud.nacos.config.data-id: application.yaml
// spring.cloud.nacos.config.ext-config[0].data-id: db.properties
// spring.cloud.nacos.config.ext-config[0].group: DB_GROUP
// spring.cloud.nacos.config.ext-config[1].data-id: redis.properties
// spring.cloud.nacos.config.ext-config[1].group: CACHE_GROUP
// 最终生效的配置 = application.yaml 覆盖 db.properties 覆盖 redis.properties#五、Nacos vs Apollo vs Spring Cloud Config 🟡
| 维度 | Nacos | Apollo | Spring Cloud Config |
|---|---|---|---|
| 推送机制 | HTTP 长轮询 | 长轮询 + 消息队列 | Git Webhook + Spring Cloud Bus |
| 架构复杂度 | 低(单进程) | 高(4 个模块) | 中(需 Git + Eureka) |
| 多环境 | 命名空间隔离 | 多环境独立部署 | profiles 隔离 |
| 灰度发布 | 支持(权重灰度) | 支持(规则灰度) | 不支持 |
| 管理界面 | 自带 Web UI | 自带 Web UI | 无 |
| 多语言 | 支持(HTTP 接口) | 支持(HTTP 接口) | 差(依赖 Spring) |
| 高可用 | 内置(无中心化) | 需要 MySQL HA | 依赖 Git 和 Eureka |
| 配置监听 | 原生 Listener | 原生 Listener | Spring 事件机制 |
| 适用场景 | 中小型微服务 | 大型/金融级 | Spring Cloud 生态 |
【面试官心理】 问到三大配置中心对比的候选人,说明他有技术选型的经验。我会追问:"如果你们的微服务有 100 个节点,Nacos 的长轮询会不会有问题?"能说出"需要部署多个 Nacos 节点,用 Nginx 做负载均衡"的,说明他有生产运维经验。
#六、生产避坑指南 🔴
#❌ 翻车点一:长轮询超时导致配置丢失
// ❌ 问题:Nacos 服务端压力大,长轮询超时
// 表现:配置变更了,但部分客户端过了很久才收到通知(甚至收不到)
// ✅ 解决方案:
// 1. 服务端增加节点,水平扩展
// 2. 客户端设置合理的超时时间
spring.cloud.nacos.config.timeout = 5000 # 5秒超时
// 3. 客户端增加重试机制
spring.cloud.nacos.config.refresh.retry = 3#❌ 翻车点二:@RefreshScope 导致 Bean 重复创建
// ❌ 问题:加了 @RefreshScope 后,每次配置变更都创建新的 Bean
// 导致原有连接未关闭,新连接又被创建
// 内存泄漏 + 连接池耗尽
@RefreshScope
@Component
public class DataSourceConfig {
@Value("${db.url}")
private String url;
@Bean
public DataSource dataSource() {
return DruidDataSourceFactory.createDataSource(properties);
// 每次配置变更,都会创建新的 DataSource
// 旧的 DataSource 如果没有 close(),就会内存泄漏
}
}
// ✅ 正确做法:不要在 @RefreshScope Bean 中创建资源
// 应该使用 @NacosConfigurationListener 手动处理
// 或者使用 SmartInitializingSingleton 在应用启动时初始化资源#❌ 翻车点三:namespace 配置错误导致环境混淆
// ❌ 危险:本地开发时,为了调试方便,把 namespace 配成了 prod
// 导致本地读取了生产配置
// 如果改了配置,会影响生产环境!
// ✅ 正确做法:
// 1. 不同环境的 namespace UUID 一定要记录在文档中
// 2. 本地开发用 dev namespace
// 3. 生产 namespace 配置要严格控制访问权限
// 获取 namespace ID 的方式:
// 在 Nacos 控制台 -> 命名空间 -> 查看 namespace ID(不是 namespace 名称)
// namespace ID 是 UUID,不是可读名称#七、面试追问链 🟡
#追问一:Nacos 长轮询和 Spring Cloud OpenFeign 有什么关系?
【面试官心理】 问这个问题的面试官,通常想确认候选人对 Spring Cloud 生态的熟悉程度。
Nacos 的配置监听底层使用 HTTP 长轮询,不依赖 OpenFeign。但在 Spring Cloud 生态中,Nacos 和 OpenFeign 是两个独立的组件:
- Nacos:提供服务发现和配置管理
- OpenFeign:提供声明式 HTTP 客户端
- 两者可以组合使用(Nacos 注册服务 + OpenFeign 调用服务)
#追问二:Nacos 的 CP 和 AP 怎么选?
【面试官心理】
问这个问题的面试官,通常想确认候选人对分布式一致性理论的理解。Nacos 既支持 CP(一致性 + 分区容错)也支持 AP(可用性 + 分区容错),通过 nacos.distro.config 和 nacos.distro.distro 配置切换。
// Nacos 的 CP/AP 切换:
// 1. 配置中心默认是 AP(保证可用性)
// - 使用 Distro 协议,异步同步数据
// - 节点间数据最终一致,但不保证强一致
// 2. 注册中心可以切换为 CP
// - 使用 Raft 协议,强一致
// - 注册信息在所有节点完全一致
// - 但节点故障时,部分服务注册可能失败
// 生产建议:
// - 注册中心:用 CP(服务发现必须准确)
// - 配置中心:用 AP(配置可稍有延迟,但服务不能断)