随着程序功能的日益复杂,程序的配置日益增多:各种功能的开关、参数的配置、服务器的地址……
对程序配置的期望值也越来越高:配置修改后实时生效,灰度发布,分环境、分集群管理配置,完善的权限、审核机制……
在这样的大环境下,传统的通过配置文件、数据库等方式已经越来越无法满足开发人员对配置管理的需求。
想必大家都深有体会,我们做的项目都伴随着各种配置文件,而且总是本地配置一套配置文件,测试服配置一套,正式服配置一套,有时候一不小心就改错了,挨骂是小事,扣绩效那可就闹大了。
而且每当项目发布的时候,配置文件也会被打包进去,也就是配置文件会跟着项目一起发布。然后每次出现问题需要我们修改配置文件的时候,我们总是得先在本地修改,然后重新发布才能让新的配置生效。
当请求压力越来越大,你的项目也会从 1 个节点变成多个节点,这个时候如果配置需要发生变化,对应的修改操作也是相同的,只需要在项目中修改一次即可,但对于发布操作工作量就比之前大了很多,因为要发布多个节点。
修改这些配置,增加的发布的工作量降低了整体的工作效率,为了能够提升工作效率,配置中心应运而生了,我们可以将配置统一存放在配置中心来进行管理。
总体而言在没有引入配置中心之前,我们都会面临以下问题:
-
配置散乱格式不标准,有的用 properties 格式,有的用 xml 格式,还有的存 DB,团队倾向自造*,做法五花八门。
-
主要采用本地静态配置,配置修改麻烦,配置修改一般需要经过一个较长的测试发布周期。在分布式微服务环境下,当服务实例很多时,修改配置费时费力。
-
易引发生产事故,这个是我亲身经历,之前在一家互联网公司,有团队在发布的时候将测试环境的配置带到生产上,引发百万级资损事故。
-
配置缺乏安全审计和版本控制功能,谁改的配置?改了什么?什么时候改的?无从追溯,出了问题也无法及时回滚。
-
增加了运维小哥哥的工作量,极大的损害了运维小哥哥和开发小哥哥的基情。
到底什么是配置中心
配置中心就是把项目中各种个样的配置、参数、开关,全部都放到一个集中的地方进行统一管理,并提供一套标准的接口。当各个服务需要获取配置的时候,就来配置中心的接口拉取。当配置中心中的各种参数有更新的时候,也能通知到各个服务实时的过来同步最新的信息,使之动态更新。
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的基本工作流程如下图所示
1.用户在配置中心对配置进行修改并发布
2.配置中心通知Apollo客户端有配置更新
3.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. 配置发布后的实时推送设计
配置中心最重要的一个特性就是实时推送,正因为有这个特性,我们才可以依赖配置中心做很多事情。如图所示。
图 1 简要描述了配置发布的大致过程。
-
用户在 Portal 中进行配置的编辑和发布。
-
Portal 会调用 Admin Service 提供的接口进行发布操作。
-
Admin Service 收到请求后,发送 ReleaseMessage 给各个 Config Service,通知 Config Service 配置发生变化。
-
Config Service 收到 ReleaseMessage 后,通知对应的客户端,基于 Http 长连接实现。
2. 发送 ReleaseMessage 的实现方式
ReleaseMessage 消息是通过 Mysql 实现了一个简单的消息队列。之所以没有采用消息中间件,是为了让 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 消息。具体代码如下所示。
@RestController
public class NotificationControllerV2 implements ReleaseMessageListener {
// 模拟配置更新, 向其中插入数据表示有更新
public static Queue<String> queue = new LinkedBlockingDeque<>();
@GetMapping("/addMsg")
public String addMsg() {
queue.add("xxx");
return "success";
}
}
消息发送之后,根据前面讲过的 Config Service 会启动一个线程定时扫描 ReleaseMessage 表,查看是否有新的消息记录,然后取通知客户端,在这里我们也会启动一个线程去扫描,具体代码如下所示。
@Component
public 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 就是当配置发生变化的时候,发送通知的消息监听器。消息监听器在得到配置发布的信息后,会通知对应的客户端,具体代码如下所示。
@RestController
public 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() 方法设置返回结果给客户端,以上就是当配置发生变化,然后通过消息监听器通知客户端的原理,那么客户端是在什么时候接入的呢?具体代码如下。
@RestController
public 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客户端的实现原理:
-
客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过Http Long Polling实现)
-
客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
-
这是一个fallback机制,为了防止推送机制失效导致配置不更新
-
客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
-
定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。
-
-
客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
-
客户端会把从服务端获取到的配置在本地文件系统缓存一份(在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置)
-
应用程序可以从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 null
String 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 null
config.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 null
String 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
@EnableApolloConfig
public class AppConfig {
@Bean
public TestJavaConfigBean javaConfigBean() {
return new TestJavaConfigBean();
}
}
2.注入多个namespace的配置到Spring中
@Configuration
@EnableApolloConfig
public 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=info
或logback-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
属性,如