实践配置中心之Apollo

Apollo(阿波罗)【Github】【Gitee】是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。

名词解释

appId-》env-》cluster-》namespace

appId

环境

开发、测试和生产等环境,是一个标签概念,不同环境可以有不同的配置项

(1) JVM system property ‘env’,

1
2
2019-02-27 15:11:13.436  INFO 89186 --- [           main] c.c.f.f.i.p.DefaultServerProvider        : Environment is set to [dev] b
y JVM system property 'env'.

(2) OS env variable ‘ENV’ nor

1
2
2019-02-27 14:49:38.939  INFO 87963 --- [           main] c.c.f.f.i.p.DefaultServerProvider        : Environment is set to [fat] b
y OS env variable 'ENV'.

(3) property ‘env’ from the properties InputStream.

集群

集群,可以简单的理解为不同的数据中心。环境下可以有不同的集群,默认值default。

namespace

配置项的集合。类似spring boot的application.properties,可以把application理解为namespace。Apollo默认为每个应用创建一个叫application的namespace,application也是默认的namespace,一般一个namespace够用了。

namespace分private和public,private代表项目专用,public代表所有项目都可以公用。

还有关联公共namespace,用于覆盖公共namespace。

格式,properties、xml、json、yml、yaml等

灰度

可以按照ip发布(private)或AppId和ip(public)发布

总体设计

apollo-overall-architecture

上图简要描述了Apollo的总体设计,我们可以从下往上看:

  • Config Service提供配置的读取、推送等功能,服务对象是Apollo客户端
  • Admin Service提供配置的修改、发布等功能,服务对象是Apollo Portal(管理界面)
  • Config Service和Admin Service都是多实例、无状态部署,所以需要将自己注册到Eureka中并保持心跳【apolloconfig数据库配置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进程中

admin service默认端口8090

config service默认端口8080,一方面是提供meta server服务(利用负载均衡),一方面是Eureka服务

portal默认端口8070

安装

SQL脚本:scripts\db\migration\configdb\V1.0.0__initialization.sqlscripts\db\migration\portaldb\V1.0.0__initialization.sql

  • 1、新建数据库和表
  • 2、修改admin、config和portal应用连接数据库的参数
  • 3、修改config数据库eureka地址
  • 4、修改portal应用env配置和portal数据库env配置

ctrip集群部署

[@lyliyongblue](https://github.com/lyliyongblue) 贡献的样例部署图

客户端设计

apollo-client-architecture

客户端接入(Spring Boot)

添加apollo-client依赖

1
2
3
4
5
<dependency>
    <groupId>com.ctrip.framework.apollo</groupId>
    <artifactId>apollo-client</artifactId>
    <version>1.4.0</version>
</dependency>

配置Apollo信息

app.id联系管理员申请,备注:如下配置是使用local环境配置,可以根据需要修改apollo.meta对应的地址。

application.yml

1
2
3
4
5
6
7
8
9
app:
  id: test-service
apollo:
  env: local
  meta: http://192.168.1.1:8080
  cacheDir: ../data/apolloCache
  bootstrap:
    enabled: true
    namespaces: application,parent

或者application.properties

1
2
3
4
5
6
7
app.id=test-service
# set apollo meta server address, adjust to actual address if necessary
apollo.env=local
apollo.meta=http://192.168.1.1:8080
apollo.cacheDir=../data/apolloCache
apollo.bootstrap.enabled=true
apollo.bootstrap.namespaces=application,parent

环境变量

针对其他非local环境,dev、fat和uat可以使用操作系统环境变量定义,pro可以使用shell脚本里面定义。

dev

1
2
3
export SPRING_PROFILES_ACTIVE=dev
export ENV=dev
export APOLLO_META=http://192.168.1.2:8080

fat

1
2
3
export SPRING_PROFILES_ACTIVE=test
export ENV=fat
export APOLLO_META=http://192.168.1.3:8080

pro

1
2
3
4
5
6
if [ -z $ENV ]
then
    export SPRING_PROFILES_ACTIVE=prod
    export ENV=pro
    export APOLLO_META=http://192.168.1.4:8080
fi

手动接收配置变化通知

监听配置变化事件只在应用真的关心配置变化,需要在配置变化时得到通知时使用,比如:数据库连接串变化后需要重建连接等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface ApolloConfigChangeListener {
  /**
   * Apollo namespace for the config, if not specified then default to application
   */
  String[] value() default {ConfigConsts.NAMESPACE_APPLICATION};

  /**
   * The keys interested by the listener, will only be notified if any of the interested keys is changed.
   * <br />
   * If not specified then will be notified when any key is changed.
   */
  String[] interestedKeys() default {};
}

ConfigChangeListener

1
2
3
4
5
6
7
public interface ConfigChangeListener {
  /**
   * Invoked when there is any config change for the namespace.
   * @param changeEvent the event for this change
   */
  public void onChange(ConfigChangeEvent changeEvent);
}

备注:如果只是希望每次都取到最新的配置的话,调用config.getProperty即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
 * Apollo配置变更手动接收处理配置
 *
 * @author ray wang
 */
@Configuration
public class ApolloChangeConfig {

    /**
     * 日志级别配置前缀,后跟loggerName
     **/
    public final static String LOGGER_TAG = "logging.level.";

    private static Logger log = LoggerFactory.getLogger(ApolloChangeConfig.class);

    private LoggingSystem loggingSystem;

    public ApolloChangeConfig(LoggingSystem loggingSystem) {
        this.loggingSystem = loggingSystem;
    }

    @ApolloConfigChangeListener
    private void onChange(ConfigChangeEvent changeEvent) {
        Set<String> keyNames = changeEvent.changedKeys();
        log.debug("changedKeys: {}", keyNames);
        for (String key : keyNames) {
            // 日志级别配置
            if (containsIgnoreCase(key, LOGGER_TAG)) {
                String strLevel = changeEvent.getChange(key).getNewValue();
                LogLevel level = LogLevel.valueOf(strLevel.toUpperCase());
                loggingSystem.setLogLevel(key.replace(LOGGER_TAG, ""), level);
                log.debug("{}:{}", key, strLevel);
            }
        }
    }

    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;
    }
}

Q&A

不指定env

如果不指定env,则由连接的meta.server决定env,env和meta.server是一一对应的

1
2
3
2019-02-28 10:09:00.637  WARN 11261 --- [           main] o.s.c.a.ConfigurationClassPostProcessor  : Cannot enhance @Configuration bean definition 'com.ctrip.framework.apollo.spring.boot.ApolloAutoConfiguration' since its singleton instance has been created too early. The typical cause is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor return type: Consider declaring such methods as 'static'.

2019-02-28 10:09:00.804  WARN 11261 --- [           main] figurationPropertiesBindingPostProcessor : Multiple PropertySourcesPlaceholderConfigurer beans registered [propertySourcesPlaceholderConfigurer, org.springframework.context.support.PropertySourcesPlaceholderConfigurer], falling back to Environment