1. Apollo 介绍

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

服务端基于 Spring Boot 和 Spring Cloud 开发,打包后可以直接运行,不需要额外安装 Tomcat 等应用容器。Java 客户端不依赖任何框架,能够运行于所有 Java 运行时环境,同时对 Spring/Spring Boot 环境也有较好的支持。

官方给出的 Apollo 基础模型非常简单:

上图简要描述了Apollo客户端的实现原理:

① 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过Http Long Polling实现)

② 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。

  • 这是一个fallback机制,为了防止推送机制失效导致配置不更新
  • 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
  • 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。

③ 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中

④ 客户端会把从服务端获取到的配置在本地文件系统缓存一份

  • 在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置

⑤ 应用程序可以从Apollo客户端获取最新的配置、订阅配置更新通知

官方给出的架构图如下:

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

  • Config Service 提供配置的读取、推送等功能,服务对象是 Apollo 客户端

  • Admin Service 提供配置的修改、发布等功能,服务对象是 Apollo Portal(Apollo 发布管理界面)

  • 通过 Apollo 的发布界面可以多环境、集群管理配置

  • Config Service 和 Admin Service 都是多实例、无状态部署,所以需要将自己注册到Eureka 中并保持心跳,在 Eureka 之上架了一层 Meta Server 用于封装 Eureka 的服务发现接口。Apollo提供了MetaServiceProvider SPI,用户可以注入自己的MetaServiceProvider来自定义Meta Server定位逻辑

  • Client 通过域名访问 Meta Server 获取 Config Service 服务列表(IP+Port),而后直接通过 IP+Port 访问服务,同时在 Client 侧会做 load balance、错误重试

为了兼容别的非Java应用场景,且Eureka(包括Ribbon软负载)原生仅支持Java客户端,如果要为多语言开发Eureka/Ribbon客户端,这个工作量很大也不可控。所以Apollo的作者引入了MetaServer这个角色,它是一个Eureka的Proxy,将Eureka的服务发现接口以更简单明确的HTTP接口的形式暴露出来,方便Client/Protal通过简单的HTTPClient就可以查询到Config/AdminService的地址列表。获取到服务实例地址列表之后,再以简单的客户端软负载(Client SLB)策略路由定位到目标实例,并发起调用。

1.1 Apollo Meta Server

Apollo 支持应用在不同的环境有不同的配置,所以需要在运行提供给 Apollo 客户端当前环境的 Apollo Meta Server 信息。默认情况下,meta server 和 config service 是部署在同一个 JVM 进程,所以 meta server 的地址就是 config service 的地址。

配置 apollo meta server 信息有多重方式,这里介绍两种(其他的参照官网):

1)在 Spring Boot 的 application.properties 或 bootstrap.properties 中指定 apollo.meta = http://config-service-url

  

2)通过 server.properties 配置文件

  • 可以在 server.properties 配置文件中指定 apollo.meta=http://config-service-url
  • 对于Mac/Linux,文件位置为/opt/settings/server.properties

  

  • 对于Windows,文件位置为C:\opt\settings\server.properties

1.2 本地缓存路径

Apollo 客户端(Java 项目所运行的服务器)会把从服务端获取到的配置在本地文件系统缓存一份,用于在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置,不影响应用正常运行。

本地缓存路径默认位于以下路径,所以请确保 /opt/data 或 C:\opt\data\ 目录存在,且应用有读写权限。

  • Mac/Linux: /opt/data/{appId}/config-cache
  • Windows: C:\opt\data\{appId}\config-cache

本地配置文件会以下面的文件名格式放置于本地缓存路径下:{appId}+{cluster}+{namespace}.properties

  • appId就是应用自己的appId,如100004458
  • cluster就是应用使用的集群,一般在本地模式下没有做过配置的话,就是default
  • namespace就是应用使用的配置namespace,一般是application client-local-cache

文件内容以 properties 格式存储,比如如果有两个key,一个是request.timeout,另一个是batch,那么文件内容就是如下格式:

request.timeout = 2000
batch = 2000

自定义缓存路径

1.0.0 版本开始支持自定义缓存路径,这里介绍通过Spring Boot的配置文件修改:可以在 Spring Boot 的 application.properties 或bootstrap.properties 中指定 apollo.cacheDir = /opt/data/some-cache-dir

2. Apollo 使用

(前提:Apollo 服务已经搭建成功)

2.1 新建项目

打开apollo-portal主页,点击“创建项目”。

输入项目信息

  • 部门:选择应用所在的部门
  • 应用 AppId:用来标识应用身份的唯一 id,格式为 string,需要和客户端 app.properties 中配置的 app.id 对应
  • 应用名称:应用名,仅用于界面展示
  • 应用负责人:选择的人默认会成为该项目的管理员,具备项目权限管理、集群创建、Namespace 创建等权限

点击提交。创建成功后,会自动跳转到项目首页:

2.2 分配项目权限

1)项目管理员权限

项目管理员拥有以下权限:

  • 可以管理项目的权限分配
  • 可以创建集群
  • 可以创建Namespace

创建项目时填写的应用负责人默认会成为项目的管理员之一,如果还需要其他人也成为项目管理员,可以按照下面步骤操作:

① 点击页面左侧的“管理项目”

② 搜索需要添加的成员并点击添加

2)配置编辑、发布权限

配置权限分为编辑和发布:

  • 编辑权限允许用户在Apollo界面上创建、修改、删除配置:配置修改后只在Apollo界面上变化,不会影响到应用实际使用的配置。
  • 发布权限允许用户在Apollo界面上发布、回滚配置:配置只有在发布、回滚动作后才会被应用实际使用到;Apollo在用户操作发布、回滚动作后实时通知到应用,并使最新配置生效。

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

① 点击application这个namespace的授权按钮

② 分配权限

2.3 添加配置项

编辑配置需要拥有这个Namespace的编辑权限,如果发现没有新增配置按钮,可以找项目管理员授权。

① 通过表格添加

② 通过文本模式编辑(可从 Java 客户端的配置文件中)

2.4 发布配置

配置只有在发布后才会真的被应用使用到,所以在编辑完配置后,需要发布配置。

发布配置需要拥有这个Namespace的发布权限,如果发现没有发布按钮,可以找项目管理员授权。

3. Java 项目读取配置

官网:Java客户端使用指南 · apolloconfig/apollo Wiki · GitHub

Apollo 客户端依赖于 AppId,Apollo Meta Server 等环境信息来工作

3.1 添加 apollo 依赖

		<dependency>
			<groupId>com.ctrip.framework.apollo</groupId>
			<artifactId>apollo-client</artifactId>
			<version>1.5.1</version>
		</dependency>

3.2 主启动类加 @EnableApolloConfig 注解

3.3 配置文件 application.yml

igi:
  profiles:
    active: uat
apollo:
  bootstrap:
    enabled: true
    namespaces: application
  # uat
  meta: http://10.***.***.***:10000
  cacheDir: /srv/apollo
app:
  id: *_server

 应用配置文件:

3.4 读取配置项

    @Value("${mapperLocations:#{null}}")
    private  String mapperLocations;

也可以添加注解批量读取:

@ConfigurationProperties(prefix = "spring.cache.redis")
public class RedisCacheProperties {
    private String keyPrefix;
    private long timeToLive;
}

3.5 Apollo监听配置变化事件

import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * Apollo 配置动态监听
 */
@Component
@Slf4j
public class ApolloChangeConfiguration {

    @ApolloConfigChangeListener({"application","application-common"}) //这个注解的value注解如果不写默认是application,但是我这里还加载了一个common的配置,所以加了namespace,因为只有一个参数,这里的value省略了
    public void onChange(ConfigChangeEvent changeEvent) {
        changeEvent.changedKeys().forEach(s -> {
                ConfigChange configChange = changeEvent.getChange(s);
                log.info("apollo key={},修改前的值 ={},修改后的值={}", s, configChange.getOldValue(), configChange.getNewValue());

        });
    }

}

下面这个主要是监听数据库、redis等那种项目一启动就初始化的配置,监听到配置改了之后就重新构建数据源。

import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfig;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
import java.util.Collections;
import org.springframework.context.ApplicationContext;
import javax.sql.DataSource;

/**
 * @Description 监听配置变化事件
 * @Date 2020/7/28
 * @Author Janet
 */
@Component
public class ApolloChangeConfiguration  {

    private Log logger = LogFactory.getLog(this.getClass());

    private final static String DATASOURCE_TAG = "db";

    @Autowired
    ApplicationContext context;

    @ApolloConfig
    Config config;

    @Bean
    public DynamicDataSource dynamicDataSource() {
        DynamicDataSource source = new DynamicDataSource();
        source.setTargetDataSources(Collections.singletonMap(DATASOURCE_TAG, dataSource()));
        return source;
    }

    @ApolloConfigChangeListener(interestedKeyPrefixes = {"spring.datasource","spring.rabbitmq","spring.redis"})
    public void onChange(ConfigChangeEvent changeEvent) {
//        changeEvent.changedKeys().forEach(s -> {
            DynamicDataSource source = context.getBean(DynamicDataSource.class);
            source.setTargetDataSources(Collections.singletonMap(DATASOURCE_TAG, dataSource()));
            source.afterPropertiesSet();
            logger.info(String.format("动态切换数据源为:-:{change:%s}", config.getProperty("spring.datasource.url", "")));
//        });
    }

    public DataSource dataSource() {
        DataSourceProperties dataSource = new DataSourceProperties();
        dataSource.setUrl(config.getProperty("spring.datasource.url", "")); //第二个参数为 defaultValue
        dataSource.setUsername(config.getProperty("spring.datasource.username", ""));
        dataSource.setPassword(config.getProperty("spring.datasource.password", ""));
//        System.out.println("dataSource--------------dataSource----------------"+dataSource.getUsername()+"     "+dataSource.getPassword()+"     "+dataSource.getUrl());
        return dataSource.initializeDataSourceBuilder().build();
    }

    class DynamicDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            return DATASOURCE_TAG;
        }
    }

}

4. 回滚已发布配置

如果发现已发布的配置有问题,可以通过点击『回滚』按钮来将客户端读取到的配置回滚到上一个发布版本。

这里的回滚机制类似于发布系统,发布系统中的回滚操作是将部署到机器上的安装包回滚到上一个部署的版本,但代码仓库中的代码是不会回滚的,从而开发可以在修复代码后重新发布。

Apollo 中的回滚也是类似的机制,点击回滚后是将发布到客户端的配置回滚到上一个已发布版本,也就是说客户端读取到的配置会恢复到上一个版本,但页面上编辑状态的配置是不会回滚的,从而开发可以在修复配置后重新发布。

5. 测试结果

以下为自己测试的一些结论:

① 在 apollo 配置中心修改配置信息后,点击发布数据会同步到 Java 客户端本地缓存文件。

② 如果Java客户端(Java项目)配置文件中配置了apollo服务器地址,则优先从服务器获取配置项,如无法正常连接服务器(服务器地址错误或其他原因),从本地缓存文件中获取配置项(如果本地的配置文件二次修改并且没有重启项则无效);

③ 如果Java客户端配置文件没有配置apollo服务器地址,则从本地的 /opt/settings/server.properties 中查看本台服务器对应的 apollo 服务器地址并连接该服务器获取配置信息;如无法连接,读取本地缓存配置文件。

修改Java客户端本地缓存文件, apollo 配置中心是不可知的。

④ a. apollo配置中心是否同步配置信息到Java客户端缓存文件,不是和本地的缓存文件内容做比较,而是和自己服务器的上一版本内容做比较。如果内容发生修改(修改配置信息并点击发布),则修改自己服务器的内容,并把新的同步到Java客户端本地缓存文件(把旧的内容全部删掉,把新的写进去)。

    b. 如果apollo配置中心没有修改配置信息以及发布,Java客户端本地缓存文件做怎样的修改apollo服务器都是不可知的,不会再同步配置信息。

⑤ 每重启一次Java项目,apollo配置中心会自动同步配置信息到Java客户端本地缓存文件。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐