架构分析
官网:Apollo配置中心
源码项目目录图:
三个服务分别如下:
apollo-adminservice:提供配置管理接口服务;服务于管理界面Portal
apollo-configservice:提供配置获取、推送接口服务;服务于Apollo客户端
apollo-portal:管理界面服务;有自己单独权限控制相关的数据库;
其他模块:
apollo-biz:adminservice与configservice业务代码模块,service\entity\dao等;
apollo-common:公共模块,提供adminservice、configservice、portal公共实体类、DTO等代码;
apollo-assembly:快速部署adminservice、configservice、portal服务工具;
apollo-buildtools:顾名思义,构建工具
配置发布流程
下面与一个配置发布的流程,对系统和关键源码进行分析:
MeteServer:apollo考虑到有的客户端的语言并不是JAVA,所以不好通过集成Erueka客户端进行服务发现,为了解决这个问题,新增了MeteServer对Eureka客户端进行封装,Client通过访问MeteServer接口获取注册在Erueka上面的ConfigService服务地址;
NginxLB:软负载SLB,因为MeteServer可以集群部署,虽然apollo的xxx.meta=http://1.2.3:8080,http://1.2.3:8080 提供多ip配置,但考虑到MeteServer的缩容扩容,apollo建议使用软负载SLB配置(比如配置为nginx域名,再通过nginx转发);
Client发现服务(apollo-client)
-
根据系统配置的环境或者/opt/settings/service.properties获取环境env;
-
根据环境从apollo-env.properties文件中获取对应环境的MetaServer服务链接;
-
通过MetaServer服务链接访问/services/config接口,获取注册在Eureka上面所有的ConfigService服务并保存在本地内存;
-
除此之外,Client还会开启一个定时器去刷新本地内存的ConfigService服务,以便剔除或增加下线或新上线的服务;
private synchronized void updateConfigServices() { //获取MetaServer + /services/config完整链接 String url = assembleMetaServiceUrl(); HttpRequest request = new HttpRequest(url); int maxRetries = 2; Throwable exception = null; //请求接口,获取服务信息,保存到本地内存 for (int i = 0; i < maxRetries; i++) { Transaction transaction = Tracer.newTransaction("Apollo.MetaService", "getConfigService"); transaction.addData("Url", url); try { HttpResponse<List<ServiceDTO>> response = m_httpUtil.doGet(request, m_responseType); transaction.setStatus(Transaction.SUCCESS); List<ServiceDTO> services = response.getBody(); if (services == null || services.isEmpty()) { logConfigService("Empty response!"); continue; } setConfigServices(services); return; } catch (Throwable ex) { Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex)); transaction.setStatus(ex); exception = ex; } finally { transaction.complete(); } try { m_configUtil.getOnErrorRetryIntervalTimeUnit().sleep(m_configUtil.getOnErrorRetryInterval()); } catch (InterruptedException ex) { //ignore } }
MeteServer发现服务(apollo-configservice)
-
根据serviceId使用EurekaClient从Eureka上面获取该serviceId下所有的服务实例;
@Override public List<ServiceDTO> getServiceInstances(String serviceId) { Application application = eurekaClient.getApplication(serviceId); if (application == null || CollectionUtils.isEmpty(application.getInstances())) { Tracer.logEvent("Apollo.Discovery.NotFound", serviceId); return Collections.emptyList(); } return application.getInstances().stream().map(instanceInfoToServiceDTOFunc) .collect(Collectors.toList()); }
发送ReleaseMessage
AdminService在配置发布后,需要通知所有的ConfigService有配置发布,从而ConfigService可以通知对应的客户端来拉取最新的配置。
从概念上来看,这是一个典型的消息使用场景,AdminService作为producer发出消息,各个ConfigService作为consumer消费消息。通过一个消息组件(Message Queue)就能很好的实现AdminService和ConfigService的解耦。
在实现上,考虑到Apollo的实际使用场景,以及为了尽可能减少外部依赖,我们没有采用外部的消息中间件,而是通过数据库实现了一个简单的消息队列。
实现方式如下:
- AdminService在配置发布后会往ReleaseMessage表插入一条消息记录,消息内容就是配置发布的AppId+Cluster+Namespace,参见DatabaseMessageSender
- ConfigService有一个线程会每秒扫描一次ReleaseMessage表,看看是否有新的消息记录,参见ReleaseMessageScanner
- ConfigService如果发现有新的消息记录,那么就会通知到所有的消息监听器(ReleaseMessageListener),如NotificationControllerV2,消息监听器的注册过程参见ConfigServiceAutoConfiguration
- NotificationControllerV2得到配置发布的AppId+Cluster+Namespace后,会通知对应的客户端
ConfigService通知客户端
上面提到了NotificationControllerV2如何得到有配置发布,那NotificationControllerV2得到配置发布后如何通知客户端的呢?
实现方式如下:
- 客户端会发起一个Http请求到Config Service的
notifications/v2
接口,也就是NotificationControllerV2,参见RemoteConfigLongPollService - NotificationControllerV2不会立即返回结果,而是通过Spring DeferredResult把请求挂起
- 如果在60秒内没有该客户端关心的配置发布,那么会返回Http状态码304给客户端
- 如果有该客户端关心的配置发布,NotificationControllerV2会调用DeferredResult的setResult方法,传入有配置变化的namespace信息,同时该请求会立即返回。客户端从返回的结果中获取到配置变化的namespace后,会立即请求Config Service获取该namespace的最新配置。
Client设计
- 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过Http Long Polling实现)
- 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
- 这是一个fallback机制,为了防止推送机制失效导致配置不更新
- 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
- 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property:
apollo.refreshInterval
来覆盖,单位为分钟。
- 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
- 客户端会把从服务端获取到的配置在本地文件系统缓存一份
- 在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置
- 应用程序可以从Apollo客户端获取最新的配置、订阅配置更新通知
public RemoteConfigRepository(String namespace) {
//...其它代码
//1.初始化拉取配置
this.trySync();
//2.开启定时任务拉取配置
this.schedulePeriodicRefresh();
//3.创建长链接
this.scheduleLongPollingRefresh();
}
Client与Spring集成原理
上面说完了如何从apollo远程获取到配置了,那获取到的配置如何通知Spring在初始化Bean的时候进行设置呢?这里就涉及到Spring的ConfigurableEnvironment和PropertySource:
- ConfigurableEnvironment
- Spring的ApplicationContext会包含一个Environment(实现ConfigurableEnvironment接口)
- ConfigurableEnvironment自身包含了很多个PropertySource
- PropertySource
- 属性源
- 可以理解为很多个Key - Value的属性配置
在运行时的结构形如:
需要注意的是,PropertySource之间是有优先级顺序的,如果有一个Key在多个property source中都存在,那么在前面的property source优先。
在理解了上述原理后,Apollo和Spring/Spring Boot集成的手段就呼之欲出了:在应用启动阶段,Apollo从远端获取配置,然后组装成PropertySource并插入到第一个即可,如下图所示:
相关代码可以参考PropertySourcesProcessor
private void initializePropertySources() {
//...其他代码
//创建属性源
CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_PROPERTY_SOURCE_NAME);
//sort by order asc
ImmutableSortedSet<Integer> orders = ImmutableSortedSet.copyOf(NAMESPACE_NAMES.keySet());
Iterator<Integer> iterator = orders.iterator();
//获取配置,添加到属性源中
while (iterator.hasNext()) {
int order = iterator.next();
for (String namespace : NAMESPACE_NAMES.get(order)) {
Config config = ConfigService.getConfig(namespace);
composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
}
}
//...其他代码
//添加到environment为第一个属性源
environment.getPropertySources().addFirst(composite);
}
配置更新到Bean
以上是Bean初始化时如何从apollo获取配置的过程,那Bean初始化成功后,apollo配置更新了,又是如何更新到Bean中呢?
- 在上面PropertySourcesProcessor执行后,创建了ConfigPropertySource;
- 当RemoteConfigRepository的长连接或定时器获取到配置更新后,回调LocalFileConfigRepository的接口;
- LocalFileConfigRepository进行配置文件本地备份后,回调DefaultConfig的接口;
- DefaultConfig异步回调AutoUpdateConfigChangeListener的onChange接口,然后通过变更的key获取有关联的Collection,最后执行SpringValue里关联的Bean进行修改对应的字段值。