二、configService配置发布与读取

143 阅读3分钟

1、配置发布与推送总览 首先我们通过官方文档中的一张图片来描述配置发布与推送的过程。

image.png 整个的过程大致是: (1)用户在管理台页面(portal)操作发布配置。 (2)admin-servece模块接受不了portal请求,将发布的配置保存到数据库中。 (3)config-service监听数据库的配置变化,读取变化的releaseMessage推送至client端。 2、portal端配置发布流程 用户在发布配置时,调用createRelease()进行配置发布,可以看到有一个publish()方法,往里跟。

截屏2022-09-03 20.35.42.png

配置发布入口

可以看到,#publish()方法中又调用了AdminServiceAPI#createRelease()方法请求adminService进行发布,接下来我们就看看admin-service都做了哪些事情?
public ReleaseDTO publish(NamespaceReleaseModel model) {
  Env env = model.getEnv();
  boolean isEmergencyPublish = model.isEmergencyPublish();
  String appId = model.getAppId();
  String clusterName = model.getClusterName();
  String namespaceName = model.getNamespaceName();
  String releaseBy = StringUtils.isEmpty(model.getReleasedBy()) ?
                     userInfoHolder.getUser().getUserId() : model.getReleasedBy();

  ReleaseDTO releaseDTO = releaseAPI.createRelease(appId, env, clusterName, namespaceName,
                                                   model.getReleaseTitle(), model.getReleaseComment(),
                                                   releaseBy, isEmergencyPublish);

  Tracer.logEvent(TracerEventType.RELEASE_NAMESPACE,
                  String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName));

  return releaseDTO;
}


public ReleaseDTO createRelease(String appId, Env env, String clusterName, String namespace,
    String releaseName, String releaseComment, String operator,
    boolean isEmergencyPublish) {
  HttpHeaders headers = new HttpHeaders();
  headers.setContentType(MediaType.parseMediaType(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"));
  MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
  parameters.add("name", releaseName);
  parameters.add("comment", releaseComment);
  parameters.add("operator", operator);
  parameters.add("isEmergencyPublish", String.valueOf(isEmergencyPublish));
  HttpEntity<MultiValueMap<String, String>> entity =
      new HttpEntity<>(parameters, headers);
  ReleaseDTO response = restTemplate.post(
      env, "apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases", entity,
      ReleaseDTO.class, appId, clusterName, namespace);
  return response;
}

3、admin-service端配置发布流程 还是一样,我们先看下admin端的入口ReleaseController#publish(),方法中也调用了一个publish(namespace, releaseName, releaseComment, operator, isEmergencyPublish )方法,但是在这个方法中并没有像portal端一样发起一个http请求,而是将收到的配置存到数据库中。

image.png 在publish方法中,先是校验了发布权限以及是否存在父namespace对象,若存在父对象,则当前的配置作为子对象灰度发布。否则的话,调用masterRelease()将配置保存到数据库中,代码如下:

private Release masterRelease(Namespace namespace, String releaseName, String releaseComment,
                              Map<String, String> configurations, String operator,
                              int releaseOperation, Map<String, Object> operationContext) {
  // 获取上一次发布的配置,本次配置保存后,同时会保存一条记录,记录本次发布配置的发布id以及上一次发布配置的id信息
  Release lastActiveRelease = findLatestActiveRelease(namespace);
  long previousReleaseId = lastActiveRelease == null ? 0 : lastActiveRelease.getId();
  // 保存本次发布的配置到Release表
  Release release = createRelease(namespace, releaseName, releaseComment,
                                  configurations, operator);
  //保存上次发布的配置信息记录
  releaseHistoryService.createReleaseHistory(namespace.getAppId(), namespace.getClusterName(),
                                             namespace.getNamespaceName(), namespace.getClusterName(),
                                             release.getId(), previousReleaseId, releaseOperation,
                                             operationContext, operator);

  return release;
}

4、config-service监听配置发布 上面讲到admin端会把发布的配置保存到release表中,而config端则是通过监听release表的变化来获取发布的配置的。 在config启动时,会自动装配ConfigServiceAutoConfiguration类,而这个类会向spring注入releaseMessagescanner这个bean,用来添加不同的监听器。可以看到,其中就包括notificationControllerV2和releaseMessageServiceWithCache,这也是apollo实现配置实时推送至client端的关键。

image.png 同时,再看看ReleaseMessagescanner这个类的构造函数,可以看到在初始化时会创建一个调度线程,这个线程的作用就是用来监听release表。 截屏2022-09-03 21.31.32.png ReleaseMessagescanner类实现了InitializingBean接口,所以在服务启动时,调度线程开始扫描数据库,当数据库插入新的配置时,则读取新的配置通知监听器。

@Override
public void afterPropertiesSet() throws Exception {
  // 获取线程调度的间隔时间大小,默认是1s
  databaseScanInterval = bizConfig.releaseMessageScanIntervalInMilli();
  maxIdScanned = loadLargestMessageId();
  executorService.scheduleWithFixedDelay(() -> {
    Transaction transaction = Tracer.newTransaction("Apollo.ReleaseMessageScanner", "scanMessage");
    try {
      // 扫描之前处理失败的配置,在上面的图片中可以看到这个bean维护了一个
      // missingReleaseMessages的map,用来记录处理失败的release
      scanMissingMessages();
      // 扫描数据库中的配置,若存在新增的配置,则推送给监听器
      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);

}

// 扫描并发送新发布的配置
private boolean scanAndSendMessages() {
  //查询500条id最大的记录
  List<ReleaseMessage> releaseMessages =
      releaseMessageRepository.findFirst500ByIdGreaterThanOrderByIdAsc(maxIdScanned);
  // 查不到记录则返回
  if (CollectionUtils.isEmpty(releaseMessages)) {
    return false;
  }
  // 发布配置
  fireMessageScanned(releaseMessages);
  //数据库返回的记录数
  int messageScanned = releaseMessages.size();
  // 获取最大的自增id
  long newMaxIdScanned = releaseMessages.get(messageScanned - 1).getId();
  // 若最大的自增id减去上次查到的最大自增id大于查到的数据库记录,则表示本次查询到的配置并非是全部的配置,存在未读到的发布配置。
  if (newMaxIdScanned - maxIdScanned > messageScanned) {
    // 将配置保存到missingReleaseMessages中
    recordMissingReleaseMessageIds(releaseMessages, maxIdScanned);
  }
  maxIdScanned = newMaxIdScanned;
  return messageScanned == 500;
}

// 推送扫描到的配置至监听器
private void fireMessageScanned(Iterable<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);
      }
    }
  }
}