攜程開源分散式配置系統Apollo服務端是如何實時更新配置的?

java金融發表於2022-04-25

引言

前面有寫過一篇《分散式配置中心apollo是如何實時感知配置被修改》,也就是客戶端client是如何知道配置被修改了,有不少讀者私信我你既然說了client端是如何感知的,那服務端又是如何知道配置被修改了。今天我們就一起來看看Apollo在Portal修改了配置檔案,怎麼通知到configService的。什麼是portal和configService 建議可以看看這一篇文章篇《分散式配置中心apollo是如何實時感知配置被修改》,裡面對這些模組都有簡單的介紹,你如果實在不想看也行,我直接截個圖過來
在這裡插入圖片描述

服務端如何感知更新

我們來看官網提供的一張圖
在這裡插入圖片描述

1.使用者在Portal操作配置釋出
2.Portal呼叫Admin Service的介面操作釋出
3.Admin Service釋出配置後,傳送ReleaseMessage給各個Config Service
4.Config Service收到ReleaseMessage後,通知對應的客戶端

上面的流程就是從Portal到ConfigService主要流程,下面我們來看看具體的細節。要知道細節我們要自己動手去除錯一把原始碼。
我們可以照著官網的文件,自己本地把專案run起來。文件寫的還是很詳細的,只要按照步驟來都能執行的起來。我們隨便新建一個專案然後去編輯下key,然後開啟瀏覽器的F12當我們點選提交按鈕的時候我們就知道她到底呼叫了那些介面,有了介面我們就知道了入口剩下的就是打斷點進行除錯了。

portal 如何獲取AdminService

在這裡插入圖片描述
根據這個方法我們是不是就可以定位到portal模組後端程式碼的controller。找到對應的controller開啟看一看基本沒有什麼業務邏輯
在這裡插入圖片描述
然後portal緊接著就是去呼叫adminService了。
在這裡插入圖片描述
根據上圖我們就可以的方法我們就可以找到對應的adminService了,portal是如何找到對應的adminService服務的,因為adminService 是可以部署多臺機器,這裡就要用到服務註冊和發現了adminService只有被註冊到服務中心,portal才可以通過服務註冊中心來獲取對應的adminService服務了。Apollo 預設是採用eureka來作為服務註冊和發現,它也提供了nacos、consul來作為服務註冊和發現,還提供了一種kubernetes不採用第三方來做服務註冊和發現,直接把服務的地址配置在資料庫。如果地址有多個可以在資料庫逗號分隔。
在這裡插入圖片描述
它提供了四種獲取服務列表的實現方式,如果我們使用的註冊中心是eureka 我們是不是需要通過eureka的api去獲取服務列表,如果我們的服務發現使用的是nacos我們是不是要通過nacos的API去獲取服務列表。。。所以Apollo提供了一個MetaService 層,封裝服務發現的細節,對Portal和Client而言,永遠通過一個Http介面獲取Admin Service和Config Service的服務資訊,而不需要關心背後實際的服務註冊和發現元件。就跟我們平時搬磚一樣沒有啥是通過增加一箇中間層解決不了的問題,一個不行那就再加一個。所以MetaService提供了兩個介面services/admin 和services/config 來分別獲取Admin Service和Config Service的服務資訊。那麼Portal 是如何來呼叫services/admin這個介面的呢?在 apollo-portal 專案裡面com.ctrip.framework.apollo.portal.component#AdminServiceAddressLocator 這個類裡面,

  • 這個類在載入的時候會通過MetaService 提供的services/admin 介面獲取adminService的服務地址進行快取。
  @PostConstruct
  public void init() {
    allEnvs = portalSettings.getAllEnvs();
    //init restTemplate
    restTemplate = restTemplateFactory.getObject();
    
    refreshServiceAddressService =
        Executors.newScheduledThreadPool(1, ApolloThreadFactory.create("ServiceLocator", true));
	// 建立延遲任務,1s後開始執行獲取AdminService服務地址
    refreshServiceAddressService.schedule(new RefreshAdminServerAddressTask(), 1, TimeUnit.MILLISECONDS);
  }

在這裡插入圖片描述
上面要去MetaService 請求地址,那麼MetaService的地址又是什麼呢?這個又如何獲取?com.ctrip.framework.apollo.portal.environment#DefaultPortalMetaServerProvider 這個類。

portal 這個模組說完了,我們接著回到adminService了。通過portal呼叫adminService的介面地址我們很快可以找到它的入口
AdminService 的實現也很簡單


  @PreAcquireNamespaceLock
@PostMapping("/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items")
  public ItemDTO create(@PathVariable("appId") String appId,
                        @PathVariable("clusterName") String clusterName,
                        @PathVariable("namespaceName") String namespaceName, @RequestBody ItemDTO dto) {
    Item entity = BeanUtils.transform(Item.class, dto);

    ConfigChangeContentBuilder builder = new ConfigChangeContentBuilder();
    Item managedEntity = itemService.findOne(appId, clusterName, namespaceName, entity.getKey());
    if (managedEntity != null) {
      throw new BadRequestException("item already exists");
    }
    entity = itemService.save(entity);
    builder.createItem(entity);
    dto = BeanUtils.transform(ItemDTO.class, entity);

    Commit commit = new Commit();
    commit.setAppId(appId);
    commit.setClusterName(clusterName);
    commit.setNamespaceName(namespaceName);
    commit.setChangeSets(builder.build());
    commit.setDataChangeCreatedBy(dto.getDataChangeLastModifiedBy());
    commit.setDataChangeLastModifiedBy(dto.getDataChangeLastModifiedBy());
    commitService.save(commit);

    return dto;
  }

PreAcquireNamespaceLock 註解

首先方法上有個@PreAcquireNamespaceLock 這個註解,這個根據名字都應該能夠去猜一個大概就是去獲取NameSpace的分散式鎖,現在分散式鎖比較常見的方式是採用redis和zookeeper。但是在這裡apollo是採用資料庫來實現的,具體怎麼細節大家可以去看看原始碼應該都看的懂,無非就是加鎖往DB裡面插入一條資料,釋放鎖然後把這個資料進行刪除。稍微有點不一樣的就是如果獲取鎖失敗,就直接返回失敗了,不會在繼續自旋或者休眠重新去獲取鎖。 因為獲取鎖失敗說明已經有其他人在你之前修改了配置,只有這個人新增的配置被髮布或者刪除之後,其他人才能繼續新增配置,這樣的話就會導致一個NameSpace只能同時被一個人修改。這個限制是預設關閉的需要我們在資料庫裡面去配置(ApolloConfigDb的ServiceConfig表)在這裡插入圖片描述
一般我們應用的配置修改應該是比較低頻的,多人同時去修改的話情況會比較少,再說有些公司是開發提交配置,測試去釋出配置,提交和修改不能是同一個人,這樣的話新增配置衝突就更少了,應該沒有必要去配置namespace.lock.switch=true一個namespace只能一個人去修改。

接下來的程式碼就非常簡單明瞭,就是一個簡單的引數判斷然後執行入庫操作了,把資料插入到Item表裡面。這是我們新增的配置資料就已經儲存了。效果如下
在這裡插入圖片描述
這時候新增的配置是不起作用的,不會推送給客戶端的。只是單純一個類似於草稿的狀態。

釋出配置

接下來我們要使上面新增的配置生效,並且推送給客戶端。同樣的我們點選發布按鈕然後就能知道對應的後端方法入口
在這裡插入圖片描述
我們通過這個介面可以直接找到adminService的方法入口

 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
    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.transform(ReleaseDTO.class, release);
  }
  • 上述程式碼就不仔細展開分析了,感興趣的可以自己斷點除錯下我們重點看下releaseService.publish 這個方法,裡面有一些灰度釋出相關的邏輯,不過這個不是本文的重點,這個方法主要是往release表插入資料。
  • 接下來就是messageSender.sendMessage這個方法了,這個方法主要是往ReleaseMessage表裡面插入一條記錄。儲存完ReleaseMessage這個表會得到相應的主鍵ID,然後把這個ID放入到一個佇列裡面。然後在載入DatabaseMessageSender的時候會預設起一個定時任務去獲取上面佇列裡面放入的訊息ID,然後找出比這這些ID小的訊息刪除掉。
    釋出流程就完了,這裡也沒有說到服務端是怎麼感知有配置修改了的。

Config Service 通知配置變化

apolloConfigService 在服務啟動的時候ReleaseMessageScanner 會啟動一個定時任務 每隔1s去去查詢ReleaseMessage裡面有沒有最新的訊息,如果有就會通知到所有的訊息監聽器比如NotificationControllerV2ConfigFileController等,這個訊息監聽器註冊是在ConfigServiceAutoConfiguration裡面註冊的。
NotificationControllerV2 得到配置釋出的 AppId+Cluster+Namespace 後,會通知對應的客戶端,這樣就從portal到configService 到 client 整個訊息通知變化就串起來了。服務端通知客戶端的具體細節可以看看《分散式配置中心apollo是如何實時感知配置被修改》
在這裡插入圖片描述

總結

這樣服務端配置如何更新的流程就完了。

1.使用者在Portal操作配置釋出
2.Portal呼叫Admin Service的介面操作釋出
3.Admin Service釋出配置後,傳送ReleaseMessage給各個Config Service
4.Config Service收到ReleaseMessage後,通知對應的客戶端

apollo的原始碼相對於其他中介軟體來說還是相對於比較簡單的,比較適合於想研究下中介軟體原始碼,又不知道如何下手的同學 。

結束

  • 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
  • 如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。
  • 感謝您的閱讀,十分歡迎並感謝您的關注。

站在巨人的肩膀
https://www.apolloconfig.com/#/zh/design/apollo-design?id=一、總體設計
https://www.iocoder.cn/Apollo/client-polling-config/

相關文章