目錄
- Controller 層
- Service 層 publish 方法
- 發送 ReleaseMessage 消息
- 總結
1. Controller 層
主版本發布即點擊主版本發布按鈕:
具體接口位置:com.ctrip.framework.apollo.adminservice.controller
包下 ReleaseController#publish
實際上灰度版本發布也是調用這個接口的。
代碼:
/**
* 主版本發布
*/
@Transactional
@RequestMapping(path = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/releases", method = RequestMethod.POST)
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 = 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.publish(namespace, releaseName, releaseComment, operator, isEmergencyPublish);
//send release message 發送消息到 ReleaseMessage
Namespace parentNamespace = namespaceService.findParentNamespace(namespace);
String messageCluster;
if (parentNamespace != null) {
messageCluster = parentNamespace.getClusterName();
} else {
messageCluster = clusterName;
}
messageSender.sendMessage(ReleaseMessageKeyGenerator.generate(appId, messageCluster, namespaceName),
Topics.APOLLO_RELEASE_TOPIC);
return BeanUtils.transfrom(ReleaseDTO.class, release);
}
該層主要做了 2 件事情,1是調用 Service 層的 public 方法做真正的發布操作,2是發送“發布消息”到數據庫——等待 ConfigService 消費。
所以,我們主要關注 Service 層的 publish 方法。
2. Service 層 publish 方法
該方法有些繁瑣,主要流程圖如下:
可以通過比對流程圖和代碼來看。
代碼如下:
@Transactional
public Release publish(Namespace namespace, String releaseName, String releaseComment,
String operator, boolean isEmergencyPublish) {
// 檢查鎖
checkLock(namespace, isEmergencyPublish, operator);
// 獲取 item
Map<String, String> operateNamespaceItems = getNamespaceItems(namespace);
// 根據當前 namespace 找到父 namespace, 也就是灰度的主版本.
Namespace parentNamespace = namespaceService.findParentNamespace(namespace);
//branch release // 父 namespace 不是 null, 說明當前就是灰度版本.
if (parentNamespace != null) {
// 發布灰度版本.
return publishBranchNamespace(parentNamespace, namespace, operateNamespaceItems,
releaseName, releaseComment, operator, isEmergencyPublish);
}
// 非灰度版本, 找到子版本
Namespace childNamespace = namespaceService.findChildNamespace(namespace);
Release previousRelease = null;
if (childNamespace != null) {
// 找到上一個版本
previousRelease = findLatestActiveRelease(namespace);
}
//master release
Map<String, Object> operationContext = Maps.newHashMap();
// 記錄是否緊急發布
operationContext.put(ReleaseOperationContext.IS_EMERGENCY_PUBLISH, isEmergencyPublish);
// 主版本發布
Release release = masterRelease(namespace, releaseName, releaseComment, operateNamespaceItems,
operator, ReleaseOperation.NORMAL_RELEASE, operationContext);
//merge to branch and auto release
// 將主版本合並到灰度版本. 並自動發布
if (childNamespace != null) {
mergeFromMasterAndPublishBranch(namespace, childNamespace, operateNamespaceItems,
releaseName, releaseComment, operator, previousRelease,
release, isEmergencyPublish);
}
return release;
}
-
檢查鎖:如果不是緊急發布,就需要檢查鎖,如果這個 namespace 的最后修改者就是當前用戶,那么就拋出異常。禁止其修改。
-
根據 namespace 獲取所有的 item,也就是配置。
-
判斷當前的 namespace 是否有父 namespace,如果有,說明當前 namespace 是灰度 namespace,則進行灰度發布(主版本發布和灰度發布邏輯不同)。
這里說下父子 namespace 在 apollo 的設計:
從圖中可以看出,namespace 和 cluster 是多對一的關系,而 cluster 有個字段:ParentClusterId,也就是說,cluster 是有層級的。每當創建一個灰度配置,實際上,就是創建了一個新的 cluster,這個新的 cluster 的名字就是 時間戳-字符串
,大概是這樣的:20180705150428-1dc5208dc9e8146b
. 然后再在這個新 cluster 下面創建新的 namespace,那么,namespace 無形中也有了層級(父子)關系。
-
如果沒有父 namespace,說明是主版本發布,那么就需要處理他的子 (灰度)版本,同時,為了后面比對灰度版本和上一個版本的區別(如果灰度修改了上一個版本的數據,就需要記錄,否則,灰度數據和主版本將無法對應),還要記錄上一個版本的 release 信息。
-
發布主版本。並保存發布歷史。
-
如果存在灰度版本,就更新灰度版本的配置,並發布灰度版本。
關於灰度版本,這里多提一句,每次發布都是一個 release,release 對象有個 configuration,包含了此次發布的全量配置,因此,灰度發布的 configuration 中,包含了每次對應的主版本的配置,如果主版本發生了變化,那么灰度版本肯定也是要變更的。所以需要重新發布灰度版本。
其中關鍵的方法就是 mergeConfiguration
,該方法表明了灰度發布的主要邏輯:
private Map<String, String> mergeConfiguration(Map<String, String> baseConfigurations,
Map<String, String> coverConfigurations) {
Map<String, String> result = new HashMap<>();
//copy base configuration
for (Map.Entry<String, String> entry : baseConfigurations.entrySet()) {
result.put(entry.getKey(), entry.getValue());
}
//update and publish
for (Map.Entry<String, String> entry : coverConfigurations.entrySet()) {
result.put(entry.getKey(), entry.getValue());
}
return result;
}
方法很簡單:兩個參數,主版本配置,灰度版本配置。首先將主版本配置保存到 Map 中,然后將灰度版本配置也 put 到 Map 中,利用 Map 唯一 Key 的特性,保證灰度版本覆蓋主版本。
所以這個方法的 put 順序決定了灰度版本覆蓋主版本。
publish 方法更多的細節不再贅述,有疑惑的地方可以交流。
3. 發送 ReleaseMessage 消息
這個發送消息的操作本來應該是 MQ,apollo 為了減少依賴,直接使用的 mysql,但已經留好了MQ 的設計。關於 ReleaseMessage 的設計,我這里引用一下 apollo 的文檔:
Admin Service在配置發布后,需要通知所有的Config Service有配置發布,從而Config Service可以通知對應的客戶端來拉取最新的配置。
從概念上來看,這是一個典型的消息使用場景,Admin Service作為producer發出消息,各個Config Service作為consumer消費消息。通過一個消息組件(Message Queue)就能很好的實現Admin Service和Config Service的解耦。
在實現上,考慮到Apollo的實際使用場景,以及為了盡可能減少外部依賴,我們沒有采用外部的消息中間件,而是通過數據庫實現了一個簡單的消息隊列。
實現方式如下:
- Admin Service在配置發布后會往ReleaseMessage表插入一條消息記錄,消息內容就是配置發布的AppId+Cluster+Namespace,參見DatabaseMessageSender
- Config Service有一個線程會每秒掃描一次ReleaseMessage表,看看是否有新的消息記錄,參見ReleaseMessageScanner
- Config Service如果發現有新的消息記錄,那么就會通知到所有的消息監聽器(ReleaseMessageListener),如NotificationControllerV2,消息監聽器的注冊過程參見ConfigServiceAutoConfiguration
- NotificationControllerV2得到配置發布的AppId+Cluster+Namespace后,會通知對應的客戶端
示意圖如下:
apollo 定義了 MessageSender 接口,定義了一個 sendMessage 方法,這個方法目前只有基於 Mysql 的實現,即 DatabaseMessageSender 實現類。
該類會將數據直接保存到數據庫。然后清理掉比剛剛存的消息舊的消息
—— 防止消息表不斷增大。
4. 總結
發布分為主版本發布,灰度版本發布,全量發布,這次說了前兩個,全量發布下次再說。
而主/灰發布的一個比較繁瑣的地方就是兩個版本的合並,灰度版本發布要合並主版本。主版本發布要更新灰度版本。
同時,灰度的設計也有點繞,中間隔了一層 cluster。
在發布成功之后,需要發送消息到數據庫,讓 ConfigService 能夠感知到此次發布,並通知客戶端。關於如何通知客戶端,下次再說。