⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://www.iocoder.cn/Apollo/portal-publish-namespace-branch-to-master/ 「芋道源码」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

阅读源码最好的方式,是使用 IDEA 进行调试 Apollo 源码,不然会一脸懵逼。

胖友可以点击「芋道源码」扫码关注,回复 git018 关键字
获得艿艿添加了中文注释的 Apollo 源码地址。

阅读源码很孤单,加入源码交流群,一起坚持!

1. 概述

老艿艿:本系列假定胖友已经阅读过 《Apollo 官方 wiki 文档》 ,特别是 《Apollo 官方 wiki 文档 —— 灰度发布使用指南》

本文接 《Apollo 源码解析 —— Portal 灰度发布》 ,分享灰度全量发布。

我们先来看看官方文档对灰度全量发布的使用指南,来理解下它的定义和流程。

如果灰度的配置测试下来比较理想,符合预期,那么就可以操作【全量发布】。

全量发布的效果是:

  1. 灰度版本的配置会合并回主版本,在这个例子中,就是主版本的 timeout 会被更新成 3000
  2. 主版本的配置会自动进行一次发布
  3. 在全量发布页面,可以选择是否保留当前灰度版本,默认为不保留。

灰度发布1 全量发布1 全量发布2

我选择了不保留灰度版本,所以发布完的效果就是主版本的配置更新、灰度版本删除。点击主版本的实例列表,可以看到10.32.21.22和10.32.21.19都使用了主版本最新的配置。

灰度发布2

灰度全量发布,和 《Apollo 源码解析 —— Portal 发布配置》 ,差异点在于,多了一步配置合并,所以代码实现上,有很多相似度。整体系统流程如下:

流程

2. Portal 侧

2.1 NamespaceBranchController

apollo-portal 项目中,com.ctrip.framework.apollo.portal.controller.NamespaceBranchController ,提供 Namespace 分支API

#merge(...) 方法,灰度全量发布,合并子 Namespace 变更的配置 Map 到父 Namespace ,并进行一次 Release 。代码如下:

 1: @PreAuthorize(value = "@permissionValidator.hasReleaseNamespacePermission(#appId, #namespaceName)")
2: @RequestMapping(value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/merge", method = RequestMethod.POST)
3: public ReleaseDTO merge(@PathVariable String appId, @PathVariable String env,
4: @PathVariable String clusterName, @PathVariable String namespaceName,
5: @PathVariable String branchName, @RequestParam(value = "deleteBranch", defaultValue = "true") boolean deleteBranch,
6: @RequestBody NamespaceReleaseModel model) {
7: // 若是紧急发布,但是当前环境未允许该操作,抛出 BadRequestException 异常
8: if (model.isEmergencyPublish() && !portalConfig.isEmergencyPublishAllowed(Env.fromString(env))) {
9: throw new BadRequestException(String.format("Env: %s is not supported emergency publish now", env));
10: }
11: // 合并子 Namespace 变更的配置 Map 到父 Namespace ,并进行一次 Release
12: ReleaseDTO createdRelease = namespaceBranchService.merge(appId, Env.valueOf(env), clusterName, namespaceName, branchName,
13: model.getReleaseTitle(), model.getReleaseComment(),
14: model.isEmergencyPublish(), deleteBranch);
15:
16: // 创建 ConfigPublishEvent 对象
17: ConfigPublishEvent event = ConfigPublishEvent.instance();
18: event.withAppId(appId)
19: .withCluster(clusterName)
20: .withNamespace(namespaceName)
21: .withReleaseId(createdRelease.getId())
22: .setMergeEvent(true)
23: .setEnv(Env.valueOf(env));
24: // 发布 ConfigPublishEvent 事件
25: publisher.publishEvent(event);
26: return createdRelease;
27: }

  • POST /apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/merge 接口,Request Body 传递 JSON 对象。
  • @PreAuthorize(...) 注解,调用 PermissionValidator#hasReleaseNamespacePermissio(appId, namespaceName) 方法,校验是否有发布配置的权限。后续文章,详细分享。
  • 第 7 至 10 行:校验若是紧急发布,但是当前环境未允许该操作,抛出 BadRequestException 异常。
  • 第 11 至 14 行:调用 NamespaceBranchService#merge(...) 方法,合并子 Namespace 变更的配置 Map 到父 Namespace ,并进行一次 Release 。
  • 第 16 至 25 行:创建 ConfigPublishEvent 对象,并调用 ApplicationEventPublisher#publishEvent(event) 方法,发布 ConfigPublishEvent 事件。这部分,我们在后续文章分享。
  • 第 26 行:返回 ReleaseDTO 对象。

2.2 NamespaceBranchService

apollo-portal 项目中,com.ctrip.framework.apollo.portal.service.NamespaceBranchService ,提供 Namespace 分支Service 逻辑。

#merge(...) 方法,调用 Admin Service API ,合并子 Namespace 变更的配置 Map 到父 Namespace ,并进行一次 Release 。代码如下:

 1: @Autowired
2: private AdminServiceAPI.NamespaceBranchAPI namespaceBranchAPI;
3: @Autowired
4: private ReleaseService releaseService;
5:
6: public ReleaseDTO merge(String appId, Env env, String clusterName, String namespaceName,
7: String branchName, String title, String comment,
8: boolean isEmergencyPublish, boolean deleteBranch) {
9: // 计算变化的 Item 集合
10: ItemChangeSets changeSets = calculateBranchChangeSet(appId, env, clusterName, namespaceName, branchName);
11: // 合并子 Namespace 变更的配置 Map 到父 Namespace ,并进行一次 Release
12: ReleaseDTO mergedResult = releaseService.updateAndPublish(appId, env, clusterName, namespaceName, title, comment,
13: branchName, isEmergencyPublish, deleteBranch, changeSets);
14: // 【TODO 6001】Tracer 日志
15: Tracer.logEvent(TracerEventType.MERGE_GRAY_RELEASE, String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName));
16: return mergedResult;
17: }

  • 第 10 行:调用 #calculateBranchChangeSet(appId, env, clusterName, namespaceName, branchName) 方法,计算变化的 Item 集合。详细解析,见 「2.2.1 calculateBranchChangeSet」

  • 第12 至 13 行:调用 ReleaseService#updateAndPublish(...) 方法,调用 Admin Service API ,合并子 Namespace 变更的配置 Map 到父 Namespace ,并进行一次 Release 。代码如下:

    @Autowired
    private AdminServiceAPI.ReleaseAPI releaseAPI;

    public ReleaseDTO updateAndPublish(String appId, Env env, String clusterName, String namespaceName,
    String releaseTitle, String releaseComment, String branchName,
    boolean isEmergencyPublish, boolean deleteBranch, ItemChangeSets changeSets) {
    return releaseAPI.updateAndPublish(appId, env, clusterName, namespaceName, releaseTitle, releaseComment, branchName,
    isEmergencyPublish, deleteBranch, changeSets);
    }

    • 方法内部,调用 ReleaseAPI#updateAndPublish(...) 方法,调用 Admin Service API ,合并子 Namespace 变更的配置 Map 到父 Namespace ,并进行一次 Release 。🙂 可能会有胖友会问,为什么不 NamespaceBranchService 直接调用 ReleaseAPI 呢?ReleaseAPI 属于 ReleaseService 模块,对外透明屏蔽该细节。这样,未来 ReleaseService 想要改实现,可能不是调用 ReleaseAPI 的方法,而是别的方法,也是非常方便的。
  • 第 15 行:【TODO 6001】Tracer 日志

2.2.1 calculateBranchChangeSet

 1: @Autowired
2: private ItemsComparator itemsComparator;
3: @Autowired
4: private UserInfoHolder userInfoHolder;
5: @Autowired
6: private NamespaceService namespaceService;
7: @Autowired
8: private ItemService itemService;
9:
10: private ItemChangeSets calculateBranchChangeSet(String appId, Env env, String clusterName, String namespaceName, String branchName) {
11: // 获得父 NamespaceBO 对象
12: NamespaceBO parentNamespace = namespaceService.loadNamespaceBO(appId, env, clusterName, namespaceName);
13: // 若父 Namespace 不存在,抛出 BadRequestException 异常。
14: if (parentNamespace == null) {
15: throw new BadRequestException("base namespace not existed");
16: }
17: // 若父 Namespace 有配置项的变更,不允许合并。因为,可能存在冲突。
18: if (parentNamespace.getItemModifiedCnt() > 0) {
19: throw new BadRequestException("Merge operation failed. Because master has modified items");
20: }
21: // 获得父 Namespace 的 Item 数组
22: List<ItemDTO> masterItems = itemService.findItems(appId, env, clusterName, namespaceName);
23: // 获得子 Namespace 的 Item 数组
24: List<ItemDTO> branchItems = itemService.findItems(appId, env, branchName, namespaceName);
25: // 计算变化的 Item 集合
26: ItemChangeSets changeSets = itemsComparator.compareIgnoreBlankAndCommentItem(parentNamespace.getBaseInfo().getId(), masterItems, branchItems);
27: // 设置 `ItemChangeSets.deleteItem` 为空。因为子 Namespace 从父 Namespace 继承配置,但是实际自己没有那些配置项,所以如果不清空,会导致这些配置项被删除。
28: changeSets.setDeleteItems(Collections.emptyList());
29: // 设置 `ItemChangeSets.dataChangeLastModifiedBy` 为当前管理员
30: changeSets.setDataChangeLastModifiedBy(userInfoHolder.getUser().getUserId());
31: return changeSets;
32: }

  • 第 11 至 20 行,父 Namespace 相关
    • 第 12 行:调用 namespaceService#loadNamespaceBO(appId, env, clusterName, namespaceName) 方法,获得父 NamespaceBO 对象。该对象,包含了 Namespace 的详细数据,包括 Namespace 的基本信息、配置集合。详细解析,点击方法链接查看,笔者已经添加详细注释。方法比较冗长,胖友耐心阅读,其目的是为了【第 17 至 20 行】的判断,是否有未发布的配置变更。
    • 第 13 至 16 行:若 Namespace 不存在,抛出 BadRequestException 异常。
    • 第 17 至 20 行:若 Namespace 有未发布的配置变更,不允许合并。因为,可能存在冲突,无法自动解决。此时,需要在 Portal 上将 Namespace 的配置进行一次发布,或者回退回历史版本。
  • 第 21 至 30 行:获得配置变更集合 ItemChangeSets 对象。该对象,我们在 《Apollo 源码解析 —— Portal 批量变更 Item》
    • 第 22 行:调用 ItemService#findItems(appId, env, clusterName, namespaceName) 方法,获得 Namespace 的 ItemDTO 数组。
    • 第 24 行:调用 ItemService#findItems(appId, env, branchName, namespaceName) 方法,获得 Namespace 的 ItemDTO 数组。
    • 第 26 行:调用 ItemsComparator#compareIgnoreBlankAndCommentItem(baseNamespaceId, baseItems, targetItems) 方法,计算变化的 Item 集合。详细解析,点击方法链接查看,笔者已经添加详细注释。
    • 第 28 行:设置 ItemChangeSets.deleteItem。因为 Namespace 从 Namespace 继承配置,但是实际自己没有那些配置项,所以如果不设置为空,会导致合并时,这些配置项被删除。

2.3 ReleaseAPI

com.ctrip.framework.apollo.portal.api.ReleaseAPI ,实现 API 抽象类,封装对 Admin Service 的 Release 模块的 API 调用。代码如下:

ReleaseAPI

3. Admin Service 侧

3.1 ReleaseController

apollo-adminservice 项目中, com.ctrip.framework.apollo.adminservice.controller.ReleaseController ,提供 Release 的 API

#updateAndPublish(...) 方法,合并子 Namespace 变更的配置 Map 到父 Namespace ,并进行一次 Release 。代码如下:

 1: /**
2: * merge branch items to master and publish master
3: *
4: * @return published result
5: */
6: @Transactional
7: @RequestMapping(path = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/updateAndPublish", method = RequestMethod.POST)
8: public ReleaseDTO updateAndPublish(@PathVariable("appId") String appId,
9: @PathVariable("clusterName") String clusterName,
10: @PathVariable("namespaceName") String namespaceName,
11: @RequestParam("releaseName") String releaseName,
12: @RequestParam("branchName") String branchName,
13: @RequestParam(value = "deleteBranch", defaultValue = "true") boolean deleteBranch, // 是否删除 Namespace 分支
14: @RequestParam(name = "releaseComment", required = false) String releaseComment,
15: @RequestParam(name = "isEmergencyPublish", defaultValue = "false") boolean isEmergencyPublish,
16: @RequestBody ItemChangeSets changeSets) {
17: // 获得 Namespace
18: Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName);
19: if (namespace == null) {
20: throw new NotFoundException(String.format("Could not find namespace for %s %s %s", appId, clusterName, namespaceName));
21: }
22: // 合并子 Namespace 变更的配置 Map 到父 Namespace ,并进行一次 Release
23: Release release = releaseService.mergeBranchChangeSetsAndRelease(namespace, branchName, releaseName, releaseComment, isEmergencyPublish, changeSets);
24: // 若需要删除子 Namespace ,则进行删除
25: if (deleteBranch) {
26: namespaceBranchService.deleteBranch(appId, clusterName, namespaceName, branchName, NamespaceBranchStatus.MERGED, changeSets.getDataChangeLastModifiedBy());
27: }
28: // 发送 Release 消息
29: messageSender.sendMessage(ReleaseMessageKeyGenerator.generate(appId, clusterName, namespaceName), Topics.APOLLO_RELEASE_TOPIC);
30: // 将 Release 转换成 ReleaseDTO 对象
31: return BeanUtils.transfrom(ReleaseDTO.class, release);
32: }

  • 第 17 至 21 行:调用 NamespaceService#findOne(ppId, clusterName, namespaceName) 方法,获得 Namespace 对象。
    • 若校验到不存在,抛出 NotFoundException 异常。
  • 第 23 行:调用 ReleaseService#mergeBranchChangeSetsAndRelease(...) 方法,合并 Namespace 变更的配置 Map 到 Namespace ,并进行一次 Release 。详细解析,见 「3.2 ReleaseService」
  • 第 25 至 27 行:若需要删除子 Namespace ,即 Portal 中选择【删除灰度版本】,调用 NamespaceBranchService#deleteBranch(...) 方法,删除 Namespace 相关的记录。详细解析,见 「3.3 NamespaceBranchService」
  • 第 29 行:调用 MessageSender#sendMessage(String message, String channel) 方法,发送发布消息。
  • 第 31 行:调用 BeanUtils#transfrom(Class<T> clazz, Object src) 方法,将 Release 转换成 ReleaseDTO 对象。

3.2 ReleaseService

apollo-biz 项目中,com.ctrip.framework.apollo.biz.service.ReleaseService ,提供 Release 的 Service 逻辑给 Admin Service 和 Config Service 。

3.2.1 mergeBranchChangeSetsAndRelease

ReleaseService#mergeBranchChangeSetsAndRelease(...) 方法,合并 Namespace 变更的配置 Map 到 Namespace ,并进行一次 Release 。代码如下:

 1: // 合并子 Namespace 变更的配置 Map 到父 Namespace ,并进行一次 Release
2: @Transactional
3: public Release mergeBranchChangeSetsAndRelease(Namespace namespace, String branchName, String releaseName,
4: String releaseComment, boolean isEmergencyPublish,
5: ItemChangeSets changeSets) {
6: // 校验锁定
7: checkLock(namespace, isEmergencyPublish, changeSets.getDataChangeLastModifiedBy());
8: // 变更的配置集 合 ItemChangeSets 对象,更新到父 Namespace 中。
9: itemSetService.updateSet(namespace, changeSets);
10:
11: // 获得子 Namespace 的最新且有效的 Release 对象
12: Release branchRelease = findLatestActiveRelease(namespace.getAppId(), branchName, namespace.getNamespaceName());
13: // 获得子 Namespace 的最新且有效的 Release 编号
14: long branchReleaseId = branchRelease == null ? 0 : branchRelease.getId();
15:
16: // 获得父 Namespace 的配置 Map
17: Map<String, String> operateNamespaceItems = getNamespaceItems(namespace);
18:
19: // 创建 Map ,用于 ReleaseHistory 对象的 `operationContext` 属性。
20: Map<String, Object> operationContext = Maps.newHashMap();
21: operationContext.put(ReleaseOperationContext.SOURCE_BRANCH, branchName);
22: operationContext.put(ReleaseOperationContext.BASE_RELEASE_ID, branchReleaseId);
23: operationContext.put(ReleaseOperationContext.IS_EMERGENCY_PUBLISH, isEmergencyPublish);
24:
25: // 父 Namespace 进行发布
26: return masterRelease(namespace, releaseName, releaseComment, operateNamespaceItems,
27: changeSets.getDataChangeLastModifiedBy(),
28: ReleaseOperation.GRAY_RELEASE_MERGE_TO_MASTER, operationContext);
29: }

  • 第 7 行:调用 #checkLock(...) 方法,校验锁定。
  • 第 9 行:调用 ItemService#updateSet(namespace, changeSets) 方法,将变更的配置集 合 ItemChangeSets 对象,更新到 Namespace 中。详细解析,在 《Apollo 源码解析 —— Portal 批量变更 Item》 中。
    • 第 17 行:调用 #getNamespaceItems(namespace) 方法,获得 Namespace 的配置 Map 。因为上面已经更新过,所以获得到的是合并后的结果。
  • 第 11 至 23 行:创建 Map ,并设置需要的 KV ,用于 ReleaseHistory 对象的 operationContext 属性。
    • 第 12 行:调用 #findLatestActiveRelease(...) 方法,获得 Namespace 的最新有效的 Release 对象。
    • 第 14 行:获得 Namespace 的最新有效的 Release 编号。
    • 第 21 至 23 行:设置 KV 到 Map 中。
  • 第 26 至 28 行:调用 #masterRelease(...) 方法, Namespace 进行发布。这块,和 《Apollo 源码解析 —— Portal 发布配置》 的逻辑就统一了,所以详细解析,见该文。

3.3 NamespaceBranchService

apollo-biz 项目中,com.ctrip.framework.apollo.biz.service.NamespaceBranchService ,提供 Namespace 分支Service 逻辑给 Admin Service 和 Config Service 。

3.3.1 deleteBranch

#deleteBranch(...) 方法,删除 Namespace 相关的记录。代码如下:

 1: @Transactional
2: public void deleteBranch(String appId, String clusterName, String namespaceName,
3: String branchName, int branchStatus, String operator) {
4: // 获得子 Cluster 对象
5: Cluster toDeleteCluster = clusterService.findOne(appId, branchName);
6: if (toDeleteCluster == null) {
7: return;
8: }
9: // 获得子 Namespace 的最后有效的 Release 对象
10: Release latestBranchRelease = releaseService.findLatestActiveRelease(appId, branchName, namespaceName);
11: // 获得子 Namespace 的最后有效的 Release 对象的编号
12: long latestBranchReleaseId = latestBranchRelease != null ? latestBranchRelease.getId() : 0;
13:
14: // 创建新的,用于表示删除的 GrayReleaseRule 的对象
15: // update branch rules
16: GrayReleaseRule deleteRule = new GrayReleaseRule();
17: deleteRule.setRules("[]");
18: deleteRule.setAppId(appId);
19: deleteRule.setClusterName(clusterName);
20: deleteRule.setNamespaceName(namespaceName);
21: deleteRule.setBranchName(branchName);
22: deleteRule.setBranchStatus(branchStatus); // Namespace 分支状态
23: deleteRule.setDataChangeLastModifiedBy(operator);
24: deleteRule.setDataChangeCreatedBy(operator);
25: // 更新 GrayReleaseRule
26: doUpdateBranchGrayRules(appId, clusterName, namespaceName, branchName, deleteRule, false, -1);
27:
28: // 删除子 Cluster
29: // delete branch cluster
30: clusterService.delete(toDeleteCluster.getId(), operator);
31:
32: // 创建 ReleaseHistory 对象,并保存
33: int releaseOperation = branchStatus == NamespaceBranchStatus.MERGED ? ReleaseOperation.GRAY_RELEASE_DELETED_AFTER_MERGE : ReleaseOperation.ABANDON_GRAY_RELEASE;
34: releaseHistoryService.createReleaseHistory(appId, clusterName, namespaceName, branchName, latestBranchReleaseId, latestBranchReleaseId,
35: releaseOperation, null, operator);
36: // 记录 Audit 到数据库中
37: auditService.audit("Branch", toDeleteCluster.getId(), Audit.OP.DELETE, operator);
38: }

  • 第 4 至 8 行:调用 ClusterService#findOne(appId, branchName) 方法,获得 Cluster 对象。
  • 第 10 行:调用 ReleaseService#findLatestActiveRelease(namespace) 方法,获得最后有效的 Release 对象。
    • 第 12 行:获得最后有效的 Release 对象的编号。
  • 第 14 至 24 行:创建新的,用于表示删除的 GrayReleaseRule 的对象。并且,当前场景,该 GrayReleaseRule 的 branchStatusMERGED
  • 第 30 行:调用 ClusterService#delte(id, operator) 方法,删除子 Cluster 相关。详细解析,见 「3.4 ClusterService」
  • 第 32 至 35 行:调用 ReleaseHistoryService#createReleaseHistory(...) 方法,创建 ReleaseHistory 对象,并保存。
  • 第 37 行:记录 Audit 到数据库中。

3.4 ClusterService

apollo-biz 项目中,com.ctrip.framework.apollo.biz.service.ClusterService ,提供 Cluster 的 Service 逻辑给 Admin Service 和 Config Service 。

3.4.1 delete

#delete(...) 方法,删除 Cluster 相关。代码如下:

@Transactional
public void delete(long id, String operator) {
// 获得 Cluster 对象
Cluster cluster = clusterRepository.findOne(id);
if (cluster == null) {
throw new BadRequestException("cluster not exist");
}
// 删除 Namespace
// delete linked namespaces
namespaceService.deleteByAppIdAndClusterName(cluster.getAppId(), cluster.getName(), operator);

// 标记删除 Cluster
cluster.setDeleted(true);
cluster.setDataChangeLastModifiedBy(operator);
clusterRepository.save(cluster);

// 记录 Audit 到数据库中
auditService.audit(Cluster.class.getSimpleName(), id, Audit.OP.DELETE, operator);
}

  • 标记删除 Cluster 和其相关的 Namespace 。代码比较简单,胖友自己看看哈。

666. 彩蛋

灰度发布结束~还有一些其他流程,胖友可以自己看看,例如放弃灰度

知识星球

文章目录
  1. 1. 1. 概述
  2. 2. 2. Portal 侧
    1. 2.1. 2.1 NamespaceBranchController
    2. 2.2. 2.2 NamespaceBranchService
      1. 2.2.1. 2.2.1 calculateBranchChangeSet
    3. 2.3. 2.3 ReleaseAPI
  3. 3. 3. Admin Service 侧
    1. 3.1. 3.1 ReleaseController
    2. 3.2. 3.2 ReleaseService
      1. 3.2.1. 3.2.1 mergeBranchChangeSetsAndRelease
    3. 3.3. 3.3 NamespaceBranchService
      1. 3.3.1. 3.3.1 deleteBranch
    4. 3.4. 3.4 ClusterService
      1. 3.4.1. 3.4.1 delete
  4. 4. 666. 彩蛋