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