Docker部署Apollo配置中心并接入若依实现动态配置
接手一个后台管理系统做二次开发,最让人抓狂的往往不是业务逻辑,而是配置散落一地:数据库地址写死在application-druid.yml、Redis密码在另一处、日志级别想临时调一下还得重新打包发版。我最近在若依框架上做一套内部系统,需求很朴素——把配置从代码里剥出来,统一放到一个地方管理,改完不用重启、不用重新构建镜像。围绕这个目标,最终的组合是Docker 部署 Apollo 配置中心,再让若依接入 Apollo。这篇内容不讲虚的,从选型、部署到改造、排错,按我实际落地的顺序完整讲一遍。适合手上有 Spring Boot 项目、想引入配置中心但不清楚从哪下手的读者,也适合正在用若依做项目、被多环境配置折磨过的朋友。下面所有操作都基于我在测试环境的实际步骤,容器、镜像、配置项均可直接参考复现。
1. 接配置中心之前,先把方案想清楚
1.1 从一次改配置要发一次版的痛苦说起
先说说为什么非得上配置中心。若依本身已经把配置拆得比较规整:application.yml放主配置,application-druid.yml放数据源,logback.xml管日志。看着挺清楚,可实际运维时问题就冒出来了。测试环境和正式环境用的是同一套构建产物,靠spring.profiles.active切配置文件,切换就意味着换环境变量、改启动参数、重启容器;某些参数比如限流阈值、开关类配置,明明只是改个数字,却要占用一次完整的 CI/CD 流程,构建、推镜像、滚动重启,前后十几分钟。
更麻烦的是多实例场景。一个服务起三台,改配置要确保三台同时生效,运维只能挨个重启,中间还会有新旧配置并存的窗口期。Apollo 解决的就是这类问题:配置集中存储、按环境隔离、支持发布后客户端准实时感知变化、支持灰度发布和版本回滚。它不是"锦上添花"的组件,而是当你的服务实例数量超过一个、环境超过两套之后,迟早要补上的一课。
1.2 Apollo、Nacos、原生配置文件的取舍逻辑
选型这块我没少纠结。常见的三个候选是 Apollo、Nacos、Spring Cloud Config,各自的定位差别其实挺大。
| 方案 | 优势 | 短板 | 适合的场景 |
|---|---|---|---|
| Apollo | 配置管理功能最完整,灰度、回滚、权限、审计齐全 | 部署组件多(Portal、ConfigService、AdminService),运维成本偏高 | 配置变更频繁、需要审批和追溯的中大型团队 |
| Nacos | 部署简单,配置和服务注册一把梭 | 配置管理侧的功能相对朴素,权限粒度粗 | 已经用 Nacos 做注册中心,想顺带管配置 |
| Spring Cloud Config | 与 Spring 生态贴合,可挂 Git 后端 | 无原生推送,需要配合 Bus,界面和权限基本没有 | 配置以文件为主、变更不频繁的老项目 |
我最终选 Apollo 的理由很实在:这套系统未来要接入多个小组,谁都可能改配置,Apollo 的命名空间权限、发布审核、变更diff 这些能力是刚需。Nacos 也能做到,但配置维度的细粒度控制还是 Apollo 更顺手。至于部署复杂,Docker 化之后其实就一份 compose 文件的事,一次搭好基本不用再操心。
注意:如果团队规模很小、只有一两个服务,说实话直接上 Nacos 更省事。Apollo 的价值在"多人协作管理配置"这个场景上才体现得出来,别为了用而用。
1.3 整体架构:客户端、Meta Server 与服务端怎么串起来
在动手之前把链路理清楚,后面排错能省一半时间。Apollo 的组件分工是这样的:
- Config Service:给客户端提供配置读取接口,同时内置了 Meta Server 的角色,客户端通过它拿到 Admin Service 的地址。
- Admin Service:给 Portal 提供配置的增删改查接口。
- Portal:Web 管理界面,本身不直接和客户端通信。
- 客户端(apollo-client):嵌在你的应用里,启动时拉一次全量配置,之后通过长轮询(长连接轮询)感知变更。
客户端拿到配置的过程大致是:先从本地app.properties或启动参数里读到app.id和env,再向 Meta Server(也就是 Config Service 的地址)请求该环境下 Admin Service 的列表,然后直接连 Admin Service 拉配置。本地还会写一份缓存文件,服务端挂掉时客户端能读本地缓存兜底,这个设计在实战里救过我一次。
若依接入 Apollo 时,本质上就是让若依的启动模块变成一个普通的 Apollo 客户端,配置的读取来源从本地 yml 变成 Apollo 的命名空间。理清这一点,后面的改造就不会迷路。
2. Docker 部署 Apollo:环境准备与容器编排
2.1 部署前的三件必查事项
正式拉镜像之前,有三件事必须先确认,否则后面一定返工。
第一是端口占用。Apollo 2.x 的三个服务默认端口分别是 Config Service 8080、Admin Service 8090、Portal 8070。若依应用本身也常跑在 8080,如果 Apollo 和若依部署在同一台机器上,端口必然打架。我的做法是把 Apollo 的 Config Service 映射到宿主机的 18080,Admin Service 映射 18090,Portal 保持 8070。容器内部端口不变,只在 compose 里改宿主机端口。
第二是内存。三个 Java 服务加上一个 MySQL,起步 4G 内存比较稳妥,2G 的机器跑起来会很挣扎,容易 OOM。如果资源紧张,可以把 Portal 单独放一台,Config Service 和 Admin Service 和应用部署在一起。
第三是数据库。Apollo 强依赖 MySQL,而且需要两个独立的库:ApolloConfigDB存配置数据,ApolloPortalDB存 Portal 的元数据(用户、权限、环境列表)。这两个库绝对不能混用,我见过有人图省事只建一个库,结果 Portal 登录后看不到任何项目,排查了半天。
提示:MySQL 版本建议 5.7 或 8.0,字符集用
utf8mb4。Apollo 官方提供的建表脚本里对字段长度有要求,用低版本 MySQL 可能报索引超长,直接上 8.0 最省心。
2.2 数据库初始化:两份脚本不能搞混
数据库初始化这一步是纯手工活,但一步都不能错。Apollo 官方仓库的scripts/sql目录下有两个文件:apolloconfigdb.sql和apolloportaldb.sql。前者导入ApolloConfigDB,后者导入ApolloPortalDB。
导入完成后,需要检查ApolloConfigDB里的ServerConfig表,这张表决定了客户端能不能找到 Admin Service。表里默认会有一条default集群的记录,ConfigService和AdminService的地址字段在容器启动时会自动注册进去。如果是单机 Docker 部署,通常不需要手动改,容器起来之后会自动写入容器 IP,这里有个坑后面会讲。
ApolloPortalDB里的ServerConfig表则是给 Portal 用的,记录每个环境对应的 Meta Server 地址。默认有 DEV、FAT、UAT、PRO 四个环境,单机测试只保留 DEV 就行,其余环境要么删掉要么改地址,否则 Portal 页面会显示一堆连不上的环境,看着很闹心。
另外 Portal 的默认管理员账号是apollo,密码admin,第一次登录后建议立刻改掉。这在测试环境无所谓,生产环境必须改,别嫌麻烦。
2.3 用 docker-compose 拉起四个服务
我用的是官方镜像apolloconfig/apollo-configservice、apollo-adminservice、apollo-portal,版本统一锁在 2.2.0,三个服务版本必须一致,混版本会出现接口不兼容。下面是我实际在用的 compose 文件,做了端口调整和环境变量注入:
version: '3.8' services: apollo-configservice: image: apolloconfig/apollo-configservice:2.2.0 container_name: apollo-configservice restart: always ports: - "18080:8080" environment: - SPRING_DATASOURCE_URL=jdbc:mysql://mysql-host:3306/ApolloConfigDB?characterEncoding=utf8&serverTimezone=Asia/Shanghai - SPRING_DATASOURCE_USERNAME=apollo - SPRING_DATASOURCE_PASSWORD=your_password networks: - apollo-net apollo-adminservice: image: apolloconfig/apollo-adminservice:2.2.0 container_name: apollo-adminservice restart: always ports: - "18090:8090" environment: - SPRING_DATASOURCE_URL=jdbc:mysql://mysql-host:3306/ApolloConfigDB?characterEncoding=utf8&serverTimezone=Asia/Shanghai - SPRING_DATASOURCE_USERNAME=apollo - SPRING_DATASOURCE_PASSWORD=your_password depends_on: - apollo-configservice networks: - apollo-net apollo-portal: image: apolloconfig/apollo-portal:2.2.0 container_name: apollo-portal restart: always ports: - "8070:8070" environment: - SPRING_DATASOURCE_URL=jdbc:mysql://mysql-host:3306/ApolloPortalDB?characterEncoding=utf8&serverTimezone=Asia/Shanghai - SPRING_DATASOURCE_USERNAME=apollo - SPRING_DATASOURCE_PASSWORD=your_password - APOLLO_PORTAL_ENVS=dev - DEV_META=http://apollo-configservice:8080 depends_on: - apollo-configservice networks: - apollo-net networks: apollo-net: driver: bridge这里有几个点值得单独说。DEV_META这个环境变量是关键,它告诉 Portal 在 dev 环境下应该去哪个地址找 Meta Server。注意这里写的是http://apollo-configservice:8080,用的是 compose 网络里的服务名,而不是宿主机映射后的 18080。如果你把 Portal 和 Config Service 分开部署到不同机器,这里就要换成 Config Service 的真实可访问地址。
APOLLO_PORTAL_ENVS=dev表示 Portal 只显示 dev 一个环境,和数据库里保留的环境数量对应。多环境的话改成dev,fat,pro这样,然后分别配置FAT_META、PRO_META。
启动之后,docker logs -f apollo-configservice观察日志,看到Started ConfigServiceApplication就说明 Config Service 起来了。Admin Service 同理。Portal 起来后浏览器访问http://宿主机IP:8070,用apollo/admin登录。
2.4 首次登录后必须做的一次体检
Portal 能登录不代表链路通了。我建议一进来就做三件事。
第一,切到"系统信息"页面,看所有环境的 Meta Server 是不是都显示"正常"。如果 dev 显示离线,多半是DEV_META配错了,或者 Config Service 没起来。
第二,进"系统参数"里确认apollo.portal.envs的值和数据库ServerConfig表里的环境数量一致,不一致会导致环境列表显示错乱。
第三,检查ApolloConfigDB的ServerConfig表。容器启动时,Config Service 和 Admin Service 会把自己的容器 IP 和端口注册进去。这个 IP 是容器内部 IP,如果你的客户端应用不是跑在同一个 Docker 网络里,就会连不上。我一开始就栽在这里:客户端在宿主机跑,拿到的是172.x.x.x这种容器网段地址,一直超时。解决办法是在 compose 里给服务指定固定网络,或者启动后手动UPDATE ServerConfig SET Value='http://宿主机IP:18090'把 Admin Service 的地址改成宿主机可访问的。生产环境通常用独立的域名或 host 网络来规避这个问题。
3. Apollo 侧配置:项目、命名空间与发布
3.1 创建项目与常见信息填写
登录 Portal 后第一件事是创建项目。需要填的是部门、AppId、应用名称、负责人。AppId 是整个链路里最重要的标识,客户端用app.id和它对应,一旦创建不建议修改,改起来要动客户端配置,很麻烦。我一般用"项目英文简称"作为 AppId,比如ruoyi-admin,和若依的启动模块名保持一致,见名知意。
创建完项目后,系统会自动生成一个默认的application命名空间,类型是 private。这个命名空间是每个项目独有的,只能本项目使用。如果多个项目共享一批配置(比如公共的 Redis 集群地址、公共的短信网关参数),就要新建一个 public 类型的命名空间,然后其他项目通过"关联公共命名空间"的方式引用。公共命名空间的设计很实用,我后面把若依的几个模块之间的公共配置都收拢到了一个 public namespace 里。
3.2 命名空间划分:把若依的配置拆开
命名空间怎么切,直接决定了后期维护的心智负担。我的划分方式是这样:
application:主配置,放端口、会话超时、日志级别这类应用级参数,对应原来application.yml的主要内容。ruoyi-datasource:数据源配置,对应原来的application-druid.yml,类型选 YAML。ruoyi-common:public 类型,放跨项目共享的中间件地址,比如 Redis、消息队列。feature-toggle:放业务开关,比如某些功能入口的启停标志。
这么拆的好处是每个命名空间的粒度适中,改数据源不会误碰业务开关,权限也能按命名空间分配。命名空间创建时要选格式,properties 和 YAML 都支持,但同一个命名空间内不要混用两种格式,否则解析出来的 key 结构会很奇怪。我统一用了 YAML,因为若依原来的配置就是 YAML,迁移时直接整段粘过去,几乎不用改格式。
注意:命名空间一旦创建,格式不能修改。想从 properties 换成 YAML 只能新建一个再删旧的,所以初始规划时想清楚。
3.3 配置录入与发布流程
录入配置这一步没什么技巧,但有两个细节容易翻车。
一是不要把整个 yml 文件原封不动粘进去。Apollo 的 YAML 命名空间里,key 就是 YAML 的层级结构,客户端拿到的spring.datasource.url其实对应嵌套结构。如果原文件里有spring.profiles.active,这个 key 在 Apollo 里是无效的,环境由env参数控制,粘进去反而会造成困惑。我的做法是把配置文件里的内容按"可在 Apollo 管理的"和"必须留在本地的"分一遍,只迁前者。
二是改完必须点发布。Apollo 的编辑和发布是两步,编辑完只是存草稿,点"发布"之后客户端才能感知到。这是新手最常犯的错误之一,改完配置发现应用没变化,九成是忘了点发布。发布时可以填写发布说明,多人协作时这个说明很有价值,出问题回滚时能快速定位是哪次变更导致的。
关于发布范围,Apollo 支持按 IP 灰度发布,先推给一台实例观察,没问题再全量。这个能力在改数据源、改线程池参数这类高风险配置时特别有用,我后面会专门讲怎么用。
3.4 版本回滚与灰度发布的实操方式
Apollo 每次发布都会生成一个版本记录,在"发布历史"里可以看到所有历史版本和变更 diff。发现配置有问题时,选中某个历史版本点"回滚",就能把配置恢复到那个状态。这个操作是秒级的,比重新发版快得多。
灰度发布的操作路径是:发布时选择"灰度发布",填入目标实例的 IP,只有这些 IP 的客户端会拿到新配置。观察一段时间(日志、监控指标),确认无异常再"全量发布"。这里有个前提:客户端上报的 IP 必须是实际可识别的 IP。在容器环境里,客户端默认上报的是容器 IP,如果多个容器 IP 重复或者不可达,灰度就会失真。解决办法是给每个实例设置唯一的apollo.instance.id,通过启动参数-Dapollo.instance.id=xxx传入。
4. 若依接入 Apollo:依赖、配置与动态刷新改造
4.1 依赖引入与版本对齐的取舍
若依是标准的 Spring Boot 多模块项目,启动模块是ruoyi-admin,配置集中在这个模块的src/main/resources下。接入 Apollo 的第一步是在ruoyi-admin的pom.xml里加客户端依赖。
这里第一个要决策的是版本。Apollo 客户端 2.x 版本对 Spring Boot 2.x 和 3.x 的支持情况不同:Spring Boot 2.4 之后默认不再加载bootstrap.yml,所以传统那种在bootstrap.yml里配app.id的方式需要额外引入spring-cloud-starter-bootstrap,比较别扭。更推荐的做法是使用apollo-client-config-data,它基于 Spring Boot 的ConfigData机制,不依赖 Spring Cloud。我的环境是若依 3.8.x(对应 Spring Boot 2.5.x),选的是:
<dependency> <groupId>com.ctrip.framework.apollo</groupId> <artifactId>apollo-client</artifactId> <version>2.2.0</version> </dependency>如果你的若依是 Spring Boot 3.x 的版本,需要 Apollo 客户端 2.2.0 以上,并且额外引入:
<dependency> <groupId>com.ctrip.framework.apollo</groupId> <artifactId>apollo-client-config-data</artifactId> <version>2.2.0</version> </dependency>为什么不选apollo-client加@EnableApolloConfig的老写法?因为老写法依赖bootstrap上下文,在 Spring Boot 2.4 以上会失效或者行为不可预期。ConfigData方式属于官方推荐的现代做法,配置加载时机更可控,也避免了和若依自带的spring.profiles.active机制打架。
提示:如果项目里已经有
spring-cloud-starter-bootstrap,两种方式都能用,但别混用。混用会导致配置被重复加载,出现"某个 key 有两个值"的诡异现象。
4.2 本地配置的剥离与保留边界
依赖加好之后,接下来是最费时间的活:决定哪些配置迁走、哪些留下。
必须留在本地的配置有三类。第一类是 Apollo 自身的连接信息,app.id、apollo.meta、env、apollo.cacheDir这些,它们要在配置中心之前就绪,不能放到配置中心里,否则就是鸡生蛋问题。第二类是打包相关的,比如spring.application.name虽然也可以迁,但保留在本地更直观。第三类是日志框架的初始化配置,logback-spring.xml里如果引用了springProperty,最好还是本地一份,避免日志配置在 Apollo 不可达时挂掉。
可以迁走的是:数据源、Redis、线程池参数、业务开关、限流阈值、文件上传路径、第三方接口地址这些。迁的时候不要贪多,先把一两个低风险的配置迁过去跑通链路,再逐步扩大范围。
本地application.yml大致会精简成这样:
server: port: 8080 spring: application: name: ruoyi-admin app: id: ruoyi-admin apollo: meta: http://你的宿主机IP:18080 bootstrap: enabled: true namespaces: application,ruoyi-datasource.yml,ruoyi-common.yml eagerLoad: enabled: true cacheDir: /data/apollo-cache autoUpdateInjectedSpringProperties: trueapollo.bootstrap.eagerLoad.enabled这个参数值得单独说。设为true之后,Apollo 的配置会在日志系统初始化之前就加载好,这样logback-spring.xml里引用 Apollo 的配置才有意义。如果不设,日志初始化时配置还没拉下来,引用会拿到默认值。autoUpdateInjectedSpringProperties默认就是 true,作用后面讲动态刷新时细说。
cacheDir一定要指定一个有写权限的目录。默认位置在用户目录下,容器环境里经常没权限,导致启动时报"无法写入缓存"。我在测试环境指定成/data/apollo-cache,并在 Dockerfile 里提前创建好目录并授权。
4.3 配置结构迁移时容易踩的三个坑
直接复制粘贴配置文件,看起来最省事,实际上坑最多。
坑一是 YAML 嵌套与扁平 key 的差异。Apollo 的 YAML 命名空间本质上还是一堆 key-value,只是 key 用点分表示层级。这意味着你在 Portal 里看到的是嵌套结构,客户端拿到的Environment里是扁平 key,两种视图都能用,但要注意 YAML 数组类型的配置(比如spring.datasource.druid.filter)迁移时容易变成[0]、[1]这种索引形式,读起来不直观。遇到数组配置,建议留在本地。
坑二是占位符引用。若依的配置里有${...}这种引用,比如日志路径里引用ruoyi.profile。跨命名空间的占位符引用在 Apollo 里是支持的,但要求被引用的命名空间也加载了,否则解析不到就会抛异常。我的处理是把互相引用的配置放到同一个命名空间里,减少跨空间引用。
坑三是配置优先级。Apollo 加载的配置会覆盖本地的同名 key,但系统属性和环境变量优先级更高。这意味着你在本地application.yml里留了一份server.port,Apollo 里也有一份,最终生效的是 Apollo 的——除非你用-Dserver.port启动参数覆盖。搞清楚这个顺序,排查"配置不生效"时能少走很多弯路。
4.4 动态刷新的边界:哪些能刷,哪些不能刷
动态刷新是 Apollo 的招牌能力,但它的边界必须清楚,否则会出现"配置改了、日志说刷新成功了、实际行为没变"的困惑。
能自动刷新的:@ConfigurationProperties注解的 Bean。Apollo 的客户端会监听配置变更,触发EnvironmentChangeEvent,Spring Boot 的ConfigurationPropertiesRebinder收到事件后重新绑定属性。若依里的RuoYiConfig就是用@ConfigurationProperties(prefix = "ruoyi")标注的,所以像文件上传路径、系统名称这类配置,发不到一秒就能生效。
需要@RefreshScope才能刷新的:@Value注入的字段。如果你在某个@Component里用@Value("${some.key}")注入了值,Apollo 虽然更新了Environment,但这个 Bean 的字段不会自动变,必须加上@RefreshScope。加了之后,Apollo 发变更会触发 Bean 重建,新值才能进来。我吃过一次亏,一个开关字段用@Value注入,改了之后死活不生效,最后定位到就是少了个注解。
不能直接刷新的:数据源。Druid 的数据源是单例,在应用启动时就初始化好了连接池,Apollo 改了spring.datasource.url之后,Environment里的值变了,但已经创建的DataSource对象不会重新建立连接。想要数据源动态切换,需要自己实现一个监听器,在收到变更事件后手动重建DataSource并替换掉 Spring 容器里的 Bean,或者直接接入若依的多数据源机制,把数据源配置作为路由条件。这块工作量大,我的做法是数据源配置留在 Apollo 管理但不做动态刷新,改完数据源手动重启,安全第一。
同理,线程池、连接池的初始大小、日志级别这些,也要评估动态刷新的可行性。日志级别可以通过 Apollo 配合 Spring Boot Actuator 的loggers端点动态改,但需要额外暴露端点,涉及安全考虑,非必要不开启。
4.5 多环境与多模块下的组织方式
若依是多模块项目,ruoyi-admin依赖ruoyi-framework、ruoyi-system、ruoyi-quartz等模块。Apollo 的配置是按app.id和应用维度组织的,所以只需要在启动模块里配好app.id和命名空间,其他模块里的@Value、@ConfigurationProperties都能正常读到 Apollo 的配置,因为它们共享同一个Environment。
多环境这块,Apollo 通过env参数区分。同一个 AppId 在 dev 和 pro 环境下可以有不同的配置值。客户端的env可以通过启动参数-Denv=pro指定,也可以通过 JVM 参数或者环境变量。我的做法是把它和容器的环境变量绑定,测试环境用-Denv=dev,生产环境用-Denv=pro,一个镜像跑两套环境,不用改代码。
需要注意的是,apollo.meta在不同环境可能指向不同的 Config Service。如果 dev 和 pro 的 Config Service 地址不同,meta也要区分。Apollo 支持在apollo.meta里配置多个地址用逗号分隔,客户端会自动选择可用的。但如果两个环境完全隔离,更稳妥的做法是用-Dapollo.meta启动参数按环境覆盖。
5. 常见问题与排查实录
5.1 客户端连不上 Meta Server 的几种典型原因
这是接入阶段最高频的问题,我按排查顺序列一遍。
第一步,确认apollo.meta的地址在客户端所在机器上能访问。在容器里跑curl http://apollo.meta地址,看返回什么。如果超时,是网络问题;如果返回 404,说明地址对了但路径不对,一般是端口写错了。
第二步,看客户端启动日志里的ApolloConfigFactory相关输出。正常会打印"Loading config from ...",能看到实际使用的app.id、env、meta。很多问题一眼就能看出来是配置没读进去,app.id变成unknown了。
第三步,如果 Meta Server 能访问,但 Admin Service 连不上,回到 2.4 节说的ServerConfig表问题。客户端从 Meta Server 拿到的 Admin Service 地址如果不可达,会报"Could not complete request"这类错误。解决办法是把表里的地址改成客户端能访问的地址。
第四步,检查本地缓存目录。如果cacheDir不可写,客户端拉配置失败后会尝试读缓存,缓存也没有就报错。日志里通常是"Load config failed, will retry"。
5.2 配置拉下来了但应用行为没变的排查路径
这个问题比连不上更隐蔽,因为日志看起来一切正常。我的排查路径是这样的:
先看 Apollo 客户端日志里有没有"Config changed"的记录。没有的话,说明配置根本没推下来,可能是没点发布,或者客户端的长轮询被防火墙掐断了。
有"Config changed"但行为没变,去看Environment里的值是不是真变了。可以临时加一个接口打印ConfigurableEnvironment里的PropertySource名称和内容,确认 Apollo 的PropertySource确实加载了、值也对了。
值对了但行为没变,就是刷新机制的问题。检查那个配置是@ConfigurationProperties还是@Value,前者应该自动刷,后者需要@RefreshScope。还要检查那个 Bean 有没有被别的 Bean 强引用,@RefreshScope重建会生成新实例,老引用还指向旧对象,需要调用方也跟着刷新。
最后一层是配置被更高优先级的来源覆盖了。系统属性、环境变量、命令行参数的优先级都高于 Apollo,用-D传了一个同名参数,Apollo 的值就永远不生效。这种情况用/actuator/env端点能看得很清楚,每个 key 的生效值和来源都有记录。
5.3 数据源、Redis 这类基础组件接入的坑
基础组件的配置迁移和普通业务配置不一样,因为它们通常在应用启动早期就初始化了。
数据源的问题前面提过,不重复。补充一点:如果坚持要动态刷新数据源,可以考虑用HikariCP的HikariDataSource配合@RefreshScope,Hikari 有close()方法能优雅关闭连接池。但实践中风险不小,连接池重建期间会有请求失败,我建议还是采用"改配置 + 手动重启"的方式,用 Apollo 的灰度发布控制重启节奏,比硬刷安全得多。
Redis 的情况稍好。Spring Data Redis 的LettuceConnectionFactory支持resetConnection(),可以在监听器里手动重置。但要注意,重置期间的请求会失败,同样建议在低峰期操作。我在测试环境试过一次动态改 Redis 密码,重置后有几秒的报错,后来改成只在测试环境开这个能力,生产环境禁用。
注意:像 JWT 密钥、加密盐值这类配置,虽然也可以放 Apollo,但最好评估一下风险。配置中心本身如果被攻破,这些敏感信息就是现成的。至少要对 Apollo 的 Portal 做访问控制,敏感配置项加上权限限制。
5.4 常见问题速查表
把上面这些整理成一张表,出问题时按表排查,比翻日志快。
| 现象 | 最可能的原因 | 处理方式 |
|---|---|---|
启动报Env is not set | 没有指定 env 参数 | 加-Denv=dev或配apollo.bootstrap |
报app.id is null | app.id没读到 | 检查app.properties或application.yml里的app.id |
| 连接超时 | meta 地址不可达 | 用 curl 验证网络,检查端口和防火墙 |
| 拉不到配置但无报错 | 命名空间没关联或没发布 | 检查命名空间关联关系和发布状态 |
| 配置改了不生效 | 没点发布 / 没加刷新注解 | 先看发布状态,再看@RefreshScope |
| 缓存目录报错 | 目录无写权限 | 指定apollo.cacheDir并授权 |
| 灰度发布不生效 | 实例 IP 重复或未上报 | 配置唯一apollo.instance.id |
| Portal 看不到项目 | 环境列表和库表不一致 | 核对ApolloPortalDB的ServerConfig |
6. 上线前的检查与几点个人经验
真正要上生产,光跑通链路不够,还得做一层加固。我列几个我认为必须确认的点。
配置中心的可用性要和业务对齐。Apollo 的客户端有本地缓存,服务端挂掉时应用能继续用缓存启动,这算是兜底。但如果应用重启时缓存也丢了(比如容器重建、缓存目录没做持久化),就起不来了。所以缓存目录要么挂载到持久化卷,要么在发布流程里保证 Config Service 的可用性。我的做法是两样都做,尤其是核心服务。
权限要提前规划。Apollo 的权限模型是"项目 + 命名空间"级别的,默认只有项目管理员能改配置。生产环境的配置,普通开发不应该有修改权限,只能看。这个权限体系最好在项目推广前就定好,否则后期再收权限会有阻力。
变更要留痕。Apollo 本身记录了发布历史,但业务侧的变更原因不会写进去。我们的做法是要求每次发布必须写发布说明,格式固定:"改了什么 + 为什么改 + 影响范围"。出现问题时能顺着说明回溯,比翻聊天记录靠谱得多。
别把所有配置都塞进去。有一类配置我坚持留在本地:和部署形态强相关的,比如server.port、spring.application.name。这类配置跟着镜像走,如果放到 Apollo,同一个镜像在测试环境是 8080、生产环境要改成 9090,还得靠启动参数覆盖,不如老老实实留在本地。
最后分享一个我个人觉得很有用的小技巧。Apollo 的灰度发布配合容器的滚动更新,可以做到"改配置零停机"。具体做法是:先灰度发布到新版本的实例组,观察几分钟,确认没问题后全量。如果发现异常,回滚配置,再回滚镜像。这个组合我用了半年多,改过高风险的数据源参数、线程池配置,都没出过事故。另外,把 Apollo 的 Portal 地址和默认账号密码做好记录,交接时能省很多事——我接手过一个项目,前任把 Portal 密码改了没留档,最后只能改数据库重置,非常尴尬。
7. 整体回顾:这套方案实际解决了什么
回头看这套组合的实际价值,不是"用上了配置中心"这个名头,而是把配置变更从一次发布流程降级成一次页面操作。若依本身是个很规整的框架,配置分层清晰,但它的分层是为"单环境部署"设计的,多环境下必然会遇到配置覆盖、灰度控制、变更追溯这些需求。Apollo 补齐的正是这块能力。
从技术实现上看,接入的核心工作量集中在三处:一是本地配置的剥离边界要划清楚,二是动态刷新的范围要评估到位,三是容器环境下 Meta Server 和实例 IP 的处理。前两处是逻辑问题,理清了就不会错;第三处是环境问题,需要根据实际部署拓扑调整。我见过不少人卡在第三处,以为是代码问题,实际上是ServerConfig表里注册了容器内网 IP。
如果你现在正打算给若依接配置中心,我的建议是先用 Docker 把 Apollo 跑起来,用一个测试项目把"客户端拉配置"这条链路跑通,再去动若依的配置。先证明链路通,再谈改造,比一上来就改主项目要稳妥得多。改造过程中始终留一份可回退的本地配置,Apollo 再怎么可靠,也不该成为应用启动的唯一依赖。