配置中心比较
前言
Github:https://github.com/HealerJean
一、常见配置中心
1、轮询机制对比
| 行为 | 短轮询 | 长轮询 |
|---|---|---|
| 客户端请求 | 每 1 秒发一次 | 发一次,等 30 秒或有数据才返回 |
| TCP 连接寿命 | 每次请求新建或快速关闭(<100ms) | 单次连接保持数秒~数十秒 |
| 服务端并发连接数 | 高频瞬时连接(可能更高) | 同时打开的连接数 ≈ 并发消费者数 |
| 是否“短连接” | 是 | 不是 |
1)短轮询(Short Polling)
- 客户端定时(如每
10s)向服务端拉取配置。 - 缺点:无法在“低延迟”和“低压力”之间取得平衡。
- 高频请求增加服务端压力;
- 配置变更存在延迟(最大为轮询间隔);
- 资源浪费(多数请求无变更)。
2)长轮询(Long Polling)
-
客户端发起请求后,服务端
hold住连接,直到:- 配置发生变更 → 立即返回新值;
- 超时(如
30s)→ 返回“未变更”(HTTP 304)。
-
优点:
- 接近实时推送(延迟 < 1s);
- 降低无效请求频率;
- 服务端资源消耗可控(异步处理 + 超时释放)。
3)长轮询实现原理

a、客户端(Java 示例)
- 使用
CloseableHttpClient发起 HTTP 请求; - 设置超时时间 > 服务端 hold 时间(如 40s > 30s);
- 收到 200 表示配置变更,304 表示无变更,继续下一次长轮询。
b、服务端(Spring Boot 示例)
- 利用
Servlet 3.0的AsyncContext实现异步响应; - 维护
dataId → List<AsyncTask>映射; - 启动定时任务(
30s)处理超时; - 当配置更新时,遍历对应
AsyncTask并立即响应。
c、代码举例
客户端:
@Slf4j
public class ConfigClientWorker {
private final CloseableHttpClient httpClient;
private final ScheduledExecutorService executorService;
public ConfigClientWorker(String url, String dataId) {
this.executorService = Executors.newSingleThreadScheduledExecutor(runnable -> {
Thread thread = new Thread(runnable);
thread.setName("client.worker.executor-%d");
thread.setDaemon(true);
return thread;
});
// ① httpClient 客户端超时时间要大于长轮询约定的超时时间
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(40000).build();
this.httpClient = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).build();
executorService.execute(new LongPollingRunnable(url, dataId));
}
class LongPollingRunnable implements Runnable {
private final String url;
private final String dataId;
public LongPollingRunnable(String url, String dataId) {
this.url = url;
this.dataId = dataId;
}
@SneakyThrows
@Override
public void run() {
String endpoint = url + "?dataId=" + dataId;
log.info("endpoint: {}", endpoint);
HttpGet request = new HttpGet(endpoint);
CloseableHttpResponse response = httpClient.execute(request);
switch (response.getStatusLine().getStatusCode()) {
case 200: {
BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity()
.getContent()));
StringBuilder result = new StringBuilder();
String line;
while ((line = rd.readLine()) != null) {
result.append(line);
}
response.close();
String configInfo = result.toString();
log.info("dataId: [{}] changed, receive configInfo: {}", dataId, configInfo);
break;
}
// ② 304 响应码标记配置未变更
case 304: {
log.info("longPolling dataId: [{}] once finished, configInfo is unchanged,
longPolling again", dataId);
break;
}
default: {
throw new RuntimeException("unExcepted HTTP status code");
}
}
executorService.execute(this);
}
}
public static void main(String[] args) throws IOException {
new ConfigClientWorker("http://127.0.0.1:8080/listener", "user");
System.in.read();
}
}
服务端:
@RestController
@Slf4j
@SpringBootApplication
public class ConfigServer {
@Data
private static class AsyncTask {
// 长轮询请求的上下文,包含请求和响应体
private AsyncContext asyncContext;
// 超时标记
private boolean timeout;
public AsyncTask(AsyncContext asyncContext, boolean timeout) {
this.asyncContext = asyncContext;
this.timeout = timeout;
}
}
// guava 提供的多值 Map,一个 key 可以对应多个 value
private Multimap<String, AsyncTask> dataIdContext = Multimaps.synchronizedSetMultimap(
HashMultimap.create());
private ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("longPolling-timeout-checker-%d")
.build();
private ScheduledExecutorService timeoutChecker = new ScheduledThreadPoolExecutor(1, threadFactory);
// 配置监听接入点
@RequestMapping("/listener")
public void addListener(HttpServletRequest request, HttpServletResponse response) {
String dataId = request.getParameter("dataId");
// 开启异步!!!
AsyncContext asyncContext = request.startAsync(request, response);
AsyncTask asyncTask = new AsyncTask(asyncContext, true);
// 维护 dataId 和异步请求上下文的关联
dataIdContext.put(dataId, asyncTask);
// 启动定时器,30s 后写入 304 响应
timeoutChecker.schedule(() -> {
if (asyncTask.isTimeout()) {
dataIdContext.remove(dataId, asyncTask);
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
// 标志此次异步线程完成结束!!!
asyncContext.complete();
}
}, 30000, TimeUnit.MILLISECONDS);
}
// 配置发布接入点
@RequestMapping("/publishConfig")
@SneakyThrows
public String publishConfig(String dataId, String configInfo) {
log.info("publish configInfo dataId: [{}], configInfo: {}", dataId, configInfo);
Collection<AsyncTask> asyncTasks = dataIdContext.removeAll(dataId);
for (AsyncTask asyncTask : asyncTasks) {
asyncTask.setTimeout(false);
HttpServletResponse response = (HttpServletResponse)asyncTask.getAsyncContext().getResponse();
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().println(configInfo);
asyncTask.getAsyncContext().complete();
}
return "success";
}
public static void main(String[] args) {
SpringApplication.run(ConfigServer.class, args);
}
}
2、动态监听机制
| 模式 | 特点 | 缺点 |
|---|---|---|
Pull |
客户端定时拉取 | 延迟高、无效请求多 |
Push |
服务端主动推送 | 需维护长连接,资源消耗大 |
Nacos(Pull + 长轮询) |
折中方案 | 实现稍复杂,但效果最佳 |
a、PULL 模式:表示客户端从服务端主动拉取数据.。
Pull 模式下,客户端需要定时从服务端拉取一次数据,由于定时带来的时间间隔,因此不能保证数据的实时性,并且在服务端配置长时间不更新的情况下,客户端的定时任务会做一些无效的 Pull 操作。
b、PUSH 模式:服务端主动把数据推送到客户端。
Push 模式下,服务端需要维持与客户端的长连接,如果客户端的数量比较多,那么服务端需要耗费大量的内存资源来保存每个资源,并且为了检测连接的有效性,还需要心跳机制来维持每个连接的状态。
c、Pull + 长轮询:
Nacos 采用的是 Pull 模式,并且采用了一种长轮询机制。客户端采用长轮询的方式定时的发起 Pull 请求,去检查服务端配置信息是否发生了变更,如果发生了变更,那么客户端会根据变更的数据获得最新的配置。
3)长连接、长轮询
-
长连接(如
WebSocket、TCPKeep-Alive):- 连接建立后长期保持,服务端需为每个连接维护状态(内存、文件描述符等)百万级连接对服务器压力极大。
-
长轮询(
LongPolling):-
基于
HTTP长连接(Keep-Alive),客户端发起一次请求后,服务端hold住连接直到有数据或超时,响应后连接可复用或关闭。 -
选择长轮询:
-
-
避免服务端为海量客户端维持长连接状态
-
省去心跳机制带来的
CPU/网络开销 -
实现无状态、易扩展、高容错的架构
一、Spring Cloud Config
Spring Cloud Config是Spring Cloud生态中的 集中式外部配置管理组件,主要用于在分布式系统中统一管理应用的配置文件(如application.yml、bootstrap.properties等)。
1、核心能力
⚠️ 注意:
Spring Cloud Config本身不提供“实时监听”能力,需依赖Spring Cloud Bus实现推送式刷新
- 集中化配置存储
- 支持将配置文件托管在 Git 仓库(默认)、SVN 或 本地文件系统;
- 支持环境隔离(如
dev/test/prod)和应用维度划分。
- 动态配置拉取
- 客户端启动时从
Config Server拉取配置; - 结合
Spring Cloud Bus+RabbitMQ/Kafka可实现 配置变更广播与自动刷新(需额外组件)。
- 客户端启动时从
- 版本控制与审计
- 基于
Git,天然支持配置的历史版本回溯、差异对比、提交记录等。
- 基于
2、基本操作(配置中心)
| 操作 | 描述 |
|---|---|
| 获取配置 | 客户端通过 /actuator/env 或启动时自动从 Config Server 拉取配置 |
| 监听配置 | 原生不支持;需集成 Spring Cloud Bus + 消息中间件实现“伪监听” |
| 发布配置 | 修改 Git 仓库中的配置文件并提交(或通过 API 触发 refresh) |
| 删除配置 | 从 Git 仓库中删除对应配置文件并提交 |
3、基础 CRUD
- 服务端(
Config Server):- 不直接存储配置,而是作为
Git/SVN/本地文件的代理层; - 启动时不会加载所有配置,而是在客户端请求时 按需拉取并缓存。
- 不直接存储配置,而是作为
- 客户端(
Config Client):- 通过
spring.cloud.config.uri指向Config Server; - 使用
@Value、@ConfigurationProperties注入配置; - 配置优先级:本地配置 <
Config Server配置(可通过spring.cloud.config.override-none=true调整)。
- 通过
4、Pull 模式 + 手动/总线刷新
Spring Cloud Config采用纯 Pull模式**,客户端仅在 **启动时** 或 **手动触发/actuator/refresh** 时拉取最新配置。 若需“近实时”更新,必须引入 **Spring Cloud Bus` 构建事件驱动架构。

1)标准 Pull 流程(无 Bus)
❌ 缺点:无法自动感知配置变更,运维成本高。
- 应用启动 → 从
Config Server拉取配置(一次性的); - 开发者修改
Git中的配置; - 客户端不会自动感知变更;
- 需手动调用
POST /actuator/refresh触发局部刷新(仅支持@RefreshScope标注的 Bean)。
修改配置后,需手动 POST 请求:curl -X POST http://client:8080/actuator/refresh
@RestController
@RefreshScope // 必须加此注解,否则 @Value 不会更新
public class TestController {
@Value("${user.name}")
private String userName;
}
2)Pull + Bus 推送(模拟“Push”)
- 修改
Git配置并提交; - 调用
Config Server的/monitor端点(或 Webhook 自动触发); Config Server向 消息队列(RabbitMQ/Kafka) 发送刷新事件;- 所有订阅该
Topic的客户端收到消息,自动执行/refresh; - 配置生效(限
@RefreshScopeBean)。

a、和 Nacos 比较
此方案实现了 “配置变更 → 自动广播 → 客户端刷新” 的闭环。
| 特性 | Spring Cloud Config(+ Bus) | Nacos |
|---|---|---|
| 通信协议 | HTTP(Config Server ↔ Client) + AMQP/Kafka(Bus) |
HTTP |
| 连接类型 | 短连接(Pull) + 消息订阅(长连接) |
HTTP 长轮询 |
| 数据获取模式 | Pull(启动/refresh 时) |
Pull(长轮询模拟 Push) |
| 实时性 | 依赖 Bus,秒级 | 秒级(<1s) |
| 存储后端 | Git/SVN/本地文件 | 内嵌 Derby / MySQL |
| 自动刷新 | 需额外集成 Bus |
原生支持 |
| 运维复杂度 | 高(需维护 Git + MQ + Webhook) | 低(开箱即用) |
b、代码配置
-
引入依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> <!-- 或 kafka --> </dependency> -
配置
RabbitMQ/Kafka; -
在 Git 仓库设置 Webhook,指向 Config Server 的
/monitor; -
客户端无需任何代码改动,自动刷新。
5、客户端实现原理
客户端在启动时一次性拉取配置,后续依赖外部触发刷新。为保证可靠性,设计了双重保障机制。
1)启动时 Pull(主流程)
- 应用启动 → 加载
bootstrap.yml→ 连接Config Server; - 请求路径示例:
http://config-server:8888/user-service/dev/master - 返回
JSON格式的配置属性; - 注入到
Spring Environment,优先级高于本地配置。
2)运行时 Refresh(被动触发)
- 仅当调用
/actuator/refresh或收到Bus事件时触发; - 限制:
- 仅更新
@RefreshScope标注的Bean; @Value需配合@RefreshScope才能生效;- 静态字段、构造器注入等无法动态更新。
- 仅更新
3)本地缓存与容灾
- 内存缓存:配置加载后存于
Environment; - 本地文件缓存:不支持(与
Nacos/Apollo不同);- 若
Config Server不可用,重启应用将失败(除非启用spring.cloud.config.fail-fast=false); - 运行中若
Server宕机,已加载配置仍可用,但无法刷新。
- 若
6、FQA
1)Spring Cloud Config 能支撑大规模客户端吗?
- 优点:无状态,可水平扩展,可通过集群部署水平扩展;
- 瓶颈:
Git仓库I/O性能(高频拉取可能触发限流);- 消息总线吞吐量
- 建议:
- 按业务域拆分多个
Config Server; - 使用企业级
Git 服务(如 GitLab Premium); - 控制刷新频率,避免雪崩。
- 按业务域拆分多个
2)为什么很多新项目转向 Nacos/Apollo?
| 维度 | Spring Cloud Config | Nacos / Apollo |
|---|---|---|
| 实时推送 | 需额外集成 Bus | 原生支持 |
| 本地缓存 | ❌ 无 | ✅ 有(磁盘 + 内存) |
| 控制台 | ❌ 无(需自建) | ✅ 内置 Web UI |
| 权限治理 | ❌ 弱 | ✅ 完善(用户/项目/环境权限) |
| 多语言支持 | ❌ Java 为主 | ✅ 多语言 SDK |
| 部署复杂度 | ⚠️ 高(Git + MQ + Webhook) | ✅ 低(一体化) |
3)如何保证配置修改后所有机器都生效?
- 原生模式:无法自动保证,需逐台调用
/actuator/refresh; - 推荐方案:集成 Spring Cloud Bus + RabbitMQ/Kafka + Git Webhook,实现全自动广播刷新;
- 替代方案:通过注册中心(
Eureka/Nacos)获取实例列表,编写脚本批量调用 refresh 接口。
二、Nacos
1、核心能力
Nacos是一个面向云原生应用的 动态服务发现、配置管理和服务管理平台,主要提供两大核心功能:
- 配置中心(
ConfigurationManagement)- 支持配置注册、动态下发、版本管理、灰度发布等;
- 实现 无需重启服务即可动态刷新配置。
- 服务注册与发现(
ServiceDiscovery)- 支持
Dubbo、Spring Cloud、gRPC、Kubernetes等多种服务类型; - 提供健康检查、负载均衡、元数据管理等能力。
- 支持
2、基本操作(配置中心)
| 操作 | 描述 |
|---|---|
| 获取配置 | 从 Nacos Config Server 读取指定 dataId 的配置内容 |
| 监听配置 | 订阅配置变更事件,支持实时响应 |
| 发布配置 | 将新的配置写入 Nacos 配置中心 |
| 删除配置 | 从配置中心移除指定配置项 |
3、基础 CRUD
- 服务端:配置存储(内存 + 持久化,如
Derby/ MySQL); - 客户端:通过
API拉取配置(getConfig(dataId, group))。
4、伪PUSH + PULL + 长轮询
Nacos采用的是Pull模式,,并且采用了一种长轮询机制。Nacos的长轮询是一种优雅的“伪推送”方案,在 实时性、性能、兼容性 之间取得了极佳的平衡,这也是其被广泛采用的重要原因。
| 特性 | Nacos(配置中心) | Kafka(消息队列) |
|---|---|---|
| 通信协议 | HTTP/HTTPS | 自定义二进制协议(基于 TCP) |
| 连接类型 | HTTP 短连接 + 服务端 hold(长轮询) | TCP 长连接 |
| 数据获取模式 | Pull(客户端拉) | Pull(客户端拉) |
| 无数据时行为 | 服务端 hold 请求最多 30s | Broker hold fetch 请求最多 fetch.wait.max.ms(如 500ms) |
| 连接复用 | 每次长轮询是独立 HTTP 请求(可复用连接池) | 同一连接持续用于多次 fetch |
1)Nacos 长轮询流程
-
客户端请求监听
/listener?dataId=xxx携带当前配置的MD5(用于比对是否变更); -
服务端处理:
- 若配置未变 → 启动
29.5s定时任务,将请求加入allSubs队列;- 为什么是 29.5s?:留出
0.5s网络/处理缓冲,避免客户端因超时断开。
- 为什么是 29.5s?:留出
- 若配置变更 → 遍历
allSubs,找到匹配的监听连接,立即返回变更内容;
- 若配置未变 → 启动
-
客户端收到响应:
- 200:拉取新配置并刷新本地缓存;
- 304:继续发起下一次长轮询。

2)服务端事件驱动
- 配置变更 → 发布
ConfigDataChangeEvent; LongPollingService.onEvent()被触发;- 遍历
allSubs,找到受影响的ClientLongPolling任务; - 通过其
AsyncContext写回响应。
3)客户端自动刷新(Spring Boot集成)
NacosContextRefresher在应用启动时注册监听器;- 配置变更 → 触发
RefreshEvent→RefreshEventListener调用refresh(); - 自动更新
@Value、@ConfigurationProperties等注解绑定的配置。
5、FQA
1)长轮询会压垮服务端吗?
千万客户端同时发起长轮询,依然可能压垮服务端。关键看:
a、 线程模型:Tomcat 线程不会被耗尽
- 如果每个请求都占用一个
Tomcat线程并阻塞30秒 → 200 线程的Tomcat只能同时处理 200 个监听请求 → 完全不可扩展。 Nacos的真实做法:- 使用
Servlet 3.0AsyncContext,将请求挂起后立即释放Tomcat线程; - 实际
hold请求的是 自定义异步任务队列 + 定时器线程池; Tomcat线程只用于接收请求和发送响应,不参与等待。
- 使用
b、内存开销
每个 AsyncTask(或 ClientLongPolling)对象包含:
AsyncContext引用(轻量);dataId、group、MD5等元信息;- 超时定时任务引用。
假设每个监听任务占 1KB 内存:
100万客户端 →1GB;1000万客户端 →10GB。
内存会成为瓶颈,但可通过以下方式缓解:
- 合并监听:一个客户端监听多个
dataId,但只发起一次/listener请求(Nacos SDK支持批量监听); - 客户端去重:相同配置的机器共享监听(如通过
agent聚合); - 服务端分片:
Nacos集群部署,按dataId或namespace分片。
2)数据存在哪里?—— 两种存储模式
a、单机模式(Standalone)默认:Derby 内嵌数据库
-
位置:Nacos 安装目录下的
data/derby-data/nacos/ └── data/ └── derby-data/ ← Derby 数据库存放处 ├── seg0/ ├── log/ └── ... -
特点:
- 无需安装外部
DB,开箱即用; - 仅支持单机部署,无法集群;
- 性能和可靠性较低,仅用于开发/测试;
- 数据文件是二进制格式,不能直接查看。
- 无需安装外部
b、生产模式:外置 MySQL(官方唯一推荐)
- 首次启动前需手动创建数据库和表;
-
SQL 脚本在 Nacos 安装包的
conf/nacos-mysql.sql中; - 生产环境必须用 MySQL,否则无法集群、无法高可用。
c、内存 vs 磁盘:运行时数据在哪?
即使用了 MySQL,Nacos 运行时仍以内存为主:
| 数据 | 存储位置 | 说明 |
|---|---|---|
| 所有配置 | 内存(ConcurrentHashMap) + MySQL |
启动时全量加载到内存,读请求走内存 |
| 临时服务实例 | 仅内存 | 客户端心跳维持,宕机自动剔除 |
| 持久化服务实例 | 内存 + MySQL |
写入 DB,宕机后重启可恢复 |
三、Apollo
Apollo(阿波罗) 是由携程框架部研发并开源的一款 生产级分布式配置中心,致力于解决微服务架构下配置管理的痛点。
1、核心能力
定位差异:
Apollo不包含服务注册与发现功能,仅聚焦于 配置管理,通常与 Eureka/Nacos/ZooKeeper 等注册中心配合使用。
- 集中式配置管理(
Configuration Management)- 支持多环境(
DEV/FAT/UAT/PROD)、多集群、多命名空间(Namespace)的配置隔离; - 提供配置版本历史、灰度发布、操作审计、权限审批等企业级治理功能;
- 配置修改后 实时推送到客户端,无需重启服务。
- 支持多环境(
- 高可用与容灾保障
- 客户端支持本地磁盘缓存,在服务端不可用时仍可恢复配置;
- 服务端无状态,支持集群部署,结合
Eureka实现服务发现与负载均衡。
2、基本操作(配置中心)
| 操作 | 描述 |
|---|---|
| 获取配置 | 客户端通过 Apollo Client 从 Config Service 拉取指定 AppId + Cluster + Namespace 的配置 |
| 监听配置 | 订阅配置变更事件,支持通过 ConfigChangeListener 实时响应更新 |
| 发布配置 | 在 Portal 界面修改并发布配置,触发全链路推送流程 |
| 回滚配置 | 支持一键回退到任意历史版本,保障配置安全 |
1)架构图
┌──────────────┐
│ Portal │ ←─ Web UI (1套)
└──────┬───────┘
│ 调用 (SLB)
┌──────▼───────┐
│ AdminService │ ←─ 每环境1套 (PROD/DEV...)
└──────┬───────┘
│ 写库 + 发 ReleaseMessage
┌──────▼───────┐
│ ConfigDB │ ←─ 每环境独立 MySQL
└──────┬───────┘
│ 轮询消费
┌──────▼───────┐
│ ConfigService│ ←─ 每环境1套,含 MetaServer + Eureka
└──────┬───────┘
│ 长轮询推送
┌──────▼───────┐
│ Apollo Client│ ←─ 集成在每个业务应用中

2)主要模块
a、 Apollo Client(客户端 SDK)
就像“订报纸”:你告诉邮局(
Meta Server)想看哪份报(Config Service),然后每天等送报员(长轮询)送新一期。
-
功能说明:
-
集成在你的应用(如
Spring Boot服务)中; -
负责从
Apollo服务端拉取配置,并监听变更; -
通过
Meta Server获取Config Service地址,再用软负载(SLB)调用它。
-
-
举个例子:你有一个订单服务
order-service,代码里用了:-
@Value("${redis.timeout:5000}") private int redisTimeout; -
启动时,
Apollo Client自动连接Meta Server(比如http://apollo-meta.dev.com); -
问:“
order-service在DEV环境的Config Service地址有哪些?” -
得到返回:
["http://config1:8080", "http://config2:8080"] -
客户端随机选一个(软负载),发起长轮询,获取
application命名空间的配置; - 当你在
Portal修改了redis.timeout=6000并发布后,Client1 秒内自动更新该字段值。
-
b、Apollo Config Service(配置服务)
注意:虽然叫“包含三个组件”,但实际是一个
Spring Boot应用进程,只是集成了多个功能。
-
功能说明:
-
核心服务,直接面向客户端;
-
提供两个关键能力:
- 拉取配置(
GET /configs/...) - 推送通知(通过长轮询 hold 请求,有变更就立即响应)
- 拉取配置(
-
- 内部包含三个逻辑组件(部署在同一 JVM):
config:真正的配置读写逻辑;meta server:对外暴露服务发现接口;Eureka:服务注册中心(用于内部服务发现)。
-
举个例子:
-
你部署了 3 台
Config Service实例:cs1,cs2,cs3; -
它们都启动了内嵌的
Eureka Server,并互相注册; -
同时,它们也作为
Eureka Client,把自己注册进去; -
对外暴露的
Meta Server接口(如/services/config)会返回这 3 个地址; -
客户端拿到后,就能做负载均衡访问。
-
c、Apollo Portal(管理控制台)
Portal是“操作入口”,不是“数据存储者”。
-
功能说明:
-
Web界面(类似后台管理系统); -
开发者在这里创建项目、编辑配置、发布、回滚、审批等;
-
它不直接连数据库改配置,而是调用
Admin Service。
-
-
举个例子:
-
运维小王登录
http://apollo-portal.com -
找到项目
order-service→ 环境PROD→ 命名空间application -
把
log.level=INFO改成WARN,点击“发布” -
Portal背后会调用Admin Service的REST API:POST /apps/order-service/envs/PROD/clusters/default/namespaces/application/releases
-
d、Apollo Admin Service(配置管理服务)
相当于“广播站”:
Admin Service发布消息,Config Service负责“喊话”给客户端。
-
功能说明:
-
专为
Portal服务的后端 API; -
处理所有配置的增删改查、发布、回滚;
-
发布成功后,会往数据库写入新配置,并插入一条
ReleaseMessage通知Config Service。
-
-
举个例子:当
Portal调用AdminService发布配置后:AdminService把新配置写入ConfigDB.release表;- 同时向
ReleaseMessage表插入一行:"order-service+default+application" - 所有
Config Service实例每100ms扫描这张表,发现消息后,立即通知所有监听该配置的客户端。
3)辅助模块
a、Eureka(服务注册与发现)
注意:Eureka 只用于 Apollo 内部服务发现,你的业务服务不需要接入它。
-
功能说明:
-
Config Service和Admin Service启动时,会向 Eureka 注册自己; -
它们也会定期发送心跳,确保服务健康;
-
部署时,
Eureka和Config Service在同一个JVM进程中(简化架构)。
-
-
举个例子:你启动
Config Service时,日志显示:-
Registering service config to eureka with instanceId cs1:8080 - 此时
Eureka的UI页面http://cs1:8080/eureka/能看到:Application:APOLLO-CONFIGSERVICE→ Instances: cs1, cs2, cs3Application:APOLLO-ADMINSERVICE→ Instances: as1, as2
-
b、Meta Server(元数据服务)
设计目的:解耦。未来即使换成
Nacos或Consul,只要Meta Server接口不变,客户端无需修改。
-
功能说明:
-
本质是一个
HTTP接口代理,封装了Eureka的复杂性; -
客户端和
Portal只需记住一个域名(如apollo-meta.dev.com),不用关心Eureka; -
它从
Eureka拉取服务列表,返回给调用方。
-
-
举个例子:
-
客户端请求:
GET http://apollo-meta.dev.com/services/config -
Meta Server内部查询 Eureka,得到:-
[ {"appName": "APOLLO-CONFIGSERVICE", "ip": "10.0.1.10", "port": 8080}, {"appName": "APOLLO-CONFIGSERVICE", "ip": "10.0.1.11", "port": 8080} ]
-
-
返回给客户端,客户端就知道该连哪台 Config Service。
-
4、精简架构流程 + 完整示例
场景:在生产环境修改数据库连接池大小
步骤分解:
- 开发者操作
- 登录
Portal(唯一部署的一套); - 选择项目
user-service→ 环境PROD→ 修改datasource.max-pool-size=20→ 点击“发布”。
- 登录
Portal调用Admin ServicePortal通过Meta Server获取Admin Service地址(如as-prod-1:8090);- 调用其
API发布配置。
Admin Service写库 + 发消息- 新配置写入
ConfigDB(PROD环境专属数据库); - 向
ReleaseMessage表插入:"user-service+default+application"
- 新配置写入
Config Service消费消息并推送PROD环境的Config Service实例每 100ms 扫描ReleaseMessage;- 发现消息后,遍历所有挂起的长轮询连接;
- 找到
user-service的客户端连接,立即返回200通知有更新。
Client拉取新配置并刷新- 客户端收到通知,调用
/configs/...拉取最新配置; - 更新内存中的
maxPoolSize值; - 如果用了
@RefreshScope或 Apollo 的SpringValueProcessor,连接池自动调整。
- 客户端收到通知,调用
- 多环境隔离
DEV/FAT/UAT/PROD各有一套独立的:Config Service+Admin ServiceConfigDB(MySQL实例或库)
- 但共用一套
Portal+PortalDB(存用户、权限、项目信息)
3、基础 CRUD
- 服务端存储:
- 配置数据持久化在
MySQL(ConfigDB) 中; - 发布记录、变更消息等元数据也存于数据库;
- 无内存缓存层,每次读取均查库(但有连接池和
SQL优化)。
- 配置数据持久化在
- 客户端访问:
- 通过
ConfigService.getConfig(appId, cluster, namespace)获取配置; - 应用代码中通过
Config.getProperty("key")或监听器获取最新值。
- 通过
4、伪PUSH + PULL + 长轮询(推拉结合)
Apollo采用 “服务端主动推送 + 客户端长轮询” 的混合模式,实现 高实时性 与 高可靠性 的统一。其本质仍是基于HTTP的 长轮询(LongPolling),但由服务端通过数据库消息队列驱动推送时机,形成“伪推送”效果。
| 特性 | Apollo(配置中心) | 传统 Pull 模型(如 Spring Cloud Config 原生) |
|---|---|---|
| 通信协议 | HTTP/HTTPS |
HTTP/HTTPS |
| 连接类型 | HTTP 长轮询(客户端发起,服务端 hold) |
短连接定时拉取 |
| 数据获取模式 | Push-driven Pull(由服务端事件触发 Pull) |
纯 Pull(固定间隔) |
| 无变更时行为 | 服务端 hold 请求最多 60 秒 |
客户端每 5 分钟主动请求一次 |
| 实时性 | 秒级生效(通常 <1s) |
分钟级(依赖刷新频率) |
| 容灾机制 | 本地磁盘缓存 + 定时 fallback Pull |
无本地缓存(重启失败风险高) |
1)Apollo 长轮询流程

- 客户端启动后,向
Config Service发起长轮询请求:/notifications/v2?appId=xxx&cluster=default¬ifications=[{"namespaceName":"application","notificationId":100}] - 服务端处理逻辑:
- 若配置未变更 → 将请求挂起,加入等待队列,最多
hold60 秒; - 若期间收到
ReleaseMessage(来自AdminService)→ 立即遍历等待队列,找到匹配的客户端连接,返回200及新notificationId;
- 若配置未变更 → 将请求挂起,加入等待队列,最多
- 客户端收到响应:
200:调用/configs/...接口拉取最新配置,并更新本地缓存;- 超时(60s 无响应):自动重试下一次长轮询。
2)服务端事件驱动
- 用户在
Portal发布配置 →Admin Service写入ConfigDB.release表,并插入一条ReleaseMessage; Config Service每100ms轮询ReleaseMessage表(模拟消息队列);- 收到消息后,发布
ReleaseMessageEvent; NotificationControllerV2监听到事件,遍历所有挂起的长轮询连接,唤醒匹配的客户端。
3)客户端自动刷新(Spring Boot 集成)
ApolloApplicationContextInitializer在启动时注入配置;SpringValueProcessor自动扫描@Value("${key}")字段;- 配置变更时:
Config.onChange()触发;- 更新
SpringValue对象; - 无需
@RefreshScope,即可动态刷新字段值(优于 Spring Cloud Config)。
5、FQA
1)为什么 Apollo 不用 Kafka/RabbitMQ?
- 降低依赖:避免引入额外中间件,简化运维;
- 强一致性要求:配置变更需严格顺序消费,数据库事务天然保证;
- 轻量级足够:
ReleaseMessage表 + 轮询机制在中小规模场景性能优异; - 兜底可靠:即使
MQ故障,数据库始终可用。
2)Apollo 和 Nacos 配置中心怎么选?
-
Apollo是配置管理领域的“专业选手” —— 功能全面、治理完善、稳定性久经考验; -
若你的系统 只需配置中心,且对 权限、审计、流程 有强需求,
Apollo是更稳妥的选择。
a、配置中心比较
| 维度 | Apollo |
Nacos |
|---|---|---|
| 核心定位 | 纯配置中心 | 配置 + 服务发现一体化 |
| 实时推送 | 数据库轮询 + 长轮询 | 长轮询 |
| 本地缓存 | 磁盘 + 内存 | 磁盘 + 内存 |
| 控制台 | 功能强大(权限/流程/审计) | 基础够用 |
| 部署复杂度 | 需部署 Portal + Config/Admin(多组件) |
单进程启动 |
| 适合场景 | 强治理、高合规、纯配置需求 | 云原生、快速上手、需服务发现 |
Hold 时间 |
30 秒(实际 29.5s) | 60 秒 |
| 触发条件 | 配置发布 → 写 ReleaseMessage 表 → 轮询消费 → 唤醒监听 |
配置变更 → 发布事件 → 遍历监听队列 |
| 连接模型 | HTTP + DeferredResult / AsyncContext |
HTTP + AsyncContext |
| 客户端 | 自动刷新 @Value(无需 @RefreshScope) |
变更后自动刷新 @Value(需 @RefreshScope 或 NacosContextRefresher) |
b、使用场景比较
| 需求 | Nacos | Apollo | 谁胜出? |
|---|---|---|---|
| 多人协作修改审批 | 无流程 | 支持发布审批流 | Apollo |
| 配置变更审计追踪 | 有简单日志 | 完整操作记录(谁、何时、改了啥) | Apollo |
| 灰度发布(按 IP/集群) | 有限支持(通过 Group/DataId 模拟) |
原生支持灰度规则 | Apollo |
| 多环境管理 | 通过 namespace 隔离 |
独立部署每套环境(DEV/FAT/PROD) | Apollo |
| 配置回滚到任意历史版本 | 支持 | 支持(界面更友好) | Apollo |
| 权限精细控制(如只读某配置) | RBAC 较弱 |
项目级 + 环境级 + 操作级权限 | Apollo |
c、推荐选择
Nacos是“多面手”:能打、能跑、能扛,适合大多数战场;Apollo是“特种兵”:专精配置治理,在高要求阵地无可替代。
| 场景 | 推荐 |
|---|---|
| 新创公司、互联网应用、快速迭代 | Nacos(一体化,省心) |
已有注册中心(如 Eureka),只需配置中心 |
Apollo(专注配置) |
| 需要审批流、操作审计、权限管控 | Apollo(唯一选择) |
使用 Dubbo / Spring Cloud Alibaba 生态 |
Nacos(无缝集成) |
| 合规要求高(等保、金融监管) | Apollo |

3)Apollo 支持百万级客户端的关键
- 携程内部 数千个应用、数十万实例 接入 Apollo;
- 日均配置变更 数百万次;
P99配置推送延迟 < 1 秒;- 服务端集群稳定运行多年,无重大故障。
| 层面 | 技术手段 | 效果 |
|---|---|---|
| 连接效率 | 长轮询 + 批量监听 | 连接数 ≈ 实例数,非配置数 |
| 线程模型 | AsyncContext 异步化 |
Tomcat 线程不被 hold,高并发无忧 |
| 内存控制 | 轻量监听对象 | 百万连接 ≈ 1~2GB 内存 |
| 水平扩展 | 无状态 Config Service + SLB |
容量随机器线性增长 |
| 容灾降级 | 本地磁盘缓存 | 服务端故障不影响业务 |
| 通知机制 | 数据库模拟 MQ | 轻量、可靠、无外部依赖 |
a、推拉结合 + 长轮询:高效利用连接资源
-
长轮询(
LongPolling)替代传统轮询-
客户端发起一次 HTTP 请求后,服务端
hold住连接最多60秒; -
若无配置变更,
60秒后返回304,客户端立即重连; -
若有变更,秒级内返回
200,触发配置拉取。
-
-
优势:
-
相比每 5 秒轮询一次(传统 Pull),网络请求量降低 95%+;
-
实时性媲美
WebSocket,但兼容性更好(仅需 HTTP)。
-
b、批量监听减少连接数
- 一个客户端可同时监听多个
namespace(如application,datasource,redis); SDK内部将多个监听合并为 单个长轮询请求(通过notifications参数列表);-
1 个应用实例 ≈ 1 个长连接,而非 N 个。
- 示例: 10 万个微服务实例,每个监听 5 个配置文件 → 传统方案需 50 万连接;Apollo 仅需 10 万长连接。
c、异步非阻塞:避免线程耗尽
-
关键技术:
Servlet 3.0 AsyncContext-
// 伪代码:Config Service 处理长轮询 @RequestMapping("/notifications/v2") public DeferredResult<Response> poll(...) { if (hasChange) { return new DeferredResult<>(newResponse); } else { // 挂起请求,不占用 Tomcat 线程 DeferredResult<Response> dr = new DeferredResult<>(60_000L); waitingQueue.add(dr); // 加入等待队列 return dr; } }
-
-
效果:
-
Tomcat 工作线程在挂起后 立即释放,可处理其他请求;
-
实际 hold 连接的是 内存中的等待队列 + 定时器线程池;
-
-
对比:若同步阻塞,
200线程最多只能 hold 200 个连接 —— 完全不可扩展。
d、轻量级内存模型:控制单连接开销
每个挂起的长轮询任务(如 DeferredResult 封装体)仅包含:
- 客户端标识:
appId、cluster、namespace - 当前配置版本号:
notificationId -
超时
Future引用(用于 60s 后自动唤醒) -
内存估算:
-
单任务 ≈ 1~1.5 KB 内存;
-
100万客户端 ≈ 1~1.5 GB 堆内存; -
现代服务器(如
32GB RAM)轻松承载。
-
e、服务端无状态 + 集群水平扩展
-
Config Service是 无状态服务,可无限横向扩容; -
客户端通过
Meta Server获取Config Service实例列表; -
客户端内置 软负载(
SLB)策略(如轮询、随机),自动分摊请求到多个实例。-
Client → MetaServer → [ConfigService-1, ConfigService-2, ..., ConfigService-N] -
100万客户端 → 分配到10个Config Service实例 → 每台仅需处理10万连接; - 单机压力大幅降低,系统整体容量线性增长。
-
f、本地缓存 + 容灾兜底:降低服务端压力
- 客户端首次拉取配置后,同时写入内存 + 磁盘缓存(路径:
/opt/data/{appId}/config-cache/); - 即使
Config Service宕机或网络中断:- 应用仍可从 本地缓存读取配置,保障业务不中断;
- 长轮询失败后自动重试,恢复后同步最新配置。
-
效果:
-
减少因短暂故障导致的“雪崩式重连”;
-
服务端无需为“高可用”承担额外读负载。
-
g、数据库消息队列:轻量可靠的通知机制
- 配置发布由
Admin Service写入ReleaseMessage表; Config Service每100ms轮询一次,批量消费消息(每次 ≤500 条);-
无需
Kafka/RabbitMQ,避免外部依赖和运维复杂度。 -
优化点:
-
ReleaseMessage表按messageId自增,查询高效; -
消费后立即删除或标记,避免表膨胀;
- 即使轮询延迟
100ms,对“秒级推送”体验无感知。
4)配置中心的设计定位 ≠ 存储大数据
- 当单配置 >
1MB时,Nacos客户端拉取延迟明显增加; Apollo在 >5MB时可能出现 OOM 或超时;- 官方均未对“大配置”做优化,因为这不是目标场景。
| 组件 | 限制点 | 后果 |
|---|---|---|
HTTP 协议 |
大多数 Web 容器(Tomcat/Nginx)默认限制请求/响应体为 1~10MB | 超过会返回 413 Request Entity Too Large |
Apollo / Nacos 客户端 |
配置加载到 JVM 堆内存(如 String 或 Map) |
20MB × 1000 个实例 = 20GB 内存浪费 |
| 数据库存储 | MySQL 的 TEXT 字段最大 64KB,MEDIUMTEXT 16MB,LONGTEXT 4GB 但 Apollo 默认用 TEXT,Nacos 用 LONGTEXT |
Apollo 可能直接写入失败;Nacos 虽能存,但性能差 |
| 长轮询推送 | 服务端需将整个大配置 序列化 + 网络传输 + 客户端反序列化 | 一次变更导致 全网带宽打满、GC 飙升、服务卡顿 |
| 本地磁盘缓存 | Apollo 缓存到 /opt/data/.../config-cache/ Nacos 缓存到 ~/nacos/config/ |
百万级数据频繁写磁盘,IO 压力巨大 |
假设你把 100 万用户白名单 存在一个 key 里:会发生什么?
- 发布一次配置 →
Config Service需向 所有监听客户端推送 20MB 数据; - 1000 个服务实例 同时接收 → 瞬间消耗
20GB网络带宽; - 每个客户端 反序列化
20MB字符串成List→ 触发 Full GC; - 应用线程暂停几百毫秒 → 接口超时、雪崩;
- 若频繁更新(如每天加
1000人)→ 运维噩梦。
5)客户端 pull 配置时,会直接打数据库(DB)吗
答案:不会。Apollo 和 Nacos 的客户端在 pull 配置时,都不会直接访问数据库(ConfigDB / Nacos DB),而是通过中间的服务层(Config Service /Config Controller)来获取数据,而这些服务层通常会做缓存优化,避免高频打库。
| 项目 | Apollo | Nacos |
|---|---|---|
| 客户端是否直连 DB? | ❌ 否 | ❌ 否 |
| pull 是否查 DB? | ⚠️ 仅缓存未命中时查一次 | ⚠️ 仅启动/恢复时加载,日常不查 |
| 主要缓存位置 | Config Service 内存(Guava Cache) | Nacos Server 内存(ConcurrentHashMap) |
| 缓存更新机制 | 发布配置 → 写 ReleaseMessage → 异步刷新缓存 |
配置变更 → 内存直接更新 + 持久化到 DB |
| 高并发 pull 压力 | 几乎无 DB 压力 | 无 DB 压力 |


