☰
Docker部署Apollo接入若依:配置中心动态刷新与容灾实践
2026/10/27 23:11:58 网站建设 项目流程

1. 先把账算清楚:若依自带的 application.yml 到底卡在哪里

第一次把 Docker 版的 Apollo 和若依接起来,多数人都会经历两个阶段:前两个小时觉得“装个配置中心能有多难”,后两天卡在“配置明明发布成功了,为什么日志里还是旧值”。这个问题通常不在 Apollo 本身,而在于对若依的配置加载顺序和 Apollo 客户端接管时机没有想明白,所以第一步不应该急着写docker-compose.yml,而要把“谁管什么”这件事画清楚。

1.1 一份配置被拆成四个位置的典型现场

若依这套脚手架在后端配置上已经算克制了,但一个稍微上点规模的项目跑起来,配置还是会散在好几个地方:application.yml放端口、文件上传路径、token 密钥和过期时间;application-druid.yml放数据源、连接池参数和慢 SQL 阈值;logback.xml里塞一个${log.path};再加上启动脚本里的-Dspring.profiles.active=prod,以及运维在服务器上改过、但没同步到仓库的环境变量。

这四份东西带来的麻烦不是“难找”,而是改动成本不对等。改一个 token 过期时间,要走一次完整的打包发布流程;改一个日志级别,得让运维上机器改文件再重启;改一个数据源的最大连接数,得掐着业务低峰期操作。真正要命的是多实例场景:十台机器,改完第九台漏了第十台,第二天排查问题时看到的日志级别和线上实际行为对不上。

配置中心要解决的正是这种“改一个值要动一整套流程”的问题,而不是“把 yml 换个地方存”。想清楚这一点,后面很多取舍就顺了——比如为什么数据源这种配置即使放进 Apollo 也照样要重启,那还有没有必要搬进去。

1.2 Apollo 接管哪些、不接管哪些

我给自己的项目划了一条比较粗但好用的界线:

  • 会随业务节奏变化的——开关类(功能灰度开关、限流开关)、阈值类(分页最大条数、导出上限、超时时间)、文案类、白名单、第三方接口地址——全部交给 Apollo。
  • 跟进程生命周期强绑定的——数据源 URL 与账号、连接池核心参数、服务端口、日志文件路径、MQ 消费者组——可以搬进 Apollo 做“单一事实来源”,但心里要清楚改了必须重启。
  • 绝对不该进配置中心的——数据库密码这类高敏感信息如果一定要放,也要配合 Apollo 的访问权限和审计来做,别指望“配置中心=保险箱”。若依默认把数据库密码明文写在application-druid.yml,搬进 Apollo 后至少能做到“谁能看、谁改过”有记录,这已经比散落在各台服务器上好很多。

还有一类容易被忽略:若依的token.secret千万不要随便在 Apollo 上改。这个值一改,所有已签发的 token 立刻失效,全站用户被踢下线。它很适合放在配置中心统一管理,但发布前一定要确认当前不是业务高峰,而且要让前端有重新登录的心理准备。

1.3 部署前的端口与资源规划

Apollo 是三个服务加一个数据库,不是单个容器。我见过不少人docker run起了 configservice 就以为装完了,结果 Portal 打不开、客户端连不上。先把这张表填好再动手:

组件容器内端口是否需要对客户端暴露建议内存说明
apollo-configservice8080是512m 起客户端读配置走它,同时内置注册中心
apollo-adminservice8090否(Portal 需要可达)512m 起给 Portal 做配置管理与发布
apollo-portal8070否(只给人访问)512m 起Web 管理界面
MySQL3306否按现有实例需要两个库:配置库与 Portal 库

内存这块别抠。三个 Java 服务默认堆参数在容器里容易触发 OOM Kill,尤其 adminservice 在批量发布时会短暂冲高。我的习惯是显式传JAVA_OPTS=-Xms512m -Xmx512m -XX:+UseG1GC,把上限钉死,避免容器内存和 JVM 堆互相猜。

端口规划里有个细节值得提前想:adminservice 的 8090 到底要不要映射到宿主机。如果 Portal 和 adminservice 跑在同一台机器的同一个 Docker 网络里,完全不用映射;只有当 Portal 部署在另一台机器上时,才需要保证那台机器能访问到 8090 和 8080。很多“Portal 里点发布一直转圈”的问题,根子就是 Portal 能连上 meta(8080),但连不上 adminservice(8090)。

2. Docker 拉起 Apollo 三件套:从建库到容器互通

2.1 三个镜像各管一段,别混着理解

这三个服务的职责差异必须先分清,否则后面排查问题时你会不知道去哪看日志:

apollo-configservice是客户端唯一直接打交道的服务。客户端启动时先问它“我的配置在哪”,然后拉取配置内容,同时它内置了注册中心(Eureka),adminservice 把自己注册进来,Portal 通过它发现 adminservice 的地址。

apollo-adminservice是“写”的一侧。Portal 上点发布,实际是 Portal 调 adminservice,adminservice 写数据库、刷新缓存。客户端不直接访问它。

apollo-portal是纯 Web 应用,自己一个库,管项目、管用户、管环境,本身不存配置内容。

理解了这条链路,很多现象就能自己解释:客户端报“找不到 meta”,那是 8080 不通;Portal 里能看到项目但发布报错,多半是 8090 不通;Portal 登录后一片空白,可能是apollo.portal.meta.servers没配对。

我用的编排大概长这样,docker-compose.yml只保留关键部分:

version: "3.8" services: apollo-db: image: mysql:8.0 container_name: apollo-db restart: always command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci environment: MYSQL_ROOT_PASSWORD: Root@Apollo123 TZ: Asia/Shanghai volumes: - /data/apollo/mysql:/var/lib/mysql ports: - "3307:3306" networks: - apollo-net apollo-configservice: image: apolloconfig/apollo-configservice:2.1.0 container_name: apollo-configservice restart: always depends_on: - apollo-db environment: SPRING_DATASOURCE_URL: jdbc:mysql://apollo-db:3306/ApolloConfigDB?characterEncoding=utf8&serverTimezone=Asia/Shanghai SPRING_DATASOURCE_USERNAME: apollo SPRING_DATASOURCE_PASSWORD: Apollo@2024 JAVA_OPTS: "-Xms512m -Xmx512m -XX:+UseG1GC" ports: - "8080:8080" networks: - apollo-net apollo-adminservice: image: apolloconfig/apollo-adminservice:2.1.0 container_name: apollo-adminservice restart: always depends_on: - apollo-configservice environment: SPRING_DATASOURCE_URL: jdbc:mysql://apollo-db:3306/ApolloConfigDB?characterEncoding=utf8&serverTimezone=Asia/Shanghai SPRING_DATASOURCE_USERNAME: apollo SPRING_DATASOURCE_PASSWORD: Apollo@2024 JAVA_OPTS: "-Xms512m -Xmx512m -XX:+UseG1GC" ports: - "8090:8090" networks: - apollo-net apollo-portal: image: apolloconfig/apollo-portal:2.1.0 container_name: apollo-portal restart: always depends_on: - apollo-adminservice environment: SPRING_DATASOURCE_URL: jdbc:mysql://apollo-db:3306/ApolloPortalDB?characterEncoding=utf8&serverTimezone=Asia/Shanghai SPRING_DATASOURCE_USERNAME: apollo SPRING_DATASOURCE_PASSWORD: Apollo@2024 JAVA_OPTS: "-Xms512m -Xmx512m -XX:+UseG1GC" ports: - "8070:8070" networks: - apollo-net networks: apollo-net: driver: bridge

三个服务必须放进同一个自定义网络,才能用容器名互相解析。这一点看着废话,但很多人图省事全用默认 bridge,结果容器名 ping 不通,只能退回去写宿主机 IP,改一次机器就崩一次。

2.2 建库建表:两个库的初始化顺序

Apollo 的两个库是从官方 SQL 脚本初始化的,一个叫ApolloConfigDB(配置本体 + 服务注册),一个叫ApolloPortalDB(Portal 的用户、项目、环境)。这两个库必须都要有,缺一个的症状完全不同:缺 ConfigDB 时 configservice 起不来;缺 PortalDB 时 Portal 报数据库表不存在。

docker exec -it apollo-db bash mysql -uroot -pRoot@Apollo123 -e " CREATE DATABASE IF NOT EXISTS ApolloConfigDB DEFAULT CHARSET utf8mb4; CREATE DATABASE IF NOT EXISTS ApolloPortalDB DEFAULT CHARSET utf8mb4; CREATE USER 'apollo'@'%' IDENTIFIED BY 'Apollo@2024'; GRANT ALL PRIVILEGES ON ApolloConfigDB.* TO 'apollo'@'%'; GRANT ALL PRIVILEGES ON ApolloPortalDB.* TO 'apollo'@'%'; FLUSH PRIVILEGES;"

然后导入脚本(脚本从 Apollo 源码的scripts/sql/目录取,两个文件:apolloconfigdb.sql和apolloportaldb.sql):

# 在宿主机执行,把脚本灌进容器里的 mysql docker exec -i apollo-db mysql -uroot -pRoot@Apollo123 < apolloconfigdb.sql docker exec -i apollo-db mysql -uroot -pRoot@Apollo123 < apolloportaldb.sql

导入后花一分钟验证一下,别急着起服务:

SELECT COUNT(*) FROM ApolloConfigDB.ServerConfig; SELECT COUNT(*) FROM ApolloPortalDB.ServerConfig;

两个数都不为 0 才算成功。我踩过一次坑:SQL 文件用 Windows 换行保存,直接 import 报了一堆语法错误,但容器日志里只有“database connection ok”,服务照样起来,直到 Portal 登录才发现权限表是空的。导入脚本前后各看一眼行数,比什么都省事。

还要留意 MySQL 8 的时区。serverTimezone没写对,Apollo 里的发布时间会整体偏 8 小时,排查问题时按时间对齐日志会非常痛苦。我在连接串里固定serverTimezone=Asia/Shanghai,同时给容器加TZ=Asia/Shanghai,两边对齐。

2.3 容器网络与 ServerConfig 里的那两处地址

服务能起来 ≠ 能串起来。Apollo 的地址信息存在ServerConfig表里,两个库各有一条关键配置,改错一个就是典型的“服务都活着但互相看不见”。

第一处:配置库里的eureka.service.url。adminservice 启动后会往这个地址注册自己,configservice 也依赖它。容器化部署时这里必须用容器名,不能用localhost:

USE ApolloConfigDB; UPDATE ServerConfig SET Value = 'http://apollo-configservice:8080/eureka/' WHERE ConfigKey = 'eureka.service.url';

我见过有人在这里写成http://127.0.0.1:8080/eureka/,在本机docker run的场景下 configservice 自己能起来,但 adminservice 永远注册不上,Portal 里点发布直接 500。

第二处:Portal 库里的 meta 地址和启用环境。Portal 需要知道每个环境的 meta 服务器在哪,这个信息是 JSON 格式:

USE ApolloPortalDB; UPDATE ServerConfig SET Value = 'DEV' WHERE ConfigKey = 'apollo.portal.envs'; UPDATE ServerConfig SET Value = '{"DEV":"http://192.168.1.100:8080"}' WHERE ConfigKey = 'apollo.portal.meta.servers';

这里我用的是宿主机真实 IP,而不是容器名。原因很直白:这条地址是要给“人”和“客户端”用的。Portal 的后端在容器里确实能用容器名,但如果你在浏览器里打开 Portal 调试,或者让宿主机之外的若依应用连上来,容器名是解析不了的。统一写宿主机 IP,能省掉后面一半的沟通成本。

还有一个容器化专属的坑值得单独拎出来:客户端连上 meta(8080)之后,meta 会返回一个具体的 configservice 实例地址,这个地址是 configservice 注册到 Eureka 时上报的。默认情况下它上报的是容器内的主机名或容器 IP,宿主机上的 Java 进程拿到这个地址后根本连不上,现象是客户端日志里先成功拿到 meta,紧接着报连接超时。解决办法有两个,我更推荐第二个:

  1. 让若依也跑在同一个 Docker 网络里(如果整个项目本来就要容器化);
  2. 给 configservice 显式指定对外 IP:在 compose 里加环境变量EUREKA_INSTANCE_IP_ADDRESS: 192.168.1.100和EUREKA_INSTANCE_PREFER_IP_ADDRESS: "true",让它注册对外可达的地址。

这个坑我当初排了整整一个下午,因为日志报的是“连接超时”而不是“找不到服务”,很容易误判成防火墙问题。

2.4 启动验证:按这个顺序看日志

docker compose up -d之后不要急着开浏览器,按下面的顺序确认,每一步都有明确的成功标志:

docker logs -f apollo-configservice # 看到 "Started ConfigServiceApplication" 且 "Registering application configservice with eureka"

然后看 adminservice:

docker logs -f apollo-adminservice # 关键行:"DiscoveryClient_ADMINSERVICE/... - registration status: 204"

204 表示注册成功。如果一直是 404 或者重试,回去查eureka.service.url。

最后看 Portal:

docker logs -f apollo-portal # "Started PortalApplication in xx seconds"

三步都过了再访问http://192.168.1.100:8070,默认账号密码是apollo/admin。登录进去第一件事就是改密码,这个默认口令是公开的,不改等于把配置中心敞着门。改完之后顺手在“系统信息”里确认一下环境列表里 DEV 是不是绿的,绿了说明 Portal 到 adminservice 的链路是通的。

3. 若依侧的改造:让 Spring Boot 在启动最早期就拿到远端配置

3.1 依赖与版本对齐

若依的后端在 3.8.x 系列上一般是 Spring Boot 2.5.x,配套的 Apollo 客户端选2.1.0就够用,JDK 8 也能跑。如果你接的是用 Spring Boot 3 的分支,客户端版本要抬到2.2.x,并且要留意javax.annotation换成jakarta.annotation之后编译期的包名问题,这一步最容易在“依赖加完了但编译不过”上卡住。

依赖加在ruoyi-admin模块里,因为它是启动模块:

<dependency> <groupId>com.ctrip.framework.apollo</groupId> <artifactId>apollo-client</artifactId> <version>2.1.0</version> </dependency>

注意不要顺手把apollo-client加到ruoyi-common或ruoyi-framework里然后指望它也生效。Apollo 客户端的初始化依赖ApplicationContext的启动流程,加在启动模块之外,要么不生效,要么出现“某些模块能读到某些模块读不到”的诡异现象——本质是同一个app.id被初始化了两次。

版本对齐还有一个坑:Apollo 客户端内部依赖了spring-context、guava等库,如果项目里已经有不同版本的同名依赖,Maven 的最近优先原则可能把 Apolly 客户端需要的版本顶掉,表现为启动时报NoSuchMethodError。加完依赖后跑一次mvn dependency:tree -Dincludes=com.google.guava看一眼,比事后猜强。

3.2 app.id、env、meta 三个参数各自的坑

这三个参数是客户端能不能连上的全部,但它们的坑点完全不同:

app.id是应用身份,必须和 Portal 里创建的项目 AppId 完全一致,大小写敏感。若依的项目建议命名成ruoyi-admin或ruoyi-<业务域>,别用中文或带空格。这个值一般写在application.yml里,或者用-Dapp.id=ruoyi-admin传。

apollo.meta是配置服务地址,就是http://192.168.1.100:8080,不带/eureka/后缀。这是高频错误,很多人把注册中心地址直接抄过来。

env是最容易忽略的一个。它不写在application.yml里生效,得通过下面几种方式之一指定:

  1. JVM 参数-Dapollo.env=DEV;
  2. 同目录下的app.properties里写env=DEV;
  3. 服务器上的/opt/settings/server.properties写env=DEV;
  4. 环境变量ENV=DEV。

我踩过的坑是:只配了apollo.meta没配env,客户端能连上、配置也能拉下来,一切看起来正常,但缓存目录落在了默认的路径下。结果同一台机器上跑了 DEV 和 UAT 两个实例,两个进程共用同一份本地缓存,A 环境改了配置,B 环境重启后读到了 A 的缓存——排查的时候人都傻了。显式写-Dapollo.env=DEV,让缓存目录天然隔离,这个习惯能省掉无数莫名其妙的“配置串了”。

启动脚本我一般这么写:

java -jar ruoyi-admin.jar \ -Dapp.id=ruoyi-admin \ -Dapollo.env=DEV \ -Dapollo.meta=http://192.168.1.100:8080 \ -Dapollo.cacheDir=/opt/data/apollo-cache \ -Dapollo.bootstrap.enabled=true

注意-D参数必须写在-jar之前,写成java -jar app.jar -Dxxx会被当成程序参数,不生效。这个低级错误每年都有人犯。

3.3 bootstrap 段怎么写,为什么要放在 application.yml

这里有个常见误解:看到“bootstrap”就以为是 Spring Cloud 的bootstrap.yml。Apollo 的apollo.bootstrap.enabled跟 Spring Cloud Config 的 bootstrap 上下文没关系,它只是一个开关,告诉 Apollo 客户端“在 Spring 环境准备阶段就把远端配置注入进去”。所以这段配置写在application.yml里就行,若依也不需要额外引入spring-cloud-starter-bootstrap。

我的application.yml顶部大概是这样:

app: id: ruoyi-admin apollo: meta: http://192.168.1.100:8080 cacheDir: /opt/data/apollo-cache bootstrap: enabled: true eagerLoad: enabled: true namespaces: application.yml,ruoyi-common.yml autoUpdateInjectedSpringProperties: true

eagerLoad.enabled: true这个开关值得单独说。Apollo 客户端的加载是通过一个ApplicationListener实现的,默认它排在日志系统初始化之后。这意味着你在 Apollo 里配的log.path、logging.level.*,在日志系统初始化时还读不到,等 Apollo 加载完,logger 已经建好了,配置等于白配。打开 eagerLoad 后,Apollo 的监听器被提到日志监听器之前,日志相关配置才真正生效。若依的logback.xml里用了${log.path},如果你打算把这个值搬到 Apollo,这个开关是必开项。

namespaces那一行也有讲究,多个 namespace 用英文逗号分隔,格式一定要和 Portal 里创建时选的格式一致。application.yml对应 yml 格式的 namespace,ruoyi-common.yml是我给公共配置留的位置。写成application而不带后缀,Portal 那边就得建 properties 格式的,两者对不上时客户端日志会提示 “namespace not found”,配置静默地不生效。

3.4 命名空间规划与配置迁移的实操顺序

一次性把若依所有配置搬进 Apollo 是自找麻烦的。我的做法是分三批,每批上线观察一天:

第一批:纯业务参数。ruoyi.profile(文件上传路径)、token.expireTime、分页上限、导出限制这些。它们改错了影响面可控,回滚也简单。

第二批:日志与非核心中间件。log.path、logging.level.com.ruoyi=debug、Redis 的连接超时。这一批能验证 eagerLoad 是否真的生效。

第三批:数据源与端口。spring.datasource.*、server.port。这批搬进去之前要有心理准备:改了必须重启,Apollo 的热更新对它们无效,后面会细说。

namespace 的划分我建议按“变更频率”来,而不是按“模块”来。按模块分的结果是,一个已经很久没动过的ruoyi-system.yml和一个天天改的ruoyi-business.yml混在一起,发布时的审批成本完全不一样;按频率分,高频变更的走轻量审批,低频变更的走严格审批,流程才跑得动。

配置值的迁移有个细节:Portal 里 key 的层级写法和 yml 是两套语法。在 yml 格式的 namespace 里,你直接粘贴原来的 yml 文本当然可以,但如果你用的是 properties 格式,嵌套结构要拍平成ruoyi.profile=/opt/upload这种形式,列表则要用key[0]、key[1]的写法。若依里有几个 List 类型的配置(比如放行的 URL 列表),拍平后很容易写错,建议这类统一用 yml 格式的 namespace,直接复制粘贴,减少出错。

4. 动态刷新的真相:哪些配置改了立刻生效,哪些必须重启

4.1 @Value 与 @ConfigurationProperties 的刷新路径差异

这是整个接入过程里最值得花时间搞清楚的一块。Apollo 客户端的自动更新能力,本质是两件事:一是远端配置变化后,把新的 key-value 写回 Spring 的Environment;二是在写回的同时,把被@Value注解标记的字段重新赋值。

第一件事对所有配置都成立,第二件事只对@Value成立(前提是apollo.autoUpdateInjectedSpringProperties为 true,默认就是)。所以你会看到一个很有意思的现象:environment.getProperty("xxx")拿到的是新值,但某个 Bean 里通过@Value("${xxx}")注入的字段也是新值,唯独通过@ConfigurationProperties绑定的对象还是旧值。

原因是@ConfigurationProperties的重新绑定依赖 Spring 的事件机制——需要有一个组件监听EnvironmentChangeEvent并触发重新绑定。这个组件在 Spring Cloud Context 里(ConfigurationPropertiesRebinder),而若依默认没引 Spring Cloud。所以你搬进 Apollo 的ruoyi.*这类绑定对象,改完 Portal 里的值,Bean 里的字段大概率是不动的。

实测下来最省事的解法有两个,按项目情况选:

  • 能用@Value就别用配置类。若依里RuoYiConfig是个@ConfigurationProperties的静态持有类,如果只是读一个上传路径,完全可以在需要的地方直接@Value("${ruoyi.profile}")。
  • 确实需要配置类的,自己挂监听手动改。下面这种写法在项目里很常见:
@Component @Slf4j public class BizConfigRefresher { @Value("${biz.export.max-rows:5000}") private int maxRows; @ApolloConfigChangeListener(value = "application.yml", interestedKeys = {"biz.export.max-rows"}) public void onChange(ConfigChangeEvent event) { ConfigChange change = event.getChange("biz.export.max-rows"); if (change != null) { log.info("导出上限由 {} 调整为 {}", change.getOldValue(), change.getNewValue()); // 如果这个值要同步给别的组件,在这里显式 set } } }

interestedKeys这个参数一定要写,不写的话 namespace 里任何一个 key 变化都会触发回调,日志会被刷屏。

4.2 若依里几类典型配置的刷新表现对照表

把常见配置按“改完要不要重启”分个类,比看文档直观得多:

配置项承载方式改完是否立即生效原因
token.expireTime@Value注入到 token 工具类是每次签发校验都重新读字段
token.secret@Value是(但所有旧 token 失效)字段被重新赋值
ruoyi.profile配置类静态字段否缺少重新绑定组件
biz.export.max-rows@Value是自动更新生效
spring.datasource.urlDruid 数据源否,必须重启连接池在启动时创建,不会因为属性变了就重建
spring.datasource.druid.max-active配置类否同上,且 Druid 不支持动态改上限
log.pathlogback 变量否,必须重启日志框架在启动时解析并打开文件句柄
logging.level.com.ruoyiSpring Boot 日志系统是Actuator 的日志端点机制,Apollo 变更会触发重设
业务开关(如biz.audit.enabled)@Value是典型的动态开关场景

这张表里最值得记住的是数据源和日志路径两行。它们说明了一件事:配置中心解决的是“不用改代码”,不是“不用重启”。把数据源搬进 Apollo 的价值在于“所有环境的连接串集中管理、改错能回滚、谁改的能查到”,而不是“改完立刻生效”。想明白这点,就不会在“为什么我改了没反应”上浪费时间。

4.3 本地缓存目录带来的“改了没反应”假象

Apollo 客户端会在本地落一份缓存,这是它的容灾设计:服务端挂了,客户端重启还能用最后一份配置起来。但这份缓存也会制造麻烦。

典型现象:你在 Portal 里改了配置并且发布成功,客户端日志也打印了“Config changed”,但业务表现还是旧的。这时先别怀疑 Apollo,按这个顺序查:

  1. 看客户端日志里拉到的 namespace 和你改的是不是同一个。有没有可能你改的是ruoyi-common.yml,但apollo.bootstrap.namespaces里只写了application.yml。
  2. 看cacheDir是否可写。目录不存在或没权限时,Apollo 会打一条 WARN 然后走内存缓存,重启后行为不一致。Docker 部署时特别容易犯,因为容器里的/opt/data默认可能不可写。
  3. 看是不是多实例。十台机器,Apollo 只会把变更推给“在线且长连接正常”的实例,如果某台机器的 8080 出口被限制,它就一直吃本地缓存。

我遇到过最隐蔽的一次:三个实例里有一个因为网络策略调整,长连接断了但没重连成功,Apollo 控制台上看它“最后上报时间”还是几小时前。排查这类问题不要只看一台的日志,去 Portal 的项目页面看实例列表,哪个实例的心跳时间不对,问题就在那。

4.4 灰度发布在若依多实例场景下的用法

配置中心的杀伤力在于“改一个值影响所有实例”,所以灰度能力必须用起来。Apollo 的灰度发布按客户端 IP 或 Label 匹配,用法上有个前提:客户端得上报一个能被识别的标识。

按 IP 灰度在容器化环境里要格外小心,因为客户端上报的 IP 是从本机网卡取的,容器里拿到的就是容器 IP,跟你以为的那台“业务机器”对不上。这种情况下我更推荐按 Label 灰度,启动时显式打标:

java -jar ruoyi-admin.jar -Dapollo.label=canary-01 ...

然后在 Portal 的灰度规则里匹配canary-01,只推给这一台。验证没问题再全量发布。

这里有个操作习惯值得坚持:任何一次配置发布前,先在灰度实例上看三样东西——启动日志里有没有报配置缺失、业务日志里的关键参数是不是新值、监控上的错误率有没有抬头。三样都正常再点全量。Apollo 的发布历史里可以一键回滚,但回滚本身也有几十秒的生效延迟,别把它当成免死金牌。

5. 上线前必须补的几件事

5.1 权限、审计与生产环境隔离

配置中心一旦成为“所有环境的配置都在这里”,它本身就变成了一个高危系统。默认的apollo/admin必须改,改完之后按角色分配:

  • 开发:对自己项目的 DEV 环境有编辑和发布权限;
  • 测试:对 TEST 环境有编辑权限,发布权限给到测试负责人;
  • 运维:对 PRO 环境有发布权限,但没有编辑权限(编辑由开发提,运维审);
  • 所有人:对 PRO 环境有只读权限,方便排查问题。

这套分法的核心是“改的人”和“发的人”分开。听起来官僚,但真出过一次事故之后就明白——一个手滑把spring.datasource.url改成测试库,如果没有第二个人看一眼,生产直接就写错库了。

生产环境隔离还有一层网络上的要求:DEV 的客户端绝对不能连到 PRO 的 meta 地址。实践里最稳的做法是每套环境一套独立的 Apollo 部署(独立库、独立容器),而不是在一个 Portal 里用 namespace 隔离。一套 Apollo 全环境共用的结果是,一个误操作就能让测试配置污染生产,而 Apollo 本身并没有强隔离机制来阻止你。

审计这块,Portal 自带的发布历史已经够用了:谁、什么时候、改了哪个 key、旧值新值是什么、什么时候回滚过,全都有记录。建议再加一条规矩:每次发布填一句变更说明,哪怕只写“调整导出上限”也行。三个月后回头看,这一句话能省掉大量考古工作。

5.2 客户端容灾:Apollo 挂了业务还能不能起来

这个问题必须在上线前想清楚并验证一遍。Apollo 客户端的容灾逻辑是:启动时先尝试连服务端,连不上就读本地缓存,缓存也没有才会失败。所以有两种失败形态:

  • 有缓存的情况:Apollo 整体挂掉,若依重启仍能正常起来,用的是最后一次成功拉取的配置。这种情况业务基本无感,但要注意配置已经“冻结”在旧值。
  • 无缓存的情况(新机器首次部署、缓存目录被清):客户端拉不到配置,所有 Apollo 来源的 key 都是缺失状态。如果spring.datasource.url只在 Apollo 里有,应用直接起不来。

我的处理办法是在本地application.yml里保留一份“兜底默认值”,尤其是数据源这类启动必需的配置。Apollo 的优先级高于本地配置,所以正常情况下远端值会覆盖本地;只有远端不可达时,本地值才会生效。这样最坏情况是“用旧配置跑起来”,而不是“整个服务起不来”。

这个兜底不是没有代价的——两份配置存在不一致的风险。所以我的规矩是:本地只留最小可启动集(数据源、端口、Redis 地址),其余全部只放在 Apollo,避免“两处都有、以哪个为准”的混乱。

还有一个细节:apollo.cacheDir一定要挂到持久化卷上。容器重启后缓存目录丢了,容灾能力就等于没有。我在 compose 里给它挂一个宿主机目录,成本极低,收益很大。

5.3 一份可复用的接入检查清单

最后把整个流程压缩成一张清单,下次接新项目可以直接照着走:

  1. 建库:ApolloConfigDB和ApolloPortalDB两个库、两份 SQL、两个行数校验。
  2. 改表:eureka.service.url用容器名,apollo.portal.meta.servers用宿主机 IP,apollo.portal.envs与环境名一致。
  3. 起服务:configservice → adminservice(看 204)→ portal,顺序不能乱。
  4. Portal 初始化:改默认密码、建项目(AppId 与客户端一致)、建 namespace(格式对齐)。
  5. 加依赖:只在启动模块加apollo-client,跑一次依赖树确认没有版本冲突。
  6. 配客户端:app.id、apollo.meta、-Dapollo.env、cacheDir四件套齐全,eagerLoad打开。
  7. 迁配置:按业务参数 → 日志 → 数据源三批走,每批观察一天。
  8. 验刷新:拿一个@Value的开关和一个数据源配置,分别验证“立即生效”和“必须重启”,确认自己理解的行为和实际一致。
  9. 做容灾:停掉 Apollo 容器,重启一次若依,确认能靠缓存起来。
  10. 定规矩:灰度先行、发布写说明、PRO 环境双人确认。

我自己在第 6 步和第 8 步上花的时间最多,也最值得。前面那些容器编排的活儿,照着文档抄一遍基本就过了;真正会反复咬人的,是“配置到底有没有生效”这个判断——它不像服务起不起来那样有明确的报错,而是安安静静地用着旧值,直到某天你发现线上行为和预期对不上,才开始翻日志。

一个额外的小技巧:在若依里加一个只读的诊断接口,把当前Environment里几个关键 key 的值和 Apollo 客户端记录的 namespace 列表打出来,出问题时先访问这个接口,比翻日志快得多。这个接口记得加权限控制,别把数据库密码打出去了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询