Apollo核心功能源码分析(1)-发布配置信息流程分析

序言:

Apollo目前是国内开源中对于配置中心这一组件来说使用率应该是最高的一个项目了,它可以快速整合入我们当前的SOA(Dubbo体系),微服务架构中(springcloud体系)。相对于之前基于zk的自我实现(开发成本太高,强依赖与zk),或springcloud config(开发成本不高,使用简单但是运行维护很是不变复杂)来说,它提供了一套基于配置管理平台,对于某一服务在不同环境(开发-DEV,测试-TEST,预生产,生产-PRO)使用不同配置或同一环境不同集群都可以做到协做管理。基于Eureka的使得整体可以做到高可用,也提供本地缓存方式,使得即使Apollo不可用也可提供暂时性的服务。还提供了用户权限管理,灰度发布等其它功能。关于此可详细从官网中进行自我学习。

1:服务端描述

看过Apollo官网的大体介绍,可以知晓在Apollo的整体架构中,有多种角色(整体看待apollo,它是一个小型的微服务架构-虽然只有可怜的3个服务:Config,Admin,Portal),如下所示是官网中整体架构图:

overall-architecture

如下所示对于上述各组件的详细描述(来自官网描述):

1.1 Config Service

  • 提供配置获取接口
  • 提供配置更新推送接口(基于Http long polling)
    • 服务端使用Spring DeferredResult实现异步化,从而大大增加长连接数量
    • 目前使用的tomcat embed默认配置是最多10000个连接(可以调整),使用了4C8G的虚拟机实测可以支撑10000个连接,所以满足需求(一个应用实例只会发起一个长连接)。
  • 接口服务对象为Apollo客户端

1.2 Admin Service

  • 提供配置管理接口
  • 提供配置修改、发布等接口
  • 接口服务对象为Portal

1.3 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一致

1.4 Eureka

  • 基于EurekaSpring Cloud Netflix提供服务注册和发现
  • Config Service和Admin Service会向Eureka注册服务,并保持心跳
  • 为了简单起见,目前Eureka在部署时和Config Service是在一个JVM进程中的(通过Spring Cloud Netflix)

1.5 Portal

  • 提供Web界面供用户管理配置
  • 通过Meta Server获取Admin Service服务列表(IP+Port),通过IP+Port访问服务
  • 在Portal侧做load balance、错误重试

1.6 Client

  • Apollo提供的客户端程序,为应用提供配置获取、实时更新等功能
  • 通过Meta Server获取Config Service服务列表(IP+Port),通过IP+Port访问服务
  • 在Client侧做load balance、错误重试

1.7 NginxLB

 

  • 和域名系统配合,协助Portal访问MetaServer获取AdminService地址列表

  • 和域名系统配合,协助Client访问MetaServer获取ConfigService地址列表

  • 和域名系统配合,协助用户访问Portal进行配置管理

ConfigService和AdminService会共享ConfigDB,ConfigDB中存放项目在某个环境中的配置信息,在多环境部署情况下ConfigService/AdminService/ConfigDB三者在每个环境(DEV/FAT/UAT/PRO)中都要部署一份。

2:用户发送配置变更整体流程分析

而对于一个配置的修改发布,上述角色都会贯穿其中,在各自的领域提供自己的贡献,如下所示是官网上发送配置的流程,实际大体也是如此,只不过少画了Meat Server逻辑角色在其中的作用。

release-message-notification-design

当使用者从界面上发布一个配置时,会经历多少个的网络io请求后才会到达client端,基于上图进行源码分析:

使用者从浏览器端发起一个http请求,此时由Portal Server服务进行第一次接受

经过上述内部流程后,通过restTemplate(该类为apollo实现RetryableRestTemplate-包含重试功能)朝adminService服务发送http请求,如下所示:

AdminServer通过该接口接受portal Server请求,并组装message进行push

写入数据库中,当数据写入成功,那么该次变更也就成功了。剩下就是Config Server的事情了

最终数据库中数据:

为什么Apollo使用数据库作为消息推送的载体:

对于一个mq消息推送机制来说,现有存在三种种机制。基于push模式,基于pull模式,以及两种都有的模式,RabbitMQ,Kafka,RocketMQ3种组件分别对应现有的3种开源的mq实现。三种模式都各有特点,push模式更适用于对时间敏感的业务需求,例如交易,定时器执行(这里说下开源分布式调度组件xxljob就是基于Netty通信的一个push模式,由身为Netty客户端的服务端push身为Netty服务端的客户端)。Apollo感觉比较特殊,为了减少避免外部组件的依赖,又因为Apollo作为一个配置中心,管理全局的配置信息,而对于一个配置信息那么它不论在生产,测试,开发环境基本变化很少,所以此处引入一个mq组件对于Apollo来说太过于笨重,所以它使用数据库ReleaseMessage表作为它的消息推送的存储。

当数据写入到数据库后,该请求就代表结束了,也就告知用户该次配置发布成功了。但对于apollo来说,实际上才完成了一半的功能。接下来就是到ConfigServer服务的时间了,它通过一个线程异步的扫描ReleaseMessage表中的Message信息,构建成一个message通过观察者模式向注册的ReleaseMessageListener发送通知。如下所示:

public class ReleaseMessageScanner implements InitializingBean {
  private static final Logger logger = LoggerFactory.getLogger(ReleaseMessageScanner.class);
  @Autowired
  private BizConfig bizConfig;
  //ReleaseMessage 数据库
  @Autowired
  private ReleaseMessageRepository releaseMessageRepository;
  private int databaseScanInterval;
  //listeners
  private List<ReleaseMessageListener> listeners;
  //周期任务调度器 100ms执行一次
  private ScheduledExecutorService executorService;
  private long maxIdScanned;

  public ReleaseMessageScanner() {
    listeners = Lists.newCopyOnWriteArrayList();
    //单线程执行
    executorService = Executors.newScheduledThreadPool(1, ApolloThreadFactory
        .create("ReleaseMessageScanner", true));
  }

  //bean初始化后执行
  @Override
  public void afterPropertiesSet() throws Exception {
    //设置周期时间 默认100ms
    databaseScanInterval = bizConfig.releaseMessageScanIntervalInMilli();
    maxIdScanned = loadLargestMessageId();
    //延时100ms 扫描数据
    executorService.scheduleWithFixedDelay((Runnable) () -> {
      Transaction transaction = Tracer.newTransaction("Apollo.ReleaseMessageScanner", "scanMessage");
      try {
        //扫描message
        scanMessages();
        transaction.setStatus(Transaction.SUCCESS);
      } catch (Throwable ex) {
        transaction.setStatus(ex);
        logger.error("Scan and send message failed", ex);
      } finally {
        transaction.complete();
      }
    }, databaseScanInterval, databaseScanInterval, TimeUnit.MILLISECONDS);

  }

  /**
   * add message listeners for release message
   * @param listener
   */
  public void addMessageListener(ReleaseMessageListener listener) {
    if (!listeners.contains(listener)) {
      listeners.add(listener);
    }
  }

  /**
   * 扫描表中的message信息 直到无message
   */
  private void scanMessages() {
    boolean hasMoreMessages = true;
    //线程被中断
    while (hasMoreMessages && !Thread.currentThread().isInterrupted()) {
      hasMoreMessages = scanAndSendMessages();
    }
  }

  /**
   * scan messages and send
   *
   * @return whether there are more messages
   */
  private boolean scanAndSendMessages() {
    //current batch is 500
    //每次从数据中拿取500条
    List<ReleaseMessage> releaseMessages =
        releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(maxIdScanned);
    //无消息结束循环
    if (CollectionUtils.isEmpty(releaseMessages)) {
      return false;
    }
    //通过listener
    fireMessageScanned(releaseMessages);
    int messageScanned = releaseMessages.size();
    maxIdScanned = releaseMessages.get(messageScanned - 1).getId();
    return messageScanned == 500;
  }

  /**
   * find largest message id as the current start point
   * @return current largest message id
   */
  private long loadLargestMessageId() {
    ReleaseMessage releaseMessage = releaseMessageRepository.findTopByOrderByIdDesc();
    return releaseMessage == null ? 0 : releaseMessage.getId();
  }

  /**
   * 通知listener 已获取数据
   * @param messages
   */
  private void fireMessageScanned(List<ReleaseMessage> messages) {
    for (ReleaseMessage message : messages) {
      //遍历所有listener 基于观察者模式
      for (ReleaseMessageListener listener : listeners) {
        try {
          //处理message
          listener.handleMessage(message, Topics.APOLLO_RELEASE_TOPIC);
        } catch (Throwable ex) {
          Tracer.logError(ex);
          logger.error("Failed to invoke message listener {}", listener.getClass(), ex);
        }
      }
    }
  }
}

那么Apollo中mq是一种模式呢,个人感觉主要是pull模式但又结合了一定的push,因为它主要是由client发起http请求到ConfigServer端(/notifications/v2),而Server会保持与该请求的60s的连接,如果在这时间内无任何对应的namespace配置变更,则返回304并定时开始下一轮的请求连接,若有则向response中写入变更的namespace信息。client收到response会立即请求Config Service获取该namespace的最新配置。源码如下所示:

在接受client请求数据后经过一系列检查初始化后,封装DeferredResultWrapper对象并写入缓存map中,实际可以理解就是client向server进行注册一次有效时长的连接。

而NotificationControllerV2也实现了ReleaseMessageListener接口,实现了handleMessage(),也就是会收到配置变更的消息。以下是handleMessage方法中核心处理代码。

至此Apollo的服务端推送逻辑结束了,下文讲述客户端pull配置信息逻辑以及对应的源码。

2:客户端

以下描述下客户端在Apollo整体架构中的作用,以及在发布配置信息流程体系中的作用如下所示是从官网copy过来的客户端整体设计图:

从上图可以看出,Apollo的客户端需要与服务端存在两种获取配置信息方式,第一种就是第一篇中描述的push方式(实际上也是客户端先发起一个60s的一个连接),一旦服务端收到用户发送的变更请求就会触发一次push。第二种就是client定时向客户端pull配置信息(定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟,不过一般通过该方式返回的结果都是304即未发生变更)。当client获取到最新的数据会写入到内存中进行缓存,并同步到具体的应用程序。同时也会将数据写入到本地中缓存,避免因为Apollo的当机导致的服务不可用。

对于Apollo的理解还是建议去查看官网描述:https://github.com/ctripcorp/apollo/wiki/ 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值