Apollo架构及源码分析

379 阅读6分钟

架构分析

官网:Apollo配置中心

参考:微服务架构~携程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:顾名思义,构建工具

项目图.png

项目包依赖图.png

配置发布流程

下面与一个配置发布的流程,对系统和关键源码进行分析:

配置发布流程图.png

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)

  1. 根据系统配置的环境或者/opt/settings/service.properties获取环境env;

  2. 根据环境从apollo-env.properties文件中获取对应环境的MetaServer服务链接;

  3. 通过MetaServer服务链接访问/services/config接口,获取注册在Eureka上面所有的ConfigService服务并保存在本地内存;

  4. 除此之外,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)

  1. 根据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的实际使用场景,以及为了尽可能减少外部依赖,我们没有采用外部的消息中间件,而是通过数据库实现了一个简单的消息队列。

实现方式如下:

  1. AdminService在配置发布后会往ReleaseMessage表插入一条消息记录,消息内容就是配置发布的AppId+Cluster+Namespace,参见DatabaseMessageSender
  2. ConfigService有一个线程会每秒扫描一次ReleaseMessage表,看看是否有新的消息记录,参见ReleaseMessageScanner
  3. ConfigService如果发现有新的消息记录,那么就会通知到所有的消息监听器(ReleaseMessageListener),如NotificationControllerV2,消息监听器的注册过程参见ConfigServiceAutoConfiguration
  4. NotificationControllerV2得到配置发布的AppId+Cluster+Namespace后,会通知对应的客户端

ConfigService通知客户端

上面提到了NotificationControllerV2如何得到有配置发布,那NotificationControllerV2得到配置发布后如何通知客户端的呢?

实现方式如下:

  1. 客户端会发起一个Http请求到Config Service的notifications/v2接口,也就是NotificationControllerV2,参见RemoteConfigLongPollService
  2. NotificationControllerV2不会立即返回结果,而是通过Spring DeferredResult把请求挂起
  3. 如果在60秒内没有该客户端关心的配置发布,那么会返回Http状态码304给客户端
  4. 如果有该客户端关心的配置发布,NotificationControllerV2会调用DeferredResult的setResult方法,传入有配置变化的namespace信息,同时该请求会立即返回。客户端从返回的结果中获取到配置变化的namespace后,会立即请求Config Service获取该namespace的最新配置。

Client设计

apollo客户端设计图.png

客户端获取配置流程图.png

  1. 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。(通过Http Long Polling实现)
  2. 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
    • 这是一个fallback机制,为了防止推送机制失效导致配置不更新
    • 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
    • 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟。
  3. 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
  4. 客户端会把从服务端获取到的配置在本地文件系统缓存一份
    • 在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置
  5. 应用程序可以从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的属性配置

在运行时的结构形如:

Client与Spring集成原理1.png

需要注意的是,PropertySource之间是有优先级顺序的,如果有一个Key在多个property source中都存在,那么在前面的property source优先。

在理解了上述原理后,Apollo和Spring/Spring Boot集成的手段就呼之欲出了:在应用启动阶段,Apollo从远端获取配置,然后组装成PropertySource并插入到第一个即可,如下图所示:

Client与Spring集成原理2.png

相关代码可以参考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中呢?

  1. 在上面PropertySourcesProcessor执行后,创建了ConfigPropertySource;
  2. 当RemoteConfigRepository的长连接或定时器获取到配置更新后,回调LocalFileConfigRepository的接口;
  3. LocalFileConfigRepository进行配置文件本地备份后,回调DefaultConfig的接口;
  4. DefaultConfig异步回调AutoUpdateConfigChangeListener的onChange接口,然后通过变更的key获取有关联的Collection,最后执行SpringValue里关联的Bean进行修改对应的字段值。

apollo配置更新调用图.png