apollo 基本原理

431 阅读3分钟

本文是对apollo原理的个人理解,搭建使用请参见上一篇文章apollo 搭建使用

1. 模块介绍

下图是单个环境的架构图

config service

提供配置的读取推送等功能,服务对象是Apollo 客户端

admin service

提供配置的修改发布等功能,服务对象是Apollo Portal (管理界面)

eureka

提供服务的注册和发现,为了部署简单起见,eureka 通常和config service一个JVM 进程中;


注: config service 和 admin service 都是多实例、无状态部署,故需要将自己部署到eureka 中并保持心跳;

在eureka 之上 加了一层Meta Service 用户封装Eureka 的服务发现接口;


apollo-client

Client 通过域名访问Meta Server 获取Config Service 服务列表(IP + PORT), 而后直接通过IP + PORT 访问服务,同时Cient 侧会做load Balance 、错误重试;

portal

portal 通过域名访问Mate Server 获取 Admin Service 服务列表(IP + PORT),而后直接过过IP + PORT 访问服务,同时Portal测会做load Balance 、错误重试

为了简化部署,我们实际上会把Config、Eureka 和 Meta Server 三个逻辑角色部署在同一个JVM 进程中;

2. 配置发布原理

admin service 发送消息

我们从Admin Service发布看起:

com.ctrip.framework.apollo.biz.message.DatabaseMessageSender#sendMessage

  /**
   * 发送消息
   */
  @Override
  @Transactional
  public void sendMessage(String message, String channel) {
    logger.info("Sending message {} to channel {}", message, channel);
    if (!Objects.equals(channel, Topics.APOLLO_RELEASE_TOPIC)) {
      logger.warn("Channel {} not supported by DatabaseMessageSender!", channel);
      return;
    }

    Tracer.logEvent("Apollo.AdminService.ReleaseMessage", message);
    Transaction transaction = Tracer.newTransaction("Apollo.AdminService", "sendMessage");
    try {
      ReleaseMessage newMessage = releaseMessageRepository.save(new ReleaseMessage(message));
      toClean.offer(newMessage.getId());
      transaction.setStatus(Transaction.SUCCESS);
    } catch (Throwable ex) {
      logger.error("Sending message to database failed", ex);
      transaction.setStatus(ex);
      throw ex;
    } finally {
      transaction.complete();
    }
  }

config service 定时任务扫描

com.ctrip.framework.apollo.biz.message.ReleaseMessageScanner

#scanAndSendMessages

#fireMessageScanned

  /**
   * scan messages and send
   *
   * @return whether there are more messages
   */
  private boolean scanAndSendMessages() {
    //current batch is 500
    List<ReleaseMessage> releaseMessages =
        releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(maxIdScanned);
    if (CollectionUtils.isEmpty(releaseMessages)) {
      return false;
    }
    fireMessageScanned(releaseMessages);
    int messageScanned = releaseMessages.size();
    maxIdScanned = releaseMessages.get(messageScanned - 1).getId();
    return messageScanned == 500;
  }


  /**
   * Notify listeners with messages loaded
   * @param messages
   */
  private void fireMessageScanned(List<ReleaseMessage> messages) {
    for (ReleaseMessage message : messages) {
      for (ReleaseMessageListener listener : listeners) {
        try {
          listener.handleMessage(message, Topics.APOLLO_RELEASE_TOPIC);
        } catch (Throwable ex) {
          Tracer.logError(ex);
          logger.error("Failed to invoke message listener {}", listener.getClass(), ex);
        }
      }
    }
  }

apollo-client 长链接拉取

apollo-client 发送获取配置请求

  private void doLongPollingRefresh(String appId, String cluster, String dataCenter, String secret) {
    final Random random = new Random();
    ServiceDTO lastServiceDto = null;
    while (!m_longPollingStopped.get() && !Thread.currentThread().isInterrupted()) {
      if (!m_longPollRateLimiter.tryAcquire(5, TimeUnit.SECONDS)) {
        //wait at most 5 seconds
        try {
          TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
        }
      }
      Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "pollNotification");
      String url = null;
      try {
        if (lastServiceDto == null) {
          List<ServiceDTO> configServices = getConfigServices();
          lastServiceDto = configServices.get(random.nextInt(configServices.size()));
        }

        url =
            assembleLongPollRefreshUrl(lastServiceDto.getHomepageUrl(), appId, cluster, dataCenter,
                m_notifications);

        logger.debug("Long polling from {}", url);
				
        //【 建立HTTP 长链接  LONG_POLLING_READ_TIMEOUT=90S】
        HttpRequest request = new HttpRequest(url);
        request.setReadTimeout(LONG_POLLING_READ_TIMEOUT);
        if (!StringUtils.isBlank(secret)) {
          Map<String, String> headers = Signature.buildHttpHeaders(url, appId, secret);
          request.setHeaders(headers);
        }

        transaction.addData("Url", url);

        final HttpResponse<List<ApolloConfigNotification>> response =   m_httpUtil.doGet(request, m_responseType);

        logger.debug("Long polling response: {}, url: {}", response.getStatusCode(), url);
        if (response.getStatusCode() == 200 && response.getBody() != null) {
          updateNotifications(response.getBody());
          updateRemoteNotifications(response.getBody());
          transaction.addData("Result", response.getBody().toString());
          notify(lastServiceDto, response.getBody());
        }

        //try to load balance
        if (response.getStatusCode() == 304 && random.nextBoolean()) {
          lastServiceDto = null;
        }

        m_longPollFailSchedulePolicyInSecond.success();
        transaction.addData("StatusCode", response.getStatusCode());
        transaction.setStatus(Transaction.SUCCESS);
      } catch (Throwable ex) {
        // 省略。。。
      } finally {
        transaction.complete();
      }
    }
  }

config-service 接受请求:

@RestController
@RequestMapping("/notifications/v2")
public class NotificationControllerV2 implements ReleaseMessageListener {
    
      @GetMapping
  public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> pollNotification(
      @RequestParam(value = "appId") String appId,
      @RequestParam(value = "cluster") String cluster,
      @RequestParam(value = "notifications") String notificationsAsString,
      @RequestParam(value = "dataCenter", required = false) String dataCenter,
      @RequestParam(value = "ip", required = false) String clientIp) {
  }

}

NotificationControllerV2 不会立刻返回结果,而是把请求挂起。考虑到会有数万客户端向服务端发起长连,

在服务端使用了async servlet (Spring DeferredResult) 来服务Http Long Polling 请求;

如果在90S内没有该客户端关心的配置发布,那么会返回Http 状态码304给客户端;

如果有该客户端关心的配置发布,NotificationV2会调动DeferredResult的setResult方法,传入有配置变化的namespace信息,同事该请求会立即返回,客户端从返回的结果中获取到配置变化的namespace后,会立即请求Config Service 获取namespace 的最新配置;

客户端的获取设计

除了之前介绍的客户端和服务端保持一个长连接,从而能第一时间获得配置更新的推送外,客户端还会定时从Apollo配置中心服务端拉取应用的最新配置

  • 这是一个备用机制,为了防止推送机制失效导致配置不更新
  • 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回304 - Not Modified
  • 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval来覆盖,单位为分钟

3. 常见问题

Config Service 和 Client是poll 还是 push

参见

攀博课堂--分布式配置中心Apollo教程

Servlet 3.0 异步处理详解