Apollo 9 — adminService 主/灰度版本發布
目錄
- 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 能夠感知到此次發布,並通知客戶端。關於如何通知客戶端,下次再說。
Apollo 9 — adminService 主/灰度版本發布