Apollo 配置发布、Namespace 与回滚治理工具手册
页面显示已修改,客户端为什么还是旧值
值班群里最常见的 Apollo 争论是:“Portal 上明明已经改成 true,为什么订单服务仍然读到 false?”这时先不要重启应用,也不要直接改数据库。Portal 中的编辑态、已经发布的 release 和客户端当前缓存是三份不同状态;只有发布动作产生新 release,ConfigService 才会把它交给客户端。
把这条链路记成四个动作更容易排查:人在 Portal 修改 namespace,Portal 调用 AdminService 写入 ApolloConfigDB;发布时 AdminService 固化一份 release;ConfigService 从同一环境的 ApolloConfigDB 读取已发布配置;客户端通过 Meta Server 找到 ConfigService,并以长轮询接收变更通知。Portal 自己使用 ApolloPortalDB 保存用户、权限和环境入口,它不在业务客户端的读配置链路上。
这也解释了网络边界:业务客户端需要访问 Meta Server 和 ConfigService,不需要访问 AdminService;Portal 需要访问各环境的 Meta Server、AdminService 和 PortalDB。Apollo 的分布式部署文档明确把 ConfigService 和 AdminService 设计在可信内网中,不应把它们直接暴露到公网。分布式部署指南给出了完整连通矩阵。
先在隔离环境跑通一次发布与回滚
Apollo v2.5.2 是 2.5 发行线的正式版本,采用 Apache-2.0 许可证。官方 release 说明从 v2.5.1 升级到 v2.5.2 不需要 schema 变更,升级顺序是 ConfigService、AdminService、Portal;实际升级前仍应重新查看 GitHub Releases,因为版本、安装包和 schema 说明会继续变化。
服务端需要 Java 17+,Java 客户端最低运行时为 Java 8;MySQL 需要 5.6.5+。第一次实验还要确认 8070、8080、8090 和体验库映射端口没有被占用。正式安装包、容器镜像和数据库初始化脚本应来自同一个 release,避免三项版本错配。
用 Docker Quick Start 看懂对象
官方 Docker Quick Start 需要从 apollo-quick-start 仓库取得 docker-compose.yml 和 sql/ 目录,然后在该目录启动:
docker compose up日志出现 Config Service、Admin Service、Portal 均已启动后,访问 http://localhost:8070。体验环境通常使用 apollo/admin,数据库可能以空 root 密码映射到宿主机 13306;这些默认值只适合单人隔离实验。官方文档还特别说明,Quick Start 的 ConfigService 默认注册容器内地址,宿主机上的外部客户端未必能直连,可先用容器内 demo 验证:
docker exec -i apollo-quick-start /apollo-quick-start/demo.sh client若日志显示服务成功而宿主机客户端超时,先检查 ConfigService 注册地址是否为容器内 IP,而不是反复重建应用。团队共享环境应使用正式的 apolloconfig/apollo-configservice、apolloconfig/apollo-adminservice、apolloconfig/apollo-portal 镜像或 release 安装包,并按官方 Docker Quick Start与分布式部署指南区分体验方案和长期方案。
团队环境先准备两类数据库
ApolloPortalDB 通常只部署一套;ApolloConfigDB 按 DEV、FAT、UAT、PRO 等环境分别部署。每套 ConfigService 和 AdminService 只连接所属环境的 ApolloConfigDB,Portal 则通过各环境 Meta Server 找到对应服务。
先导入当前 release 自带的 apolloportaldb.sql 和 apolloconfigdb.sql,再做最小核验:
SELECT `Id`, `Key`, `Value`, `Comment`
FROM `ApolloPortalDB`.`ServerConfig`
LIMIT 1;
SELECT `Id`, `Key`, `Value`, `Comment`
FROM `ApolloConfigDB`.`ServerConfig`
LIMIT 1;ConfigService、AdminService 的数据源指向 ApolloConfigDB,Portal 的数据源指向 ApolloPortalDB:
# ConfigService / AdminService
spring.datasource.url=jdbc:mysql://mysql.example.com:3306/ApolloConfigDB?useSSL=true&characterEncoding=utf8
spring.datasource.username=${APOLLO_CONFIG_DB_USER}
spring.datasource.password=${APOLLO_CONFIG_DB_PASSWORD}
# Portal
spring.datasource.url=jdbc:mysql://mysql.example.com:3306/ApolloPortalDB?useSSL=true&characterEncoding=utf8
spring.datasource.username=${APOLLO_PORTAL_DB_USER}
spring.datasource.password=${APOLLO_PORTAL_DB_PASSWORD}URL 决定服务读写哪套状态,用户名决定数据库权限边界,密码必须由部署环境注入。把 PRO 的 AdminService 错连到 DEV 数据库,表现可能不是启动失败,而是 Portal 在一个环境编辑、客户端却从另一套 release 读取,这比显式报错更危险。
生产形态至少让 ConfigService 和 AdminService 各有两个实例,并通过可达地址注册;Portal 可独立扩容,客户端入口前放内部负载均衡。若采用 Kubernetes,官方 Helm chart 会创建三个 Deployment,并使用 Kubernetes 原生服务发现;若采用虚拟机安装包,则按数据库发现或保留的 Eureka 模式配置。两种发现模式不能靠猜,升级前要核对运行 profile、注册记录和客户端实际拿到的服务地址。
正向实验:让一个 release 真正到达客户端
在 Portal 创建应用 te-order-service,选择 DEV、default cluster 和 application namespace,新增:
feature.order.verify=true
order.timeout-ms=3000保存后先不要发布。此时编辑态已经进入数据库,但下面的客户端 API 仍应返回上一个 release;第一次创建且从未发布时,也可能没有对应配置。然后填写可追踪的发布标题和原因,点击发布,再读取:
curl -sS \
"http://127.0.0.1:8080/configs/te-order-service/default/application"
curl -sS \
"http://127.0.0.1:8080/configfiles/json/te-order-service/default/application"/configs 响应应包含 appId、cluster、namespaceName、releaseKey 和 configurations;/configfiles/json 直接返回键值对象。releaseKey 是定位“客户端读到哪次发布”的关键证据,不能只看值是否碰巧正确。非 properties namespace 要在名称中保留 .yaml、.yml 或 .json 后缀,否则请求的是另一个 namespace。
这里四个字段分别决定不同分支:
appId 标识应用,必须与客户端 app.id 一致。env 选择哪套 ConfigService 和 ApolloConfigDB,通常由 Meta Server 地址间接决定。cluster 支持机房或实例组差异化;找不到指定 cluster 时可能回退到 default,所以要记录实际结果。
namespace 是配置、权限和发布的基本单元;公开 namespace 与应用私有 namespace 还有覆盖关系。
接入 Java 服务并观察动态变化
项目依赖应跟随团队锁定的 Apollo Java 客户端版本。客户端最小配置如下,Meta Server 地址应来自环境变量或部署平台,而不是打进公共制品:
app.id=te-order-service
apollo.meta=${APOLLO_META:http://127.0.0.1:8080}
apollo.cluster=default
apollo.bootstrap.enabled=true
apollo.bootstrap.namespaces=application直接 API 读取适合需要显式默认值和变更监听的代码:
Config config = ConfigService.getAppConfig();
boolean enabled = config.getBooleanProperty("feature.order.verify", false);
config.addChangeListener(event -> {
if (event.isChanged("feature.order.verify")) {
System.out.printf("release changed: %s -> %s%n",
event.getChange("feature.order.verify").getOldValue(),
event.getChange("feature.order.verify").getNewValue());
}
});Apollo 客户端启动时先从 ConfigService 拉取配置并写本地缓存,随后通过 HTTP 长轮询等待通知。通知只说明“可能有新 release”,客户端仍要再次拉取并比较;ConfigService 短暂不可用时,客户端可以用本地缓存启动,但新扩容实例没有历史缓存时不能把这一点当成灾备承诺。日志和指标至少要能回答实际 appId、cluster、namespace、Meta Server、最后成功更新时间和本地缓存目录。
反向实验:稳定复现“保存了但没生效”
把 order.timeout-ms 从 3000 改成 5000,只点保存,不点发布,然后连续执行两次读取:
curl -sS \
"http://127.0.0.1:8080/configfiles/json/te-order-service/default/application"预期仍是 3000。故障证据是 Portal 显示编辑差异,而 release 历史没有新记录,/configs 的 releaseKey 也没有变化。这证明问题位于“编辑态到 release”之间,而不是客户端刷新慢。
接着发布 5000,确认读取结果变化,再在 release 历史中回滚到上一版本。回滚后客户端可读值应恢复为 3000,但 Portal 的编辑态仍可能保留 5000。Apollo 回滚的是对客户端生效的 release,不是抹掉草稿或删除数据库历史。若回滚后立刻误点发布,草稿会再次生成新 release,因此回滚后的第一步应是核对编辑差异和 release id。
灰度发布同样遵循“先形成灰度编辑态,再发布灰度 release”。创建灰度版本后,为测试实例的 IP 或 Apollo label 配规则,将 order.timeout-ms=5000 只发给目标实例;目标实例应读到 5000,主版本实例仍读到 3000。规则保存后可实时改变匹配对象,配置值变更仍需再次灰度发布。验证通过后全量发布会把灰度配置合并回主版本;验证失败则放弃灰度,不要用主版本回滚代替清理灰度分支。Kubernetes 实例 IP 易变,优先使用稳定 label,并把“实际命中实例列表”作为灰度证据。Portal 用户指南的灰度章节给出了主版本、灰度规则和全量发布的状态变化。
把 OpenAPI 变成受控发布入口
Portal 的 OpenAPI 面向发布流水线和自动化工具。管理员先创建第三方应用并授权可操作的 namespace;请求头的 Authorization 字段直接携带该 consumer token。token 只能管理已经授权的资源,不应给发布脚本复用 Portal 管理员身份。
下面的脚本先修改配置,再创建 release。变量只从密钥系统和流水线上下文注入:
base="http://127.0.0.1:8070/openapi/v1"
resource="envs/DEV/apps/te-order-service/clusters/default/namespaces/application"
curl -fsS -X PUT \
-H "Authorization: ${APOLLO_OPENAPI_TOKEN}" \
-H "Content-Type: application/json" \
"${base}/${resource}/items/order.timeout-ms?createIfNotExists=true" \
-d '{
"key": "order.timeout-ms",
"value": "5000",
"comment": "increase downstream timeout",
"dataChangeCreatedBy": "release-bot",
"dataChangeLastModifiedBy": "release-bot"
}'
curl -fsS -X POST \
-H "Authorization: ${APOLLO_OPENAPI_TOKEN}" \
-H "Content-Type: application/json" \
"${base}/${resource}/releases" \
-d '{
"releaseTitle": "order-timeout-canary",
"releaseComment": "change request CHG-EXAMPLE",
"releasedBy": "config-reviewer"
}'成功响应中的 release 信息要保存到变更记录。回滚使用 release id,而不是配置 key:
curl -fsS -X PUT \
-H "Authorization: ${APOLLO_OPENAPI_TOKEN}" \
"${base}/envs/DEV/releases/${APOLLO_RELEASE_ID}/rollback?operator=config-reviewer"如果返回 401,先检查 token 是否缺失、过期或格式错误;403 通常表示第三方应用没有目标 namespace 的操作授权;PRO 环境还可能因为 namespace 锁和编辑、发布分离而拒绝同一操作者。官方 OpenAPI 文档列出了 item、release 和 rollback 接口,升级自动化客户端前应按目标版本重新核对。
故障证据要沿调用链收集
“读不到配置”至少有五种不同故障,第一证据也不同:
| 现象 | 第一证据 | 常见原因 | 修复后再验证 |
|---|---|---|---|
| Portal 可改,客户端仍是旧值 | release 历史与 releaseKey | 只保存未发布 | 发布后比较新旧 releaseKey |
| Portal 看不到某环境 | ApolloPortalDB 的环境与 Meta Server 配置 | 环境未挂载或地址错误 | Portal 能发现该环境服务 |
| 客户端连接超时 | Meta Server 返回的实例地址 | 注册了容器内 IP、网卡选择错误 | 从客户端网络访问每个 ConfigService |
| 某些实例没有灰度值 | 灰度实例列表、IP/label | 规则未命中或 label 未上报 | 目标与非目标实例分别读取 |
| 重启后回到旧值 | 本地缓存与服务端 release | 启动时连错环境或缓存兜底 | 记录 Meta Server 和最后 release |
| AccessKey 拒绝 | ConfigService 鉴权日志、机器时间 | appId 不匹配、密钥失效或时钟偏差 | 使用正确 appId/key 再请求 |
Apollo v2.5.2 修复了 ConfigService AccessKey 认证时对 appId 的校验,因此从旧版本升级时应把“错误 appId + 正确密钥必须失败”加入反向回归。AccessKey 不是 OpenAPI token:前者保护客户端读取,后者保护 Portal 管理接口。
架构选择从失败域开始
单人实验可以接受 Quick Start;小团队共享环境需要三服务、外部 MySQL、账号体系、内部域名和备份;多环境生产形态应让每个环境拥有独立 ConfigService、AdminService、ApolloConfigDB,并共享一套或少量 Portal。这样 PRO 数据库故障不会直接污染 DEV,但 Portal 故障会暂时阻断管理操作,已运行客户端仍可读取缓存并尝试连接 ConfigService。
ConfigService 横向扩容主要承接客户端拉取、长轮询和通知压力;AdminService 承接编辑与发布;Portal 承接人和 OpenAPI 流量。容量规划不能只看 QPS,还要看客户端实例数、namespace 订阅数、发布频率、单 namespace item 数、release 历史量和数据库连接。发布风暴会同时增加数据库读写、ConfigService 通知和客户端回源,应该按应用或 namespace 分批,而不是让全公司在同一时刻刷新。
Apollo 适合需要可视化编辑、权限、审核、灰度、回滚和多语言客户端的集中配置。若配置天然随代码版本发布、团队更重视 Git 审核且无需 Portal 灰度,Git-backed 配置方案成本更低;若值是数据库密码、证书私钥等高敏感 secret,优先由专门密钥系统提供短期凭证,Apollo 只保存引用或非敏感开关。配置中心不是密钥保险箱,也不是服务注册中心。
权限、敏感数据与长期治理
namespace 是最小治理单元。编辑权与发布权应分离,PRO 环境启用发布审核或 namespace lock;OpenAPI token 只授予所需 operation、appId、env 和 namespace,设过期时间并记录最后使用;客户端 AccessKey 按应用和环境隔离。ConfigService、AdminService、MySQL 均留在内网,Portal 也应经过统一认证和访问控制。
以下内容不得进入配置值、源码、截图或普通日志:数据库口令、私钥、长期云凭证、真实 OpenAPI token、客户端 AccessKey。即使 Apollo 有查看权限和 AccessKey,Portal 管理员、数据库管理员和备份介质仍可能接触明文。对高敏感值应记录数据分级、密钥系统引用、轮换负责人和泄漏处置动作。
长期运行时至少观察这些趋势:
ConfigService 拉取错误率、长轮询超时与实例分布是否随客户端规模异常增长。AdminService 发布失败率、发布耗时和数据库连接池等待是否在发布高峰抬升。namespace 与 item 数量、单项大小、release 历史量是否持续无界增长。
客户端最后成功同步时间、本地缓存回退次数和不同 releaseKey 的实例数量。OpenAPI 401/403、异常操作者、越权尝试和 token 长期未使用情况。
阈值应从实例规模、发布 SLO 和数据库基线推导,不使用脱离负载的万能数字。团队每次变更至少保存 appId、env、cluster、namespace、变更前后 release id、操作者、灰度目标、观察结果和回滚入口;升级时先备份两类数据库,在隔离环境执行 schema 检查和客户端兼容回归,再按 release 说明的组件顺序滚动。
清理实验环境而不留下误用入口
Quick Start 完成后停止并删除容器与实验卷:
docker compose down -v若使用外部 MySQL,先确认数据库名称和环境标签,再由数据库 owner 删除实验用 ApolloPortalDB、ApolloConfigDB 和低权限账号;不要把示例清理命令直接用于共享实例。撤销 OpenAPI token、客户端 AccessKey 和测试用户,删除内部 DNS 或负载均衡入口,并确认仓库、终端历史和流水线日志中没有真实凭证。
正式环境的退出顺序相反:先停止配置变更,导出 app/namespace/release 清单并验证应用已迁移,再撤销客户端访问,最后下线 Portal、AdminService、ConfigService 和数据库。只关 Portal 不代表 Apollo 已退出,因为客户端仍可能依赖 ConfigService 和本地缓存继续运行。
上线前做最后一次贯通检查:用非管理员账号编辑、由另一账号发布,目标客户端拿到新 release;未授权 token 收到 403;错误 appId 的 AccessKey 请求失败;灰度只命中目标实例;回滚恢复旧 release 且编辑态被单独处理。任何一项只能写成待执行,不能用页面可打开替代完整证据。
