Apollo配置中心这话题,说难不难,说简单也真不简单。我前后帮三个团队落地过Apollo,覆盖了从单机demo到双机房高可用部署,中间踩过的坑加起来够写一本书了。今天这篇就是把我这几年折腾下来的完整套路整理出来,从架构原理到数据库初始化、服务部署、客户端接入、动态刷新、灰度发布,再到生产环境的运维注意事项,照着走,基本能让你从零搭出一套能上生产的Apollo配置中心。
1. 为什么需要Apollo:先聊清楚配置之痛
1.1 从几百个配置文件说起
先说个场景。单体应用时代,配置文件虽然多,但改起来还凑合——改完重启,完事。到了微服务架构下,服务拆成十几个甚至几十个,每个服务又分dev、test、prod好几套环境,配置文件的数量直接爆炸。更崩溃的是,很多配置是运行时才需要调整的,比如功能开关、限流阈值、白名单列表,每次都要走一遍“改配置-打包-发布-重启”的流程,一次发布光等重启就要好几分钟,如果碰上紧急故障要临时调参数,这个过程简直是要命。
我当年接手的一个项目就是这种状况,每次改个开关,运维同学都得登录好几台机器,逐个改文件再逐个重启,动不动就需要跟业务方协调变更窗口。当时团队里有人提议自己写个配置管理工具,幸好及时打住——配置中心这件事,成熟的开源方案已经做得足够好了,自己造轮子大概率是个坑。
1.2 Apollo的核心设计思路
Apollo(阿波罗)是携程开源的配置管理中心,它的设计思路说白了就四个字:集中+实时。集中是指所有环境、所有服务的配置都收口到一套中心化的管理系统里;实时指的是配置变更后,客户端能在秒级拿到最新值,不需要重启应用。
有四个设计细节我觉得特别值得说。第一,Apollo把配置拆成了“配置发布”和“应用读取”两条链路,配置修改后需要显式发布,发布完客户端才能感知,这避免了配置修改了一半就被读走的尴尬。第二,客户端通过长轮询感知配置变化,不是每次请求都去查数据库,性能开销很小。第三,配置有版本管理,每次发布都留痕,可以对比、回滚。第四,支持灰度发布,可以先让一小部分实例生效,验证没问题再全量推,这对生产环境来说太重要了。
1.3 Apollo和Nacos怎么选
被问到最多的问题就是Apollo和Nacos选哪个。简单说说我的看法。Apollo胜在配置管理功能深度:灰度发布、权限管理、配置导出导入、操作审计这些能力都是内置的,比较适合中大型团队,尤其是对变更管控有要求的场景。Nacos的优势在于它同时具备注册中心和配置中心的能力,如果团队已经在用Nacos做服务发现,顺手把配置也放进去,可以少维护一套系统,技术栈更统一。
我个人的建议是:如果你只是需要一个轻量的配置管理,且不想多维护一套系统,Nacos够用;如果配置变更频繁、对权限和审计有硬性要求,或者公司里多个团队共用一套配置中心,Apollo的成熟度更高。当然本文主要讲Apollo,Nacos就不展开对比了。
2. 架构拆解:部署之前先搞明白这几件事
2.1 四个核心模块各管什么
Apollo的整体架构里有四个角色,理解它们各自的职责,后面部署就不会蒙圈。
ConfigService是整个架构的核心服务,负责配置的读取和推送。客户端连的就是它,它还会跟数据库里的配置变更做实时同步,一旦有配置发布,立刻通知客户端。AdminService负责配置的管理操作,比如创建、修改、发布配置,Portal页面上的操作最终都是打到这个服务上的。Portal就是大家平时操作的可视化Web界面,它可以管理多个环境,每个环境可以部署独立的ConfigService和AdminService,通过Portal统一切换。底层存储用的是MySQL,Apollo会把配置数据、发布历史、操作日志都存在数据库里。
这里要特别注意一个设计:Portal只连AdminService,客户端只连ConfigService,两类流量是隔离的。管理操作再频繁,也不会直接影响客户端获取配置的性能。
2.2 高可用部署应该怎么规划
Apollo支持多环境管理,每个环境可以理解为一套独立的命名空间。生产环境建议至少两个ConfigService实例,用SLB(负载均衡)在前面挂着,配置一个VIP域名给客户端连接。AdminService同样至少两个实例,Portal也建议至少两个避免单点。数据库用MySQL主从同步,主库挂了从库顶上。
资源规划方面,我以一个小型团队(几十个服务、几千个实例)为例给出参考配置。ConfigService和AdminService各至少2核4G内存,Portal可以2核2G起步,数据库用4核8G基本够用,磁盘按增量情况扩容。如果是单机demo,2核4G的机器把三个服务和MySQL都跑起来也没问题,但千万别拿这个配置去跑生产,这是很多新手栽跟头的地方——开始图省事用单机部署,后面要平滑扩容就很狼狈。
2.3 环境隔离策略怎么定
环境隔离这件事,强烈建议从第一天就规划好。常见的分法是dev、fat(功能测试)、uat(用户验收)、prod四套。开发环境随意,配置出错影响面小;UAT环境尽量跟生产保持一致,包括开关状态和参数值,这样测试才有意义;生产环境的权限必须收紧,不是谁都能改的。
环境隔离这件事上我踩过一个坑。最初我把所有环境搭在一套Apollo上,只是用不同的Cluster(集群)区分,结果开发同学在dev环境发布了配置,由于用户权限配置不当,直接影响了生产。后来老老实实拆成独立环境,各环境用自己的数据库和服务,权限彻底隔离,虽然多耗了几台机器,但心里踏实。记住一个原则:生产环境的配置变更,权限要最小化。
3. 从零搭建:数据库初始化与部署实操
3.1 初始化数据库的两种方式
Apollo建库有官方提供的SQL脚本。1.x版本脚本在scripts/sql目录下,包含apolloconfigdb.sql和apolloportaldb.sql。2.0版本改动较大,拆成了多份SQL脚本,建议直接从GitHub拉取对应tag的源码去找到配套脚本,别在网盘上随便找别人打包的,脚本版本和服务版本不匹配会导致各种诡异问题,这个我碰到过两三次。
执行建库时权限账号要设置好。ConfigDB的账号建议只授权给它对应的库,不要给全局权限。数据库字符集使用utf8mb4。
如果要接入已有Apollo环境,或者需要做数据迁移,就不要用初始化脚本了,而是用Apollo自带的数据导入导出功能,在Portal界面操作即可,它会保留配置的命名空间、发布历史这些元数据。
3.2 Docker Compose快速部署
我平时最快上手的方式是用Docker Compose跑一整套,适合测试环境和本地开发。生产环境也可以用,但需要调优的地方不少。下面这个编排是我实践过可用的。
version: "3" services: db: image: mysql:8.0 container_name: apollo-db environment: MYSQL_ROOT_PASSWORD: root-pass MYSQL_DATABASE: apollo volumes: - ./mysql-data:/var/lib/mysql - ./initdb:/docker-entrypoint-initdb.d ports: - "3306:3306" config-service: image: apolloconfig/apollo-configservice container_name: apollo-config environment: SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/ApolloConfigDB?useSSL=false SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: root-pass ports: - "8080:8080" depends_on: - db admin-service: image: apolloconfig/apollo-adminservice container_name: apollo-admin environment: SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/ApolloConfigDB?useSSL=false SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: root-pass ports: - "8090:8090" depends_on: - db portal: image: apolloconfig/apollo-portal container_name: apollo-portal environment: SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/ApolloPortalDB?useSSL=false SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: root-pass APOLLO_PORTAL_ENVS: dev APOLLO_PORTAL_ENV_DEV_META: http://config-service:8080 ports: - "8070:8070" depends_on: - db - config-service用这种方式要注意环境变量里元数据地址的写法。APOLLO_PORTAL_ENVS定义了Portal管理哪些环境,APOLLO_PORTAL_ENV_DEV_META是dev环境的ConfigService地址,如果还有fat环境,就要加上APOLLO_PORTAL_ENV_FAT_META和APOLLO_PORTAL_ENVS里面要加fat。很多新手少配置了其中一个,导致Portal里切到某环境就报“无法获取环境信息”的错误,其实就是Env和Meta没对应上。
3.3 配置环境变量关键参数解析
在部署脚本里,有几个参数是必须搞清楚的。
SPRING_DATASOURCE_URL是数据库连接地址,ConfigService和AdminService连的是ConfigDB,Portal连的是PortalDB,这两个数据库千万不能连反了,我见过有人图省事让所有服务都连同一个配置库,结果Portal报一堆NPE,排查了老半天。
APOLLO_PORTAL_ENVS是在Portal里声明需要管理哪些环境,多个环境用逗号分隔,格式如dev,fat,uat,prod。APOLLO_PORTAL_ENV_${ENV}_META是每个环境对应的ConfigService地址列表,注意是ConfigService不是AdminService,这个地址写给客户端和Portal做环境信息展示用的,写错会导致Portal页面无法加载该环境下的配置。
如果是物理机或虚拟机部署,官方提供了专门的scripts目录,可以分别启动三个服务。流程一般是:建库执行SQL、修改启动脚本里的数据库连接配置、启动ConfigService和AdminService、启动Portal。顺序上建议先启动Config和Admin再启动Portal,让Portal能正确拉取环境信息。
3.4 启动后的健康检查
服务启动完成后,别急着连客户端,先把三个服务都检查一遍。
检查方式很简单:ConfigService和AdminService都有健康检查接口,直接浏览器访问,返回类似{"status":"UP"}或者字符串就说明存活。Portal可以访问登录页确认Web界面正常打开。
我习惯用curl检查完了再确认端口。比如ConfigService默认8080,AdminService默认8090,Portal默认8070。如果端口有冲突,可以在启动脚本或者容器启动参数里通过server.port覆盖,但注意Apollo内部有些逻辑依赖端口判断eureka地址,改端口的同时需要在启动参数里把eureka的注册地址也一起改了,否则实例注册信息对不上。这部分最容易出问题,建议尽量使用官方默认端口,真需要改的话先把文档里关于端口配置的章节完整读一遍再动手。
4. 客户端接入与动态刷新实战
4.1 Spring Boot项目引入Apollo依赖
服务端部署好之后,最重要的事情就是让业务系统连上它。这里以Spring Boot项目为例说明。
在pom.xml里加依赖。Apollo官方提供了spring-boot的starter,用起来最省事。
<dependency> <groupId>com.ctrip.framework.apollo</groupId> <artifactId>apollo-client</artifactId> <version>2.1.0</version> </dependency>如果是Spring Boot项目,建议再加上apollo-client自带的spring boot支持(2.x版本已集成),只需要在启动类或者配置里启用。接着在项目配置文件里指定apollo的meta地址:
app.id=your-app-id apollo.meta=http://config-service-addr:8080 apollo.bootstrap.enabled=true apollo.bootstrap.namespaces=applicationapp.id是应用在Apollo里的唯一标识,Portal里创建项目时要保持一致。apollo.meta指向ConfigService的地址,注意是客户端能访问到的地址——如果客户端在容器里,就用容器网络的地址,如果是跨机房,就要用到SLB或外网映射,我见过把localhost写在配置里导致客户端连不上Apollo的,排查起来毫无头绪,其实就这么个低级错误。
3.x版本的Apollo客户端在使用上有一些新变化,比如支持了配置加密,加载方式也有调整,但整体接入思路不变。建议根据自己项目的Spring Boot版本选择合适的客户端版本,比如Spring Boot 2.x用apollo-client 1.9.x或2.x即可,Spring Boot 3.x建议用最新的客户端版本。
4.2 @Value和@ApolloConfig的用法
配置加载进Spring环境之后,最常用的就是@Value注解。
@Service public class FeatureFlagService { @Value("${new.feature.enabled:false}") private boolean newFeatureEnabled; @Value("${order.timeout.seconds:30}") private int timeoutSeconds; }这样写的配置,在Apollo发布变更后,Spring容器里的值会自动刷新。注意,@Value注解能动态刷新,是Apollo客户端在Spring启动时注册了一个监听器,感知到配置变化后通过Spring的Environment更新机制同步进去的。如果变量不是通过@Value注入的,而是存在static变量里,那抱歉,它是不会自动更新的。
另外还有一个@ApolloConfig注解,可以直接注入Config对象,手动获取配置项数值,适合在非Spring托管的类里使用,或者是有自定义缓存逻辑的场景。
@Service public class ApolloAwareService { @ApolloConfig private Config config; public String getFeatureSetting(String key) { return config.getProperty(key, "default"); } }这种方式拿到的配置不会自动同步到字段,需要自己调用config.getProperty去取值。对大多数业务场景,我建议优先用@Value,简单直观,出问题也容易排查。
4.3 监听配置变更做自定义处理
有些场景只刷新字段不够,比如数据库连接池的调整、线程池参数的调整,这些资源在运行时已经创建了,单纯改字段值没有意义,你得在配置变更时手动去重建或者调整。
这时候要用Apollo的ChangeListener机制。
@Slf4j @Component public class DataSourceConfigListener { @ApolloConfigChangeListener("application") public void onChange(ConfigChangeEvent changeEvent) { for (String changedKey : changeEvent.changedKeys()) { ConfigChange change = changeEvent.getChange(changedKey); log.info("配置变更: key={}, oldValue={}, newValue={}, changeType={}", changedKey, change.getOldValue(), change.getNewValue(), change.getChangeType()); } // 在这里根据变更的key做线程池、连接池、缓存参数的动态调整 rebuildThreadPool(); } }监听器里拿到的ConfigChangeEvent包含所有变更的key和值。千万注意一点:监听器里要做的事情尽量轻量,别在里面做耗时的IO操作,比如调远程接口、写文件、重建重量级对象,因为这些操作会影响配置变更的响应链路。我见过有人在监听器里同步重建数据库连接池,导致网络抖动时配置推送延迟,客户端好几分钟才感知到变更。
4.4 动态刷新生效原理简述
明白刷新原理,对你排查问题很有帮助。Apollo客户端启动时会向ConfigService发起长轮询请求,如果服务端有配置发布,会立刻把变更推给客户端。客户端拿到变更后,会更新本地缓存的Config,并触发Spring的Environment刷新。整个过程不是基于Spring Cloud Bus那种消息广播模式,而是客户端主动长轮询,所以依赖的是客户端与ConfigService之间的网络连通性。
有一点必须强调:客户端本地会缓存一份配置在/opt/data/{appId}/config-cache目录(默认路径),即使服务端暂时连不上,应用重启后也会用缓存配置启动,不会因为配置中心故障就直接挂掉。这个设计非常实用,但也会带来一个问题:如果缓存文件里的配置是旧的,服务端已发布新值但客户端连不上,应用就会用旧配置运行。所以网络连通性监控必须要做。
4.5 常见生效问题排查清单
项目接入Apollo后,最常被问到的就是“为什么我改了配置,应用里没有生效”。这里列几个高频原因。
最常见的是忘了指定namespace。Apollo里配置是分namespace管理的,默认是application,如果配置加在了别的namespace里,而客户端没有在apollo.bootstrap.namespaces里声明加载,那自然看不到。
其次是@Value注解的变量被static修饰了。Apollo的动态刷新机制更新的是Spring管理的bean属性,static字段不走这个过程,得改成实例字段,或者用监听器自己处理。
还有一种常见情况是项目里同时引用了Nacos和Apollo的配置,两边都加载了同一个key,结果被后加载的一方覆盖了。这种配置冲突的排查比较费劲,建议在项目初期就确定好配置中心的唯一来源,别混用。
5. 发布管理进阶:灰度、权限与审计
5.1 配置中心的权限模型怎么配
Apollo的权限模型很实用,它分成三层:项目维度、环境维度和操作维度。默认情况下的项目拥有者(Owner)可以管理项目、分配权限,普通成员只能查看。环境维度可以单独授权,比如开发可以改dev,但不能碰prod。
实际运营中,我建议给生产环境配置“发布审批人”角色,也就是配置修改后需要指定的人确认才能发布。Apollo原生支持发布审批吗?1.x版本没有内置完整的审批流,2.x版本强化了权限管理能力,配合组织架构可以做更细粒度的控制。如果没有现成的审批功能,很多团队的做法是:生产环境的修改权限只授予运维和小组负责人,普通开发只有dev环境的修改权限,这样风险已经控制住大半。
5.2 灰度发布的操作步骤
Apollo的灰度发布是在配置发布时实现的。具体操作是:进入配置编辑页面,修改配置项之后,不直接“发布”,而是选择“灰度发布”,这时候系统会提示你选择需要灰度的机器或集群,匹配条件可以按IP或者集群标签。
实际操作时可以用标签规则匹配一部分实例。比如先在Apollo里建一个Cluster(集群),叫gray-cluster,然后灰度发布只发到这些实例。验证灰度节点日志正常后,再执行“全量发布”,把配置推给剩余所有实例。如果需要回退,可以用“回滚”功能,恢复到指定历史版本的配置。
有一点务必记住:灰度发布是“发布操作”层面的灰度,不是让你在配置内容里手动写两套值。Apollo的界面会展示当前灰度值和全量值,很容易区分,操作前看清楚再点。
5.3 发布回滚与审计日志
配置发布出问题之后的回滚操作要果断。Apollo里每个namespace都有发布历史,可以查看每次发布的内容diff,一键回滚到历史版本。回滚本质上也是一次新的发布,会在审计记录里再留一条记录,这在排查问题时非常有用。
审计日志方面我的建议是每天同步一份数据库的ReleaseHistory表和Audit表到分析系统,做变更合规检查。如果公司有安全合规要求,要对生产配置的修改行为留痕至少半年。Apollo自带的操作日志在PortalDB中,字段很清晰,包含操作人、操作时间、操作类型、操作对象。
6. 生产环境运维与踩坑实录
6.1 配置文件生命周期:环境、集群、命名空间
很多团队初期只用默认的application命名空间,所有配置一股脑塞进去,后面会越来越难管。Apollo里环境(Environment)、集群(Cluster)、命名空间(Namespace)三个概念交织在一起,如果规划不好,配置会混乱成一锅粥。
我的推荐做法是:公共配置放在common命名空间,比如Redis地址、MQ topic、基础超时时间,所有项目共享一份;各项目私有的配置放在自己项目的application命名空间;如果同一个项目在不同机房或集群下参数差异大,再考虑按Cluster拆分。命名空间加载时,Apollo从大到小有一整套优先级规则,默认是配置粒度越具体优先级越高,建议发布前先按官方文档确认规则,别到时候疑惑为什么某些环境的配置没生效。
6.2 监控告警:该盯哪些指标
Apollo服务端本身基于Spring Cloud Eureka做服务发现,因此需要关注的监控指标主要有几块。
第一,ConfigService的推送性能。可以看它的Http接口的响应时间,如果平均RT明显升高,那可能是长期连接的负载太高,需要考虑扩容。
第二,AdminService的数据库连接池情况。Apollo的写操作都会经过它,如果数据库连接池被打满,会导致Portal页面无法发布配置,看日志会看到连接获取超时。此时优先检查数据库连接数和慢查询。
第三,客户端连接数。ConfigService在线客户端的数量可以通过它的metrics端点查看,如果达到单机瓶颈,就水平扩容并检查SLB负载均衡策略。
第四,数据库的慢查询和磁盘空间。Apollo的Release表会随发布次数不断增长,长期不清理会拖慢查询,需要定期做归档。
6.3 数据库备份与恢复策略
Apollo的数据库是整个系统最核心的资产,配置数据丢了比服务宕机还可怕,因为服务宕机可以重启拉起来,配置数据丢了是要人工重建的。备份策略一定要认真做。
ConfigDB和PortalDB都要做全量备份加binlog归档。单机环境可以每天一次mysqldump全备,生产环境请使用云数据库的自动备份功能或自建MySQL主从复制加每天全备。恢复演练也要定期做,别等到真出事才发现备份文件是坏的。我有一年帮同事处理过一次配置库被误操作的恢复,因为备份完整,最后只少了五分钟的变更记录,基本无损。试想如果没有备份,几百个服务的几千个配置项全部要人工恢复,那个工作量是不可想象的。
6.4 常见故障排查速查表
| 现象 | 排查方向 | 常见解法 |
|---|---|---|
| Portal页面打不开 | Portal服务和PortalDB | 检查Portal健康检查接口、数据库连接 |
| 能登录Portal但看不到某环境配置 | 环境Meta配置 | 检查APOLLO_PORTAL_ENV_XX_META是否配置且指向正确 |
| 客户端启动报“获取配置失败” | 网络、app.id、meta地址 | 确认客户端能访问ConfigService,app.id与Portal项目一致 |
| 配置修改发布了但客户端没生效 | namespace、缓存、监听器 | 检查namespace是否加载、是否有监听器报错、客户端缓存路径 |
| @Value注入的值为null | 扫描包路径、类未注入Spring | 确认类被Spring管理且包路径被扫描 |
| 灰度发布后部分实例拿到新值部分没拿到 | 集群规则、实例标签 | 确认灰度规则匹配正确、发布是否已全量 |
| 配置中心访问慢 | 数据库慢查询、连接池打满 | 检查ConfigDB慢日志、扩容连接池或实例 |
| 客户端缓存文件损坏 | 磁盘、缓存路径权限 | 删除缓存目录后重启应用 |
6.5 版本升级与平滑迁移
Apollo社区迭代较快,老项目在升级时需要特别小心。升级ConfigService和AdminService前,先看官方发布的升级文档,注意是否包含数据库表结构变更。通常在PortalDB和ConfigDB里都有schema_version表记录版本,升级过程有专门的upgrade脚本目录。
如果是从1.x升到2.x,要注意服务端和客户端版本需要兼容,尤其服务端降级客户端的兼容性——新服务端一般能兼容老客户端,但新客户端连老服务端可能会报错。我经历过的升级方式比较保守:先升级ConfigService和AdminService,确认连接客户端无误后再批量升级客户端,最后升级Portal。
关于配置迁移,Apollo提供了导出导入功能,也可以直接对数据库表做操作,但强烈不建议在生产环境直接在数据库层面改配置数据——绕过发布流程的修改没有审计记录,而且容易写坏数据格式,出了问题极难定位。一切变更都通过Portal来做,才是正确姿势。
7. 实战经验补充:那些文档里不写但你该知道的事
7.1 客户端缓存目录的姿势
前面提过客户端会在本地缓存配置,这个目录默认在/opt/data/{appId}/config-cache。有几个细节你可能不知道:这个目录的权限要保证应用启动用户可写,否则客户端会在启动时警告回退到临时目录;缓存里的JSON文件可以直接打开看,排查问题时非常方便。
我曾经遇到一个诡异现象:配置中心发布的值明明是对的,但客户端拿到的还是老值。后来看缓存文件才发现,原来开发同学手动修改过缓存目录里的文件,客户端启动时以本地缓存优先级更高,覆盖了远程配置。所以这个目录要保持干净,别手动动它。
7.2 多环境客户端配置管理
如果一个应用同时要接入dev和prod两套Apollo环境,常见的做法是在不同构建环境里使用不同的apollo.meta地址。在Spring Boot项目里,可以在application-{profile}.yml里配置不同profile对应不同meta。更优雅的方案是用Apollo的Meta Server机制,在配置文件里统一配一个域名,由它内部根据环境自动路由到对应ConfigService。生产环境部署时使用域名而不是裸IP是个好习惯,这样后端扩缩容时客户端不需要改配置。
7.3 配置项规范命名
配置项的命名规范看似小事,长期来看影响很大。我的建议是:统一使用小写字母和点号分隔,按模块前缀组织,比如order.timeout.seconds、payment.retry.maxAttempts。避免使用无意义的缩写,比如ot、payRtyMax这种只适合写代码的人自己看的命名。同时不要在一个配置项里塞太多信息,比如把多个IP拼在一个字符串里用逗号分隔,这种做法要拆分,否则动态调整时无法精确控制某一个值。
参数命名和值的可读性,直接关系到后来接手的人能不能快速理解这个配置是干什么的。一个五分钟就能掌握的规范,可以为你省下未来不知道多少个排查的小时。
配置中心这条路上,Apollo已经是相当成熟的选择了。跟着这篇文章的思路一步步来,从部署到接入,再到灰度发布和运维监控,你手里就有一套完整可用的配置管理方案了。如果你在实操中遇到上面没覆盖到的问题,欢迎分享出来一起交流,配置管理的坑都是大家一个个踩平了的。