分布式配置中心服務端如何實時更新?
服務端如何感知更新
我們來看官網提供的一張圖:
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里面有沒有最新的消息,如果有就會通知到所有的消息監聽器比如NotificationControllerV2、ConfigFileController等,這個消息監聽器注冊是在ConfigServiceAutoConfiguration里面注冊的。NotificationControllerV2 得到配置發布的 AppId+Cluster+Namespace 后,會通知對應的客戶端,這樣就從portal到configService 到 client 整個消息通知變化就串起來了。
總結
這樣服務端配置如何更新的流程就完了。
1.用戶在Portal操作配置發布。
2.Portal調用Admin Service的接口操作發布。
3.Admin Service發布配置后,發送ReleaseMessage給各個Config Service。
4.Config Service收到ReleaseMessage后,通知對應的客戶端”apollo的源碼相對于其他中間件來說還是相對于比較簡單的,比較適合于想研究下中間件源碼,又不知道如何下手的同學 。