Apollo(阿波罗)是携程开源的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,支持配置热发布并实时推送到应用端,并且具备规范的权限及流程治理等特性,适用于分布式微服务配置管理场景
程序功能日益复杂,程序配置日益增多:各种功能开关、参数配置、服务器地址...对程序配置的期望也越来越高:热部署并实时生效、灰度发布、分环境分集群管理配置、完善的权限审核机制...在这样的背景下,Apollo配置中心应运而生。Apollo支持四个维度Key-Value格式的配置* Application(应用) 实际使用配置的应用,Apollo客户端在运行时需要知道当前应用是谁,从而可以去获取对应的配置。每个应用都有对应的身份标识--appId,需要在代码中配置
Apollo在创建项目的时候,都会默认创建一个"application"的Namespace,"application"是个应用自身使用的。例如Spring Boot中项目的默认配置文件application.yaml,这里application.yaml就等同于"application"的Namespace。对于大多数应用来说,"application"Namespace已经能满足日常配置使用场景
客户端获取"application"Namespace的代码如下
Config config = ConfigService.getAppConfig()
客户端获取非"application"Namespace的代码如下
Config config = ConfigService.getConfig(namespaceName)
Namespace的格式 配置文件有多种格式,properties、xml、yml、yaml、json等,同样Namespace也具有这些格式tips: 非properties格式的namespace,在客户端使用时需要调用ConfigService.getConfigFile(String namespace, ConfigFileFormat configFileFormat)
来获取,如果使用Htpp接口直接调用时,对应的namespace参数需要传入namespace的名字加上后缀名,如datasource.jsonNamespace的获取权限分类 此处权限相是对于Apollo客户端来说的private(私有的)权限 private权限的Namespace,只能被所属的应用获取到。一个应用尝试获取其他应用private的Namespace,Apollo客户端会报"404"异常
public(公共的)权限 具有public权限的Namespace,能被任何应用获取
Namespace的类型
使用场景 部门级别共享的配置、小组级别共享的配置、几个项目之间共享的配置、中间件客户端的配置
k1 = v1
k2 = v2
然后应用A有一个关联类型的Namespace关联此公共Namespace,且以新值v3覆盖配置项k1。那么在应用A实际运行时,获取到的公共Namespace的配置为
k1 = v3
k2 = v2
使用场景 假设RPC框架的配置(如:timeout)有以下要求
* 提供一份全公司默认的配置,且可动态调整
* RPC客户端项目可以自定义某些配置项且可动态调整
结合Apollo的公共类型的Namespace和关联类型的Namespace。RPC团队在Apollo上维护一个叫“rpc-client”的公共Namespace,在"rpc-client"Namespace上配置默认的参数值。rpc-client.jar里的代码读取"rpc-client"Namespace的配置即可;如需要调整默认的配置,只需要修改公共类型"rpc-client"Namespace的配置;如果客户端项目想要自定义或动态修改某些配置项,只需要在Apollo自己项目下关联"rpc-client",就能创建关联类型"rpc-client"的Namespace,然后在关联类型下修改配置项即可。这里rpc-client.jar是在应用容器里运行的,所以rpc-client获取到"rpc-client"Namespace的配置是应用的关联类型的Namespace加上公共类型的Namespace
例子 如下图,有三个应用:应用A、应用B、应用C
应用A有两个私有类型的Namespace:application和NS-Private,以及一个关联类型的Namespace:NS-Public
应用B有一个私有类型的Namespace:application,以及一个公共类型的Namespace:NS-Public
应用C只有一个私有类型的Namespace:application
![](https://img-blog.csdnimg.cn/20190929223513136.jpeg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1N1eTFfXw==,size_16,color_FFFFFF,t_70)
应用A获取Apollo配置
//application
Config appConfig = ConfigService.getAppConfig();
appConfig.getProperty("k1", null); // k1 = v11
appConfig.getProperty("k2", null); // k2 = v21
//NS-Private
Config privateConfig = ConfigService.getConfig("NS-Private");
privateConfig.getProperty("k1", null); // k1 = v3
privateConfig.getProperty("k3", null); // k3 = v4
//NS-Public,覆盖公共类型配置的情况,k4被覆盖
Config publicConfig = ConfigService.getConfig("NS-Public");
publicConfig.getProperty("k4", null); // k4 = v6 cover
publicConfig.getProperty("k6", null); // k6 = v6
publicConfig.getProperty("k7", null); // k7 = v7
应用B获取Apollo配置
//application
Config appConfig = ConfigService.getAppConfig();
appConfig.getProperty("k1", null); // k1 = v12
appConfig.getProperty("k2", null); // k2 = null
appConfig.getProperty("k3", null); // k3 = v32
//NS-Private,由于没有NS-Private Namespace 所以获取到default value
Config privateConfig = ConfigService.getConfig("NS-Private");
privateConfig.getProperty("k1", "default value");
//NS-Public
Config publicConfig = ConfigService.getConfig("NS-Public");
publicConfig.getProperty("k4", null); // k4 = v5
publicConfig.getProperty("k6", null); // k6 = v6
publicConfig.getProperty("k7", null); // k7 = v7
应用C获取Apollo配置
//application
Config appConfig = ConfigService.getAppConfig();
appConfig.getProperty("k1", null); // k1 = v12
appConfig.getProperty("k2", null); // k2 = null
appConfig.getProperty("k3", null); // k3 = v33
//NS-Private,由于没有NS-Private Namespace 所以获取到default value
Config privateConfig = ConfigService.getConfig("NS-Private");
privateConfig.getProperty("k1", "default value");
//NS-Public,公共类型的Namespace,任何项目都可以获取到
Config publicConfig = ConfigService.getConfig("NS-Public");
publicConfig.getProperty("k4", null); // k4 = v5
publicConfig.getProperty("k6", null); // k6 = v6
publicConfig.getProperty("k7", null); // k7 = v7
ChangeListener 以上代码可以看出,在客户端Namespace映射成一个Config对象,Namespace配置变更的监听器是注册在Config对象上
Config appConfig = ConfigService.getAppConfig();appConfig.addChangeListener(new ConfigChangeListener() {public void onChange(ConfigChangeEvent changeEvent) {//do something}})
在应用A中监听 NS-Private 的 Namespace代码如下
Config privateConfig = ConfigService.getConfig("NS-Private");
privateConfig.addChangeListener(new ConfigChangeListener() {
public void onChange(ConfigChangeEvent changeEvent) {
//do something
}
})
## 在应用A、应用B和应用C中监听NS-Public Namespace代码如下
Config publicConfig = ConfigService.getConfig("NS-Public");
publicConfig.addChangeListener(new ConfigChangeListener() {
public void onChange(ConfigChangeEvent changeEvent) {
//do something
}
})
配置的几大属性
配置获取规则 仅当应用自定义了集群或namespace才需要。有了cluster概念后,配置的规则就显得重要了,比如应用部署在A机房,但是并没有在Apollo新建cluster或者在运行时指定了cluster=SomeCluster,但是并没有在Apollo新建cluster,这时候Apollo的行为是怎样的?下面介绍配置获取的规则应用自身配置的获取规则当应用使用下面的语句获取配置时,称之为获取应用自身的配置,也就是应用自身的application namespace的配置
Config config = ConfigService.getAppConfig();
这种情况的配置获取规则简而言之如下
所以,如果应用部署在A数据中心,但是用户没有在Apollo创建cluster,那么获取的配置就是默认cluster(default)的;如果应用部署在A数据中心,同时在运行时指定了apollo.cluster=SomeCluster,但是没有在Apollo创建cluster,那么获取的配置就是A数据中心cluster的配置,如果A数据中心cluster没有配置的话,那么获取的配置就是默认cluster(default)的
* 公共组件配置的获取规则
以`FX.Hermes.Producer`为例,hermes producer是hermes发布的公共组件。当使用下面的语句获取配置时,称之为获取公共组件的配置
Config config = ConfigService.getConfig("FX.Hermes.Producer")
对于这种情况获取配置规则,简而言之如下
FX.Hermes.Producer
namespace的配置 FX.Hermes.Producer
namespace的配置 通过这种方式实现对框架组件的配置管理,框架组件提供方提供配置的默认值,应用如果有特殊需求可以自行覆盖
总体设计
**Apollo架构V1** 如果不考虑分布式微服务架构中的服务发现问题,Apollo的最简架构如下图所示
![Apollo架构V1](https://img-blog.csdnimg.cn/20190929223514974.jpeg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1N1eTFfXw==,size_16,color_FFFFFF,t_70)
要点
* ConfigService是一个独立的微服务,服务于Client进行配置获取
* Client和ConfigService保持长连接,通过一种推拉结合(push & pull)的模式,在实现配置实时更新的同时,保证配置更新不丢失
* AdminService是一个独立的微服务,服务于Portal进行配置管理。Portal通过调用AdminService进行配置管理和发布
* ConfigService和AdminService共享ConfigDB,ConfigDB中存放项目在某个环境中的配置信息。ConfigService/AdminService/ConfigDB三者在每个环境(DEV/FAT/UAT/PRO)中都要部署一份
* Protal有一个独立的PortalDB,存放用户权限、项目和配置的元数据信息。Protal只需部署一份,它可以管理多套环境
**Apollo架构 V2** 为了保证高可用,ConfigService和AdminService都是无状态以集群方式部署的,这时候就存在一个服务发现的问题:Client怎么找到ConfigService?Portal怎么找到AdminService?为了解决这个问题,Apollo在其架构中引入Eureka服务注册中心组件,实现微服务间的服务注册和发现,更新后的架构如下图所示
![Apollo架构V2](https://img-blog.csdnimg.cn/20190929223515253.jpeg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1N1eTFfXw==,size_16,color_FFFFFF,t_70)
要点
* ConfigService和AdminService启动后都会注册到Eureka服务注册中心,并定期发送存活心跳
* Eureka采用集群方式部署,使用分布式一致性协议保证每个实例的状态最终一致
Apollo架构V3 Eureka是自带服务发现的Java客户端的,如果Apollo只支持Java客户端接入,不支持其它语言客户端接入的话,那么Client和Portal只需要引入Eureka的Java客户端,就可以实现服务发现功能。发现目标服务后,通过客户端软负载(SLB,例如Ribbon)就可以路由到目标服务实例。这是一个经典的微服务架构,基于Eureka实现服务注册发现 客户端Ribbon配合实现软路由,如下图所示
Apollo架构V4
为支持多语言客户端接入,Apollo引入MetaServer角色,它其实是一个Eureka的Proxy,将Eureka的服务发现接口以更简单明确的HTTP接口的形式暴露出来,方便Client/Protal通过简单的HTTPClient就可以查询到ConfigService/AdminService的地址列表。获取到服务实例地址列表之后,再以简单的客户端软负载(Client SLB)策略路由定位到目标实例,并发起调用
另一个问题,MetaServer本身也是无状态以集群方式部署的,那么Client/Protal该如何发现MetaServer呢?一种传统的做法是借助硬件或者软件负载均衡器,在携程采用的是扩展后的NginxLB(Software Load Balancer),由运维为MetaServer集群配置一个域名,指向NginxLB集群,NginxLB再对MetaServer进行负载均衡和流量转发。Client/Portal通过域名 NginxLB间接访问MetaServer集群
Apollo架构V5
还剩下最后一个环节,Portal也是无状态的以集群方式部署的,用户如何发现和访问Portal?答案也是简单的传统做法,用户通过域名 NginxLB间接访问Portal集群。所以V5版本是包括用户端的最终的Apollo架构全貌,如下图所示
配置发布后的实时推送设计 在配置中心中,一个重要的功能就是配置发布后实时推送到客户端。下面我们简要看一下这块是怎么设计实现的
![服务端设计](https://img-blog.csdnimg.cn/20190929223516857.jpeg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1N1eTFfXw==,size_16,color_FFFFFF,t_70)
上图简要描述了配置发布的大致过程
1. 用户在Portal操作发布配置
2. Portal调用Admin Service的接口操作发布
3. Admin Service发布配置后,发送ReleaseMessage给各Config Service
4. Config Service收到ReleaseMessage后通知对应的客户端
**发送ReleaseMessage的实现方式** Admin Service在配置发布后,需要通知所有的Config Service有配置发布,从而Config Service可以通知对应的客户端来拉取最新的配置。从概念上看,这是一个典型的消息使用场景,Admin Service作为Producer发出消息,各个Config Service作为consumer消费消息。通过一个消息组件(Message Queue)就能很好地实现Admin Service和Config Service的解耦。在实现上,Apollo为尽量减少外部依赖,没有采用外部的消息中间件,而是通过数据库实现了一个简单的消息队列
实现方式如下
1. Admin Service在配置发布后会往ReleaseMessage表插入一条消息记录,消息内容就是配置发布的AppId Cluster Namespace
2. Config Service有一个线程会每秒扫描一次ReleaseMessage表,看是否有新的消息记
3. Config Service如果发现有新的消息记录,那么会通知到所有的消息监听器(ReleaseMessageListener),例如NotificationControllerV2
4. 消息监听器得到配置发布的AppId Cluster Namespace后,会通知对应的客户端
示意图如下
![配置更新通知](https://img-blog.csdnimg.cn/20190929223517170.jpeg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1N1eTFfXw==,size_16,color_FFFFFF,t_70)
Config Service通知客户端的实现方式 消息监听器在得知有新的配置发布后是如何通知到客户端的呢?其实现方式如下
名称解析
普通应用接入指南
公共组件接入步骤公共组件接入步骤几乎与普通应用接入一致,唯一的区别是公共组件需要建立自己的唯一Namespace
1.创建项目
2.项目管理员权限
3.创建Namespace
4.添加配置项
5.发布配置
6.应用读取配置
应用覆盖公共组件配置步骤
1.关联公共组件Namespace
2.覆盖公共组件配置
3.发布配置
多个AppId共享同一份配置在一些情况下,尽管应用本身不是公共组件,但还是需要在多个AppId之间共用同一份配置,这种情况下如果希望实现多个AppId使用同一份配置的话,基本概念和公共组件的配置是一致的。具体来说,就是在其中一个AppId下创建一个namespace,写入公共的配置信息,然后在各个项目中读取该namespace的配置即可;如果某个AppId需要覆盖公共的配置信息,那么在该AppId下关联公共的namespace并写入需要覆盖的配置即可
应用接入策略这里考虑非Java语言客户端接入--直接通过Http接口获取配置
**HTTP接口说明**
**URL** {config_server_url}/configfiles/json/{appId}/{clusterName}/{namespaceName}?ip={clientIp}
**Method** GET
**参数说明 **
![file](https://img-blog.csdnimg.cn/20190929223517956.jpeg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1N1eTFfXw==,size_16,color_FFFFFF,t_70)
**HTTP接口返回格式** 该HTTP接口返回的是JSON格式、UTF-8编码,包含了对应namespace中所有的配置项。返回内容Sample如下
{
"portal.elastic.document.type":"biz",
"portal.elastic.cluster.name":"hermes-es-fws"
}
*TIPS 通过{configserverurl}/configfiles/{appId}/{clusterName}/{namespaceName}?ip={clientIp}可以获取到properties形式的配置*
* 不带缓存的HTTP接口从Apollo读取配置
该接口会直接从数据库中获取配置,可以配合配置推送通知实现实时更新配置
**URL** {config_server_url}/configs/{appId}/{clusterName}/{namespaceName}?releaseKey={releaseKey}&ip={clientIp}
**Method** GET
**参数说明**
![file](https://img-blog.csdnimg.cn/20190929223518610.jpeg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1N1eTFfXw==,size_16,color_FFFFFF,t_70)
该HTTP接口返回的是JSON格式、UTF-8编码。如果配置没有变化(传入的releaseKey和服务端的相等),则返回HttpStatus 304,Response Body为空;如果配置有变化,则会返回HttpStatus 200,Response Body为对应namespace的meta信息以及其中所有的配置项。返回内容Sample如下
{
"appId": "100004458",
"cluster": "default",
"namespaceName": "application",
"configurations": {
"portal.elastic.document.type":"biz",
"portal.elastic.cluster.name":"hermes-es-fws"
},
"releaseKey": "20170430092936-dee2d58e74515ff3"
}
**配置更新推送实现思路** 建议参考Apollo的Java实现RemoteConfigLongPollService.java
初始化 首先需要确定哪些namespace需要配置更新推送,Apollo的实现方式是程序第一次获取某个namespace的配置时就会来注册一下,我们就知道有哪些namespace需要配置更新推送了。初始化后的结果就是得到一个notifications的Map,内容是namespaceName -> notificationId(初始值为-1)。运行过程中如果发现有新的namespace需要配置更新推送,直接塞到notifications这个Map里面即可
请求服务 有了notifications这个Map之后,就可以请求服务了。这里先描述一下请求服务的逻辑,具体的URL参数和说明请参见后面的接口说明
1.请求远端服务,带上自己的应用信息以及notifications信息
2.服务端针对传过来的每一个namespace和对应的notificationId,检查notificationId是否是最新的
3.如果都是最新的,则保持住请求60秒,如果60秒内没有配置变化,则返回HttpStatus 304。如果60秒内有配置变化,则返回对应namespace的最新notificationId, HttpStatus 200
4.如果传过来的notifications信息中发现有notificationId比服务端老,则直接返回对应namespace的最新notificationId, HttpStatus 200
5.客户端拿到服务端返回后,判断返回的HttpStatus
6.如果返回的HttpStatus是304,说明配置没有变化,重新执行第1步
7.如果返回的HttpStauts是200,说明配置有变化,针对变化的namespace重新去服务端拉取配置,参见1.3 通过不带缓存的Http接口从Apollo读取配置。同时更新notifications map中的notificationId。重新执行第1步
HTTP接口说明
URL {config_server_url}/notifications/v2?appId={appId}&cluster={clusterName}¬ifications={notifications}
Method GET
参数说明
TIPS 由于服务端会hold住60秒,所以请确保客户端访问服务端的超时时间要大于60秒;记得对参数进行URL Encode
HTTP返回格式 该Http接口返回的是JSON格式、UTF-8编码,包含了有变化的namespace和最新的notificationId。返回内容Sample如下
[{
"namespaceName": "application",
"notificationId": 101
}]
官方展示的部署策略,生产环境部署一套Apollo-Portal ApolloPortalDB,其他环境(PRO、UAT、FAT、DEV)单独部署MetaServer AdminService ConfigService,使用独立数据库ApolloConfigDB及应用服务;MetaServer和Config Service部署在同一个JVM进程内,Admin Service部署在同一台服务器的另一个JVM进程内。部署示例如下图网络策略 分布式部署的时候,apollo-configservice和apollo-adminservice需要把自己的IP和端口注册到Meta Server(apollo-configservice本身)。Apollo客户端和Portal会从Meta Server获取服务的地址(IP PORT),然后通过服务地址直接访问。apollo-configservice和apollo-adminservice是基于内网可信网络设计的,所以出于安全考虑,请不要将apollo-configservice和apollo-adminservice直接暴露在公网
部署步骤
创建数据库 Apollo服务端依赖于MYSQL数据库,所以需要事先创建并完成初始化
获取安装包 Apollo服务端安装包共3个: Apollo-AdminService、Apollo-ConfigService、Apollo-Portal
部署Apollo服务端 获取安装包后就可以部署到测试和生产环境
文章较为全面介绍开源分布式配置中心Apollo的设计、使用、应用接入及部署方法,目前客户端只有Java和.Net版本,其他语言客户端的接入可以通过HTTP接口的方式定时拉取更新配置或通过Http Long Polling机制实时推送,实现应用感知配置更新