主版本發佈即點擊主版本發佈按鈕:java
具體接口位置:com.ctrip.framework.apollo.adminservice.controller
包下 ReleaseController#publish
實際上灰度版本發佈也是調用這個接口的。
代碼:mysql
/** * 主版本發佈 */ @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 消費。git
因此,咱們主要關注 Service 層的 publish 方法。github
該方法有些繁瑣,主要流程圖以下:sql
能夠經過比對流程圖和代碼來看。數據庫
代碼以下:併發
@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 的最後修改者就是當前用戶,那麼就拋出異常。禁止其修改。app
根據 namespace 獲取全部的 item,也就是配置。ui
判斷當前的 namespace 是否有父 namespace,若是有,說明當前 namespace 是灰度 namespace,則進行灰度發佈(主版本發佈和灰度發佈邏輯不一樣)。spa
這裏說下父子 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 方法更多的細節再也不贅述,有疑惑的地方能夠交流。
這個發送消息的操做原本應該是 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 實現類。
該類會將數據直接保存到數據庫。而後清理掉比剛剛存的消息舊的消息
—— 防止消息表不斷增大。
發佈分爲主版本發佈,灰度版本發佈,全量發佈,此次說了前兩個,全量發佈下次再說。
而主/灰髮布的一個比較繁瑣的地方就是兩個版本的合併,灰度版本發佈要合併主版本。主版本發佈要更新灰度版本。
同時,灰度的設計也有點繞,中間隔了一層 cluster。
在發佈成功以後,須要發送消息到數據庫,讓 ConfigService 可以感知到這次發佈,並通知客戶端。關於如何通知客戶端,下次再說。