Apollo在基础架构中的实践经验
- 2019 年 11 月 25 日
- 笔记
本文来自李伟超同学的投稿,如果你有好的文章也欢迎联系我。 微服务配置中心 Apollo 使用指南,以下文档根据 apollo wiki 整理而来,部分最佳实践说明和代码改造基于笔者的工作经验整理而来,如有问题欢迎沟通。
1 配置中心
在拆分为微服务架构前,曾经的单体应用只需要管理一套配置。而拆分为微服务后,每一个系统都有自己的配置,并且都各不相同,而且因为服务治理的需要,有些配置还需要能够动态改变,如业务参数调整或需要熔断限流等功能,配置中心就是解决这个问题的。
1.1 配置的基本概念
- 配置是独立于程序的只读变量
- 同个应用在不同的配置有不同的行为
- 应用不应该改变配置
- 配置伴随应用的整个生命周期
- 初始化参数和运行参数
- 配置可以有多种加载方式
- 配置需要治理
- 权限控制(应用级别、编辑发布隔离等)
- 多环境集群配置管理
- 框架类组件配置管理
1.2 配置中心
- 配置注册与反注册
- 配置治理
- 配置变更订阅
1.3 Spring Environment
Environment 是 Spring 容器中对于应用环境两个关键因素(profile & properties)的一个抽象。
- profile
profile 是一个逻辑的分组,当 bean 向容器中注册的时候,仅当配置激活时生效。
## 配置文件使用 spring.profiles.active=xxx
## 硬编码注解形式使用 @org.springframework.context.annotation.Profile
- properties
Properties 在几乎所有应用程序中都扮演着重要的角色,并且可能来自各种各样的来源:properties 文件、JVM系统属性、系统环境变量、JNDI、Servlet Context 参数、ad-hoc Properties 对象、Map 等等。Environment 与 Properties 的关系是为用户提供一个方便的服务接口,用于配置属性源并从它们中解析属性。
- Spring 中的扩展点
- spring framework 提供了便捷的方式添加自定义数据源策略添加到 Spring Enviroment 中,如 @PropertySource。https://docs.spring.io/spring/docs/5.1.8.RELEASE/spring-framework-reference/core.html#beans-using-propertysource
- spring boot 提供了相关的扩展方式,如 EnviromentPostProcessor 相关的。https://docs.spring.io/spring-boot/docs/2.1.6.RELEASE/reference/html/boot-features-external-config.html
- spring boot 同时也提供在开始之前自定义环境扩展。https://docs.spring.io/spring-boot/docs/2.1.6.RELEASE/reference/html/howto-spring-boot-application.html#howto-customize-the-environment-or-application-context
2 Apollo 简介
2.1 简介
Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。
Apollo 支持4个维度管理 Key-Value 格式的配置:
- application (应用)
这个很好理解,就是实际使用配置的应用,Apollo 客户端在运行时需要知道当前应用是谁,从而可以去获取对应的配置。每个应用都需要有唯一的身份标识,我们认为应用身份是跟着代码走的,所以需要在代码中配置,具体信息请参见 Java 客户端使用指南。
- environment (环境)
配置对应的环境,Apollo 客户端在运行时需要知道当前应用处于哪个环境,从而可以去获取应用的配置。我们认为环境和代码无关,同一份代码部署在不同的环境就应该能够获取到不同环境的配置,所以环境默认是通过读取机器上的配置(server.properties中的env属性)指定的,不过为了开发方便,我们也支持运行时通过 System Property 等指定,具体信息请参见Java客户端使用指南。
- cluster (集群)
一个应用下不同实例的分组,比如典型的可以按照数据中心分,把上海机房的应用实例分为一个集群,把北京机房的应用实例分为另一个集群。对不同的cluster,同一个配置可以有不一样的值,如 zookeeper 地址。集群默认是通过读取机器上的配置(server.properties中的idc属性)指定的,不过也支持运行时通过 System Property 指定,具体信息请参见Java客户端使用指南。
- namespace (命名空间)
一个应用下不同配置的分组,可以简单地把 namespace 类比为文件,不同类型的配置存放在不同的文件中,如数据库配置文件,RPC配置文件,应用自身的配置文件等。应用可以直接读取到公共组件的配置 namespace,如 DAL,RPC 等。应用也可以通过继承公共组件的配置 namespace 来对公共组件的配置做调整,如DAL的初始数据库连接数。
同时,Apollo 基于开源模式开发,开源地址:https://github.com/ctripcorp/apollo
2.2 基础模型
如下即是Apollo的基础模型:
- 用户在配置中心对配置进行修改并发布
- 配置中心通知Apollo客户端有配置更新
- Apollo客户端从配置中心拉取最新的配置、更新本地配置并通知到应用

3 Apollo 架构说明
Apollo 项目本身就使用了 Spring Boot & Spring Cloud 开发。
3.1 服务端

上图简要描述了Apollo的总体设计,我们可以从下往上看:
- Config Service 提供配置的读取、推送等功能,服务对象是Apollo客户端。
- Admin Service 提供配置的修改、发布等功能,服务对象是Apollo Portal(管理界面)。
- Config Service 和 Admin Service 都是多实例、无状态部署,所以需要将自己注册到 Eureka 中并保持心跳
- 在 Eureka 之上我们架了一层 Meta Server 用于封装 Eureka 的服务发现接口 Client 通过域名访问 Meta Server 获取 Config Service 服务列表(IP+Port),而后直接通过 IP+Port 访问服务,同时在 Client 侧会做 load balance、错误重试
- Portal 通过域名访问 Meta Server 获取 Admin Service 服务列表(IP+Port),而后直接通过 IP+Port 访问服务,同时在 Portal 侧会做 load balance、错误重试
- 为了简化部署,我们实际上会把 Config Service、Eureka 和 Meta Server 三个逻辑角色部署在同一个 JVM 进程中。
3.2 客户端

- 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。
- 客户端还会定时从 Apollo 配置中心服务端拉取应用的最新配置。
- 这是一个fallback机制,为了防止推送机制失效导致配置不更新
- 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 – Not Modified
- 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定 System Property: apollo.refreshInterval 来覆盖,单位为分钟。
- 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
- 客户端会把从服务端获取到的配置在本地文件系统缓存一份
- 在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置
- 应用程序从Apollo客户端获取最新的配置、订阅配置更新通知
长连接实现上是使用的异步+轮询实现
4 Apollo 高可用部署
在 Apollo 架构说明中我们提到过 client 和 portal 都是在客户端负载均衡,根据 ip+port 访问服务,所以 config service 和 admin service 是无状态的,可以水平扩展的,portal service 根据使用 slb 绑定多台服务器达到切换,meta server 同理。
场景 |
影响 |
降级 |
原因 |
---|---|---|---|
某台config service下线 |
无影响 |
|
Config service无状态,客户端重连其它config service |
所有config service下线 |
客户端无法读取最新配置,Portal无影响 |
客户端重启时,可以读取本地缓存配置文件 |
|
某台admin service下线 |
无影响 |
|
Admin service无状态,Portal重连其它admin service |
所有admin service下线 |
客户端无影响,portal无法更新配置 |
|
|
某台portal下线 |
无影响 |
|
Portal域名通过slb绑定多台服务器,重试后指向可用的服务器 |
全部portal下线 |
客户端无影响,portal无法更新配置 |
|
|
某个数据中心下线 |
无影响 |
|
多数据中心部署,数据完全同步,Meta Server/Portal域名通过slb自动切换到其它存活的数据中心 |
5 Apollo 使用说明
5.1 使用说明
Apollo使用指南
https://github.com/ctripcorp/apollo/wiki/Apollo%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97
Java客户端使用指南
https://github.com/ctripcorp/apollo/wiki/Java%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97
5.2 最佳实践
在 Spring Boot & Spring Cloud 中使用。
- 每个应用都需要有唯一的身份标识,我们认为应用身份是跟着代码走的,所以需要在代码中配置。关于应用身份标识,应用标识对第三方中间件应该是统一的,扩展支持 apollo 身份标识和 spring.application.name 一致(具体查看 fusion-config-apollo 中代码),其他中间件同理。
- 应用开发过程中如使用代码中的配置,应该充分利用 Spring Environment Profile,增加本地逻辑分组 local,非开发阶段关闭 local 逻辑分组。同时关闭 apollo 远程获取配置,在 VM options 中增加 -Denv=local。

以下代码是扩展 apollo 应用标识使用 spring.application.name,并增加监控配置,监控一般是基础架构团队提供的功能,从基础框架硬编码上去,业务侧做到完全无感知。
import com.ctrip.framework.apollo.ConfigService; import com.ctrip.framework.apollo.spring.config.PropertySourcesConstants; import com.ctrip.framework.foundation.internals.io.BOMInputStream; import com.ctrip.framework.foundation.internals.provider.DefaultApplicationProvider; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplicationRunListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.annotation.Order; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.PropertiesPropertySource; import org.springframework.util.StringUtils; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.Properties; import java.util.Set; /** * ApolloSpringApplicationRunListener * <p> * SpringApplicationRunListener 接口说明 * https://blog.csdn.net/u011179993/article/details/51555690https://blog.csdn.net/u011179993/article/details/51555690 * * @author Weichao Li ([email protected]) * @since 2019-08-15 */ @Order(value = ApolloSpringApplicationRunListener.APOLLO_SPRING_APPLICATION_RUN_LISTENER_ORDER) @Slf4j public class ApolloSpringApplicationRunListener implements SpringApplicationRunListener { public static final int APOLLO_SPRING_APPLICATION_RUN_LISTENER_ORDER = 1; private static final String APOLLO_APP_ID_KEY = "app.id"; private static final String SPRINGBOOT_APPLICATION_NAME = "spring.application.name"; private static final String CONFIG_CENTER_INFRA_NAMESPACE = "infra.monitor"; public ApolloSpringApplicationRunListener(SpringApplication application, String[] args) { } /** * 刚执行run方法时 */ @Override public void starting() { } /** * 环境建立好时候 * * @param env 环境信息 */ @Override public void environmentPrepared(ConfigurableEnvironment env) { Properties props = new Properties(); props.put(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, true); props.put(PropertySourcesConstants.APOLLO_BOOTSTRAP_EAGER_LOAD_ENABLED, true); env.getPropertySources().addFirst(new PropertiesPropertySource("apolloConfig", props)); // 初始化appId this.initAppId(env); // 初始化基础架构提供的默认配置,需在项目中关联公共 namespaces this.initInfraConfig(env); } /** * 上下文建立好的时候 * * @param context 上下文 */ @Override public void contextPrepared(ConfigurableApplicationContext context) { } /** * 上下文载入配置时候 * * @param context 上下文 */ @Override public void contextLoaded(ConfigurableApplicationContext context) { } @Override public void started(ConfigurableApplicationContext context) { } @Override public void running(ConfigurableApplicationContext context) { } @Override public void failed(ConfigurableApplicationContext context, Throwable exception) { } /** * 初始化 apollo appId * * @param env 环境信息 */ private void initAppId(ConfigurableEnvironment env) { String apolloAppId = env.getProperty(APOLLO_APP_ID_KEY); if (StringUtils.isEmpty(apolloAppId)) { //此处需要判断一下 meta-inf 下的文件中的 app id apolloAppId = getAppIdByAppPropertiesClasspath(); if (StringUtils.isEmpty(apolloAppId)) { String applicationName = env.getProperty(SPRINGBOOT_APPLICATION_NAME); if (!StringUtils.isEmpty(applicationName)) { System.setProperty(APOLLO_APP_ID_KEY, applicationName); } else { throw new IllegalArgumentException("config center must config app.id in " + DefaultApplicationProvider.APP_PROPERTIES_CLASSPATH); } } else { System.setProperty(APOLLO_APP_ID_KEY, apolloAppId); } } else { System.setProperty(APOLLO_APP_ID_KEY, apolloAppId); } } /** * 初始化基础架构提供的配置 * * @param env 环境信息 */ private void initInfraConfig(ConfigurableEnvironment env) { com.ctrip.framework.apollo.Config apolloConfig = ConfigService.getConfig(CONFIG_CENTER_INFRA_NAMESPACE); Set<String> propertyNames = apolloConfig.getPropertyNames(); if (propertyNames != null && propertyNames.size() > 0) { Properties properties = new Properties(); for (String propertyName : propertyNames) { properties.setProperty(propertyName, apolloConfig.getProperty(propertyName, null)); } EnumerablePropertySource enumerablePropertySource = new PropertiesPropertySource( CONFIG_CENTER_INFRA_NAMESPACE, properties); env.getPropertySources().addLast(enumerablePropertySource); } } /** * 从 apollo 默认配置文件中取 app.id 的值,调整优先级在 spring.application.name 之前 * * @return apollo app id */ private String getAppIdByAppPropertiesClasspath() { try { InputStream in = Thread.currentThread().getContextClassLoader() .getResourceAsStream(DefaultApplicationProvider.APP_PROPERTIES_CLASSPATH); if (in == null) { in = DefaultApplicationProvider.class .getResourceAsStream(DefaultApplicationProvider.APP_PROPERTIES_CLASSPATH); } Properties properties = new Properties(); if (in != null) { try { properties.load(new InputStreamReader(new BOMInputStream(in), StandardCharsets.UTF_8)); } finally { in.close(); } } if (properties.containsKey(APOLLO_APP_ID_KEY)) { String appId = properties.getProperty(APOLLO_APP_ID_KEY); log.info("App ID is set to {} by app.id property from {}", appId, DefaultApplicationProvider.APP_PROPERTIES_CLASSPATH); return appId; } } catch (Throwable ignore) { } } }
5.3 动态刷新
支持 Apollo 配置自动刷新类型,支持 @Value @RefreshScope @ConfigurationProperties 以及日志级别的动态刷新。具体代码查看下文链接。
- @Value
@Value Apollo 本身就支持了动态刷新,需要注意的是如果@Value 使用了 SpEL 表达式,动态刷新会失效。
支持动态刷新
@Value("${simple.xxx}") private String simpleXxx;
不支持动态刷新
@Value("#{'${simple.xxx}'.split(',')}") private List<String> simpleXxxs;
- @RefreshScope
RefreshScope(org.springframework.cloud.context.scope.refresh)是 Spring Cloud 提供的一种特殊的 scope 实现,用来实现配置、实例热加载。
动态实现过程:
配置变更时,调用 refreshScope.refreshAll() 或指定 bean。提取标准参数(System,jndi,Servlet)之外所有参数变量,把原来的Environment里的参数放到一个新建的 Spring Context 容器下重新加载,完事之后关闭新容器。提取更新过的参数(排除标准参数) ,比较出变更项,发布环境变更事件,RefreshScope 用新的环境参数重新生成Bean。重新生成的过程很简单,清除 refreshscope 缓存幷销毁 Bean,下次就会重新从 BeanFactory 获取一个新的实例(该实例使用新的配置)。
- @ConfigurationProperties
apollo 默认是不支持 ConfigurationProperties 刷新的,这块需要配合 EnvironmentChangeEvent 刷新的。
- 日志级别
apollo 默认是不支持日志级别刷新的,这块需要配合 EnvironmentChangeEvent 刷新的。
- EnvironmentChangeEvent(Spring Cloud 提供)
当观察到 EnvironmentChangeEvent 时,它将有一个已更改的键值列表,应用程序将使用以下内容:1,重新绑定上下文中的任何 @ConfigurationProperties bean,代码见org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder。2,为logging.level.*中的任何属性设置记录器级别,代码见 org.springframework.cloud.logging.LoggingRebinder。
支持动态刷新
import com.ctrip.framework.apollo.model.ConfigChangeEvent; import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.context.environment.EnvironmentChangeEvent; import org.springframework.cloud.context.scope.refresh.RefreshScope; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Configuration; /** * LoggerConfiguration * * @author Weichao Li ([email protected]) * @since 2019/11/14 */ @Configuration @Slf4j public class ApolloRefreshConfiguration implements ApplicationContextAware { private ApplicationContext applicationContext; @Autowired private RefreshScope refreshScope; @ApolloConfigChangeListener private void onChange(ConfigChangeEvent changeEvent) { applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys())); refreshScope.refreshAll(); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } }
注意原有配置如果有日志级别需要初始化。
import com.ctrip.framework.apollo.Config; import com.ctrip.framework.apollo.spring.annotation.ApolloConfig; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LoggingSystem; import org.springframework.context.annotation.Configuration; import javax.annotation.PostConstruct; import java.util.Set; /** * logging 初始化 * * @author Weichao Li ([email protected]) * @since 2019/11/14 */ @Configuration @Slf4j public class LoggingConfiguration { private static final String LOGGER_TAG = "logging.level."; private static final String DEFAULT_LOGGING_LEVEL = "info"; @Autowired private LoggingSystem loggingSystem; @ApolloConfig private Config config; @PostConstruct public void changeLoggingLevel() { Set<String> keyNames = config.getPropertyNames(); for (String key : keyNames) { if (containsIgnoreCase(key, LOGGER_TAG)) { String strLevel = config.getProperty(key, DEFAULT_LOGGING_LEVEL); LogLevel level = LogLevel.valueOf(strLevel.toUpperCase()); loggingSystem.setLogLevel(key.replace(LOGGER_TAG, ""), level); } } } private static boolean containsIgnoreCase(String str, String searchStr) { if (str == null || searchStr == null) { return false; } int len = searchStr.length(); int max = str.length() - len; for (int i = 0; i <= max; i++) { if (str.regionMatches(true, i, searchStr, 0, len)) { return true; } } return false; } }
6 Apollo 最佳实践 – 配置治理
6.1 权限控制
由于配置能改变程序的行为,不正确的配置甚至能引起灾难,所以对配置的修改必须有比较完善的权限控制。应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。所有的操作都有审计日志,可以方便地追踪问题
- everyone 要有自己的账户(最主要的前置条件)
- 每一个项目都至少有一个 owner(项目管理员,项目管理员拥有以下权限)
- 可以管理项目的权限分配
- 可以创建集群
- 可以创建 Namespace
- 项目管理员(owner)根据组织结构分配配置权限
- 编辑权限允许用户在 Apollo 界面上创建、修改、删除配置
- 配置修改后只在 Apollo 界面上变化,不会影响到应用实际使用的配置
- 发布权限允许用户在 Apollo 界面上发布、回滚配置
- 配置只有在发布、回滚动作后才会被应用实际使用到
- Apollo在用户操作发布、回滚动作后实时通知到应用,并使最新配置生效
- 编辑权限允许用户在 Apollo 界面上创建、修改、删除配置
- 项目管理员管理权限界面

项目创建完,默认没有分配配置的编辑和发布权限,需要项目管理员进行授权。
1 点击application这个namespace的授权按钮

2 分配修改权限

3 分配发布权限

6.2 Namespace
Namespace 权限分类
apollo 获取权限分类分为私有的和公共的。
- private (私有的)
private权限的Namespace,只能被所属的应用获取到。一个应用尝试获取其它应用private的Namespace,Apollo会报“404”异常。
- public (公共的)
public权限的Namespace,能被任何应用获取。
Namespace 的分类
Namespace 有三种类型,私有类型,公共类型,关联类型(继承类型)。
Apollo 私有类型 Namespace 使用说明
私有类型的 Namespace 具有 private 权限。例如服务默认的“application” Namespace 就是私有类型。
1 使用场景
- 服务自身的配置(如数据库、业务行为等配置)
2 如何使用私有类型 Namespace
一个应用下不同配置的分组,可以简单地把namespace类比为文件,不同类型的配置存放在不同的文件中,如数据库配置文件,业务属性配置,配置文件等
Apollo 公共类型 Namespace 使用说明
公共类型的 Namespace 具有 public 权限。公共类型的 Namespace 相当于游离于应用之外的配置,且通过 Namespace 的名称去标识公共 Namespace,所以公共的 Namespace 的名称必须全局唯一。
1 使用场景
- 部门级别共享的配置
- 小组级别共享的配置
- 几个项目之间共享的配置
- 中间件客户端的配置
2 如何使用公共类型 Namespace
- 代码侵入型
@EnableApolloConfig({"application", "poizon-infra.jaeger"})
- 配置方式形式
# will inject 'application' namespace in bootstrap phase apollo.bootstrap.enabled = true # will inject 'application', 'poizon-infra.jaeger' namespaces in bootstrap phase apollo.bootstrap.namespaces = application,poizon-infra.jaeger
Apollo 关联类型 Namespace 使用说明
关联类型又可称为继承类型,关联类型具有 private 权限。关联类型的 Namespace 继承于公共类型的 Namespace,用于覆盖公共 Namespace 的某些配置。
使用建议
- 基础框架部分的统一配置,如 DAL 的常用配置
- 基础架构的公共组件的配置,如监控,熔断等公共组件配置