Apollo(阿波罗)是携程开源的分布式配置中心,可以集中化管理应用不一样环境、不一样集群的配置,支持配置热发布并实时推送到应用端,而且具有规范的权限及流程治理等特性,适用于分布式微服务配置管理场景java
程序功能日益复杂,程序配置日益增多:各类功能开关、参数配置、服务器地址...对程序配置的指望也愈来愈高:热部署并实时生效、灰度发布、分环境分集群管理配置、完善的权限审核机制...在这样的背景下,Apollo配置中心应运而生。Apollo支持四个维度Key-Value格式的配置* Application(应用) 实际使用配置的应用,Apollo客户端在运行时须要知道当前应用是谁,从而能够去获取对应的配置。每一个应用都有对应的身份标识--appId,须要在代码中配置数据库
Apollo在建立项目的时候,都会默认建立一个"application"的Namespace,"application"是个应用自身使用的。例如Spring Boot中项目的默认配置文件application.yaml,这里application.yaml就等同于"application"的Namespace。对于大多数应用来讲,"application"Namespace已经能知足平常配置使用场景json
客户端获取"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
复制代码
应用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对象上app
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的最简架构以下图所示

要点
* 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服务注册中心组件,实现微服务间的服务注册和发现,更新后的架构以下图所示

要点
* 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集群
引入MetaServer和NginxLB以后的架构以下图
Apollo架构V5
还剩下最后一个环节,Portal也是无状态的以集群方式部署的,用户如何发现和访问Portal?答案也是简单的传统作法,用户经过域名+NginxLB间接访问Portal集群。因此V5版本是包括用户端的最终的Apollo架构全貌,以下图所示
配置发布后的实时推送设计 在配置中心中,一个重要的功能就是配置发布后实时推送到客户端。下面咱们简要看一下这块是怎么设计实现的
复制代码
上图简要描述了配置发布的大体过程
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后,会通知对应的客户端
示意图以下
复制代码
Config Service通知客户端的实现方式 消息监听器在得知有新的配置发布后是如何通知到客户端的呢?其实现方式以下
客户端设计上图简要描述了Apollo客户端的实现原理
名称解析
普通应用接入指南
公共组件接入步骤公共组件接入步骤几乎与普通应用接入一致,惟一的区别是公共组件须要创建本身的惟一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复制代码
**参数说明 **
复制代码
**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复制代码
**参数说明**
复制代码
该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机制实时推送,实现应用感知配置更新