Apollo 8 — ConfigService 异步轮询接口的实现
源码
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 传递到服务端进行查询。
是时候上源码了。
@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);
// Spring 的异步对象: timeout 60s, 返回304
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()) {
// 拿出 key
String normalizedNamespace = notificationEntry.getKey();
// 拿出 value
ApolloConfigNotification notification = notificationEntry.getValue();
/* 添加到 namespaces Set */
namespaces.add(normalizedNamespace);
// 添加到 client 端的通知, key 是 namespace, values 是 messageId
clientSideNotifications.put(normalizedNamespace, notification.getNotificationId());
// 如果不相等, 记录客户端名字
if (!Objects.equals(notification.getNamespaceName(), normalizedNamespace)) {
// 记录 key = 标准名字, value = 客户端名字
deferredResultWrapper.recordNamespaceNameNormalizedResult(notification.getNamespaceName(), normalizedNamespace);
}
}// 记在 namespaces 集合, clientSideNotifications 也put (namespace, notificationId)
// 组装得到需要观察的 key,包括公共的.
Multimap<String, String> watchedKeysMap =
watchKeysUtil.assembleAllWatchKeys(appId, cluster, namespaces, dataCenter);// namespaces 是集合
// 得到 value; 这个 value 也就是 appId + cluster + namespace
Set<String> watchedKeys = Sets.newHashSet(watchedKeysMap.values());
// 从缓存得到最新的发布消息
List<ReleaseMessage> latestReleaseMessages =// 根据 key 从缓存得到最新发布的消息.
releaseMessageService.findLatestReleaseMessagesGroupByMessages(watchedKeys);
/* 如果不关闭, 这个请求将会一直持有一个数据库连接. 影响并发能力. 这是一个 hack 操作*/
entityManagerUtil.closeEntityManager();
// 计算出新的通知
List<ApolloConfigNotification> newNotifications =
getApolloConfigNotifications(namespaces, clientSideNotifications, watchedKeysMap,
latestReleaseMessages);
// 不是空, 理解返回结果, 不等待
if (!CollectionUtils.isEmpty(newNotifications)) {
deferredResultWrapper.setResult(newNotifications);
} else {
// 设置 timeout 回调:打印日志
deferredResultWrapper
.onTimeout(() -> logWatchedKeys(watchedKeys, "Apollo.LongPoll.TimeOutKeys"));
// 设置完成回调:删除 key
deferredResultWrapper.onCompletion(() -> {
//取消注册
for (String key : watchedKeys) {
deferredResults.remove(key, deferredResultWrapper);
}
});
//register all keys 注册
for (String key : watchedKeys) {
this.deferredResults.put(key, deferredResultWrapper);
}
}
// 立即返回
return deferredResultWrapper.getResult();/** @see DeferredResultHandler 是关键 */
}
注释写了很多了,再简单说说逻辑:
- 解析 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 方法。方法内容如下:
@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);
//create a new list to avoid ConcurrentModificationException 构造一个新 list ,防止并发失败
List<DeferredResultWrapper> results = Lists.newArrayList(deferredResults.get(content));
// 创建通知对象
ApolloConfigNotification configNotification = new ApolloConfigNotification(changedNamespace, message.getId());
configNotification.addMessage(content, message.getId());
//do async notification if too many clients 如果有大量的客户端(100)在等待,使用线程池异步处理
if (results.size() > bizConfig.releaseMessageNotificationBatch()) {
// 大量通知批量处理
largeNotificationBatchExecutorService.submit(() -> {
for (int i = 0; i < results.size(); i++) { // 循环
/*
* 假设一个公共 Namespace 有10W 台机器使用,如果该公共 Namespace 发布时直接下发配置更新消息的话,
* 就会导致这 10W 台机器一下子都来请求配置,这动静就有点大了,而且对 Config Service 的压力也会比较大。
* 即"惊群效应"
*/
if (i > 0 && i % bizConfig.releaseMessageNotificationBatch() == 0) {// 如果处理了一批客户端,休息一下(100ms)
TimeUnit.MILLISECONDS.sleep(bizConfig.releaseMessageNotificationBatchIntervalInMilli());
}
results.get(i).setResult(configNotification);// 通知每个等待的 HTTP 请求
}
});
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 异步对象,即实现了消息实时通知,也让应用程序实现异步解耦。
Apollo 8 — ConfigService 异步轮询接口的实现的更多相关文章
- Android中实现异步轮询上传文件
前言 前段时间要求项目中需要实现一个刷卡考勤的功能,因为涉及到上传图片文件,为加快考勤的速度,封装了一个异步轮询上传文件的帮助类 效果 先上效果图 设计思路 数据库使用的框架是GreenDao,一个 ...
- Apollo 3 定时/长轮询拉取配置的设计
前言 如上图所示,Apollo portal 更新配置后,进行轮询的客户端获取更新通知,然后再调用接口获取最新配置.不仅仅只有轮询,还有定时更新(默认 5 分钟一次).目的就是让客户端能够稳定的获取到 ...
- 浅谈JS异步轮询和单线程机制
单线程特点执行异步操作 js是单线程语言,浏览器只分配给js一个主线程,用来执行任务(函数),但一次只能执行一个任务,这些任务就会排队形成一个任务队列排队等候执行.一般而已,相对耗时的操作是要通过异步 ...
- Postman实现数字签名,Session依赖, 接口依赖, 异步接口结果轮询
Script(JS)为Postman赋予无限可能 基于Postman 6.1.4 Mac Native版 演示结合user_api_demo实现 PS 最近接到任务, 要把几种基本下单接口调试和持续集 ...
- Slickflow.NET 开源工作流引擎基础介绍(十) -- 邮件轮询异步发送模块集成
前言:在任务数据生成时,为了让办理任务的用户及时获取到待办任务的主题和内容,需要发送通知类的消息,而电子邮件和手机端的短信通知则是比较普通的消息发送.本文是针对电子邮件异步发送模块的实现来做实例说明. ...
- nacos 使用 servlet 异步处理客户端配置长轮询
config 客户端 ClientWorker#ClientWorker 构造方法中启动定时任务 ClientWorker.LongPollingRunnable 长轮询的任务,在 run 方法的结尾 ...
- 与现代传感器的接口:轮询ADC驱动程序
与现代传感器的接口:轮询ADC驱动程序 Interfacing with modern sensors: Polled ADC drivers 我们研究了在现代嵌入式应用程序中,开发人员应该如何创建一 ...
- STM32学习笔记(五) USART异步串行口输入输出(轮询模式)
学习是一个简单的过程,只要有善于发掘的眼睛,总能学到新知识,然而如何坚持不懈的学习却很困难,对我亦如此,生活中有太多的诱惑,最后只想说一句勿忘初心.闲话不多扯,本篇讲诉的是异步串行口的输入输出,串口在 ...
- JS中的异步以及事件轮询机制
一.JS为何是单线程的? JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事.那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊.(在JAVA和c#中的异步 ...
随机推荐
- Cbv源码简单分析图
重点:cbv源码的简单分析(有后续)
- JS模块化工具require.js教程(二):基本知识
前一篇:JS模块化工具我们以非常简单的方式引入了requirejs,这一篇将讲述一下requirejs中的一些基本知识,包括API使用方式等 基本API require会定义三个变量:define,r ...
- 从git远程仓库Checkout项目到本地
一.登录coding 并且项目已创建好 已经是项目的组员 二.打开idea 1.弹出如下页面 复制远程项目上的SSH(URL)到下框URL 并且Test测试 成功就Clone即可 2.Clone ...
- 与Servlet相关的类
有4个有关的类,通过servlet可以获得其中的三个,然后通过ServletConfig间接获取ServletContext.1. ServletConfig该类是在Servlet容器初始化Servl ...
- Spark机器学习解析下集
上次我们讲过<Spark机器学习(上)>,本文是Spark机器学习的下部分,请点击回顾上部分,再更好地理解本文. 1.机器学习的常见算法 常见的机器学习算法有:l 构造条件概率:回归分 ...
- 3D轮播图
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- 吴恩达机器学习笔记21-正则化线性回归(Regularized Linear Regression)
对于线性回归的求解,我们之前推导了两种学习算法:一种基于梯度下降,一种基于正规方程. 正则化线性回归的代价函数为: 如果我们要使用梯度下降法令这个代价函数最小化,因为我们未对theta0进行正则化,所 ...
- 第56节:ArrayList,LinkedList和String
import java.util.ArrayList; public class Demo{ public static void main(String[] args) throws Excepti ...
- 第二节:Java入门第一行代码
前言 大家好,今天带来Java入门第一行代码的概述,希望你们喜欢 第一行代码 学习Java基础课程,学会使用eclipse,eclipse为Java集成开发坏境IDE,创建第一个Java源文件代码: ...
- psnr的定义和python实现
psnr是“Peak Signal to Noise Ratio”的缩写,即峰值信噪比,是一种评价图像的客观标准,它具有局限性,一般是用于最大值信号和背景噪音之间的一个工程项目. peak的中文意思是 ...