携程架构部开源的配置中心Apollo深度解读

随着程序功能的日益复杂,程序的配置日益增多:各种功能的开关、参数的配置、服务器的地址……

对程序配置的期望值也越来越高:配置修改后实时生效,灰度发布,分环境、分集群管理配置,完善的权限、审核机制……

在这样的大环境下,传统的通过配置文件、数据库等方式已经越来越无法满足开发人员对配置管理的需求。

想必大家都深有体会,我们做的项目都伴随着各种配置文件,而且总是本地配置一套配置文件,测试服配置一套,正式服配置一套,有时候一不小心就改错了,挨骂是小事,扣绩效那可就闹大了。

而且每当项目发布的时候,配置文件也会被打包进去,也就是配置文件会跟着项目一起发布。然后每次出现问题需要我们修改配置文件的时候,我们总是得先在本地修改,然后重新发布才能让新的配置生效。

当请求压力越来越大,你的项目也会从 1 个节点变成多个节点,这个时候如果配置需要发生变化,对应的修改操作也是相同的,只需要在项目中修改一次即可,但对于发布操作工作量就比之前大了很多,因为要发布多个节点。

修改这些配置,增加的发布的工作量降低了整体的工作效率,为了能够提升工作效率,配置中心应运而生了,我们可以将配置统一存放在配置中心来进行管理。

总体而言在没有引入配置中心之前,我们都会面临以下问题:

  1. 配置散乱格式不标准,有的用 properties 格式,有的用 xml 格式,还有的存 DB,团队倾向自造*,做法五花八门。

  2. 主要采用本地静态配置,配置修改麻烦,配置修改一般需要经过一个较长的测试发布周期。在分布式微服务环境下,当服务实例很多时,修改配置费时费力。

  3. 易引发生产事故,这个是我亲身经历,之前在一家互联网公司,有团队在发布的时候将测试环境的配置带到生产上,引发百万级资损事故。

  4. 配置缺乏安全审计和版本控制功能,谁改的配置?改了什么?什么时候改的?无从追溯,出了问题也无法及时回滚。

  5. 增加了运维小哥哥的工作量,极大的损害了运维小哥哥和开发小哥哥的基情。

到底什么是配置中心

配置中心就是把项目中各种个样的配置、参数、开关,全部都放到一个集中的地方进行统一管理,并提供一套标准的接口。当各个服务需要获取配置的时候,就来配置中心的接口拉取。当配置中心中的各种参数有更新的时候,也能通知到各个服务实时的过来同步最新的信息,使之动态更新。

Apollo 简介

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

Apollo支持4个维度管理Key-Value格式的配置(下面会详细说明):

1、application (应用)
2、environment (环境)

3、cluster (集群)
4、namespace (命名空间)

什么是配置

既然Apollo定位于配置中心,那么在这里有必要先简单介绍一下什么是配置。

按照我们的理解,配置有以下几个属性:

  • 配置是独立于程序的只读变量

    • 配置首先是独立于程序的,同一份程序在不同的配置下会有不同的行为。

    • 其次,配置对于程序是只读的,程序通过读取配置来改变自己的行为,但是程序不应该去改变配置。

    • 常见的配置有:DB Connection Str、Thread Pool Size、Buffer Size、Request Timeout、Feature Switch、Server Urls等。

  • 配置伴随应用的整个生命周期

    • 配置贯穿于应用的整个生命周期,应用在启动时通过读取配置来初始化,在运行时根据配置调整行为。

  • 配置可以有多种加载方式

    • 配置也有很多种加载方式,常见的有程序内部hard code,配置文件,环境变量,启动参数,基于数据库等

  • 配置需要治理

    • 还有一类比较特殊的配置 - 框架类组件配置,比如CAT客户端的配置。

    • 虽然这类框架类组件是由其他团队开发、维护,但是运行时是在业务实际应用内的,所以本质上可以认为框架类组件也是应用的一部分。

    • 这类组件对应的配置也需要有比较完善的管理方式。

    • 同一份程序在不同的环境(开发,测试,生产)、不同的集群(如不同的数据中心)经常需要有不同的配置,所以需要有完善的环境、集群配置管理

    • 由于配置能改变程序的行为,不正确的配置甚至能引起灾难,所以对配置的修改必须有比较完善的权限控制

    • 权限控制

    • 不同环境、集群配置管理

    • 框架类组件配置管理

为什么使用Apollo,Apollo有哪些特征

正是基于配置的特殊性,所以Apollo从设计之初就立志于成为一个有治理能力的配置发布平台,目前提供了以下的特性:

  • 统一管理不同环境、不同集群的配置

    • Apollo提供了一个统一界面集中式管理不同环境(environment)、不同集群(cluster)、不同命名空间(namespace)的配置。

    • 同一份代码部署在不同的集群,可以有不同的配置,比如zookeeper的地址等

    • 通过命名空间(namespace)可以很方便地支持多个不同应用共享同一份配置,同时还允许应用对共享的配置进行覆盖

  • 配置修改实时生效(热发布)

    • 用户在Apollo修改完配置并发布后,客户端能实时(1秒)接收到最新的配置,并通知到应用程序

  • 版本发布管理

    • 所有的配置发布都有版本概念,从而可以方便地支持配置的回滚

  • 灰度发布

    • 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例

  • 权限管理、发布审核、操作审计

    • 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节,从而减少人为的错误。

    • 所有的操作都有审计日志,可以方便地追踪问题

  • 客户端配置信息监控

    • 可以在界面上方便地看到配置在被哪些实例使用

  • 提供Java和.Net原生客户端

    • 提供了Java和.Net的原生客户端,方便应用集成

    • 支持Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便应用使用(需要Spring 3.1.1+)

    • 同时提供了Http接口,非Java和.Net应用也可以方便地使用

  • 提供开放平台API

    • Apollo自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。不过Apollo出于通用性考虑,不会对配置的修改做过多限制,只要符合基本的格式就能保存,不会针对不同的配置值进行针对性的校验,如数据库用户名、密码,Redis服务地址等

    • 对于这类应用配置,Apollo支持应用方通过开放平台API在Apollo进行配置的修改和发布,并且具备完善的授权和权限控制

  • 部署简单

    • 配置中心作为基础服务,可用性要求非常高,这就要求Apollo对外部依赖尽可能地少

    • 目前唯一的外部依赖是MySQL,所以部署非常简单,只要安装好Java和MySQL就可以让Apollo跑起来

    • Apollo还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行时参数

Apollo整体架构

首先我们来看看Applo的基本工作流程如下图所示

携程架构部开源的配置中心Apollo深度解读

1.用户在配置中心对配置进行修改并发布
2.配置中心通知Apollo客户端有配置更新
3.Apollo客户端从配置中心拉取最新的配置、更新本地配置并通知到应用

接下来我们来看看Apollo的整体架构图

携程架构部开源的配置中心Apollo深度解读

 

上图简要描述了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进程中

Why Eureka

为什么我们采用Eureka作为服务注册中心,而不是使用传统的zk、etcd呢?我大致总结了一下,有以下几方面的原因:

  • 它提供了完整的Service Registry和Service Discovery实现
    首先是提供了完整的实现,并且也经受住了Netflix自己的生产环境考验,相对使用起来会比较省心。
    和Spring Cloud无缝集成

  • 的项目本身就使用了Spring Cloud和Spring Boot,同时Spring Cloud还有一套非常完善的开源代码来整合Eureka,所以使用起来非常方便。

  • 另外,Eureka还支持在我们应用自身的容器中启动,也就是说我们的应用启动完之后,既充当了Eureka的角色,同时也是服务的提供者。这样就极大的提高了服务的可用性。

  • 这一点是我们选择Eureka而不是zk、etcd等的主要原因,为了提高配置中心的可用性和降低部署复杂度,我们需要尽可能地减少外部依赖。
    Open Source

  • 最后一点是开源,由于代码是开源的,所以非常便于我们了解它的实现原理和排查问题。

各模块概要介绍

Config Service

  • 提供配置获取接口

  • 提供配置更新推送接口(基于Http long polling)

  • 服务端使用Spring DeferredResult实现异步化,从而大大增加长连接数量

  • 目前使用的tomcat embed默认配置是最多10000个连接(可以调整),使用了4C8G的虚拟机实测- 可以支撑10000个连接,所以满足需求(一个应用实例只会发起一个长连接)。接口服务对象为Apollo客户端

Admin Service

  • 提供配置管理接口

  • 提供配置修改、发布等接口

  • 接口服务对象为Portal

Meta Server

  • Portal通过域名访问Meta Server获取Admin Service服务列表(IP+Port)

  • Client通过域名访问Meta Server获取Config Service服务列表(IP+Port)

  • Meta Server从Eureka获取Config Service和Admin Service的服务信息,相当于是一个Eureka Client
    增设一个Meta Server的角色主要是为了封装服务发现的细节,对Portal和Client而言,永远通过一个

  • Http接口获取Admin Service和Config Service的服务信息,而不需要关心背后实际的服务注册和发现组件

  • Meta Server只是一个逻辑角色,在部署时和Config Service是在一个JVM进程中的,所以IP、端口和Config Service一致

Eureka

  • 基于Eureka和Spring Cloud Netflix提供服务注册和发现

  • Config Service和Admin Service会向Eureka注册服务,并保持心跳

  • 为了简单起见,目前Eureka在部署时和Config Service是在一个JVM进程中的(通过Spring Cloud Netflix)

Portal

  • 提供Web界面供用户管理配置

  • 通过Meta Server获取Admin Service服务列表(IP+Port),通过IP+Port访问服务

  • 在Portal侧做load balance、错误重试

Client

  • Apollo提供的客户端程序,为应用提供配置获取、实时更新等功能

  • 通过Meta Server获取Config Service服务列表(IP+Port),通过IP+Port访问服务

  • 在Client侧做load balance、错误重试

Apollo核心概念介绍

1、application

1、Apollo 客户端在运行时需要知道当前应用是谁,从而可以根据不同的应用来获取对应应用的配置。

2、每个应用都需要有唯一的身份标识,可以在代码中配置 app.id 参数来标识当前应用,Apollo 会根据此指来辨别当前应用。

2、environment

在实际开发中,我们的应用经常要部署在不同的环境中,一般情况下分为 开发、测试、生产 等等不同环境,不同环境中的配置也是不同的,在 Apollo 中默认提供了

四种环境:

FAT:功能测试环境

UAT:集成测试环境

DEV:开发环境

PRO:生产环境

在程序中如果想指定使用哪个环境,可以配置变量 env 的值为对应环境名称即可。

3、cluster

1、一个应用下不同实例的分组,比如典型的可以按照数据中心分,把上海机房的应用实例分为一个集群,把北京机房的应用实例分为另一个集群。

2、对不同的集群,同一个配置可以有不一样的值,比如说上面所指的两个北京、上海两个机房设置两个集群,都有 mysql 配置参数,其中参数中配置的地址是不一样的。

4、namespace

一个应用中不同配置的分组,可以简单地把 namespace 类比为不同的配置文件,不同类型的配置存放在不同的文件中,如数据库配置文件,RPC 配置文件等。

熟悉 SpringBoot 的都知道,SpringBoot 项目都有一个默认配置文件 application.yml,如果还想用多个配置,可以创建多个配置文件来存放不同的配置信息,通过

指定 spring.profiles.active 参数指定应用不同的配置文件。这里的 namespace 概念与其类似,将不同的配置放到不同的配置 namespace 中。

Namespace 分为两种权限,分别为:

public(公共的):public权限的 Namespace,能被任何应用获取。
private(私有的):只能被所属的应用获取到。一个应用尝试获取其它应用 private 的 Namespace,Apollo 会报 “404” 异常。

 

Apollo实时发布配置

1. 配置发布后的实时推送设计

配置中心最重要的一个特性就是实时推送,正因为有这个特性,我们才可以依赖配置中心做很多事情。如图所示。

携程架构部开源的配置中心Apollo深度解读

携程架构部开源的配置中心Apollo深度解读

图 1 简要描述了配置发布的大致过程。

  • 用户在 Portal 中进行配置的编辑和发布。

  • Portal 会调用 Admin Service 提供的接口进行发布操作。

  • Admin Service 收到请求后,发送 ReleaseMessage 给各个 Config Service,通知 Config Service 配置发生变化。

  • Config Service 收到 ReleaseMessage 后,通知对应的客户端,基于 Http 长连接实现。

2. 发送 ReleaseMessage 的实现方式

ReleaseMessage 消息是通过 Mysql 实现了一个简单的消息队列。之所以没有采用消息中间件,是为了让 Apollo 在部署的时候尽量简单,尽可能减少外部依赖,如图所示。

携程架构部开源的配置中心Apollo深度解读

 

上图简要描述了发送 ReleaseMessage 的大致过程:

  • Admin Service 在配置发布后会往 ReleaseMessage 表插入一条消息记录。

  • Config Service 会启动一个线程定时扫描 ReleaseMessage 表,来查看是否有新的消息记录。

  • Config Service 发现有新的消息记录,就会通知到所有的消息监听器。

  • 消息监听器得到配置发布的信息后,就会通知对应的客户端。

3. Config Service 通知客户端的实现方式

通知采用基于 Http 长连接实现,主要分为下面几个步骤:

  • 客户端会发起一个 Http 请求到 Config Service 的 notifications/v2 接口。

  • notifications/v2 接口通过 Spring DeferredResult 把请求挂起,不会立即返回。

  • 如果在 60s 内没有该客户端关心的配置发布,那么会返回 Http 状态码 304 给客户端。

  • 如果发现配置有修改,则会调用 DeferredResult 的 setResult 方法,传入有配置变化的 namespace 信息,同时该请求会立即返回。

  • 客户端从返回的结果中获取到配置变化的 namespace 后,会立即请求 Config Service 获取该 namespace 的最新配置。

4. 源码解析实时推送设计

Apollo 推送涉及的代码比较多,本教程就不做详细分析了,笔者把推送这里的代码稍微简化了下,给大家进行讲解,这样理解起来会更容易。

当然,这些代码比较简单,很多细节就不做考虑了,只是为了能够让大家明白 Apollo 推送的核心原理。

发送 ReleaseMessage 的逻辑我们就写一个简单的接口,用队列存储,测试的时候就调用这个接口模拟配置有更新,发送 ReleaseMessage 消息。具体代码如下所示。

@RestControllerpublic class NotificationControllerV2 implements ReleaseMessageListener {// 模拟配置更新, 向其中插入数据表示有更新public static Queue<String> queue = new LinkedBlockingDeque<>();  @GetMapping("/addMsg")  public String addMsg() {    queue.add("xxx");    return "success";  }}

消息发送之后,根据前面讲过的 Config Service 会启动一个线程定时扫描 ReleaseMessage 表,查看是否有新的消息记录,然后取通知客户端,在这里我们也会启动一个线程去扫描,具体代码如下所示。

@Componentpublic class ReleaseMessageScanner implements InitializingBean {  @Autowired  private NotificationControllerV2 configController;  @Override  public void afterPropertiesSet() throws Exception {    // 定时任务从数据库扫描有没有新的配置发布    new Thread(() -> {      for (;;) {        String result = NotificationControllerV2.queue.poll();        if (result != null) {        ReleaseMessage message = new ReleaseMessage();        message.setMessage(result);        configController.handleMessage(message);      }    }  }).start();  ;  }}

循环读取 NotificationControllerV2 中的队列,如果有消息的话就构造一个 Release-Message 的对象,然后调用 NotificationControllerV2 中的 handleMessage() 方法进行消息的处理。

ReleaseMessage 就一个字段,模拟消息内容,具体代码如下所示。

public class ReleaseMessage {  private String message;
  public void setMessage(String message) {    this.message = message;  }
  public String getMessage() {    return message;  }}

接下来,我们来看 handleMessage 做了哪些工作。

NotificationControllerV2 实现了 ReleaseMessageListener 接口,ReleaseMessageListener 中定义了 handleMessage() 方法,具体代码如下所示。

public interface ReleaseMessageListener {    void handleMessage(ReleaseMessage message);}

handleMessage 就是当配置发生变化的时候,发送通知的消息监听器。消息监听器在得到配置发布的信息后,会通知对应的客户端,具体代码如下所示。

@RestControllerpublic class NotificationControllerV2 implements ReleaseMessageListener {
  private final Multimap<String, DeferredResultWrapper> deferredResults = Multimaps.synchronizedSetMultimap(HashMultimap.create());

  @Override  public void handleMessage(ReleaseMessage message) {    System.err.println("handleMessage:" + message);    List<DeferredResultWrapper> results = Lists.newArrayList(deferredResults.get("xxxx"));    for (DeferredResultWrapper deferredResultWrapper : results) {      List<ApolloConfigNotification> list = new ArrayList<>();      list.add(new ApolloConfigNotification("application", 1));      deferredResultWrapper.setResult(list);    }  }}

Apollo 的实时推送是基于 Spring DeferredResult 实现的,在 handleMessage() 方法中可以看到是通过 deferredResults 获取 DeferredResult,deferredResults 就是第一行的 Multimap,Key 其实就是消息内容,Value 就是 DeferredResult 的业务包装类 DeferredResultWrapper,我们来看下 DeferredResultWrapper 的代码,代码如下所示。

public class DeferredResultWrapper {  private static final long TIMEOUT = 60 * 1000;// 60 seconds
  private static final ResponseEntity<List<ApolloConfigNotification>> NOT_MODIFIED_RESPONSE_LIST = new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
  private DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> result;
  public DeferredResultWrapper() {    result = new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE_LIST);  }
  public void onTimeout(Runnable timeoutCallback) {    result.onTimeout(timeoutCallback);  }

  public void onCompletion(Runnable completionCallback) {    result.onCompletion(completionCallback);  }
  public void setResult(ApolloConfigNotification notification) {    setResult(Lists.newArrayList(notification));  }
  public void setResult(List<ApolloConfigNotification> notifications) {    result.setResult(new ResponseEntity<>(notifications, HttpStatus.OK));  }
  public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> getResult() {    return result;  }}

通过 setResult() 方法设置返回结果给客户端,以上就是当配置发生变化,然后通过消息监听器通知客户端的原理,那么客户端是在什么时候接入的呢?具体代码如下。

@RestControllerpublic class NotificationControllerV2 implements ReleaseMessageListener {
// 模拟配置更新, 向其中插入数据表示有更新  public static Queue<String> queue = new LinkedBlockingDeque<>();  private final Multimap<String, DeferredResultWrapper> deferredResults = Multimaps.synchronizedSetMultimap(HashMultimap.create());

  @GetMapping("/getConfig")  public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> getConfig() {    DeferredResultWrapper deferredResultWrapper = new DeferredResultWrapper();    List<ApolloConfigNotification> newNotifications = getApolloConfigNotifications();    if (!CollectionUtils.isEmpty(newNotifications)) {      deferredResultWrapper.setResult(newNotifications);    } else {      deferredResultWrapper.onTimeout(() -> {    System.err.println("onTimeout");    });
  deferredResultWrapper.onCompletion(() -> {    System.err.println("onCompletion");  });  deferredResults.put("xxxx", deferredResultWrapper);  }  return deferredResultWrapper.getResult();  }
  private List<ApolloConfigNotification> getApolloConfigNotifications() {    List<ApolloConfigNotification> list = new ArrayList<>();    String result = queue.poll();    if (result != null) {    list.add(new ApolloConfigNotification("application", 1));  }    return list;  }}

NotificationControllerV2 中提供了一个 /getConfig 的接口,客户端在启动的时候会调用这个接口,这个时候会执行 getApolloConfigNotifications() 方法去获取有没有配置的变更信息,如果有的话证明配置修改过,直接就通过 deferredResultWrapper.setResult(newNotifications) 返回结果给客户端,客户端收到结果后重新拉取配置的信息覆盖本地的配置。

如果 getApolloConfigNotifications() 方法没有返回配置修改的信息,则证明配置没有发生修改,那就将 DeferredResultWrapper 对象添加到 deferredResults 中,等待后续配置发生变化时消息监听器进行通知。

同时这个请求就会挂起,不会立即返回,挂起是通过 DeferredResultWrapper 中的下面这部分代码实现的,具体代码如下所示。

private static final long TIMEOUT = 60 * 1000; // 60 seconds
private static final ResponseEntity<List<ApolloConfigNotification>> NOT_MODIFIED_RESPONSE_LIST           = new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
private DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> result;
public DeferredResultWrapper() {  result = new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE_LIST);}

在创建 DeferredResult 对象的时候指定了超时的时间和超时后返回的响应码,如果 60s 内没有消息监听器进行通知,那么这个请求就会超时,超时后客户端收到的响应码就是 304。

整个 Config Service 的流程就走完了,接下来我们来看一下客户端是怎么实现的,我们简单地写一个测试类模拟客户端注册,具体代码如下所示。

public class ClientTest {    public static void main(String[] args) {        reg();    }      private static void reg() {        System.err.println("注册");        String result = request("http://localhost:8081/getConfig");        if (result != null) {        // 配置有更新, 重新拉取配置        // ......        }        // 重新注册        reg();    }      private static String request(String url) {        HttpURLConnection connection = null;        BufferedReader reader = null;        try {            URL getUrl = new URL(url);            connection = (HttpURLConnection) getUrl.openConnection();            connection.setReadTimeout(90000);            connection.setConnectTimeout(3000);            connection.setRequestMethod("GET");            connection.setRequestProperty("Accept-Charset", "utf-8");            connection.setRequestProperty("Content-Type", "application/json");            connection.setRequestProperty("Charset", "UTF-8");            System.out.println(connection.getResponseCode());            if (200 == connection.getResponseCode()) {                reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));                StringBuilder result = new StringBuilder();                String line = null;                while ((line = reader.readLine()) != null) {                    result.append(line);                }                System.out.println("结果 " + result);                return result.toString();            }        } catch (IOException e) {            e.printStackTrace();        } finally {            if (connection != null) {                connection.disconnect();            }        }        return null;    }}

首先启动 /getConfig 接口所在的服务,然后启动客户端,然后客户端就会发起注册请求,如果有修改直接获取到结果,则进行配置的更新操作。如果无修改,请求会挂起,这里客户端设置的读取超时时间是 90s,大于服务端的 60s 超时时间。
每次收到结果后,无论是有修改还是无修改,都必须重新进行注册,通过这样的方式就可以达到配置实时推送的效果。
我们可以调用之前写的 /addMsg 接口来模拟配置发生变化,调用之后客户端就能马上得到返回结果。

Apollo客户端设计

携程架构部开源的配置中心Apollo深度解读

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

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

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

    • 这是一个fallback机制,为了防止推送机制失效导致配置不更新

    • 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified

    • 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。

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

  4. 客户端会把从服务端获取到的配置在本地文件系统缓存一份(在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置)

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

Apollo客户端用法

Apollo支持API方式和Spring整合方式,该怎么选择用哪一种方式?

  • API方式灵活,功能完备,配置值实时更新(热发布),支持所有Java环境。

  • Spring方式接入简单,结合Spring有N种酷炫的玩法,如

    • 代码中直接使用,如:@Value("${someKeyFromApollo:someDefaultValue}")

    • 配置文件中使用替换placeholder,如:spring.datasource.url: ${someKeyFromApollo:someDefaultValue}

    • 直接托管spring的配置,如在apollo中直接配置spring.datasource.url=jdbc:mysql://localhost:3306/somedb?characterEncoding=utf8

    • Placeholder方式:

    • Spring boot的@ConfigurationProperties方式

    • 从v0.10.0开始的版本支持placeholder在运行时自动更新,具体参见PR #972。(v0.10.0之前的版本在配置变化后不会重新注入,需要重启才会更新,如果需要配置值实时更新,可以参考后续3.2.2 Spring Placeholder的使用的说明)

  • Spring方式也可以结合API方式使用,如注入Apollo的Config对象,就可以照常通过API方式获取配置了:

    @ApolloConfig
    private Config config; //inject config for namespace application
  • 更多有意思的实际使用场景和示例代码,请参考apollo-use-cases

1、API使用方式

API方式是最简单、高效使用Apollo配置的方式,不依赖Spring框架即可使用。

获取默认namespace的配置(application)

Config config = ConfigService.getAppConfig(); //config instance is singleton for each namespace and is never nullString someKey = "someKeyFromDefaultNamespace";String someDefaultValue = "someDefaultValueForTheKey";String value = config.getProperty(someKey, someDefaultValue);

通过上述的config.getProperty可以获取到someKey对应的实时最新的配置值。

另外,配置值从内存中获取,所以不需要应用自己做缓存。

监听配置变化事件

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

如果只是希望每次都取到最新的配置的话,只需要按照上面的例子,调用config.getProperty即可。

Config config = ConfigService.getAppConfig(); //config instance is singleton for each namespace and is never nullconfig.addChangeListener(new ConfigChangeListener() {    @Override    public void onChange(ConfigChangeEvent changeEvent) {        System.out.println("Changes for namespace " + changeEvent.getNamespace());        for (String key : changeEvent.changedKeys()) {            ConfigChange change = changeEvent.getChange(key);            System.out.println(String.format("Found change - key: %s, oldValue: %s, newValue: %s, changeType: %s", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType()));        }    }});

获取公共Namespace的配置

String somePublicNamespace = "CAT";Config config = ConfigService.getConfig(somePublicNamespace); //config instance is singleton for each namespace and is never nullString someKey = "someKeyFromPublicNamespace";String someDefaultValue = "someDefaultValueForTheKey";String value = config.getProperty(someKey, someDefaultValue);

获取非properties格式namespace的配置

1.yaml/yml格式的namespace

apollo-client 1.3.0版本开始对yaml/yml做了更好的支持,使用起来和properties格式一致。

Config config = ConfigService.getConfig("application.yml");String someKey = "someKeyFromYmlNamespace";String someDefaultValue = "someDefaultValueForTheKey";String value = config.getProperty(someKey, someDefaultValue);

 

2.非yaml/yml格式的namespace

获取时需要使用ConfigService.getConfigFile接口并指定Format,如ConfigFileFormat.XML

String someNamespace = "test";ConfigFile configFile = ConfigService.getConfigFile("test", ConfigFileFormat.XML);String content = configFile.getContent();

Spring整合方式

配置

Apollo也支持和Spring整合(Spring 3.1.1+),只需要做一些简单的配置就可以了。

Apollo目前既支持比较传统的基于XML的配置,也支持目前比较流行的基于Java(推荐)的配置。

如果是Spring Boot环境,建议参照3.2.1.3 Spring Boot集成方式(推荐)配置。

需要注意的是,如果之前有使用org.springframework.beans.factory.config.PropertyPlaceholderConfigurer的,请替换成org.springframework.context.support.PropertySourcesPlaceholderConfigurer。Spring 3.1以后就不建议使用PropertyPlaceholderConfigurer了,要改用PropertySourcesPlaceholderConfigurer。

如果之前有使用<context:property-placeholder>,请注意xml中引入的spring-context.xsd版本需要是3.1以上(一般只要没有指定版本会自动升级的),建议使用不带版本号的形式引入,如:http://www.springframework.org/schema/context/spring-context.xsd

注1:yaml/yml格式的namespace从1.3.0版本开始支持和Spring整合,注入时需要填写带后缀的完整名字,比如application.yml

注2:非properties、非yaml/yml格式(如xml,json等)的namespace暂不支持和Spring整合。

基于XML的配置

注:需要把apollo相关的xml namespace加到配置文件头上,不然会报xml语法错误。

1.注入默认namespace的配置到Spring中

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:apollo="http://www.ctrip.com/schema/apollo"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd       http://www.ctrip.com/schema/apollo http://www.ctrip.com/schema/apollo.xsd">    <!-- 这个是最简单的配置形式,一般应用用这种形式就可以了,用来指示Apollo注入application namespace的配置到Spring环境中 -->    <apollo:config/>    <bean class="com.ctrip.framework.apollo.spring.TestXmlBean">        <property name="timeout" value="${timeout:100}"/>        <property name="batch" value="${batch:200}"/>    </bean></beans>

2.注入多个namespace的配置到Spring中

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:apollo="http://www.ctrip.com/schema/apollo"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd       http://www.ctrip.com/schema/apollo http://www.ctrip.com/schema/apollo.xsd">    <!-- 这个是最简单的配置形式,一般应用用这种形式就可以了,用来指示Apollo注入application namespace的配置到Spring环境中 -->    <apollo:config/>    <!-- 这个是稍微复杂一些的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring环境中 -->    <apollo:config namespaces="FX.apollo,application.yml"/>    <bean class="com.ctrip.framework.apollo.spring.TestXmlBean">        <property name="timeout" value="${timeout:100}"/>        <property name="batch" value="${batch:200}"/>    </bean></beans>

3.注入多个namespace,并且指定顺序

Spring的配置是有顺序的,如果多个property source都有同一个key,那么最终是顺序在前的配置生效。

apollo:config如果不指定order,那么默认是最低优先级。

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:apollo="http://www.ctrip.com/schema/apollo"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd       http://www.ctrip.com/schema/apollo http://www.ctrip.com/schema/apollo.xsd">    <apollo:config order="2"/>    <!-- 这个是最复杂的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring环境中,并且顺序在application前面 -->    <apollo:config namespaces="FX.apollo,application.yml" order="1"/>    <bean class="com.ctrip.framework.apollo.spring.TestXmlBean">        <property name="timeout" value="${timeout:100}"/>        <property name="batch" value="${batch:200}"/>    </bean></beans>

基于Java的配置(推荐)

相对于基于XML的配置,基于Java的配置是目前比较流行的方式。

注意@EnableApolloConfig要和@Configuration一起使用,不然不会生效。

1.注入默认namespace的配置到Spring中

//这个是最简单的配置形式,一般应用用这种形式就可以了,用来指示Apollo注入application namespace的配置到Spring环境中@Configuration@EnableApolloConfigpublic class AppConfig {  @Bean  public TestJavaConfigBean javaConfigBean() {    return new TestJavaConfigBean();  }}

2.注入多个namespace的配置到Spring中

@Configuration@EnableApolloConfigpublic class SomeAppConfig {http://www.jintianxuesha.com/  @Bean  public TestJavaConfigBean javaConfigBean() {    return new TestJavaConfigBean();  }}
//这个是稍微复杂一些的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring环境中@Configuration@EnableApolloConfig({"FX.apollo", "application.yml"})public class AnotherAppConfig {}

3.注入多个namespace,并且指定顺序

//这个是最复杂的配置形式,指示Apollo注入FX.apollo和application.yml namespace的配置到Spring环境中,并且顺序在application前面@Configuration@EnableApolloConfig(order = 2)public class SomeAppConfig {  @Bean  public TestJavaConfigBean javaConfigBean() {    return new TestJavaConfigBean();  }}@Configuration@EnableApolloConfig(value = {"FX.apollo", "application.yml"}, order = 1)public class AnotherAppConfig {}

Spring Boot集成方式(推荐)

Spring Boot除了支持上述两种集成方式以外,还支持通过application.properties/bootstrap.properties来配置,该方式能使配置在更早的阶段注入,比如使用@ConditionalOnProperty的场景或者是有一些spring-boot-starter在启动阶段就需要读取配置做一些事情(如dubbo-spring-boot-project),所以对于Spring Boot环境建议通过以下方式来接入Apollo(需要0.10.0及以上版本)。

使用方式很简单,只需要在application.properties/bootstrap.properties中按照如下样例配置即可。

注入默认application namespace的配置示例

  #will inject 'application' namespace in bootstrap phase  apollo.bootstrap.enabled = true

注入非默认application namespace或多个namespace的配置示例

  apollo.bootstrap.enabled = true  # will inject 'application', 'FX.apollo' and 'application.yml' namespaces in bootstrap phase  apollo.bootstrap.namespaces = application,FX.apollo,application.yml

将Apollo配置加载提到初始化日志系统之前(1.2.0+)

从1.2.0版本开始,如果希望把日志相关的配置(如logging.level.root=infologback-spring.xml中的参数)也放在Apollo管理,那么可以额外配置apollo.bootstrap.eagerLoad.enabled=true来使Apollo的加载顺序放到日志系统加载之前,不过这会导致Apollo的启动过程无法通过日志的方式输出(因为执行Apollo加载的时候,日志系统压根没有准备好呢!所以在Apollo代码中使用Slf4j的日志输出便没有任何内容),更多信息可以参考PR 1614。参考配置示例如下:

  # will inject 'application' namespace in bootstrap phase  apollo.bootstrap.enabled = true  # put apollo initialization before logging system initialization  apollo.bootstrap.eagerLoad.enabled=true

Spring Placeholder的使用

Spring应用通常会使用Placeholder来注入配置,使用的格式形如{someKey:someDefaultValue},如someKey:someDefaultValue,如{timeout:100}。冒号前面的是key,冒号后面的是默认值。

建议在实际使用时尽量给出默认值,以免由于key没有定义导致运行时错误。

从v0.10.0开始的版本支持placeholder在运行时自动更新,具体参见PR #972。

如果需要关闭placeholder在运行时自动更新功能,可以通过以下两种方式关闭:

1. 通过设置System Property apollo.autoUpdateInjectedSpringProperties,如启动时传入-Dapollo.autoUpdateInjectedSpringProperties=false

2.通过设置META-INF/app.properties中的apollo.autoUpdateInjectedSpringProperties属性,如

上一篇:Apollo自动驾驶实践——第0讲:导论


下一篇:分布式配置中心Apollo