一、概述
在之前讲解配置发布的时候,
NotificationControllerV2
得到配置发布的AppId+Cluster+Namespace
后,会通知对应的客户端 ,实现在handleMessage
方法中具体的实现流程是:
- 客户端发起长轮询请求
NotificationControllerV2
不会立即返回结果,而是通过Spring Deferredresult
把请求挂起。- 如果
60s
没有客户端关心的配置发布,那么就会返回403给客户端- 如果有客户端关心的配置,
NotificationControllerV2
会调用Deferredresult
的setResult
方法,传入有配置变化的namespace
信息,同时请求会立即返回,然后客户端从返回的结果中拿到配置变化的namespace
,会立即请求Config Service
获取namespace
的最新配置
二.代码流程
1. NotificationControllerV2#pollNotification
方法
这里代码很长,但是逻辑很简单,首先就是把各种映射关系保存,然后
Watch
住,等待配置发生变化后的通知,如果有新的通知,设置结果结束长轮询,没新的通知,注册到DeferredResult
中,等待有配置变更或超时。
@GetMapping
public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> pollNotification(
@RequestParam(value = "appId") String appId,
@RequestParam(value = "cluster") String cluster,
@RequestParam(value = "notifications") String notificationsAsString,
@RequestParam(value = "dataCenter", required = false) String dataCenter,
@RequestParam(value = "ip", required = false) String clientIp) {
//解析 notificationsAsString 参数,创建 ApolloConfigNotification 数组
List<ApolloConfigNotification> notifications = null;
try {
notifications =
gson.fromJson(notificationsAsString, notificationsTypeReference);
} catch (Throwable ex) {
Tracer.logError(ex);
}
if (CollectionUtils.isEmpty(notifications)) {
throw new BadRequestException("Invalid format of notifications: " + notificationsAsString);
}
// 过滤并创建 ApolloConfigNotification Map
Map<String, ApolloConfigNotification> filteredNotifications = filterNotifications(appId, notifications);
if (CollectionUtils.isEmpty(filteredNotifications)) {
throw new BadRequestException("Invalid format of notifications: " + notificationsAsString);
}
// 创建 DeferredResultWrapper 对象
DeferredResultWrapper deferredResultWrapper = new DeferredResultWrapper(bizConfig.longPollingTimeoutInMilli());
// Namespace 集合
Set<String> namespaces = Sets.newHashSetWithExpectedSize(filteredNotifications.size());
// 客户端的通知 ma 。 key 为 Namespace 名, value 为通知编号
Map<String, Long> clientSideNotifications = Maps.newHashMapWithExpectedSize(filteredNotifications.size());
// 循环 ApolloConfigNotification Map。初始化上述变量
for (Map.Entry<String, ApolloConfigNotification> notificationEntry : filteredNotifications.entrySet()) {
String normalizedNamespace = notificationEntry.getKey();
ApolloConfigNotification notification = notificationEntry.getValue();
// 添加到 `namespace` 中
namespaces.add(normalizedNamespace);
// 添加到 `clientSideNotifications` 中
clientSideNotifications.put(normalizedNamespace, notification.getNotificationId());
// 记录名字被归一化的 Namespace 。 因为,最终返回给客户端,使用原始的 Namespace 名字,否则客户端无法识别
if (!Objects.equals(notification.getNamespaceName(), normalizedNamespace)) {
deferredResultWrapper.recordNamespaceNameNormalizedResult(notification.getNamespaceName(), normalizedNamespace);
}
}
// 组装 Watch Key Multimap
Multimap<String, String> watchedKeysMap =
watchKeysUtil.assembleAllWatchKeys(appId, cluster, namespaces, dataCenter);
// 生成 Watch Key 集合
Set<String> watchedKeys = Sets.newHashSet(watchedKeysMap.values());
/**
* 1、set deferredResult before the check, for avoid more waiting
* If the check before setting deferredResult,it may receive a notification the next time
* when method handleMessage is executed between check and set deferredResult.
*/
//注册超时事件
deferredResultWrapper
.onTimeout(() -> logWatchedKeys(watchedKeys, "Apollo.LongPoll.TimeOutKeys"));
//注册结束事件
deferredResultWrapper.onCompletion(() -> {
//unregister all keys
// 移除 Watch Key + DeferredResultWrapper 出 `deferredResults`
//
for (String key : watchedKeys) {
deferredResults.remove(key, deferredResultWrapper);
}
logWatchedKeys(watchedKeys, "Apollo.LongPoll.CompletedKeys");
});
//register all keys
//注册 Watch Key + DeferredResultWrapper 到 `deferredResults` 中,等待配置发生变化后通知
for (String key : watchedKeys) {
this.deferredResults.put(key, deferredResultWrapper);
}
logWatchedKeys(watchedKeys, "Apollo.LongPoll.RegisteredKeys");
logger.debug("Listening {} from appId: {}, cluster: {}, namespace: {}, datacenter: {}",
watchedKeys, appId, cluster, namespaces, dataCenter);
/**
* 2、check new release
*/
//获得 Watch Key 集合中,每个 Watch Key 对应的 ReleaseMessage 记录
List<ReleaseMessage> latestReleaseMessages =
releaseMessageService.findLatestReleaseMessagesGroupByMessages(watchedKeys);
/**
* Manually close the entity manager.
* Since for async request, Spring won't do so until the request is finished,
* which is unacceptable since we are doing long polling - means the db connection would be hold
* for a very long time
*/
//手动关闭实体管理器。由于对于异步请求,Spring 在请求完成之前不会这样做,这是不可接受的,因为我们正在进行长时间轮询 - 意味着数据库连接将保持很长时间
entityManagerUtil.closeEntityManager();
//获取配置更新的结果
List<ApolloConfigNotification> newNotifications =
getApolloConfigNotifications(namespaces, clientSideNotifications, watchedKeysMap,
latestReleaseMessages);
//若有新的通知,直接设置 DeferredResult 的结果,从而结束常轮训
if (!CollectionUtils.isEmpty(newNotifications)) {
deferredResultWrapper.setResult(newNotifications);
}
return deferredResultWrapper.getResult();
}
NotificationControllerV2#handleMessage
方法
在之前我们梳理过,当配置变更的时候会发送消息,通知对应的监听者,然后就会调用到
NotificationControllerV2
的HandleMessage
方法,这里逻辑也很简单,从deferredResults中获取所有监听这个消息的客户端,然后遍历数组,异步通知对应的客户端。这里有个比较设计好的点就是,当通知的客户端比较多,使用线程池异步通知,为了避免惊群效应,会每推送N
个客户端,sleep
一段时间,假设一个公共Namespace
有10w
台机器,那么当下发配置变更消息的时候,就导致这10w
台机器都来请求配置,惊群效应,对Config Service
的压力也比较大。
public void handleMessage(ReleaseMessage message, String channel) {
logger.info("message received - channel: {}, message: {}", channel, message);
String content = message.getMessage();
Tracer.logEvent("Apollo.LongPoll.Messages", content);
// 仅处理 APOLLO_RELEASE_TOPIC
if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(content)) {
return;
}
// 获得对应的 Namespace 名字
String changedNamespace = retrieveNamespaceFromReleaseMessage.apply(content);
if (Strings.isNullOrEmpty(changedNamespace)) {
logger.error("message format invalid - {}", content);
return;
}
if (!deferredResults.containsKey(content)) {
return;
}
//create a new list to avoid ConcurrentModificationException
// 创建 DeferredResultWrapper 数组,避免并发问题
List<DeferredResultWrapper> results = Lists.newArrayList(deferredResults.get(content));
// 创建 ApolloConfigNotification 对象
ApolloConfigNotification configNotification = new ApolloConfigNotification(changedNamespace, message.getId());
configNotification.addMessage(content, message.getId());
//do async notification if too many clients
// 若需要通知的客户端过多,使用 ExecutorService 异步通知,避免`惊群效应`
if (results.size() > bizConfig.releaseMessageNotificationBatch()) {
largeNotificationBatchExecutorService.submit(() -> {
logger.debug("Async notify {} clients for key {} with batch {}", results.size(), content,
bizConfig.releaseMessageNotificationBatch());
for (int i = 0; i < results.size(); i++) {
// 每 N 个K客户端,Sleep 一段事件
if (i > 0 && i % bizConfig.releaseMessageNotificationBatch() == 0) {
try {
TimeUnit.MILLISECONDS.sleep(bizConfig.releaseMessageNotificationBatchIntervalInMilli());
} catch (InterruptedException e) {
//ignore
}
}
logger.debug("Async notify {}", results.get(i));
// 设置结果
results.get(i).setResult(configNotification);
}
});
return;
}
logger.debug("Notify {} clients for key {}", results.size(), content);
for (DeferredResultWrapper result : results) {
result.setResult(configNotification);
}
logger.debug("Notification completed");
}
3. AppNamespaceServiceWithCache
类
这个类将AppNamespace缓存在内存,提高ConfigService的查询性能,这个适合我们借鉴下,缓存方式如下:
- 启动时,全量初始化
AppNamespace
到缓存- 考虑
AppnNamespace
新增,后台定时任务,定期增量初始化AppNamespace
到缓存- 考虑
AppNamespace
更新与删除,后台定时更新,定时全量重建AppNamespace
到缓存
4. AppNamespaceServiceWithCache#afterPropertiesSet
方法
首先从db加载元数据到缓存,然后启动两个定时任务,一个执行全量重建,一个进行增量初始化。
public void afterPropertiesSet() throws Exception {
// 从 ServerConfig 中,读取定时任务的周期配置
populateDataBaseInterval();
// 全量初始化 AppNamespace 缓存
scanNewAppNamespaces(); //block the startup process until load finished
// 创建定时任务,全量重构 AppNamespace 缓存
scheduledExecutorService.scheduleAtFixedRate(() -> {
Transaction transaction = Tracer.newTransaction("Apollo.AppNamespaceServiceWithCache",
"rebuildCache");
try {
// 全量重建 AppNamespace缓存
this.updateAndDeleteCache();
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
transaction.setStatus(ex);
logger.error("Rebuild cache failed", ex);
} finally {
transaction.complete();
}
}, rebuildInterval, rebuildInterval, rebuildIntervalTimeUnit);
//创建定时任务,增量初始化 AppNamespace 缓存
scheduledExecutorService.scheduleWithFixedDelay(this::scanNewAppNamespaces, scanInterval,
scanInterval, scanIntervalTimeUnit);
}
updateAndDeleteCache
方法
全量重建,更新和删除AppNamespace缓存,从数据库中查询最新的AppNamespace信息,然后处理更新和删除的情况
private void updateAndDeleteCache() {
// 从缓存中获得所有的 AppNamespace 编号集合
List<Long> ids = Lists.newArrayList(appNamespaceIdCache.keySet());
if (CollectionUtils.isEmpty(ids)) {
return;
}
// 每 500 一批,从数据库中查询最新的 AppNamespace 信息
List<List<Long>> partitionIds = Lists.partition(ids, 500);
for (List<Long> toRebuild : partitionIds) {
Iterable<AppNamespace> appNamespaces = appNamespaceRepository.findAllById(toRebuild);
if (appNamespaces == null) {
continue;
}
//handle updated
//处理更新的情况
Set<Long> foundIds = handleUpdatedAppNamespaces(appNamespaces);
//handle deleted
// 处理删除的情况
handleDeletedAppNamespaces(Sets.difference(Sets.newHashSet(toRebuild), foundIds));
}
}
handleUpdatedAppNamespaces
方法
处理AppNamespace更新的情况,这个很简单,就是使用最新的覆盖缓存中的
private Set<Long> handleUpdatedAppNamespaces(Iterable<AppNamespace> appNamespaces) {
Set<Long> foundIds = Sets.newHashSet();
for (AppNamespace appNamespace : appNamespaces) {
foundIds.add(appNamespace.getId());
// 获得缓存中的 AppNamespace 对象
AppNamespace thatInCache = appNamespaceIdCache.get(appNamespace.getId());
// 从 DB 中查询到的 AppNamespace 的更新时间更大,才认为是更新
if (thatInCache != null && appNamespace.getDataChangeLastModifiedTime().after(thatInCache
.getDataChangeLastModifiedTime())) {
// 添加到 appNamespaceIdCache 中
appNamespaceIdCache.put(appNamespace.getId(), appNamespace);
String oldKey = assembleAppNamespaceKey(thatInCache);
String newKey = assembleAppNamespaceKey(appNamespace);
// 添加到 appNamespaceCache 中
appNamespaceCache.put(newKey, appNamespace);
//in case appId or namespaceName changes
// 当 appId 或 namespaceName发生改变时,将老的移出去,
if (!newKey.equals(oldKey)) {
appNamespaceCache.remove(oldKey);
}
//添加到 publishAppNamespaceCache 中
if (appNamespace.isPublic()) { //新的是共用类型
// 添加到 publicAppNamespaceCache 中
publicAppNamespaceCache.put(appNamespace.getName(), appNamespace);
//in case namespaceName changes
// 当 namespaceName 发生改变的情况,将老的移除publicAppNamespaceCache
if (!appNamespace.getName().equals(thatInCache.getName()) && thatInCache.isPublic()) {
publicAppNamespaceCache.remove(thatInCache.getName());
}
} else if (thatInCache.isPublic()) {
//just in case isPublic changes
publicAppNamespaceCache.remove(thatInCache.getName());
}
logger.info("Found AppNamespace changes, old: {}, new: {}", thatInCache, appNamespace);
}
}
return foundIds;
}
handleDeletedAppNamespaces
处理删除的情况
这里就是直接从缓存中移除对应的AppNamespace
private void handleDeletedAppNamespaces(Set<Long> deletedIds) {
if (CollectionUtils.isEmpty(deletedIds)) {
return;
}
for (Long deletedId : deletedIds) {
// 从 appNamespaceIdCache 中移除
AppNamespace deleted = appNamespaceIdCache.remove(deletedId);
if (deleted == null) {
continue;
}
//从 appNamespaceCache 中移除
appNamespaceCache.remove(assembleAppNamespaceKey(deleted));
if (deleted.isPublic()) {
//从 publicAppNamespaceCache 中移除
AppNamespace publicAppNamespace = publicAppNamespaceCache.get(deleted.getName());
// in case there is some dirty data, e.g. public namespace deleted in some app and now created in another app
if (publicAppNamespace == deleted) {
publicAppNamespaceCache.remove(deleted.getName());
}
}
logger.info("Found AppNamespace deleted, {}", deleted);
}
}
- 增量更新
scanNewAppNamespaces
方法
private void scanNewAppNamespaces() {
Transaction transaction = Tracer.newTransaction("Apollo.AppNamespaceServiceWithCache",
"scanNewAppNamespaces");
try {
// 加载新的 AppNamespace 们
this.loadNewAppNamespaces();
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
transaction.setStatus(ex);
logger.error("Load new app namespaces failed", ex);
} finally {
transaction.complete();
}
}
loadNewAppNamespaces
方法
拉取最新的数据,然后合并到缓存中
private void loadNewAppNamespaces() {
boolean hasMore = true;
while (hasMore && !Thread.currentThread().isInterrupted()) {
//current batch is 500
//获取大于 maxIdScanned 的 500 条 Appnamespace 记录,按照 id 升序
List<AppNamespace> appNamespaces = appNamespaceRepository
.findFirst500ByIdGreaterThanOrderByIdAsc(maxIdScanned);
if (CollectionUtils.isEmpty(appNamespaces)) {
break;
}
// 合并到 AppNamespace 缓存中
mergeAppNamespaces(appNamespaces);
// 获得新的 maxIdScanned , 取最后一条记录
int scanned = appNamespaces.size();
maxIdScanned = appNamespaces.get(scanned - 1).getId();
//若拉取不足 500 条,说明无新消息了
hasMore = scanned == 500;
logger.info("Loaded {} new app namespaces with startId {}", scanned, maxIdScanned);
}
}
mergeAppNamespaces
方法
这里就是使用最新的覆盖缓存中原有的
private void mergeAppNamespaces(List<AppNamespace> appNamespaces) {
for (AppNamespace appNamespace : appNamespaces) {
// 添加到 'appNamespaceCache' 中
appNamespaceCache.put(assembleAppNamespaceKey(appNamespace), appNamespace);
// 添加到 'appNamespaceIdCache'
appNamespaceIdCache.put(appNamespace.getId(), appNamespace);
// 若是公用类型,则添加到 'publicAppNamespaceCache' 中
if (appNamespace.isPublic()) {
publicAppNamespaceCache.put(appNamespace.getName(), appNamespace);
}
}
}
ReleaseMessageServiceWithCache
类
缓存ReleaseMessage的service实现累,通过将ReleaseMessage缓存在内存中,提高查询性能,缓存方式如下:
- 启动时,初始化ReleaseMessage 到缓存
- 洗澡能时候,基于 ReleaseMessageListener,通知有新的ReleaseMessage,根据是否有消息间隙,直接使用该消息还是从数据库读取
ReleaseMessageServiceWithCache#afterPropertiesSet
方法
在初始化方法中,首先会初始拉取Release Message到缓存,然后又创建了一个定时任务增量拉取消息到缓存,用以处理初始化阶段消息遗漏问题,至于为什么会遗漏,场景如下:
- 20:00:00 程序启动过程中,当前 release message 有 5 条
- 20:00:01 loadReleaseMessages(0); 执行完成,获取到 5 条记录
- 20:00:02 有一条 release message 新产生,但是因为程序还没启动完,所以不会触发 handle message 操作
- 20:00:05 程序启动完成,但是第三步的这条新的 release message 漏了
- 20:10:00 假设这时又有一条 release message 产生,这次会触发 handle message ,同时会把第三步的那条 release message 加载到
所以,定期刷的机制就是为了解决第三步中产生的release message问题。
当程序启动完,handleMessage生效后,就不需要再定期扫了
public void afterPropertiesSet() throws Exception {
//从 ServiceConfig 中,读取任务的周期配置
populateDataBaseInterval();
//block the startup process until load finished
//this should happen before ReleaseMessageScanner due to autowire
// 初始拉取 ReleaseMessage 到缓存
loadReleaseMessages(0);
// 创建定时任务,增量拉取 ReleaseMessage 到缓存,用以处理初始化期间,产生的 ReleaseMessage 遗漏的问题
executorService.submit(() -> {
while (doScan.get() && !Thread.currentThread().isInterrupted()) {
Transaction transaction = Tracer.newTransaction("Apollo.ReleaseMessageServiceWithCache",
"scanNewReleaseMessages");
try {
// 增量拉取 ReleaseMessage 到缓存
loadReleaseMessages(maxIdScanned);
transaction.setStatus(Transaction.SUCCESS);
} catch (Throwable ex) {
transaction.setStatus(ex);
logger.error("Scan new release messages failed", ex);
} finally {
transaction.complete();
}
try {
scanIntervalTimeUnit.sleep(scanInterval);
} catch (InterruptedException e) {
//ignore
}
}
});
}
ReleaseMessageServiceWithCache#loadReleaseMessages
方法
初始化,全量拉取元数据到缓存中
private void loadReleaseMessages(long startId) {
boolean hasMore = true;
while (hasMore && !Thread.currentThread().isInterrupted()) {
//current batch is 500
// 获得大于 maxIdScanned 的 500 条 ReleaseMessage 记录,按照 id 升序
List<ReleaseMessage> releaseMessages = releaseMessageRepository
.findFirst500ByIdGreaterThanOrderByIdAsc(startId);
if (CollectionUtils.isEmpty(releaseMessages)) {
break;
}
// 合并到 ReleaseMessage 缓存
releaseMessages.forEach(this::mergeReleaseMessage);
int scanned = releaseMessages.size();
// 获取新的 maxIdScanned ,取最后一条记录
startId = releaseMessages.get(scanned - 1).getId();
// 若拉取不足 500 条,说明无新消息了
hasMore = scanned == 500;
logger.info("Loaded {} release messages with startId {}", scanned, startId);
}
}
ReleaseMessageServiceWithCache#handleMessage
方法
这里首先会关闭增量拉取的定时任务,后续通过 ReleaseMessageScanner 通知即可,然后判断消息和前一条收到的消息之间有无间隙,没有直接合并,有说明,有遗漏,增量拉取。
public void handleMessage(ReleaseMessage message, String channel) {
//Could stop once the ReleaseMessageScanner starts to work
// 关闭增量拉取定时任务的执行
doScan.set(false);
logger.info("message received - channel: {}, message: {}", channel, message);
// 仅处理 APOLLO_RELEASE_TOPIC
String content = message.getMessage();
Tracer.logEvent("Apollo.ReleaseMessageService.UpdateCache", String.valueOf(message.getId()));
if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(content)) {
return;
}
//计算 gap
long gap = message.getId() - maxIdScanned;
// 若无空缺,直接合并
if (gap == 1) {
mergeReleaseMessage(message);
//若有空缺 gap ,增量拉取
} else if (gap > 1) {
//gap found!
loadReleaseMessages(maxIdScanned);
}
}