上一篇文章《揭秘 Apollo 配置更新奥秘:推拉结合,新鲜配置一手掌握!》中,我们提到,Apollo的配置更新采用了长轮询的方式。今天我们深入探究下长轮询是如何实现的。
Apollo中长轮询的实现细节
1. 关键组件
在 Apollo 的长轮询实现中,有几个关键的组件和数据结构起着至关重要的作用。首先是 NotificationControllerV2 这个类,它就像是整个长轮询机制的 “指挥官”,负责协调各方资源,处理客户端请求并与服务器端的其他组件进行交互。
@RestController
@RequestMapping("/notifications/v2")
public class NotificationControllerV2 implements ReleaseMessageListener {
private static final Logger logger = LoggerFactory.getLogger(NotificationControllerV2.class);
private final Multimap<String, DeferredResultWrapper> deferredResults =
Multimaps.synchronizedSetMultimap(TreeMultimap.create(String.CASE_INSENSITIVE_ORDER, Ordering.natural()));
private static final Type notificationsTypeReference =
new TypeToken<List<ApolloConfigNotification>>() {
}.getType();
}
在这个类中,有一个核心的属性,Multimap<String, DeferredResultWrapper> deferredResults ,它管理着配置键与延迟结果包装器之间的映射关系,确保每个配置键都能准确找到对应的延迟结果处理逻辑,如注册、取消注册还是通知结果返回等。
2. 请求参数
@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) {
……
}
appId用于唯一标识应用、cluster:指明配置所在的集群,不同集群可能有不同的配置设置,确保客户端获取到正确集群下的配置信息、notifications:以字符串形式传递的配置通知信息,虽然此时可能还只是一些原始数据,但却是后续处理的关键线索,以及可选的 dataCenter(数据中心信息,用于更精准地定位配置资源)和 clientIp(客户端 IP 地址,可能用于日志记录、访问控制等多种用途)。
3. 请求参数处理与校验
服务器端接收到请求后,它会使用 Gson 这个强大的 JSON 解析工具,将 notifications 字符串解析为 List 类型的列表,并做非空判断。
try {
notifications =
gson.fromJson(notificationsAsString, notificationsTypeReference);
} catch (Throwable ex) {
Tracer.logError(ex);
}
if (CollectionUtils.isEmpty(notifications)) {
throw BadRequestException.invalidNotificationsFormat(notificationsAsString);
}
4. 配置通知过滤与规范化
Map<String, ApolloConfigNotification> filteredNotifications = filterNotifications(appId, notifications);
调用 filterNotifications 方法对通知信息进行过滤和规范化处理。在这个过程中,它会去除那些 namespaceName 为空或 null 的无效通知。
在处理的过程中,带.properties的命名空间的名称会被规范化。
5. 初始化延迟结果及相关数据结构
长轮询的准备工作都在这里了。**服务端会一个 DeferredResultWrapper 对象,这个对象包裹着实际的延迟结果,并为其设置了超时时间(60s)。**一旦超时,服务器就知道不能再让客户端继续等待了,需要进行相应的处理。
同时,服务器还创建了 Set namespaces 集合和 Map<String, Long> clientSideNotifications 映射表,分别用于存储配置通知涉及的命名空间信息和每个命名空间对应的客户端侧通知 ID。
// longPollingTimeoutInMilli为60s
DeferredResultWrapper deferredResultWrapper = new DeferredResultWrapper(bizConfig.longPollingTimeoutInMilli());
// 过滤并标准化的namespace的集合
Set<String> namespaces = Sets.newHashSetWithExpectedSize(filteredNotifications.size());
// key是namespace,value是notificationId
Map<String, Long> clientSideNotifications = Maps.newHashMapWithExpectedSize(filteredNotifications.size());
// 初始化的逻辑
for (Map.Entry<String, ApolloConfigNotification> notificationEntry : filteredNotifications.entrySet()) {
String normalizedNamespace = notificationEntry.getKey();
ApolloConfigNotification notification = notificationEntry.getValue();
namespaces.add(normalizedNamespace);
clientSideNotifications.put(normalizedNamespace, notification.getNotificationId());
if (!Objects.equals(notification.getNamespaceName(), normalizedNamespace)) {
deferredResultWrapper.recordNamespaceNameNormalizedResult(notification.getNamespaceName(), normalizedNamespace);
}
}
6. 注册监控配置key
服务器会根据前面准备好的信息,调用 watchKeysUtil 的 assembleAllWatchKeys 方法,组装出所有需要监控的配置键信息,存储在 watchedKeysMap 中。然后,从这个 Multimap 中提取出所有的配置键,构建一个 Set 类型的 watchedKeys 集合。
// key是namespace, value是appId, clusterName, namespace, dataCenter拼接成的String
Multimap<String, String> watchedKeysMap =
watchKeysUtil.assembleAllWatchKeys(appId, cluster, namespaces, dataCenter);
Set<String> watchedKeys = Sets.newHashSet(watchedKeysMap.values());
watchedKeysMap的value与客户端侧通知 ID的映射关系被存储在ReleaseMessage这张表中,ID就是主键ID。
7. 检查配置更新与结果返回
调用 releaseMessageService 的 findLatestReleaseMessagesGroupByMessages 方法,传入监控的配置键列表,获取最新的配置发布消息列表 latestReleaseMessages。
然后,服务器会根据这些最新消息以及之前准备的各种数据(如命名空间、客户端通知 ID 等),调用 getApolloConfigNotifications 方法生成新的配置通知信息列表 newNotifications。
如果这个列表不为空,那就说明有新的配置更新需要通知客户端,服务器会立即将这些新通知设置到对应的 DeferredResultWrapper 对象中,让客户端能够及时获取到最新的配置信息。
List<ReleaseMessage> latestReleaseMessages =
releaseMessageService.findLatestReleaseMessagesGroupByMessages(watchedKeys);
List<ApolloConfigNotification> newNotifications =
getApolloConfigNotifications(namespaces, clientSideNotifications, watchedKeysMap,
latestReleaseMessages);
最后,无论是否有新的配置通知,服务器都会将延迟结果返回给客户端,客户端则可以根据返回结果进行相应的处理,比如更新本地的配置缓存,确保应用始终使用最新的配置。
deferredResultWrapper
.onTimeout(() -> logWatchedKeys(watchedKeys, "Apollo.LongPoll.TimeOutKeys"));
deferredResultWrapper.onCompletion(() -> {
//unregister all keys
for (String key : watchedKeys) {
deferredResults.remove(key, deferredResultWrapper);
}
logWatchedKeys(watchedKeys, "Apollo.LongPoll.CompletedKeys");
});
// 这里的key就是namespace, value是appId, clusterName, namespace, dataCenter拼接成的String
for (String key : watchedKeys) {
this.deferredResults.put(key, deferredResultWrapper);
}
logWatchedKeys(watchedKeys, "Apollo.LongPoll.RegisteredKeys");