Apollo配置中心从零搭建到高可用运维实战
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的成熟度更高。当然本文主要讲ApolloNacos就不展开对比了。2. 架构拆解部署之前先搞明白这几件事2.1 四个核心模块各管什么Apollo的整体架构里有四个角色理解它们各自的职责后面部署就不会蒙圈。ConfigService是整个架构的核心服务负责配置的读取和推送。客户端连的就是它它还会跟数据库里的配置变更做实时同步一旦有配置发布立刻通知客户端。AdminService负责配置的管理操作比如创建、修改、发布配置Portal页面上的操作最终都是打到这个服务上的。Portal就是大家平时操作的可视化Web界面它可以管理多个环境每个环境可以部署独立的ConfigService和AdminService通过Portal统一切换。底层存储用的是MySQLApollo会把配置数据、发布历史、操作日志都存在数据库里。这里要特别注意一个设计Portal只连AdminService客户端只连ConfigService两类流量是隔离的。管理操作再频繁也不会直接影响客户端获取配置的性能。2.2 高可用部署应该怎么规划Apollo支持多环境管理每个环境可以理解为一套独立的命名空间。生产环境建议至少两个ConfigService实例用SLB负载均衡在前面挂着配置一个VIP域名给客户端连接。AdminService同样至少两个实例Portal也建议至少两个避免单点。数据库用MySQL主从同步主库挂了从库顶上。资源规划方面我以一个小型团队几十个服务、几千个实例为例给出参考配置。ConfigService和AdminService各至少2核4G内存Portal可以2核2G起步数据库用4核8G基本够用磁盘按增量情况扩容。如果是单机demo2核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?useSSLfalse 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?useSSLfalse 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?useSSLfalse 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连的是ConfigDBPortal连的是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默认8080AdminService默认8090Portal默认8070。如果端口有冲突可以在启动脚本或者容器启动参数里通过server.port覆盖但注意Apollo内部有些逻辑依赖端口判断eureka地址改端口的同时需要在启动参数里把eureka的注册地址也一起改了否则实例注册信息对不上。这部分最容易出问题建议尽量使用官方默认端口真需要改的话先把文档里关于端口配置的章节完整读一遍再动手。4. 客户端接入与动态刷新实战4.1 Spring Boot项目引入Apollo依赖服务端部署好之后最重要的事情就是让业务系统连上它。这里以Spring Boot项目为例说明。在pom.xml里加依赖。Apollo官方提供了spring-boot的starter用起来最省事。dependency groupIdcom.ctrip.framework.apollo/groupId artifactIdapollo-client/artifactId version2.1.0/version /dependency如果是Spring Boot项目建议再加上apollo-client自带的spring boot支持2.x版本已集成只需要在启动类或者配置里启用。接着在项目配置文件里指定apollo的meta地址app.idyour-app-id apollo.metahttp://config-service-addr:8080 apollo.bootstrap.enabledtrue apollo.bootstrap.namespacesapplicationapp.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地址确认客户端能访问ConfigServiceapp.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已经是相当成熟的选择了。跟着这篇文章的思路一步步来从部署到接入再到灰度发布和运维监控你手里就有一套完整可用的配置管理方案了。如果你在实操中遇到上面没覆盖到的问题欢迎分享出来一起交流配置管理的坑都是大家一个个踩平了的。