分布式配置中心服務(wù)端如何實時更新?
服務(wù)端如何感知更新
我們來看官網(wǎng)提供的一張圖:
1.用戶在Portal操作配置發(fā)布。
2.Portal調(diào)用Admin Service的接口操作發(fā)布。
3.Admin Service發(fā)布配置后,發(fā)送ReleaseMessage給各個Config Service。
4.Config Service收到ReleaseMessage后,通知對應(yīng)的客戶端。
上面的流程就是從Portal到ConfigService主要流程,下面我們來看看具體的細(xì)節(jié)。要知道細(xì)節(jié)我們要自己動手去調(diào)試一把源碼。我們可以照著官網(wǎng)的文檔,自己本地把項目run起來。文檔寫的還是很詳細(xì)的,只要按照步驟來都能運行的起來。我們隨便新建一個項目然后去編輯下key,然后打開瀏覽器的F12當(dāng)我們點擊提交按鈕的時候我們就知道她到底調(diào)用了那些接口,有了接口我們就知道了入口剩下的就是打斷點進(jìn)行調(diào)試了。
portal 如何獲取AdminService
根據(jù)這個方法我們是不是就可以定位到portal模塊后端代碼的controller。找到對應(yīng)的controller打開看一看基本沒有什么業(yè)務(wù)邏輯。
然后portal緊接著就是去調(diào)用adminService了。
根據(jù)上圖我們就可以的方法我們就可以找到對應(yīng)的adminService了,portal是如何找到對應(yīng)的adminService服務(wù)的,因為adminService 是可以部署多臺機(jī)器,這里就要用到服務(wù)注冊和發(fā)現(xiàn)了adminService只有被注冊到服務(wù)中心,portal才可以通過服務(wù)注冊中心來獲取對應(yīng)的adminService服務(wù)了。Apollo 默認(rèn)是采用eureka來作為服務(wù)注冊和發(fā)現(xiàn),它也提供了nacos、consul來作為服務(wù)注冊和發(fā)現(xiàn),還提供了一種kubernetes不采用第三方來做服務(wù)注冊和發(fā)現(xiàn),直接把服務(wù)的地址配置在數(shù)據(jù)庫。如果地址有多個可以在數(shù)據(jù)庫逗號分隔。
它提供了四種獲取服務(wù)列表的實現(xiàn)方式,如果我們使用的注冊中心是eureka 我們是不是需要通過eureka的api去獲取服務(wù)列表,如果我們的服務(wù)發(fā)現(xiàn)使用的是nacos我們是不是要通過nacos的API去獲取服務(wù)列表。。。所以Apollo提供了一個MetaService 層,封裝服務(wù)發(fā)現(xiàn)的細(xì)節(jié),對Portal和Client而言,永遠(yuǎn)通過一個Http接口獲取Admin Service和Config Service的服務(wù)信息,而不需要關(guān)心背后實際的服務(wù)注冊和發(fā)現(xiàn)組件。就跟我們平時搬磚一樣沒有啥是通過增加一個中間層解決不了的問題,一個不行那就再加一個。所以MetaService提供了兩個接口services/admin 和services/config 來分別獲取Admin Service和Config Service的服務(wù)信息。那么Portal 是如何來調(diào)用services/admin這個接口的呢?
在 apollo-portal 項目里面com.ctrip.framework.apollo.portal.component#AdminServiceAddressLocator 這個類里面。
- 這個類在加載的時候會通過MetaService 提供的services/admin 接口獲取adminService的服務(wù)地址進(jìn)行緩存。
@PostConstruct
public void init() {
allEnvs = portalSettings.getAllEnvs();
//init restTemplate
restTemplate = restTemplateFactory.getObject();
refreshServiceAddressService =
Executors.newScheduledThreadPool(1, ApolloThreadFactory.create("ServiceLocator", true));
// 創(chuàng)建延遲任務(wù),1s后開始執(zhí)行獲取AdminService服務(wù)地址
refreshServiceAddressService.schedule(new RefreshAdminServerAddressTask(), 1, TimeUnit.MILLISECONDS);
}
上面要去MetaService 請求地址,那么MetaService的地址又是什么呢?這個又如何獲取?com.ctrip.framework.apollo.portal.environment#DefaultPortalMetaServerProvider 這個類。
portal 這個模塊說完了,我們接著回到adminService了。通過portal調(diào)用adminService的接口地址我們很快可以找到它的入口 AdminService 的實現(xiàn)也很簡單。
@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 這個注解,這個根據(jù)名字都應(yīng)該能夠去猜一個大概就是去獲取NameSpace的分布式鎖,現(xiàn)在分布式鎖比較常見的方式是采用redis和zookeeper。但是在這里apollo是采用數(shù)據(jù)庫來實現(xiàn)的,具體怎么細(xì)節(jié)大家可以去看看源碼應(yīng)該都看的懂,無非就是加鎖往DB里面插入一條數(shù)據(jù),釋放鎖然后把這個數(shù)據(jù)進(jìn)行刪除。稍微有點不一樣的就是如果獲取鎖失敗,就直接返回失敗了,不會在繼續(xù)自旋或者休眠重新去獲取鎖。因為獲取鎖失敗說明已經(jīng)有其他人在你之前修改了配置,只有這個人新增的配置被發(fā)布或者刪除之后,其他人才能繼續(xù)新增配置,這樣的話就會導(dǎo)致一個NameSpace只能同時被一個人修改。這個限制是默認(rèn)關(guān)閉的需要我們在數(shù)據(jù)庫里面去配置(ApolloConfigDb的ServiceConfig表)
一般我們應(yīng)用的配置修改應(yīng)該是比較低頻的,多人同時去修改的話情況會比較少,再說有些公司是開發(fā)提交配置,測試去發(fā)布配置,提交和修改不能是同一個人,這樣的話新增配置沖突就更少了,應(yīng)該沒有必要去配置namespace.lock.switch=true一個namespace只能一個人去修改。
接下來的代碼就非常簡單明了,就是一個簡單的參數(shù)判斷然后執(zhí)行入庫操作了,把數(shù)據(jù)插入到Item表里面。這是我們新增的配置數(shù)據(jù)就已經(jīng)保存了。效果如下:
這時候新增的配置是不起作用的,不會推送給客戶端的。只是單純一個類似于草稿的狀態(tài)。
發(fā)布配置
接下來我們要使上面新增的配置生效,并且推送給客戶端。同樣的我們點擊發(fā)布按鈕然后就能知道對應(yīng)的后端方法入口。
我們通過這個接口可以直接找到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);
}
- 上述代碼就不仔細(xì)展開分析了,感興趣的可以自己斷點調(diào)試下我們重點看下releaseService.publish 這個方法,里面有一些灰度發(fā)布相關(guān)的邏輯,不過這個不是本文的重點,這個方法主要是往release表插入數(shù)據(jù)。
- 接下來就是messageSender.sendMessage這個方法了,這個方法主要是往ReleaseMessage表里面插入一條記錄。保存完ReleaseMessage這個表會得到相應(yīng)的主鍵ID,然后把這個ID放入到一個隊列里面。然后在加載DatabaseMessageSender的時候會默認(rèn)起一個定時任務(wù)去獲取上面隊列里面放入的消息ID,然后找出比這這些ID小的消息刪除掉。發(fā)布流程就完了,這里也沒有說到服務(wù)端是怎么感知有配置修改了的。
Config Service 通知配置變化
apolloConfigService 在服務(wù)啟動的時候ReleaseMessageScanner 會啟動一個定時任務(wù) 每隔1s去去查詢ReleaseMessage里面有沒有最新的消息,如果有就會通知到所有的消息監(jiān)聽器比如NotificationControllerV2、ConfigFileController等,這個消息監(jiān)聽器注冊是在ConfigServiceAutoConfiguration里面注冊的。NotificationControllerV2 得到配置發(fā)布的 AppId+Cluster+Namespace 后,會通知對應(yīng)的客戶端,這樣就從portal到configService 到 client 整個消息通知變化就串起來了。
總結(jié)
這樣服務(wù)端配置如何更新的流程就完了。
1.用戶在Portal操作配置發(fā)布。
2.Portal調(diào)用Admin Service的接口操作發(fā)布。
3.Admin Service發(fā)布配置后,發(fā)送ReleaseMessage給各個Config Service。
4.Config Service收到ReleaseMessage后,通知對應(yīng)的客戶端”apollo的源碼相對于其他中間件來說還是相對于比較簡單的,比較適合于想研究下中間件源碼,又不知道如何下手的同學(xué) 。