携程Apollo之配置实时生效

219 阅读4分钟

一.Apollo 发布配置流程图

apollo架构.jpg

step1: 客户端发起长轮询请求

此时由于没有配置的变更所以当前请求被暂时挂起ds

step2: 管理员在后台发布配置

step3: portal调用adminService进行基础信息的落库-> releaseMessage,releaseHistory,release

step4: configService 定时扫描releaseMessage表重新触发客户端请求

step5: 客户端触发configChanngeListener 变更配置信息

二 源码分析 [talk is cheap show me the code]

step1 客户端发起长轮询请求

RemoteConfigLongPollService # doLongPollingRefresh

logger.info("doLongPollingRefresh  start");
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) {
      //选择一个configService
      //直接随机选就可以  无状态部署的
      List<ServiceDTO> configServices = getConfigServices();
      lastServiceDto = configServices.get(random.nextInt(configServices.size()));
    }
    //拼接请求的URL
    url =
        assembleLongPollRefreshUrl(lastServiceDto.getHomepageUrl(), appId, cluster, dataCenter,
            m_notifications);
    logger.info("long polling url:{}",url);

    logger.info("Long polling from {}", url);

    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);

    //发起请求等待response
    final HttpResponse<List<ApolloConfigNotification>> response =
        m_httpClient.doGet(request, m_responseType);

请求的URL 示例:http://IP:8080/notifications/v2?cluster=default&appId=100004458&ip=IP&notifications=%5B%7B%22namespaceName%22%3A%22application%22%2C%22notificationId%22%3A9%7D%5D

step2 configService 处理长轮询请求

configService:   NotificationControllerV2 # pollNotification
 @GetMapping
  public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> pollNotification(
      @RequestParam(value = "appId") String appId,
      @RequestParam(value = "cluster") String cluster,
      @RequestParam(value = "notifications") String notificationsAsString, //这里是客户端监听的所有namespace
      @RequestParam(value = "dataCenter", required = false) String dataCenter,
      @RequestParam(value = "ip", required = false) String clientIp) {
    List<ApolloConfigNotification> notifications = null;
logger.info("poll  start config service===>appid:{},cluster:{},notificationsAsString:{}",appId,cluster,notificationsAsString);
    try {
      notifications =
          gson.fromJson(notificationsAsString, notificationsTypeReference);
    } catch (Throwable ex) {
      Tracer.logError(ex);
    }

    if (CollectionUtils.isEmpty(notifications)) {
      throw new BadRequestException("Invalid format of notifications: " + notificationsAsString);
    }
    //过滤
    Map<String, ApolloConfigNotification> filteredNotifications = filterNotifications(appId, notifications);

    if (CollectionUtils.isEmpty(filteredNotifications)) {
      throw new BadRequestException("Invalid format of notifications: " + notificationsAsString);
    }
    //构建一个DeferredResult 如果没有结果暂时将请求挂起
    DeferredResultWrapper deferredResultWrapper = new DeferredResultWrapper(bizConfig.longPollingTimeoutInMilli());
    Set<String> namespaces = Sets.newHashSetWithExpectedSize(filteredNotifications.size());
    Map<String, Long> clientSideNotifications = Maps.newHashMapWithExpectedSize(filteredNotifications.size());
    
   //....根据client传递的参数设置namespace
   
    //构建watchKey
    Multimap<String, String> watchedKeysMap =
        watchKeysUtil.assembleAllWatchKeys(appId, cluster, namespaces, dataCenter);


    logger.info("poll watchedKeysMap:{}",watchedKeysMap);

    Set<String> watchedKeys = Sets.newHashSet(watchedKeysMap.values());

    /**
     * 1、set deferredResult before the check, for avoid more waiting
     * If the check before setting deferredResult,it may receive a notification the next time
     * when method handleMessage is executed between check and set deferredResult.
     */
    deferredResultWrapper
          .onTimeout(() -> logWatchedKeys(watchedKeys, "Apollo.LongPoll.TimeOutKeys"));

    deferredResultWrapper.onCompletion(() -> {
    //设置返回结果后的回调
      //unregister all keys
      logger.info("onCompletion  ===>watchedKeys will remove:{}",watchedKeys);

      for (String key : watchedKeys) {
        deferredResults.remove(key, deferredResultWrapper);
      }
      logWatchedKeys(watchedKeys, "Apollo.LongPoll.CompletedKeys");
    });

    //register all keys
    for (String key : watchedKeys) {
      this.deferredResults.put(key, deferredResultWrapper);
    }

    logWatchedKeys(watchedKeys, "Apollo.LongPoll.RegisteredKeys");
    logger.debug("Listening {} from appId: {}, cluster: {}, namespace: {}, datacenter: {}",
        watchedKeys, appId, cluster, namespaces, dataCenter);

    /**
     * 2、check new release
     */
    List<ReleaseMessage> latestReleaseMessages =
        releaseMessageService.findLatestReleaseMessagesGroupByMessages(watchedKeys);

    /**
     * Manually close the entity manager.
     * Since for async request, Spring won't do so until the request is finished,
     * which is unacceptable since we are doing long polling - means the db connection would be hold
     * for a very long time
     */
    entityManagerUtil.closeEntityManager();

    List<ApolloConfigNotification> newNotifications =
        getApolloConfigNotifications(namespaces, clientSideNotifications, watchedKeysMap,
            latestReleaseMessages);

    if (!CollectionUtils.isEmpty(newNotifications)) {
        //设置结果 返回给客户端
      deferredResultWrapper.setResult(newNotifications);
    }

    return deferredResultWrapper.getResult();
  }

step3 客户端提交发布版本

adminService--->releaseController

@Transactional
@PostMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases")
public ReleaseDTO publish(@PathVariable("appId") String appId,
                          @PathVariable("clusterName") String clusterName,
                          @PathVariable("namespaceName") String namespaceName,
                          @RequestParam("name") String releaseName,
                          @RequestParam(name = "comment", required = false) String releaseComment,
                          @RequestParam("operator") String operator,
                          @RequestParam(name = "isEmergencyPublish", defaultValue = "false") boolean isEmergencyPublish) {
  //查询namespace                        
  Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName);
  if (namespace == null) {
    throw new NotFoundException("Could not find namespace for %s %s %s", appId, clusterName,
        namespaceName);
  }
  //进行版本的发布: 存储至release表 存储发布历史 releaseHistory
  Release release = releaseService.publish(namespace, releaseName, releaseComment, operator, isEmergencyPublish);

  //send release message
  Namespace parentNamespace = namespaceService.findParentNamespace(namespace);
  String messageCluster;
  if (parentNamespace != null) {
    messageCluster = parentNamespace.getClusterName();
  } else {
    messageCluster = clusterName;
  }
  //存储releaseMessage 供configService扫描
  messageSender.sendMessage(ReleaseMessageKeyGenerator.generate(appId, messageCluster, namespaceName),
                            Topics.APOLLO_RELEASE_TOPIC);
  return BeanUtils.transform(ReleaseDTO.class, release);
}

step4 configService 定时扫描releaseMessage 进行更新配置的通知

ReleaseMessageScanner

private boolean scanAndSendMessages() {
  //current batch is 500
  List<ReleaseMessage> releaseMessages =
          //maxIdScanned 上次拉取的id 找他之后的500条
      releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(maxIdScanned);
  if (CollectionUtils.isEmpty(releaseMessages)) {
    return false;
  }
  //Notify ReleaseMessageListener
  fireMessageScanned(releaseMessages);
  int messageScanned = releaseMessages.size();
  long newMaxIdScanned = releaseMessages.get(messageScanned - 1).getId();
  // check id gaps, possible reasons are release message not committed yet or already rolled back
  //下面这个表达式成立代表的是当前ID断开了 也就是有任务还没有提交或者已经回滚
  if (newMaxIdScanned - maxIdScanned > messageScanned) {
    //记录下当前的可能回滚或者是还没有提交的id
    recordMissingReleaseMessageIds(releaseMessages, maxIdScanned);
  }
  maxIdScanned = newMaxIdScanned;
  return messageScanned == 500;
}

step5 扫描到消息 返回客户端

@Override
public void handleMessage(ReleaseMessage message, String channel) {
  logger.info("message received - channel: {}, message: {}", channel, message);

  String content = message.getMessage();
  Tracer.logEvent("Apollo.LongPoll.Messages", content);
  if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(content)) {
    return;
  }
  //解析出修改的nameSpace
  String changedNamespace = retrieveNamespaceFromReleaseMessage.apply(content);

  if (Strings.isNullOrEmpty(changedNamespace)) {
    logger.error("message format invalid - {}", content);
    return;
  }

  if (!deferredResults.containsKey(content)) {
    return;
  }

  // 为了避免并发修改异常重新创建一个 List 取出当前关注这个消息的请求
  List<DeferredResultWrapper> results = Lists.newArrayList(deferredResults.get(content));

  ApolloConfigNotification configNotification = new ApolloConfigNotification(changedNamespace, message.getId());
  configNotification.addMessage(content, message.getId());

  //do async notification if too many clients......

  for (DeferredResultWrapper result : results) {
      //为之前缓存的请求设置result 返回给客户端
    result.setResult(configNotification);
  }
  logger.debug("Notification completed");
}

step6 客户端接收到消息 处理

@Override
protected synchronized void sync() {
  Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig");

  try {
    //历史消息
    ApolloConfig previous = m_configCache.get();
    //获取最新的消息
    ApolloConfig current = loadApolloConfig();

    logger.info("previous:{}   current:{}",GSON.toJson(previous),GSON.toJson(current));

    //reference equals means HTTP 304
    //如果不一样则更新缓存并触发一系列的listener 进行配置的更新
    if (previous != current) {
      logger.debug("Remote Config refreshed!");
      m_configCache.set(current);
      this.fireRepositoryChange(m_namespace, this.getConfig());
    }

    if (current != null) {
      Tracer.logEvent(String.format("Apollo.Client.Configs.%s", current.getNamespaceName()),
          current.getReleaseKey());
    }

    transaction.setStatus(Transaction.SUCCESS);
  } catch (Throwable ex) {
    transaction.setStatus(ex);
    throw ex;
  } finally {
    transaction.complete();
  }
}

更新@Value的配置

AutoUpdateConfigChangeListener # onChange

@Override
public void onChange(ConfigChangeEvent changeEvent) {
  Set<String> keys = changeEvent.changedKeys();
  if (CollectionUtils.isEmpty(keys)) {
    return;
  }
  for (String key : keys) {
    // 1. check whether the changed key is relevant
    Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key);
    if (targetValues == null || targetValues.isEmpty()) {
      continue;
    }

    // 2. update the value
    for (SpringValue val : targetValues) {
      updateSpringValue(val);
    }
  }
}

总结

以上就是Apollo我们在后台进行配置更新的时候服务端和客户端需要做的事情 可能有些东西被一笔带过了 如果感兴趣的话可以去看一下源码 搭建一下源码的运行环境 自己打点日志 debug一下 能够更清晰的认识整个过程!