## 源码
Apollo 长轮询的实现,是通过客户端轮询 /notifications/v2
接口实现的。具体代码在 com.ctrip.framework.apollo.configservice.controller.NotificationControllerV2.java。
这个类也是实现了 ReleaseMessageListener 监控,表明他是一个消息监听器,当有新的消息时,就会调用他的 hanlderMessage 方法。这个具体我们后面再说。
该类只有一个 rest 接口: pollNotification 方法。返回值是 DeferredResult,这是 Spring 支持 Servlet 3 的一个类,关于异步同步的不同,可以看笔者的另一篇文章 异步 Servlet 和同步 Servlet 的性能测试 。
该接口提供了几个参数:
appId appId
cluster 集群名称
notificationsAsString 通知对象的 json 字符串
dataCenter,idc 属性
clientIp 客户端 IP, 非必传,为了扩展吧估计
大家有么有觉得少了什么? namespace 。
当然,没有 namespace 这个重要的参数是不存在的。
参数在 notificationsAsString 中。客户端会将自己所有的 namespace 传递到服务端进行查询。
是时候上源码了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 @RequestMapping(method = RequestMethod.GET) public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> pollNotification ( @RequestParam(value = "appId") String appId,// appId @RequestParam(value = "cluster") String cluster,// default @RequestParam(value = "notifications") String notificationsAsString,// json 对象 List<ApolloConfigNotification> @RequestParam(value = "dataCenter", required = false) String dataCenter,// 基本用不上, idc 属性 @RequestParam(value = "ip", required = false) String clientIp) { List<ApolloConfigNotification> notifications = gson.fromJson(notificationsAsString, notificationsTypeReference); DeferredResultWrapper deferredResultWrapper = new DeferredResultWrapper (); Set<String> namespaces = Sets.newHashSet(); Map<String, Long> clientSideNotifications = Maps.newHashMap(); Map<String, ApolloConfigNotification> filteredNotifications = filterNotifications(appId, notifications); 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); } } Multimap<String, String> watchedKeysMap = watchKeysUtil.assembleAllWatchKeys(appId, cluster, namespaces, dataCenter); Set<String> watchedKeys = Sets.newHashSet(watchedKeysMap.values()); List<ReleaseMessage> latestReleaseMessages = releaseMessageService.findLatestReleaseMessagesGroupByMessages(watchedKeys); entityManagerUtil.closeEntityManager(); List<ApolloConfigNotification> newNotifications = getApolloConfigNotifications(namespaces, clientSideNotifications, watchedKeysMap, latestReleaseMessages); if (!CollectionUtils.isEmpty(newNotifications)) { deferredResultWrapper.setResult(newNotifications); } else { deferredResultWrapper .onTimeout(() -> logWatchedKeys(watchedKeys, "Apollo.LongPoll.TimeOutKeys" )); deferredResultWrapper.onCompletion(() -> { for (String key : watchedKeys) { deferredResults.remove(key, deferredResultWrapper); } }); for (String key : watchedKeys) { this .deferredResults.put(key, deferredResultWrapper); } } return deferredResultWrapper.getResult(); }
注释写了很多了,再简单说说逻辑:
解析 JSON 字符串为 List< ApolloConfigNotification> 对象。
创建 Spring 异步对象。
处理过滤 namespace。
根据 namespace 生成需要监听的 key,格式为 appId + cluster + namespace,包括公共 namespace。并获取最新的 Release 信息。
关闭 Spring 实例管理器,释放数据库资源。
根据刚刚得到的 ReleaseMessage,和客户端的 ReleaseMessage 的版本进行对比,生成新的配置通知对象集合。
如果不是空 —— 立即返回给客户端,结束此次调用。如果没有,进入第 8 步。
设置 timeout 回调方法 —— 打印日志。再设置完成回调方法:删除注册的 key。
对客户端感兴趣的 key 进行注册,这些 key 都对应着 deferredResultWrapper 对象,可以认为他就是客户端。
返回 Spring 异步对象。该请求将被异步挂起。
Apollo 的 DeferredResultWrapper 保证了 Spring 的 DeferredResult 对象,泛型内容是 List, 构造这个对象,默认的 timeout 是 60 秒,即挂起 60 秒。同时,对 setResult 方法进行包装,加入了对客户端 key 和服务端 key 的一个映射(大小写不一致) 。
我们刚刚说,Apollo 会将这些 key 注册起来。那么什么时候使用呢,异步对象被挂起,又是上面时候被唤醒呢?
答案就在 handleMessage 方法里。我们刚刚说他是一个监听器,当消息扫描器扫描到新的消息时,会通知所有的监听器,也就是执行 handlerMessage 方法。方法内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Override public void handleMessage (ReleaseMessage message, String channel) { String content = message.getMessage(); if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(content)) { return ; } String changedNamespace = retrieveNamespaceFromReleaseMessage.apply(content); List<DeferredResultWrapper> results = Lists.newArrayList(deferredResults.get(content)); ApolloConfigNotification configNotification = new ApolloConfigNotification (changedNamespace, message.getId()); configNotification.addMessage(content, message.getId()); if (results.size() > bizConfig.releaseMessageNotificationBatch()) { largeNotificationBatchExecutorService.submit(() -> { for (int i = 0 ; i < results.size(); i++) { if (i > 0 && i % bizConfig.releaseMessageNotificationBatch() == 0 ) { TimeUnit.MILLISECONDS.sleep(bizConfig.releaseMessageNotificationBatchIntervalInMilli()); } results.get(i).setResult(configNotification); } }); return ; } for (DeferredResultWrapper result : results) { result.setResult(configNotification); } }
笔者去除了一些日志和一些数据判断。大致的逻辑如下:
消息类型必须是 “apollo-release”。然后拿到消息里的 namespace 内容。
根据 namespace 从注册器里拿出 Spring 异步对象集合 。
创建通知对象。
如果有超过 100 个客户端在等待,那么就使用线程池批量执行通知。否则就同步慢慢执行。
每处理 100 个客户端就休息 100ms,防止发生惊群效应,导致大量客户端调用配置获取接口,引起服务抖动。
循环调用 Spring 异步对象的 setResult 方法,让其立即返回。
具体的流程图如下:
其中,灰色区域是扫描器的异步线程,黄色区域是接口的同步线程。他们共享 deferredResults 这个线程安全的 Map,实现异步解耦和实时通知客户端。
总结 好了,这就是 Apollo 的长轮询接口,客户端会不断的轮询服务器,服务器会 Hold住 60 秒,这是通过 Servlet 3 的异步 + NIO 来实现的,能够保持万级连接(Tomcat 默认 10000)。
通过一个线程安全的 Map + 监听器,让扫描器线程和 HTTP 线程共享 Spring 异步对象,即实现了消息实时通知,也让应用程序实现异步解耦。