什么是长轮询

article/2025/10/14 8:24:11

  • 短轮询 vs 长轮询
    • 短轮询
    • 长轮询
  • 长轮询的原理
    • demo
    • tomcat线程池
    • AsyncContext源码分析

短轮询 vs 长轮询

在看apollo和nacos等配置中心的源码的时候发现,配置更新的实时感知都是采用的长轮询的方式。那么什么是长轮询的呢?在讲解长轮询之前我们先了解一下什么是短轮询。

短轮询

首先说轮询的场景:轮询,顾名思义,就是一遍一遍的查询。比如配置中心修改配置的这种场景,我们业务方的系统需要及时感知到关心的配置是否有更新。能想到最简单的方式就是不断地发http请求,然后配置中心接收到请求之后,实时返回结果,告诉客户端关心的配置是否有更新。

image-20220728095940953

这是最简单也是最容易想到的实现方式,但是它有自己的弊端:到底多久请求一次呢?如果频率较高,那么就会导致服务端压力大;如果请求的频率放低,那么客户端感知变更的及时性就会降低。

长轮询就不存在这样的问题,下面对长轮询进行介绍。

长轮询

长轮询的含义就是:客户端发起请求,如果服务端的数据没有发生变更,那么就hold住请求,直到服务端的数据发生了变更,或者达到了一定的时间就会返回。这样就减少了客户端和服务端不断频繁连接和传递数据的过程,并且不会消耗服务端太多资源。

image-20220728205157523

这里大家可能会有两个疑问:

  1. 为什么达到时间就返回,既然 是长轮询,为什么不一直hold住请求,直到数据发生变更再返回呢?
  2. 服务端hold住难道不消耗线程吗,不是线程一直阻塞在那里吗?

第一个问题主要有两个层面的考虑,一是连接稳定性的考虑,长轮询在传输层本质上还是走的 TCP 协议,如果服务端假死、fullgc 等异常问题,或者是重启等常规操作,长轮询没有应用层的心跳机制,仅仅依靠 TCP 层的心跳保活很难确保可用性,所以一次长轮询设置一定的超时时间也是在确保可用性。二是在配置中心的使用过程中,用户可能随时新增配置监听,而在此之前,长轮询可能已经发出,新增的配置监听无法包含在旧的长轮询中,所以在配置中心的设计中,一般会在一次长轮询结束后,将新增的配置监听给捎带上,而如果长轮询没有超时时间,只要配置一直不发生变化,响应就无法返回,新增的配置也就没法设置监听了。

第二个问题可以这样理解:既然是通过让服务端长时间hold住请求实现长轮询的,那么必然不会是让服务端线程阻塞在那里等待数据变更的,我们都知道tomcat线程池默认是200,话句话就是说,服务端的线程是很宝贵的资源,如果有200个这样的长轮询的请求把线程阻塞在那里,也这个服务器基本就属于宕机的状态了,其他什么请求也处理不了了。所以这里说的hold住请求,并不是让线程一直阻塞着,而是tomcat线程把request和response引用放在服务端全局的集合中,由单独的一个或几个线程处理这些请求,而tomcat线程把本次请求的request和response引用放入全局集合中之后,当前的使命就算是完成了,从而可以被调度去处理其他请求。

长轮询的原理

在上面长轮询的图中我们看到,客户端侧标注了一个timeout时间90s,服务端最长的hold时间是80s,两个时间只是个示例,代表的是服务端hold的时间要小于客户端设置的超时时间。这也很容易理解,如果服务端的hold时间大于客户端设置的超时时间,那么大概率客户端会出现timeout异常,这是非常不优雅的。

上节讲了,长轮询不可能一直占用tomcat的线程池,所以需要采用异步响应的方式去实现,而比较方便实现异步http的方式就是Servlet3.0提供的AsyncContext 机制。

asyncContext是为了把主线程返回给tomcat线程池,不影响服务对其他客户端请求。会有线程专门处理这个长轮询,但并不是说每一个长轮询的http请求都要用一个线程阻塞在那。而是把长轮询的request的引用在一个集合中存起来,用一个或几个线程专门处理一批客户端的长轮询请求,这样就不需要为每一个长轮询单独分配线程阻塞在那了,从而大大降低了资源的消耗,

demo

@RestController
public class ConfigServer {@Dataprivate static class AsyncTask {// 长轮询请求的上下文,包含请求和响应体private AsyncContext asyncContext;// 超时标记private boolean timeout;public AsyncTask(AsyncContext asyncContext, boolean timeout) {this.asyncContext = asyncContext;this.timeout = timeout;}}// guava 提供的多值 Map,一个 key 可以对应多个 value,这个就是我们上节说的全局集合,不会随着请求的结束而销毁private volatile Multimap<String, AsyncTask> dataIdContext = Multimaps.synchronizedSetMultimap(HashMultimap.create());private ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("longPolling-timeout-checker-%d").build();// 创建一个延时任务,这个相当于单独的一个守护线程,所有长轮询的任务的超时检查都由这个线程处理private ScheduledExecutorService timeoutChecker = new ScheduledThreadPoolExecutor(1, threadFactory);//  客户端通过请求这个接口用于感知到配置是否有变更@RequestMapping("/listener")public void addListener(HttpServletRequest request, HttpServletResponse response) {String dataId = request.getParameter("dataId");// 开启异步,这里是将客户端请求的request和response包装成AsyncContext对象,AsyncContext对象又被asyncTask包装AsyncContext asyncContext = request.startAsync(request, response);AsyncTask asyncTask = new AsyncTask(asyncContext, true);// 把asyncTask放入到dataIdContext中,这样即使走下面的异步任务,当前主线程的任务结束,当前请求也会被hold住dataIdContext.put(dataId, asyncTask);// 启动定时器,30s 后写入 304 响应,timeoutChecker.schedule(() -> {if (asyncTask.isTimeout()) {dataIdContext.remove(dataId, asyncTask);response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);asyncContext.complete();}}, 30000, TimeUnit.MILLISECONDS);}//  配置发布接入点,这里是修改配置的入口,@RequestMapping("/publishConfig")public String publishConfig(String dataId, String configInfo) {// 对应的配置从dataIdContext中取出Collection<AsyncTask> asyncTasks = dataIdContext.removeAll(dataId);for (AsyncTask asyncTask : asyncTasks) {asyncTask.setTimeout(false);// 设置response并返回客户端HttpServletResponse response = (HttpServletResponse)asyncTask.getAsyncContext().getResponse();response.setStatus(HttpServletResponse.SC_OK);response.getWriter().println(configInfo);asyncTask.getAsyncContext().complete();}return "success";}

tomcat线程池

上面是长轮询的demo,有些同学可能还是不太理解线程和request的区别,下面大概讲解一个请求在服务端处理的整个过程。

  • tcp三次握手后
  • Acceptor线程处理 socket accept
  • Acceptor线程处理 注册registered OP_READ到多路复用器
  • ClientPoller线程 监听多路复用器的事件(OP_READ)触发
  • 从tomcat的work线程池取一个工作线程来处理socket[http-nio-8080-exec-xx],下面几个步骤也都是在work线程中进行处理的
  • 因为是http协议所以用Http11Processor来解析协议
  • CoyoteAdapter来适配包装成Request和Response对象
  • 开始走pipeline管道(Valve),最后一个invoke的是把我们的servlet对象包装的StandardWrapperValve管道

接下来就走到我们的servlet,由于是我们是异步的servlet,当在tomcat的work线程中调用startAsync(),会创建了一个异步的上下文(AsyncContext),并且异步的上下文(AsyncContext)会设置这个状态机状态为 STARTING, 然后把这个异步上下文放到了我们的自定义线程池中去执行,对于tomcat的work线程而言,servlet调用就结束了!在这个时候request和response由于被AsyncContext对象引用,是不会被释放的。虽然Request和Response没有释放,但是这根work线程回到tomcat的线程池中去了。

过程可以通过下图解释:

image-20220803095200111

AsyncContext源码分析

AsyncContext asyncContext = request.startAsync(request, response);
asyncContext.complete();

实现异步主要就是这两步

  1. 通过request创建一个asyncContext对象
  2. 通过asyncContext调用complete方法完成数据返回

先看第一步的源码:

Requestpublic AsyncContext startAsync(ServletRequest request, ServletResponse response) {if (!this.isAsyncSupported()) {  //要开启异步处理支持,否则在这一步直接就会抛异常IllegalStateException ise = new IllegalStateException(sm.getString("request.asyncNotSupported"));log.warn(sm.getString("coyoteRequest.noAsync", new Object[]{StringUtils.join(this.getNonAsyncClassNames())}), ise);throw ise;} else {if (this.asyncContext == null) {this.asyncContext = new AsyncContextImpl(this);}// 创建并调用setStarted方法this.asyncContext.setStarted(this.getContext(), request, response, request == this.getRequest() && response == this.getResponse().getResponse());// 设置超时时间this.asyncContext.setTimeout(this.getConnector().getAsyncTimeout());return this.asyncContext;}
}
  1. 业务方法开启异步化上下文AsynContext;释放tomcat当前处理线程;
  2. tomcat判断当前请求是否开启了异步化,如果开启则不关闭响应流Response,也不进行用户响应的返回;
AsyncContextImplpublic void setStarted(Context context, ServletRequest request, ServletResponse response, boolean originalRequestResponse) {Object var5 = this.asyncContextLock;synchronized(this.asyncContextLock) {this.request.getCoyoteRequest().action(ActionCode.ASYNC_START, this);this.context = context;this.servletRequest = request;this.servletResponse = response;this.hasOriginalRequestAndResponse = originalRequestResponse;this.event = new AsyncEvent(this, request, response);List<AsyncListenerWrapper> listenersCopy = new ArrayList();listenersCopy.addAll(this.listeners);this.listeners.clear();Iterator i$ = listenersCopy.iterator();while(i$.hasNext()) {AsyncListenerWrapper listener = (AsyncListenerWrapper)i$.next();try {listener.fireOnStartAsync(this.event);} catch (Throwable var11) {ExceptionUtils.handleThrowable(var11);log.warn("onStartAsync() failed for listener of type [" + listener.getClass().getName() + "]", var11);}}}
}

第二步的源码

public void complete() {if (log.isDebugEnabled()) {this.logDebug("complete   ");}this.check();this.request.getCoyoteRequest().action(ActionCode.ASYNC_COMPLETE, (Object)null);
}

源码暂时没看太深入,先记个todo吧,等后续深入看下。。。

参考:

https://blog.csdn.net/weixin_45727359/article/details/113533256

https://www.shuzhiduo.com/A/o75NNk1x5W/


http://chatgpt.dhexx.cn/article/u5t5AwBf.shtml

相关文章

js轮询

一、案例效果 使得数据实时变化&#xff0c;可以随时暂停和播放 二、代码案例 html <button id"button">暂停</button>js let timerId 1 // 模拟计时器id&#xff0c;唯一性let timerObj {} // 计时器存储器function getData() {return new Promi…

php实现异步轮询

文章目录 一、前言二、工欲善其事1、curl是伪异步请求2、鸟哥推荐的方法中有curl 三、异步轮询&#xff08;fsockopen&#xff09;1、模拟异步轮询的demo2、响应页面代码3、测试结果4、fsockopen(): unable to connect to 错误 四、问题以及反思1、无法调试返回2、占用进程3、最…

java 轮询请求_使用RxJava来实现网络请求轮询功能

原标题:使用RxJava来实现网络请求轮询功能 近日有媒体报道称,腾讯重金入股永辉超市旗下生鲜超市超级物种,目前交易已经完成。受此刺激,永辉超市股价迅速涨停,午后临时停牌。若此举成行,超级物种将更有底气对垒阿里巴巴的盒马鲜生,生鲜商超的新零售市场将展开激烈争战。 …

android轮询

目录 轮询实现方案&#xff1a; Timer Handler RxJava 1.Interval 2.repeatWhen 轮询实现方案&#xff1a; 方案一&#xff1a; Timer Thread 实现思路&#xff1a;使用timer定时执行TimerTask 缺点&#xff1a;如果有异步任务&#xff0c;下次任务开始执行时需要判断…

nginx轮询

创建容器1&#xff1a; docker create -it --name zyr1 centos:7 /bin/bash docker start zyr1 进入容器&#xff1a; docker exec -it zyr1 /bin/bash 安装ipconfig命令 yum provides ifconfig 安装nginx依赖 yum -y install openssl openssl-devel prce-devel zlib z…

python 轮询mysql_python 轮询

1. 轮询 三天之后,小钱才拿到这个快递 总结 快递不能及时的传达 小钱儿 - 卒 客户端浪费极大资源 老程头儿 -痴呆 资源浪费也很严重 HTTP无法跟踪定义客户端 无状态 2. 长轮询 缺陷: 消息实时性不高 传达室茶室的资源有限 占用资源 客户端线程资源占用 3. 长连接 总结 占用的空…

java 轮询http_HTTP轮询模型

HTTP轮询模型 长短轮询 http协议是一种client-server模型的应用层协议&#xff0c;这种c-s的模式虽然大多数情况都能满足需求&#xff0c;但是某些场景也需要服务端能够将一些信息实时的推送到客户端&#xff0c;即实现服务器向客户端推消息的功能。 比如&#xff1a; 配置管理…

七种轮询介绍(后附实践链接)

我有一个朋友&#xff5e; 做了一个小破站&#xff0c;现在要实现一个站内信web消息推送的功能&#xff0c;对&#xff0c;就是下图这个小红点&#xff0c;一个很常用的功能。 不过他还没想好用什么方式做&#xff0c;这里我帮他整理了一下几种方案&#xff0c;并简单做了实现…

linux cgroup 死循环,Linux CGroup 基础

CGroup V1 1. CGroup 概念Task: 任务&#xff0c;也就是进程&#xff0c;但这里的进程和我们通常意义上的 OS 进程有些区别&#xff0c;在后面会提到。 CGroup: 控制组&#xff0c;一个 CGroup 就是一组按照某种标准划分的Tasks。这里的标准就是 Subsystem 配置。换句话说&…

linux cgroup 原理,Cgroup框架的实现

CGoup核心主要创建一系列sysfs文件&#xff0c;用户空间可以通过这些节点控制CGroup各子系统行为&#xff0c;以及各子系统模块根据参数。在执行过程中或调度进程到不同CPU上&#xff0c;或控制CPU占用时间&#xff0c;或控制IO带宽等等。另外&#xff0c;在每个系统的proc文件…

CGroup的原理和使用

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、主要功能二、基本概念三、cgroups子系统介绍四、cgroups 层级结构五、数据结构 前言 Linux CGroup全称Linux Control Group&#xff0c; 是Linux内核的一个…

Linux cgroup介绍

本文参考网上一些资料&#xff0c;结合实际应用&#xff0c;简要介绍一下cgroup。 为什么要有cgroup Linux系统中经常有个需求就是希望能限制某个或者某些进程的分配资源。也就是能完成一组容器的概念&#xff0c;在这个容器中&#xff0c;有分配好的特定比例的cpu时间&#…

漫谈cgroup

什么是cgroup cgroup 是linux内核的一个功能&#xff0c;用来限制、控制与分离一个进程组的资源&#xff08;如CPU、内存、磁盘I/O等&#xff09;。它是由 Google 的两位工程师进行开发的&#xff0c;自 2008 年 1 月正式发布的 Linux 内核 v2.6.24 开始提供此能力。 cgroup …

容器中的Cgroup

文章目录 容器中的CgroupCgroup概念容器化两个关键核心现代容器化带来的优势什么是Cgroup Cgroup的一些测试测试CPU和内存使用情况CPU 周期限制 CPU Core 控制CPU 配额控制参数的混合使用内存限额Block IO 的限制bps 和 iops的限制 容器中的Cgroup Cgroup概念 容器化两个关键…

Cgroup 资源配置

目录 一、Cgroup定义 二、使用stress压力测试工具测试cpu和内存状态 1、创建一个dockerfile文件 2、创建镜像 3、创建容器 ①创建容器 ②、创建容器并产生10个子函数进程 三、CPU 周期 1、实现方案 2、实验 四、CPU Core控制 五、docker build 一、Cgroup定义 cg…

cgroup资源配置

一、cgroup介绍二、利用stress 压力测试工具来测试三、CPU控制1、仅用率控制&#xff08;权重&#xff09;2、周期限制方法一&#xff1a;在命令行里直接设置方法二&#xff1a;创建容器后&#xff0c;关闭容器在文件里直接修改方法三&#xff1a;进入容器查看 3、cpu核心数4、…

【CGroup原理篇】3. CGroup使用指南

写在前面 这里先从整体上概述cgroup的创建,挂载,参数配置和卸载,后面的章节中会一一介绍每个子系统的详细使用方法和使用案例。 一、使用Linux命令管理CGroup 1.1挂载cgroup临时文件系统 mount -t tmpfs cgroup_root /sys/fs/cgroup 1.2 创建挂载层级需要的目录 mkdir /sy…

Linux CGroup 原理

Linux CGroup 原理 1、CGroup简介 cgroups是Linux下控制一个&#xff08;或一组&#xff09;进程的资源限制机制&#xff0c;全称是control groups&#xff0c;可以对cpu、内存等资源做精细化控制。 开发者可以直接基于cgroups来进行进程资源控制&#xff0c;比如8核的机器上…

LINUX CGROUP总结

简介: Linux CGroup全称Linux Control Group&#xff0c; 是Linux内核的一个功能&#xff0c;用来限制&#xff0c;控制与分离一个进程组群的资源&#xff08;如CPU、内存、磁盘输入输出等&#xff09;。这个项目最早是由Google的工程师在2006年发起&#xff08;主要是Paul Men…

cgroup 简介

cgroup 的功能在于将一台计算机上的资源&#xff08;CPU&#xff0c;memory&#xff0c;network&#xff09;进行分片&#xff0c;来防止进程间不利的资源抢占。 术语 cgroup&#xff1a;关联一组 task 和一组 subsystem 的配置参数。 一个 task 对应一个进程&#xff0c;cg…