ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Apollo配置中心多环境部署实战:从单环境到生产级隔离

Apollo配置中心多环境部署实战:从单环境到生产级隔离

1. 为什么 Apollo 的环境搭建值得单独拿出来聊

Apollo 这个配置中心,但凡在微服务架构里待过一阵子的后端开发,基本都绕不开它。我最早接触它是在一个十来个人的小团队里,当时配置散落在各个服务的application.yml里,改一个数据库连接池大小要挨个服务改一遍、重新打包、重新上线,运维和开发互相甩锅的场面至今记忆犹新。后来引入 Apollo 之后,配置集中管理、实时推送、灰度发布这些能力确实把我们从泥潭里拽了出来。

但问题也随之而来:单环境搭建容易,多环境搭建才是真正让人头疼的地方。很多教程只告诉你把 Quick Start 跑起来,本地能访问 8070 端口就完事了,可一旦要区分开发、测试、预发、生产,涉及 Portal 和 Config Service 的部署拓扑、数据库分库、环境隔离、客户端如何指定环境等等,坑就一个接一个冒出来。我见过不少团队在测试环境用单环境模式凑合,结果上线前手忙脚乱重新搭一套,配置迁移、Meta Server 地址、集群名称全对不上,白白浪费两三天。

这篇内容就是把我自己从零搭 Apollo 单环境、再到多环境拆分部署的完整过程梳理一遍。适合谁看:正在选型配置中心的后端开发、需要独立负责一套配置中心部署的运维同学、以及已经用上 Apollo 但环境隔离做得一团糟想重构的团队。我会把每一步为什么这么做、参数怎么算、哪里容易翻车都讲清楚,尽量让你照着做就能复现,而不是看完还是一头雾水。

需要提前说明的是,Apollo 的部署方式有好几种,官方提供了 Quick Start 脚本、Docker 镜像、以及手动部署包。我下面讲的以手动部署包 + 自建数据库为主线,因为这是生产环境最常见、也最能讲清楚原理的方式。Quick Start 那种一键脚本我会在单环境部分提一下,但不会作为重点,毕竟它隐藏了太多细节,出了问题你根本不知道从哪查。

2. Apollo 核心概念与部署架构拆解

2.1 三个核心角色:Portal、Config Service、Admin Service

在动手之前,必须先把 Apollo 的几个核心组件搞清楚,否则后面配环境的时候你会分不清哪个服务该连哪个库、哪个端口该暴露给谁。

Config Service:面向客户端(也就是你的业务服务)的配置读取服务。业务服务启动时通过它拉取配置,运行时通过它接收配置变更推送。它提供的是 HTTP 接口,默认端口 8080。这个服务是无状态的,可以水平扩展多个实例,前面挂个负载均衡就行。

Admin Service:面向 Portal 的管理服务,负责配置的增删改查、发布、回滚等写操作。默认端口也是 8080,但通常和 Config Service 部署在同一台机器上,通过不同端口区分。它同样是无状态的。

Portal:面向人的 Web 管理界面,你平时登录进去改配置、发布配置的那个页面就是它。默认端口 8070。Portal 本身不直接操作配置数据库,而是通过调用 Admin Service 的接口来完成操作。这一点很关键,很多人以为 Portal 直连数据库,结果排查问题时方向就错了。

Meta Server:严格来说它不是一个独立进程,而是 Config Service 和 Admin Service 在启动时暴露的一个/services/config和/services/admin接口,客户端通过它来发现服务地址。你可以把它理解成一个"服务注册与发现"的轻量实现,Apollo 没有依赖 Eureka、Nacos 这类注册中心,而是自己用最简单的 HTTP 接口搞定了。

2.2 数据库拆分:为什么 Portal 和 Config 要分库

这是多环境部署里最容易踩坑的地方。Apollo 官方明确建议:Portal 使用独立的数据库,每个环境的 Config Service 和 Admin Service 使用各自独立的数据库。

为什么这么设计?核心原因是环境隔离。假设你把开发环境和测试环境的配置放在同一个库里,通过Env字段区分,看似省事,但一旦有人写错 SQL 或者程序有 bug,很容易造成跨环境的数据污染。更严重的是,生产环境的配置如果和测试环境共库,任何一次误操作都可能直接影响线上。

所以正确的做法是:

组件数据库说明
PortalApolloPortalDB全局唯一,存用户、权限、环境列表等
Dev 环境 Config/AdminApolloConfigDB(Dev)每个环境一套
Test 环境 Config/AdminApolloConfigDB(Test)每个环境一套
Prod 环境 Config/AdminApolloConfigDB(Prod)每个环境一套

注意 Portal 库只有一个,它通过ApolloPortalDB.ServerConfig表里的apollo.portal.envs配置项来决定管理哪些环境。而每个环境的 Config 库里存的是该环境所有项目的配置项、发布记录、灰度规则等。

2.3 环境与集群的关系:别把这两个概念搞混

Apollo 里有两个容易混淆的概念:Env(环境)和Cluster(集群)。

Env 是物理隔离的,不同 Env 对应不同的 Config Service 地址、不同的数据库,通常部署在不同的机器甚至不同的机房。比如 Dev、Test、Prod 就是三个 Env。

Cluster 是逻辑隔离的,同一个 Env 下可以有多个 Cluster,比如default、shanghai、beijing。Cluster 主要用于同环境内不同机房或不同分组的配置差异化。比如同一个测试环境,上海机房的机器读shanghai集群的配置,北京机房的读beijing集群的配置。

很多团队一开始只用default集群,这没问题。但如果你把 Cluster 当成 Env 来用,比如用dev-cluster、test-cluster来区分环境,那就大错特错了,因为它们的 Config Service 地址和数据库是同一套,根本起不到隔离作用。

3. 单环境搭建:从零到能登录改配置

3.1 环境准备与依赖清单

单环境搭建相对简单,我列一下需要准备的东西:

  • JDK:Apollo 服务端要求 JDK 8+,我实测 JDK 11 也能跑,但建议用 JDK 8,兼容性最稳。
  • MySQL:5.7 或 8.0 都行,我用的 5.7。字符集必须是utf8mb4,否则中文配置项会乱码。
  • 部署包:从 Apollo 的 Release 页面下载apollo-configservice、apollo-adminservice、apollo-portal三个包,版本要一致,我用的 2.1.0。
  • 机器:单环境一台 4C8G 的机器足够,三个服务都跑在上面。

数据库初始化脚本在部署包的scripts/sql目录下,分别是apolloportaldb.sql和apolloconfigdb.sql。先建两个库,然后分别导入。

mysql -uroot -p -e "CREATE DATABASE ApolloPortalDB DEFAULT CHARACTER SET utf8mb4;" mysql -uroot -p -e "CREATE DATABASE ApolloConfigDB DEFAULT CHARACTER SET utf8mb4;" mysql -uroot -p ApolloPortalDB < apolloportaldb.sql mysql -uroot -p ApolloConfigDB < apolloconfigdb.sql

导入完成后,记得检查ApolloPortalDB.ServerConfig表里的apollo.portal.envs值,单环境默认是dev,不用改。

3.2 数据库连接配置的修改要点

三个服务都需要改数据库连接。Config Service 和 Admin Service 连的是ApolloConfigDB,Portal 连的是ApolloPortalDB。

配置文件位置在各自包的config/application-github.properties。我以 Config Service 为例:

spring.datasource.url = jdbc:mysql://127.0.0.1:3306/ApolloConfigDB?characterEncoding=utf8&serverTimezone=Asia/Shanghai spring.datasource.username = apollo spring.datasource.password = 你的密码

这里有几个细节值得说:

第一,serverTimezone一定要加,尤其是 MySQL 8.0,不加会报时区错误。我一般统一用Asia/Shanghai。

第二,数据库账号不要用 root,建一个专用账号,只给ApolloConfigDB的读写权限。生产环境这点是硬要求。

第三,characterEncoding=utf8虽然写的是 utf8,但 MySQL 的 utf8 其实是 utf8mb3,真正支持 emoji 的是 utf8mb4。不过 Apollo 的配置项一般用不到 emoji,所以这样写没问题,库的字符集保证是 utf8mb4 就行。

3.3 启动顺序与端口规划

三个服务的启动顺序有讲究:先启动 Config Service 和 Admin Service,最后启动 Portal。因为 Portal 启动时会去探测各环境的 Meta Server 是否可用,如果 Config Service 没起来,Portal 会报环境不可用。

端口方面,Config Service 和 Admin Service 默认都是 8080,如果部署在同一台机器上,必须改其中一个。我的做法是:

  • Config Service:8080
  • Admin Service:8090
  • Portal:8070

改端口在config/application-github.properties里加一行server.port=8090即可。

启动命令很简单:

# Config Service nohup java -jar apollo-configservice-2.1.0.jar > /dev/null 2>&1 & # Admin Service nohup java -jar apollo-adminservice-2.1.0.jar > /dev/null 2>&1 & # Portal nohup java -jar apollo-portal-2.1.0.jar > /dev/null 2>&1 &

启动后检查日志,看到Started ConfigServiceApplication之类的字样就说明成功了。然后浏览器访问http://你的IP:8070,默认账号apollo,密码admin。

注意:第一次登录后立刻改掉默认密码,这是基本的安全习惯。我见过太多测试环境的 Apollo 用默认密码裸奔,配置里的数据库密码、第三方密钥全暴露。

3.4 单环境下的客户端接入验证

服务端跑起来只是第一步,客户端能不能正常拉配置才是关键。建一个 Spring Boot 项目,引入依赖:

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

然后在application.properties里配置:

app.id=my-test-app apollo.meta=http://你的IP:8080

app.id是应用在 Apollo 里的唯一标识,需要在 Portal 里先创建好项目。apollo.meta指向 Config Service 的地址。

启动客户端,如果日志里出现Load config from Apollo并且能读到你在 Portal 里配的值,那就说明单环境链路完全通了。

4. 多环境搭建:拆分、隔离与联调

4.1 多环境部署拓扑设计

单环境跑通之后,多环境的核心思路就是复制 Config Service + Admin Service + 数据库,Portal 保持唯一。

假设我们要搭 Dev、Test、Prod 三个环境,拓扑是这样的:

环境Config ServiceAdmin Service数据库
Dev10.0.0.1:808010.0.0.1:8090ApolloConfigDB_Dev
Test10.0.0.2:808010.0.0.2:8090ApolloConfigDB_Test
Prod10.0.0.3:808010.0.0.3:8090ApolloConfigDB_Prod
Portal10.0.0.4:8070-ApolloPortalDB

Portal 只有一套,它通过配置知道要去管理哪些环境。每个环境的 Config Service 和 Admin Service 部署在各自独立的机器上,数据库也完全独立。

这种拓扑的好处是故障隔离:Prod 的 Config Service 挂了,不影响 Dev 和 Test。同时安全隔离也到位:Dev 的机器访问不到 Prod 的数据库。

4.2 Portal 端多环境配置的关键参数

Portal 要管理多个环境,需要在ApolloPortalDB.ServerConfig表里改几个配置。这张表是 Portal 的全局配置表,改完需要重启 Portal 生效。

第一个是apollo.portal.envs,把值改成dev,test,prod,逗号分隔,不要有空格。

第二个是各环境的 Meta Server 地址,配置项格式是apollo.portal.meta.servers,值是一个 JSON:

{ "dev": "http://10.0.0.1:8080", "test": "http://10.0.0.2:8080", "prod": "http://10.0.0.3:8080" }

这个配置告诉 Portal,当用户切换到某个环境时,应该去哪个 Config Service 读取数据。

第三个是apollo.portal.envs对应的环境名称显示,可以通过apollo.portal.envs配合前端配置来改,但一般用默认的 dev/test/prod 就够了。

改完这三个配置,重启 Portal,登录后就能在右上角看到环境切换的下拉框了。

4.3 各环境 Config Service 的差异化配置

每个环境的 Config Service 除了数据库连接不同,还有几个配置需要根据环境调整。

apollo.cluster:默认是default,如果你的环境内有多个集群,这里要改。单集群的话不用动。

apollo.eureka.server.enabled:Apollo 2.x 之后默认不依赖 Eureka 了,这个配置一般不用管。但如果你用的是老版本,可能需要关注。

server.port:前面说过,同机部署要改端口,不同机部署可以都用 8080。

apollo.config-service.url:这个配置在 Admin Service 里,用于 Admin Service 调用 Config Service 的接口。如果两个服务不在同一台机器,需要显式指定。

我踩过的一个坑是:Admin Service 和 Config Service 部署在不同机器,但apollo.config-service.url没配,导致 Admin Service 启动时找不到 Config Service,Portal 里发布配置一直转圈。后来在 Admin Service 的配置文件里加上:

apollo.config-service.url=http://10.0.0.1:8080

问题才解决。这个配置在官方文档里提得不多,但跨机部署时是必须的。

4.4 客户端如何指定环境:三种方式对比

客户端接入多环境时,需要告诉 Apollo Client 去哪个环境拉配置。有三种方式:

方式一:通过apollo.meta直接指定。这是最直接的方式,在application.properties里写死对应环境的 Config Service 地址。缺点是不同环境要打不同的包,不推荐。

方式二:通过 JVM 参数指定。启动时加-Dapollo.meta=http://10.0.0.1:8080,这样同一个包可以在不同环境用不同参数启动。比方式一灵活,但参数长了容易写错。

方式三:通过app.properties文件指定。在META-INF/app.properties里写apollo.meta,但这样也是打死的。

方式四(推荐):通过环境变量或启动脚本动态指定。比如在容器化部署时,通过环境变量APOLLO_META注入。Apollo Client 会自动读取这个环境变量。

我实际项目里用的是方式四,配合 CI/CD 流水线,不同环境注入不同的APOLLO_META,同一个镜像可以部署到任意环境,非常干净。

注意:apollo.meta的优先级是 JVM 参数 > 环境变量 > 配置文件。如果你发现配置没生效,先检查是不是被更高优先级的配置覆盖了。

5. 实操过程中踩过的坑与排查技巧

5.1 环境列表不显示或显示不全

Portal 登录后右上角环境下拉框是空的,或者只显示部分环境。这个问题我遇到过两次。

第一次是因为apollo.portal.envs配置改完没重启 Portal。这个配置是启动时加载的,改完必须重启。

第二次是因为apollo.portal.meta.servers的 JSON 格式写错了,多了一个逗号或者少了引号。Portal 启动时如果解析这个 JSON 失败,会静默忽略,导致环境列表为空。排查方法是看 Portal 的日志,搜索meta.servers关键字,如果有解析异常会有堆栈。

5.2 配置发布后客户端不生效

这是最常见的问题。配置在 Portal 里改了、发布了,但客户端还是读的旧值。

排查思路分三步:

第一步,确认客户端连的是哪个环境。看客户端启动日志里的apollo.meta地址,是不是你改配置的那个环境。我见过有人在 Dev 环境改配置,但客户端连的是 Test 环境,当然不生效。

第二步,确认客户端的app.id和 Portal 里的项目对应。如果app.id写错了,客户端会拉到一个空配置或者报错。

第三步,确认客户端的监听是否正常。Apollo Client 通过长轮询(Long Polling)接收配置变更通知。如果客户端和服务端之间的网络有防火墙拦截,长轮询会失败,配置就不会实时推送。可以看客户端日志里有没有Long polling failed之类的错误。

如果长轮询确实不通,Apollo 有兜底机制:客户端会定时全量拉取配置,默认间隔是 5 分钟。所以即使推送失败,最多 5 分钟后也会生效。但这个延迟对很多场景来说太长了,还是要解决网络问题。

5.3 数据库连接池耗尽

Apollo 服务端默认的数据库连接池是 HikariCP,最大连接数默认 10。在配置项多、客户端多的情况下,这个数可能不够。

我遇到过一次 Prod 环境 Portal 响应特别慢,查日志发现大量Connection is not available错误。后来把ApolloConfigDB和ApolloPortalDB的最大连接数都调到 50,问题解决。

调整方式是在各服务的application-github.properties里加:

spring.datasource.hikari.maximum-pool-size=50

具体调多大,要看你的客户端数量和配置读取频率。一个经验值是:每 100 个客户端实例配 10 个数据库连接。当然这只是一个粗略估算,实际要结合监控来调。

5.4 常见问题速查表

问题现象可能原因排查方法
Portal 环境下拉框为空apollo.portal.envs未配置或未重启检查 ServerConfig 表并重启 Portal
客户端拉不到配置app.id错误或apollo.meta指向错误环境看客户端启动日志
配置发布后不生效长轮询失败或客户端未监听检查网络和客户端日志
Portal 响应慢数据库连接池耗尽查日志中的连接错误,调大连接池
Admin Service 启动失败apollo.config-service.url未配置检查 Admin Service 配置文件
中文配置乱码数据库字符集不是 utf8mb4检查库和表的字符集

6. 一些部署之外的实战心得

6.1 权限管理别偷懒

Apollo Portal 支持细粒度的权限管理,可以按项目、按环境分配权限。我见过不少团队为了省事,所有人都用apollo管理员账号,结果有人误删了生产配置,追责都追不到。

建议的做法是:每个开发只给 Dev 和 Test 环境的修改权限,Prod 环境的修改权限只给少数几个人。Apollo 的权限模型是基于角色的,可以在 Portal 的"管理员工具"里配置。

另外,Apollo 支持配置的发布审核,Prod 环境可以开启"发布需要审核",这样即使有权限的人误操作,也需要第二个人确认才能生效。这个功能在关键业务上非常值得开启。

6.2 配置备份与迁移

Apollo 的配置都存在数据库里,所以备份数据库就是备份配置。我一般会写一个定时任务,每天凌晨把ApolloConfigDB_Prod全量导出,保留最近 30 天。

迁移方面,Apollo 提供了 Open API,可以通过接口导出某个项目的所有配置,再导入到另一个环境。跨环境迁移配置时,我一般用这个接口写脚本,比手动复制粘贴靠谱得多。

需要注意的是,不同环境的数据库是独立的,不能直接跨库复制表数据,因为表结构里有关联关系,直接复制容易出问题。用 Open API 是最稳妥的方式。

6.3 监控告警要跟上

Apollo 本身提供了一些监控指标,比如配置读取次数、推送成功率等。但这些指标默认不暴露,需要配置apollo.monitor相关的参数。

我一般会在 Config Service 前面挂一个负载均衡,然后在负载均衡层面监控 8080 端口的健康检查。同时用 Prometheus 抓取 Apollo 的 JMX 指标,重点监控:

  • 配置读取 QPS
  • 长轮询连接数
  • 数据库连接池使用率
  • JVM 内存和 GC

告警阈值方面,长轮询连接数突降往往意味着客户端大面积掉线,这个告警优先级最高。数据库连接池使用率超过 80% 就要关注,超过 90% 必须处理。

6.4 版本升级的注意事项

Apollo 的版本升级不算频繁,但每次升级都要谨慎。我经历过一次从 1.9 升级到 2.1,踩了一个坑:2.x 版本默认不再依赖 Eureka,但数据库表结构有变化。

升级前必须做两件事:第一,备份所有环境的数据库;第二,仔细阅读官方 Release Notes 里的数据库变更说明。Apollo 的升级脚本在scripts/sql目录下,按版本号顺序执行。

另外,升级顺序也有讲究:先升级 Config Service 和 Admin Service,最后升级 Portal。因为 Portal 依赖 Admin Service 的接口,如果先升级 Portal,可能会出现接口不兼容的情况。

升级完成后,不要急着走,观察至少 30 分钟,重点看客户端的长轮询是否正常、配置发布是否成功、Portal 页面是否正常。确认无误后再清理旧版本的包。

6.5 容器化部署的取舍

现在很多团队用 Kubernetes 部署 Apollo。我的看法是:Config Service 和 Admin Service 适合容器化,Portal 和数据库不适合。

Config Service 和 Admin Service 是无状态的,容器化后可以方便地扩缩容。但要注意,容器化后 Meta Server 的地址会变,客户端需要通过 Service 名称来访问,而不是 Pod IP。

Portal 是有状态的(它连着自己的数据库),容器化后如果 Pod 重启,会话会丢失,用户体验不好。而且 Portal 的访问量通常不大,没必要容器化。

数据库就更不用说了,生产环境的数据库一般都用独立的物理机或云数据库,不会跑在容器里。

如果一定要容器化 Portal,建议把会话存储改成 Redis,这样 Pod 重启后会话不丢。但这又引入了新的依赖,增加了复杂度,需要权衡。

7. 写在最后的一点个人体会

Apollo 这套东西,单环境搭建可能半天就能搞定,但多环境搭建和后续的运维才是真正考验人的地方。我最大的体会是:环境隔离这件事,前期多花一天时间做干净,后期能省下一周的排查时间。数据库分库、Meta Server 地址分离、权限隔离,这三件事做到位,后面基本不会出大问题。

另外,不要迷信一键脚本。Quick Start 确实方便,但它隐藏了太多细节,一旦出问题你连从哪查都不知道。我建议每个负责 Apollo 部署的同学,至少手动搭一次单环境,把每个配置文件、每个数据库表的作用都搞清楚,这样后面遇到问题才能快速定位。

最后分享一个小技巧:Apollo 的配置项支持占位符,比如${db.password},可以引用系统环境变量或者其他配置项。这个功能在多环境部署时特别有用,可以把不同环境不同的值通过环境变量注入,配置文件本身保持一份,减少出错概率。具体用法是在配置值里写${环境变量名:默认值},Apollo 会自动解析。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进