本文来自李伟超同学的投稿,如果你有好的文章也欢迎联系我。

微服务配置中心 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的基础模型:

  1. 用户在配置中心对配置进行修改并发布

  2. 配置中心通知Apollo客户端有配置更新

  3. 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 (liweichao0102@gmail.com) * @since 2019-08-15 */@Order(value = ApolloSpringApplicationRunListener.APOLLO_SPRING_APPLICATION_RUN_LISTENER_ORDER)@Slf4jpublic 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 (liweichao0102@gmail.com) * @since 2019/11/14 */@Configuration@Slf4jpublic 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 (liweichao0102@gmail.com) * @since 2019/11/14 */@Configuration@Slf4jpublic 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在用户操作发布、回滚动作后实时通知到应用,并使最新配置生效

  • 项目管理员管理权限界面

             

项目创建完,默认没有分配配置的编辑和发布权限,需要项目管理员进行授权。

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 phaseapollo.bootstrap.enabled = true# will inject 'application', 'poizon-infra.jaeger' namespaces in bootstrap phaseapollo.bootstrap.namespaces = application,poizon-infra.jaeger

Apollo 关联类型 Namespace 使用说明

关联类型又可称为继承类型,关联类型具有 private 权限。关联类型的 Namespace 继承于公共类型的 Namespace,用于覆盖公共 Namespace 的某些配置。

使用建议

  • 基础框架部分的统一配置,如 DAL 的常用配置

  • 基础架构的公共组件的配置,如监控,熔断等公共组件配置

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐