目錄
- UI 界面
- Portal 服務
- admin 服務
- 總結
1. UI 界面
2. Portal 服務
當我們點擊上面的發布按鈕的時候,調用的當然是 portal 的接口。具體代碼如下:
/**
* 全量發布
* @param appId SampleApp
* @param env DEV
* @param clusterName default
* @param namespaceName application
* @param branchName 分支/灰度名稱
* @param deleteBranch true
* @param model {"releaseTitle":"20180716220550-gray-release-merge-to-master","releaseComment":"","isEmergencyPublish":false}
* @return
*/
@PreAuthorize(value = "@permissionValidator.hasReleaseNamespacePermission(#appId, #namespaceName)")
@RequestMapping(value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/merge", method = RequestMethod.POST)
public ReleaseDTO merge(@PathVariable String appId, @PathVariable String env,
@PathVariable String clusterName, @PathVariable String namespaceName,
@PathVariable String branchName, @RequestParam(value = "deleteBranch", defaultValue = "true") boolean deleteBranch,
@RequestBody NamespaceReleaseModel model) {
// 如果是緊急發布,但該環境不允許緊急發布,拋出異常
if (model.isEmergencyPublish() && !portalConfig.isEmergencyPublishAllowed(Env.fromString(env))) {
throw new BadRequestException(String.format("Env: %s is not supported emergency publish now", env));
}
// 合並主版本和灰度版本, 得到一個發布 dto
ReleaseDTO createdRelease = namespaceBranchService.merge(appId, Env.valueOf(env), clusterName, namespaceName, branchName,
model.getReleaseTitle(), model.getReleaseComment(),
model.isEmergencyPublish(), deleteBranch);
ConfigPublishEvent event = ConfigPublishEvent.instance();
event.withAppId(appId)
.withCluster(clusterName)
.withNamespace(namespaceName)
.withReleaseId(createdRelease.getId())
.setMergeEvent(true)
.setEnv(Env.valueOf(env));
publisher.publishEvent(event);// 發送郵件
return createdRelease;
}
接口職責不多:是否符合緊急發布的數據校驗,調用 Service, 發布“配置發布”事件(發送郵件)。
看看調用 Service 的過程,該方法稱為 merge ,實際上就是合並灰度和主版本的配置。代碼如下:
public ReleaseDTO merge(String appId, Env env, String clusterName, String namespaceName,
String branchName, String title, String comment,
boolean isEmergencyPublish, boolean deleteBranch) {
// 計算 changeSets
ItemChangeSets changeSets = calculateBranchChangeSet(appId, env, clusterName, namespaceName, branchName);
// 調用 admin 服務
ReleaseDTO mergedResult =
releaseService.updateAndPublish(appId, env, clusterName, namespaceName, title, comment,
branchName, isEmergencyPublish, deleteBranch, changeSets);
Tracer.logEvent(TracerEventType.MERGE_GRAY_RELEASE,
String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName));
return mergedResult;
}
做了 2 件事情: 計算 change 集合,調用 admin 服務。很明顯,計算 change 對於 protal 非常重要。
calculateBranchChangeSet 方法主要將灰度配置和主版本配置合並。
代碼:
private ItemChangeSets calculateBranchChangeSet(String appId, Env env, String clusterName, String namespaceName,
String branchName) {
NamespaceBO parentNamespace = namespaceService.loadNamespaceBO(appId, env, clusterName, namespaceName);// 父版本 namespace
if (parentNamespace == null) {
throw new BadRequestException("base namespace not existed");
}
if (parentNamespace.getItemModifiedCnt() > 0) {
throw new BadRequestException("Merge operation failed. Because master has modified items");
}
List<ItemDTO> masterItems = itemService.findItems(appId, env, clusterName, namespaceName);// 主版本 items
List<ItemDTO> branchItems = itemService.findItems(appId, env, branchName, namespaceName);// 子版本 items
ItemChangeSets changeSets = itemsComparator.compareIgnoreBlankAndCommentItem(parentNamespace.getBaseInfo().getId(),
masterItems, branchItems);// 得到 changeSet
changeSets.setDeleteItems(Collections.emptyList());// 防止誤刪除,emm,灰度的內容並不是全量的,因此上面的計算有些問題,並且目前沒有刪除功能。所以這里可以置空。
changeSets.setDataChangeLastModifiedBy(userInfoHolder.getUser().getUserId());
return changeSets;
}
步驟:
- 獲取主版本的 namespace 詳細信息,用於數據檢驗,id 賦值。
- 獲取主版本的所有 item 配置,再獲取灰度版本的所有 item 配置,注意,灰度版本的 item 只有其自身新增的和修改的配置,不是全量的(這將導致后面一個奇怪的現象)。
- 比較兩者差異,得到 change 集合。
- 設置 deleteList 為空 —— 奇怪現象(
灰度的內容並不是全量的,因此上面的計算有些問題,並且目前沒有刪除功能。所以這里可以置空, 並且防止誤刪除
)。 - 設置修改人。
這里需要注意的是計算差異到底是怎么計算的,為什么后面有置空 deleteItem 的操作。
我就不貼全部的方法了,貼一下對刪除操作有影響的代碼:
/** 比較,忽略空格,返回一個改變的 items */
public ItemChangeSets compareIgnoreBlankAndCommentItem(long baseNamespaceId, List<ItemDTO> baseItems, List<ItemDTO> targetItems){
// 忽略新增/修改 item 代碼......
// 處理刪除,但這個邏輯似乎不對. 不過此類不知道數據來源,工具類沒有問題.
for (ItemDTO item: baseItems){// 主版本
String key = item.getKey();
ItemDTO targetItem = targetItemMap.get(key);
if(targetItem == null){//delete// 如果灰度版本里沒有,說明刪除了.
changeSets.addDeleteItem(item);// 添加進刪除集合
}
}
return changeSets;
}
可以看到,這段代碼里,循環主版本,逐個對比灰度版本,如果灰度版本里沒有,就添加進 delete 集合,而我們知道,灰度版本的 item 只有修改的和新增的,這時,將導致誤刪除。
但這個工具類的計算是沒有問題的,有問題的是外層數據的完整性。
因此需要在外面打個補丁:changeSets.setDeleteItems(Collections.emptyList());
好,計算完 changeSet,就要調用 admin 服務了,並且把 changeSet 傳遞過去,然后返回一個 release 對象,表示發布成功,並發布事件。
在分析 admin 之前,總結一下 protal 的流程:
3. admin 服務
從 portal 的代碼中,可以看到,調用的是 admin 的 updateAndPublish 方法接口,看看這個接口:
位置 : com.ctrip.framework.apollo.adminservice.controller.ReleaseController.java
代碼如下:
@Transactional
@RequestMapping(path = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/updateAndPublish", method = RequestMethod.POST)
public ReleaseDTO updateAndPublish(@PathVariable("appId") String appId,// 應用名稱
@PathVariable("clusterName") String clusterName,//集群
@PathVariable("namespaceName") String namespaceName,// 主版本名稱
@RequestParam("releaseName") String releaseName, // 發布名稱
@RequestParam("branchName") String branchName,// 灰度名稱 cluster
@RequestParam(value = "deleteBranch", defaultValue = "true") boolean deleteBranch,// 是否刪除灰度
@RequestParam(name = "releaseComment", required = false) String releaseComment,// 評論
@RequestParam(name = "isEmergencyPublish", defaultValue = "false") boolean isEmergencyPublish,// 是否緊急發布
@RequestBody ItemChangeSets changeSets) {// 這個是 portal 發來的
Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName);// 找到分支
if (namespace == null) {
throw new NotFoundException(String.format("Could not find namespace for %s %s %s", appId,
clusterName, namespaceName));
}
// 合並改變 並且發布
Release release = releaseService.mergeBranchChangeSetsAndRelease(namespace, branchName, releaseName,
releaseComment, isEmergencyPublish, changeSets);
// 是否刪除分支
if (deleteBranch) {
namespaceBranchService.deleteBranch(appId, clusterName, namespaceName, branchName,
NamespaceBranchStatus.MERGED, changeSets.getDataChangeLastModifiedBy());
}
// 保存發布消息到數據庫
messageSender.sendMessage(ReleaseMessageKeyGenerator.generate(appId, clusterName, namespaceName),
Topics.APOLLO_RELEASE_TOPIC);
return BeanUtils.transfrom(ReleaseDTO.class, release);
}
這個接口接受 portal 調用,比較有趣的點是,這里的 changeSet 是 portal 計算的,而不是 admin 自己計算的。
然后,controller 層比較簡單,數據校驗,調用 Service,發送消息。
當然主要看看 Service。
主要是 releaseService 的 mergeBranchChangeSetsAndRelease 方法,看名字,任務很多:合並分支修改集合,並且發布。
代碼如下:
@Transactional
public Release mergeBranchChangeSetsAndRelease(Namespace namespace, String branchName, String releaseName,
String releaseComment, boolean isEmergencyPublish,
ItemChangeSets changeSets) {
// 檢查鎖
checkLock(namespace, isEmergencyPublish, changeSets.getDataChangeLastModifiedBy());
/// 更新 item
itemSetService.updateSet(namespace, changeSets);
// 找到最新發布的 release
Release branchRelease = findLatestActiveRelease(namespace.getAppId(), branchName, namespace
.getNamespaceName());
// release Id
long branchReleaseId = branchRelease == null ? 0 : branchRelease.getId();
// 找到當前 namespace 的所有 Item(剛剛更新的)
Map<String, String> operateNamespaceItems = getNamespaceItems(namespace);
Map<String, Object> operationContext = Maps.newHashMap();
// 構造操作上下文 sourceBranch=灰度名稱 baseReleaseId=最新的releaseId isEmergencyPublish=是否緊急發布, 用於構建發布歷史
operationContext.put(ReleaseOperationContext.SOURCE_BRANCH, branchName);
operationContext.put(ReleaseOperationContext.BASE_RELEASE_ID, branchReleaseId);
operationContext.put(ReleaseOperationContext.IS_EMERGENCY_PUBLISH, isEmergencyPublish);
// ReleaseHistory Audit 主版本
return masterRelease(namespace, releaseName, releaseComment, operateNamespaceItems,
changeSets.getDataChangeLastModifiedBy(),
// 灰度合並回主分支發布
ReleaseOperation.GRAY_RELEASE_MERGE_TO_MASTER, operationContext);
}
代碼很簡單,步驟:
- 檢查鎖,和普通發布一樣,判斷修改者和發布者是不是同一個人。
- 根據 Portal 傳遞來的 changeSets 更新 item。
- 找到最新發布的 release(構建發布歷史的上下文)。
- 發布主版本。
其中,updateSet 方法比較重要,要看看他是怎么更新 item 的。
方法很長,總之,就是將 changeSet 的內容保存到主版本的 namespace 下。
@Transactional
public ItemChangeSets updateSet(String appId, String clusterName,
String namespaceName, ItemChangeSets changeSet) {
// 最后改變數據的人
String operator = changeSet.getDataChangeLastModifiedBy();
// 改變數據的詳細信息
ConfigChangeContentBuilder configChangeContentBuilder = new ConfigChangeContentBuilder();
// 如果創建了新的
if (!CollectionUtils.isEmpty(changeSet.getCreateItems())) {
// 循環
for (ItemDTO item : changeSet.getCreateItems()) {
// 轉換
Item entity = BeanUtils.transfrom(Item.class, item);
entity.setDataChangeCreatedBy(operator);
entity.setDataChangeLastModifiedBy(operator);
// 保存 item 到數據庫
Item createdItem = itemService.save(entity);
// 保存到 builder createItems List 中
configChangeContentBuilder.createItem(createdItem);
}
// 最后記錄審核
auditService.audit("ItemSet", null, Audit.OP.INSERT, operator);
}
// 如果有修改的數據
if (!CollectionUtils.isEmpty(changeSet.getUpdateItems())) {
for (ItemDTO item : changeSet.getUpdateItems()) {
// 轉換並尋找
Item entity = BeanUtils.transfrom(Item.class, item);
Item managedItem = itemService.findOne(entity.getId());
// 不存在拋出異常
if (managedItem == null) {
throw new NotFoundException(String.format("item not found.(key=%s)", entity.getKey()));
}
// 之前的數據
Item beforeUpdateItem = BeanUtils.transfrom(Item.class, managedItem);
//protect. only value,comment,lastModifiedBy,lineNum can be modified
// 將之前數據內容更新
managedItem.setValue(entity.getValue());
managedItem.setComment(entity.getComment());
managedItem.setLineNum(entity.getLineNum());
managedItem.setDataChangeLastModifiedBy(operator);
// 更新
Item updatedItem = itemService.update(managedItem);
// 更新 builder 中 value
configChangeContentBuilder.updateItem(beforeUpdateItem, updatedItem);
}
// 最后審核 itemSet
auditService.audit("ItemSet", null, Audit.OP.UPDATE, operator);
}
// 如果有刪除的
if (!CollectionUtils.isEmpty(changeSet.getDeleteItems())) {
for (ItemDTO item : changeSet.getDeleteItems()) {
// 數據庫刪除
Item deletedItem = itemService.delete(item.getId(), operator);
// 添加到 builder 中
configChangeContentBuilder.deleteItem(deletedItem);
}
// 審核
auditService.audit("ItemSet", null, Audit.OP.DELETE, operator);
}
// 如果 builder 中有內容
if (configChangeContentBuilder.hasContent()){
// 創建提交記錄
createCommit(appId, clusterName, namespaceName,
configChangeContentBuilder.build(), // 將 build 變成 json 保存
changeSet.getDataChangeLastModifiedBy());
}
return changeSet;
}
在成功更新 itme 之后,便可以進行最終的發布了,發布很簡單,就不展開講了。
然后看看刪除灰度,默認是要刪除的。
步驟:
- 找到灰度發布的最新 release。
- 更新灰度規則,置空灰度規則。
- 刪除灰度 cluster 和關聯的 namespace。置於灰度為什么和 cluster 關聯,而不是和 namespace 關聯,這是因為最初的 apollo 沒有設計灰度,后面加上灰度的時候,為了避免 namespace 大幅修改,就在 cluster 里加入父子邏輯了(咨詢過作者)。
- 記錄發布歷史。根據是否 merge 記錄是放棄灰度還是合並后刪除,方便審計。
發布操作有很多類型,apollo 的常量如下:
public interface ReleaseOperation {
int NORMAL_RELEASE = 0;//普通發布
int ROLLBACK = 1;// 回滾
int GRAY_RELEASE = 2;// 灰度發布
int APPLY_GRAY_RULES = 3;// 灰度規則更新
int GRAY_RELEASE_MERGE_TO_MASTER = 4;// 灰度合並回主分支發布
int MASTER_NORMAL_RELEASE_MERGE_TO_GRAY = 5;// 主分支發布灰度自動發布
int MATER_ROLLBACK_MERGE_TO_GRAY = 6;// 主分支回滾灰度自動發布
int ABANDON_GRAY_RELEASE = 7;//放棄灰度
int GRAY_RELEASE_DELETED_AFTER_MERGE = 8;// 灰度版本合並后刪除
}
總結一下 admin 的發布流程:
4. 總結
將 portal 和 admin 組合起來看,下圖: