前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、常见配置中心

1、轮询机制对比

行为 短轮询 长轮询
客户端请求 每 1 秒发一次 发一次,等 30 秒或有数据才返回
TCP 连接寿命 每次请求新建或快速关闭(<100ms) 单次连接保持数秒~数十秒
服务端并发连接数 高频瞬时连接(可能更高) 同时打开的连接数 ≈ 并发消费者数
是否“短连接” 不是

1)短轮询(Short Polling

  • 客户端定时(如每 10s)向服务端拉取配置。
  • 缺点:无法在“低延迟”和“低压力”之间取得平衡
    • 高频请求增加服务端压力;
    • 配置变更存在延迟(最大为轮询间隔);
    • 资源浪费(多数请求无变更)。

2)长轮询(Long Polling

  • 客户端发起请求后,服务端 hold 住连接,直到:

    • 配置发生变更 → 立即返回新值;
    • 超时(如 30s)→ 返回“未变更”(HTTP 304)。
  • 优点:

    • 接近实时推送(延迟 < 1s);
    • 降低无效请求频率;
    • 服务端资源消耗可控(异步处理 + 超时释放)。

3)长轮询实现原理

image-20240327210046316

a、客户端(Java 示例)

  • 使用 CloseableHttpClient 发起 HTTP 请求;
  • 设置超时时间 > 服务端 hold 时间(如 40s > 30s);
  • 收到 200 表示配置变更,304 表示无变更,继续下一次长轮询。

b、服务端(Spring Boot 示例)

  • 利用 Servlet 3.0AsyncContext 实现异步响应;
  • 维护 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 服务端主动推送 需维护长连接,资源消耗大
NacosPull + 长轮询) 折中方案 实现稍复杂,但效果最佳

a、PULL 模式:表示客户端从服务端主动拉取数据.。

Pull 模式下,客户端需要定时从服务端拉取一次数据,由于定时带来的时间间隔,因此不能保证数据的实时性并且在服务端配置长时间不更新的情况下,客户端的定时任务会做一些无效的 Pull 操作。

b、PUSH 模式:服务端主动把数据推送到客户端。

Push 模式下,服务端需要维持与客户端的长连接如果客户端的数量比较多,那么服务端需要耗费大量的内存资源来保存每个资源,并且为了检测连接的有效性,还需要心跳机制来维持每个连接的状态。

c、Pull + 长轮询:

Nacos 采用的是 Pull 模式,并且采用了一种长轮询机制。客户端采用长轮询的方式定时的发起 Pull 请求,去检查服务端配置信息是否发生了变更,如果发生了变更,那么客户端会根据变更的数据获得最新的配置。

3)长连接、长轮询

  • 长连接(如 WebSocketTCP Keep-Alive

    • 连接建立后长期保持,服务端需为每个连接维护状态(内存、文件描述符等)百万级连接对服务器压力极大
  • 长轮询(Long Polling

    • 基于 HTTP 长连接Keep-Alive),客户端发起一次请求后,服务端 hold 住连接直到有数据或超时,响应后连接可复用或关闭

    • 选择长轮询:

  • 避免服务端为海量客户端维持长连接状态

  • 省去心跳机制带来的 CPU/网络开销

  • 实现无状态、易扩展、高容错的架构

一、Spring Cloud Config

Spring Cloud ConfigSpring Cloud 生态中的 集中式外部配置管理组件,主要用于在分布式系统中统一管理应用的配置文件(如 application.ymlbootstrap.properties 等)。

1、核心能力

⚠️ 注意:Spring Cloud Config 本身不提供“实时监听”能力,需依赖 Spring Cloud Bus 实现推送式刷新

  1. 集中化配置存储
    • 支持将配置文件托管在 Git 仓库(默认)、SVN本地文件系统
    • 支持环境隔离(如 dev / test / prod)和应用维度划分。
  2. 动态配置拉取
    • 客户端启动时从 Config Server 拉取配置;
    • 结合 Spring Cloud Bus + RabbitMQ/Kafka 可实现 配置变更广播与自动刷新(需额外组件)。
  3. 版本控制与审计
    • 基于 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` 构建事件驱动架构。

image-20240328154524992

1)标准 Pull 流程(无 Bus)

缺点:无法自动感知配置变更,运维成本高。

  1. 应用启动 → 从 Config Server 拉取配置(一次性的);
  2. 开发者修改 Git 中的配置;
  3. 客户端不会自动感知变更
  4. 需手动调用 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”)

  1. 修改 Git 配置并提交;
  2. 调用 Config Server/monitor 端点(或 Webhook 自动触发);
  3. Config Server消息队列(RabbitMQ/Kafka) 发送刷新事件;
  4. 所有订阅该 Topic 的客户端收到消息,自动执行 /refresh
  5. 配置生效(限 @RefreshScope Bean)。

image-20240328154835655

a、和 Nacos 比较

此方案实现了 “配置变更 → 自动广播 → 客户端刷新” 的闭环。

特性 Spring Cloud Config(+ Bus) Nacos
通信协议 HTTPConfig Server ↔ Client) + AMQP/Kafka(Bus) HTTP
连接类型 短连接(Pull) + 消息订阅(长连接) HTTP 长轮询
数据获取模式 Pull(启动/refresh 时) Pull(长轮询模拟 Push)
实时性 依赖 Bus,秒级 秒级(<1s)
存储后端 Git/SVN/本地文件 内嵌 Derby / MySQL
自动刷新 需额外集成 Bus 原生支持
运维复杂度 高(需维护 Git + MQ + Webhook) 低(开箱即用)

b、代码配置

  1. 引入依赖:

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bus-amqp</artifactId> <!-- 或 kafka -->
    </dependency>
    
  2. 配置 RabbitMQ/Kafka

  3. 在 Git 仓库设置 Webhook,指向 Config Server 的 /monitor

  4. 客户端无需任何代码改动,自动刷新。

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 是一个面向云原生应用的 动态服务发现、配置管理和服务管理平台,主要提供两大核心功能:

  1. 配置中心(Configuration Management
    • 支持配置注册、动态下发、版本管理、灰度发布等;
    • 实现 无需重启服务即可动态刷新配置
  2. 服务注册与发现(Service Discovery
    • 支持 DubboSpring CloudgRPCKubernetes 等多种服务类型;
    • 提供健康检查、负载均衡、元数据管理等能力。

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 长轮询流程

  1. 客户端请求监听 /listener?dataId=xxx 携带当前配置的 MD5(用于比对是否变更);

  2. 服务端处理:

    • 若配置未变 → 启动 29.5s 定时任务,将请求加入 allSubs 队列;
      • 为什么是 29.5s?:留出 0.5s 网络/处理缓冲,避免客户端因超时断开。
    • 若配置变更 → 遍历 allSubs,找到匹配的监听连接,立即返回变更内容;
  3. 客户端收到响应:

    • 200:拉取新配置并刷新本地缓存;
    • 304:继续发起下一次长轮询。

    image-20240327201022936

2)服务端事件驱动

  • 配置变更 → 发布 ConfigDataChangeEvent
  • LongPollingService.onEvent() 被触发;
  • 遍历 allSubs,找到受影响的 ClientLongPolling 任务;
  • 通过其 AsyncContext 写回响应。

3)客户端自动刷新(Spring Boot集成)

  • NacosContextRefresher 在应用启动时注册监听器;
  • 配置变更 → 触发 RefreshEventRefreshEventListener 调用 refresh()
  • 自动更新 @Value@ConfigurationProperties 等注解绑定的配置。

5、FQA

1)长轮询会压垮服务端吗?

千万客户端同时发起长轮询,依然可能压垮服务端。关键看:

a、 线程模型Tomcat 线程不会被耗尽

  • 如果每个请求都占用一个 Tomcat 线程并阻塞 30 秒 → 200 线程的 Tomcat 只能同时处理 200 个监听请求 → 完全不可扩展
  • Nacos 的真实做法:
    • 使用 Servlet 3.0 AsyncContext,将请求挂起后立即释放 Tomcat 线程;
    • 实际 hold 请求的是 自定义异步任务队列 + 定时器线程池
    • Tomcat 线程只用于接收请求和发送响应,不参与等待。

b、内存开销

每个 AsyncTask(或 ClientLongPolling)对象包含:

  • AsyncContext 引用(轻量);
  • dataIdgroupMD5 等元信息;
  • 超时定时任务引用。

假设每个监听任务占 1KB 内存

  • 100 万客户端 → 1GB
  • 1000 万客户端 → 10GB

内存会成为瓶颈,但可通过以下方式缓解:

  • 合并监听:一个客户端监听多个 dataId,但只发起一次 /listener 请求(Nacos SDK 支持批量监听);
  • 客户端去重:相同配置的机器共享监听(如通过 agent 聚合);
  • 服务端分片:Nacos 集群部署,按 dataIdnamespace 分片。

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 磁盘:运行时数据在哪?

即使用了 MySQLNacos 运行时仍以内存为主

数据 存储位置 说明
所有配置 内存(ConcurrentHashMap) + MySQL 启动时全量加载到内存,读请求走内存
临时服务实例 仅内存 客户端心跳维持,宕机自动剔除
持久化服务实例 内存 + MySQL 写入 DB,宕机后重启可恢复

三、Apollo

Apollo(阿波罗) 是由携程框架部研发并开源的一款 生产级分布式配置中心,致力于解决微服务架构下配置管理的痛点。

1、核心能力

定位差异Apollo 不包含服务注册与发现功能,仅聚焦于 配置管理,通常与 Eureka/Nacos/ZooKeeper 等注册中心配合使用。

  1. 集中式配置管理(Configuration Management
    • 支持多环境(DEV/FAT/UAT/PROD)、多集群、多命名空间(Namespace)的配置隔离;
    • 提供配置版本历史、灰度发布、操作审计、权限审批等企业级治理功能;
    • 配置修改后 实时推送到客户端,无需重启服务。
  2. 高可用与容灾保障
    • 客户端支持本地磁盘缓存,在服务端不可用时仍可恢复配置;
    • 服务端无状态,支持集群部署,结合 Eureka 实现服务发现与负载均衡。

2、基本操作(配置中心)

操作 描述
获取配置 客户端通过 Apollo ClientConfig 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│ ←─ 集成在每个业务应用中

image-20240328100336574

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-serviceDEV 环境的 Config Service 地址有哪些?”

    • 得到返回:["http://config1:8080", "http://config2:8080"]

    • 客户端随机选一个(软负载),发起长轮询,获取 application 命名空间的配置;

    • 当你在 Portal 修改了 redis.timeout=6000 并发布后,Client 1 秒内自动更新该字段值。

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 ServiceREST 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 调用 Admin Service 发布配置后:

    1. Admin Service 把新配置写入 ConfigDB.release 表;
    2. 同时向 ReleaseMessage 表插入一行:"order-service+default+application"
    3. 所有 Config Service 实例每 100ms 扫描这张表,发现消息后,立即通知所有监听该配置的客户端。

3)辅助模块

a、Eureka(服务注册与发现)

注意:Eureka 只用于 Apollo 内部服务发现,你的业务服务不需要接入它。

  • 功能说明:

    • Config ServiceAdmin Service 启动时,会向 Eureka 注册自己;

    • 它们也会定期发送心跳,确保服务健康;

    • 部署时,EurekaConfig 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, cs3
      • Application: APOLLO-ADMINSERVICE → Instances: as1, as2

b、Meta Server(元数据服务)

设计目的:解耦。未来即使换成 NacosConsul,只要 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、精简架构流程 + 完整示例

场景:在生产环境修改数据库连接池大小

步骤分解:

  1. 开发者操作
    • 登录 Portal(唯一部署的一套);
    • 选择项目 user-service → 环境 PROD → 修改 datasource.max-pool-size=20 → 点击“发布”。
  2. Portal 调用 Admin Service
    • Portal 通过 Meta Server 获取 Admin Service 地址(如 as-prod-1:8090);
    • 调用其 API 发布配置。
  3. Admin Service 写库 + 发消息
    • 新配置写入 ConfigDBPROD 环境专属数据库);
    • ReleaseMessage 表插入:"user-service+default+application"
  4. Config Service 消费消息并推送
    • PROD 环境的 Config Service 实例每 100ms 扫描 ReleaseMessage
    • 发现消息后,遍历所有挂起的长轮询连接;
    • 找到 user-service 的客户端连接,立即返回 200 通知有更新。
  5. Client 拉取新配置并刷新
    • 客户端收到通知,调用 /configs/... 拉取最新配置;
    • 更新内存中的 maxPoolSize 值;
    • 如果用了 @RefreshScope 或 Apollo 的 SpringValueProcessor,连接池自动调整。
  6. 多环境隔离
    • DEV/FAT/UAT/PROD 各有一套独立的:
      • Config Service + Admin Service
      • ConfigDBMySQL 实例或库)
    • 但共用一套 Portal + PortalDB(存用户、权限、项目信息)

3、基础 CRUD

  • 服务端存储:
    • 配置数据持久化在 MySQLConfigDB 中;
    • 发布记录、变更消息等元数据也存于数据库;
    • 无内存缓存层,每次读取均查库(但有连接池和 SQL 优化)。
  • 客户端访问:
    • 通过 ConfigService.getConfig(appId, cluster, namespace) 获取配置;
    • 应用代码中通过 Config.getProperty("key") 或监听器获取最新值。

4、伪PUSH + PULL + 长轮询(推拉结合)

Apollo 采用 “服务端主动推送 + 客户端长轮询” 的混合模式,实现 高实时性高可靠性 的统一。其本质仍是基于 HTTP长轮询(Long Polling,但由服务端通过数据库消息队列驱动推送时机,形成“伪推送”效果。

特性 Apollo(配置中心) 传统 Pull 模型(如 Spring Cloud Config 原生)
通信协议 HTTP/HTTPS HTTP/HTTPS
连接类型 HTTP 长轮询(客户端发起,服务端 hold) 短连接定时拉取
数据获取模式 Push-driven Pull(由服务端事件触发 Pull) Pull(固定间隔)
无变更时行为 服务端 hold 请求最多 60 秒 客户端每 5 分钟主动请求一次
实时性 秒级生效(通常 <1s 分钟级(依赖刷新频率)
容灾机制 本地磁盘缓存 + 定时 fallback Pull 无本地缓存(重启失败风险高)

1)Apollo 长轮询流程

image-20251110212715545

  1. 客户端启动后,向 Config Service 发起长轮询请求: /notifications/v2?appId=xxx&cluster=default&notifications=[{"namespaceName":"application","notificationId":100}]
  2. 服务端处理逻辑:
    • 若配置未变更 → 将请求挂起,加入等待队列,最多 hold 60 秒
    • 若期间收到 ReleaseMessage(来自 Admin Service)→ 立即遍历等待队列,找到匹配的客户端连接,返回 200 及新 notificationId
  3. 客户端收到响应:
    • 200:调用 /configs/... 接口拉取最新配置,并更新本地缓存;
    • 超时(60s 无响应):自动重试下一次长轮询。

2)服务端事件驱动

  • 用户在 Portal 发布配置 → Admin Service 写入 ConfigDB.release 表,并插入一条 ReleaseMessage
  • Config Service100ms 轮询 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)ApolloNacos 配置中心怎么选?

  • Apollo 是配置管理领域的“专业选手” —— 功能全面、治理完善、稳定性久经考验;

  • 若你的系统 只需配置中心,且对 权限、审计、流程 有强需求,Apollo 是更稳妥的选择

a、配置中心比较

维度 Apollo Nacos
核心定位 纯配置中心 配置 + 服务发现一体化
实时推送 数据库轮询 + 长轮询 长轮询
本地缓存 磁盘 + 内存 磁盘 + 内存
控制台 功能强大(权限/流程/审计) 基础够用
部署复杂度 需部署 Portal + Config/Admin(多组件) 单进程启动
适合场景 强治理、高合规、纯配置需求 云原生、快速上手、需服务发现
Hold 时间 30 秒(实际 29.5s) 60 秒
触发条件 配置发布 → 写 ReleaseMessage 表 → 轮询消费 → 唤醒监听 配置变更 → 发布事件 → 遍历监听队列
连接模型 HTTP + DeferredResult / AsyncContext HTTP + AsyncContext
客户端 自动刷新 @Value(无需 @RefreshScope 变更后自动刷新 @Value(需 @RefreshScopeNacosContextRefresher

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

image-20251110223917006

3)Apollo 支持百万级客户端的关键

  • 携程内部 数千个应用、数十万实例 接入 Apollo;
  • 日均配置变更 数百万次
  • P99 配置推送延迟 < 1 秒
  • 服务端集群稳定运行多年,无重大故障
层面 技术手段 效果
连接效率 长轮询 + 批量监听 连接数 ≈ 实例数,非配置数
线程模型 AsyncContext 异步化 Tomcat 线程不被 hold,高并发无忧
内存控制 轻量监听对象 百万连接 ≈ 1~2GB 内存
水平扩展 无状态 Config Service + SLB 容量随机器线性增长
容灾降级 本地磁盘缓存 服务端故障不影响业务
通知机制 数据库模拟 MQ 轻量、可靠、无外部依赖

a、推拉结合 + 长轮询:高效利用连接资源

  • 长轮询(Long Polling)替代传统轮询

    • 客户端发起一次 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 封装体)仅包含:

  • 客户端标识:appIdclusternamespace
  • 当前配置版本号: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 万客户端 → 分配到 10Config Service 实例 → 每台仅需处理 10 万连接;

    • 单机压力大幅降低,系统整体容量线性增长。

f、本地缓存 + 容灾兜底:降低服务端压力

  • 客户端首次拉取配置后,同时写入内存 + 磁盘缓存(路径:/opt/data/{appId}/config-cache/);
  • 即使 Config Service 宕机或网络中断:
    • 应用仍可从 本地缓存读取配置,保障业务不中断;
    • 长轮询失败后自动重试,恢复后同步最新配置。
  • 效果

    • 减少因短暂故障导致的“雪崩式重连”;

    • 服务端无需为“高可用”承担额外读负载。

g、数据库消息队列:轻量可靠的通知机制

  • 配置发布由 Admin Service 写入 ReleaseMessage 表;
  • Config Service100ms 轮询一次,批量消费消息(每次 ≤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 内存浪费
数据库存储 MySQLTEXT 字段最大 64KB,MEDIUMTEXT 16MB,LONGTEXT 4GB 但 Apollo 默认用 TEXTNacosLONGTEXT Apollo 可能直接写入失败;Nacos 虽能存,但性能差
长轮询推送 服务端需将整个大配置 序列化 + 网络传输 + 客户端反序列化 一次变更导致 全网带宽打满、GC 飙升、服务卡顿
本地磁盘缓存 Apollo 缓存到 /opt/data/.../config-cache/ Nacos 缓存到 ~/nacos/config/ 百万级数据频繁写磁盘,IO 压力巨大

假设你把 100 万用户白名单 存在一个 key 里:会发生什么?

  1. 发布一次配置Config Service 需向 所有监听客户端推送 20MB 数据
  2. 1000 个服务实例 同时接收 → 瞬间消耗 20GB 网络带宽
  3. 每个客户端 反序列化 20MB 字符串成 List → 触发 Full GC;
  4. 应用线程暂停几百毫秒 → 接口超时、雪崩
  5. 若频繁更新(如每天加 1000 人)→ 运维噩梦

5)客户端 pull 配置时,会直接打数据库(DB)吗

答案:不会。ApolloNacos 的客户端在 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 压力

ContactAuthor