Tomcat长轮询原理与源码解析

系列文章目录和关于我

零丶长轮询的引入

最近在看工作使用到的diamond配置中心原理,发现大多数配置中心在推和拉模型上做的选择出奇的一致选择了基于长轮询的拉模型

  • 基于拉模型的客户端轮询的方案

    客户端通过轮询方式发现服务端的配置变更事件。轮询的频率决定了动态配置获取的实时性。

    • 优点:简单、可靠。
    • 缺点:应用增多时,较高的轮询频率给整个配置中心服务带来巨大的压力。

    另外,从配置中心的应用场景上来看,是一种写少读多的系统,客户端大多数轮询请求都是没有意义的,因此这种方案不够高效。

  • 基于推模型的客户端长轮询的方案

    基于Http长轮询模型,实现了让客户端在没有发生动态配置变更的时候减少轮询。这样减少了无意义的轮询请求量,提高了轮询的效率;也降低了系统负载,提升了整个系统的资源利用率。

一丶何为长轮询

长轮询 本质上是原始轮询技术的一种更有效的形式。

它的出现是为了解决:向服务器发送重复请求会浪费资源,因为必须为每个新传入的请求建立连接,必须解析请求的 HTTP 头部,必须执行对新数据的查询,并且必须生成和交付响应(通常不提供新数据)然后必须关闭连接并清除所有资源。

  • 从tomcat服务器的角度就是客户端不停请求,每次都得解析报文封装成Request,Response对象,并且占用线程池中的一个线程。
  • 并且每次轮询都要进行tcp握手,挥手,网卡发起中断,操作系统处理中断从内核空间拷贝数据到用户空间,一通忙活服务端返回 配置未修改(配置中心没有修改配置,客户端缓存的配置和配置中心一致,所以是白忙活)

长轮询是一种服务器选择尽可能长的时间保持和客户端连接打开的技术仅在数据变得可用或达到超时阙值后才提供响应而不是在给到客户端的新数据可用之前,让每个客户端多次发起重复的请求

简而言之,就是服务端并不是立马写回响应,而是hold住一段时间,如果这段时间有数据需要写回(例如配置的修改,新配置需要写回)再写回,然后浏览器再发送一个新请求,从而实现及时性,节省网络开销的作用。

二丶使用等待唤醒机制写一个简单的“长轮询”(脱裤子放屁)

package com.cuzzz.springbootlearn.longpull;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock; @RestController
@RequestMapping("long-pull")
public class MyController implements InitializingBean { /**
* 处理任务的线程
*/
private ThreadPoolExecutor processExecutor;
/**
* 等待唤醒的锁
*/
private static final ReentrantLock lock = new ReentrantLock();
/**
* 当请求获取配置的时候,在此condition上等待一定时间
* 当修改配置的时候通过这个condition 通知其他获取配置的线程
*/
private static final Condition condition = lock.newCondition(); @GetMapping
public void get(HttpServletRequest request, HttpServletResponse response) throws ExecutionException, InterruptedException {
//组转成任务
Task<String> task = new Task<String>(request, response,
() -> "拿配置" + System.currentTimeMillis());
//提交到线程池
Future<?> submit = processExecutor.submit(task);
//tomcat线程阻塞于此
submit.get();
} /**
* 模拟修改配置
*
* 唤醒其他获取配置的线程
*/
@PostMapping
public String post(HttpServletRequest request, HttpServletResponse response) {
lock.lock();
try {
condition.signalAll();
}finally {
lock.unlock();
}
return "OK";
} static class Task<T> implements Runnable {
private HttpServletResponse response;
/**
* 等待时长
*/
private final long timeout;
private Callable<T> task; public Task(HttpServletRequest request, HttpServletResponse response, Callable<T> task) {
this.response = response; String time = request.getHeader("time-out");
if (time == null){
//默认等待10秒
this.timeout = 10;
}else {
this.timeout = Long.parseLong(time);
}
this.task = task;
} @Override
public void run() {
lock.lock();
try { //超市等待
boolean await = condition.await(timeout, TimeUnit.SECONDS);
//超时
if (!await) {
throw new TimeoutException();
}
//获取配置
T call = task.call();
//写回
ServletOutputStream outputStream = response.getOutputStream();
outputStream.write(("没超时拿当前配置:" + call).getBytes(StandardCharsets.UTF_8));
} catch (TimeoutException | InterruptedException exception) {
//超时或者线程被中断
try {
ServletOutputStream outputStream = response.getOutputStream();
T call = task.call();
outputStream.write(("超时or中断拿配置:" + call).getBytes(StandardCharsets.UTF_8));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
} @Override
public void afterPropertiesSet() { int cpuNums = Runtime.getRuntime().availableProcessors(); processExecutor
= new ThreadPoolExecutor(cpuNums, cpuNums * 2, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.CallerRunsPolicy());
}
}

使用get方法反问的请求回被提交到线程池进行await等待,使用post方法的请求回唤醒这些线程。

但是这个写法有点脱裤子放屁

为什么会出现这种情况,直接提交到线程池异步执行不可以么,加入我们删除上面submit.get方法会发现其实什么结果都不会,这是因为异步提交到线程池后,tomcat已经结束了这次请求,并没有维护这个连接,所以没有办法写回结果。

如果不删除这一行,tomcat线程阻塞住我们可以写回结果,但是其实没有达到配置使用长轮询的初衷——"解放tomcat线程,让配置中心服务端可以处理更多请求"。

所以我们现在陷入一个尴尬的境地,怎么解决昵?看下去

三丶Tomcat Servlet 3.0长轮询原理

1.AsyncContext实现长轮询

package com.cuzzz.springbootlearn.longpull;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*; @RestController
@RequestMapping("long-pull3")
public class MyController2 { private static final ScheduledExecutorService procesExecutor
= Executors.newSingleThreadScheduledExecutor();
/**
* 记录配置改变的map
*/
private static final ConcurrentHashMap<String, String> configCache
= new ConcurrentHashMap<>();
/**
* 记录长轮询的任务
*/
private static final ConcurrentLinkedDeque<AsyncTask> interestQueue
= new ConcurrentLinkedDeque<>(); static {
//每2秒看一下释放配置变更,或者任务超时
procesExecutor.scheduleWithFixedDelay(() -> {
List<AsyncTask>needRemove = new ArrayList<>();
for (AsyncTask asyncTask : interestQueue) {
if (asyncTask.timeout()) {
asyncTask.run();
needRemove.add(asyncTask);
continue;
}
if (configCache.containsKey(asyncTask.configId)) {
needRemove.add(asyncTask);
asyncTask.run();
}
}
interestQueue.removeAll(needRemove);
}, 1, 2, TimeUnit.SECONDS);
} static class AsyncTask implements Runnable {
private final AsyncContext asyncContext;
private final long timeout;
private static long startTime;
private String configId; AsyncTask(AsyncContext asyncContext) {
this.asyncContext = asyncContext;
HttpServletRequest request = (HttpServletRequest) asyncContext.getRequest();
String timeStr = request.getHeader("time-out");
if (timeStr == null) {
timeout = 10;
} else {
timeout = Long.parseLong(timeStr);
}
//关注的配置key,应该getParameter的,无所谓
this.configId = request.getHeader("config-id");
if (this.configId == null) {
this.configId = "default";
} //开始时间
startTime = System.currentTimeMillis();
} //是否超时
public boolean timeout() {
return (System.currentTimeMillis() - startTime) / 1000 > timeout;
} @Override
public void run() { String result = "开始于" + System.currentTimeMillis() + "--";
try {
if (timeout()) {
result = "超时: " + result;
} else {
result += configCache.get(this.configId);
} result += "--结束于:" + System.currentTimeMillis();
ServletResponse response = asyncContext.getResponse();
response.getOutputStream().write(result.getBytes(StandardCharsets.UTF_8)); //后续将交给tomcat线程池处理,将给客户端响应
asyncContext.complete();
} catch (IOException e) {
throw new RuntimeException(e);
} } } @GetMapping
public void get(HttpServletRequest request, HttpServletResponse response) {
//打印处理的tomcate线程id
System.out.println("线程id" + Thread.currentThread().getId());
//添加一个获取配置的异步任务
interestQueue.add(new AsyncTask(asyncContext));
//开启异步
AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(0);
//监听器打印最后回调的tomcat线程id
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) throws IOException {
System.out.println("线程id" + Thread.currentThread().getId());
}
//...剩余其他方法
}); //立马就会释放tomcat线程池资源
System.out.println("tomcat主线程释放");
} @PostMapping
public void post(HttpServletRequest request) {
String c = String.valueOf(request.getParameter("config-id"));
if (c.equals("null")){
c = "default";
}
String v = String.valueOf(request.getParameter("value"));
configCache.put(c, v);
}
}

上面演示利用AsyncContext tomcat是如何实现长轮询

这种方式的优势在于:解放了tomcat线程,其实tomcat的线程只是运行了get方法中的代码,然后立马可以去其他请求,真正获取配置更改的是我们的单线程定时2秒去轮询。

2.实现原理

2.1 tomcat处理一个请求的流程

  • Connector是客户端连接到Tomcat容器的服务点,它提供协议服务来将引擎与客户端各种协议隔离开来

    在Connector组件中创建了Http11NioProtocol组件,Http11NioProtocol默认持有NioEndpoin,NioEndpoint中持有Acceptor和Poller,并且启动的时候会启动一个线程运行Acceptor

  • Acceptor服务器端监听客户端的连接,会启动线程一直执行

    每接收一个客户端连接就轮询一个Poller组件,添加到Poller组件的事件队列中。,每接收一个客户端连接就轮询一个Poller组件,添加到Poller组件的事件队列中。

  • Poller组件持有多路复用器selector,poller组件不停从自身的事件队列中将事件取出注册到自身的多路复用器上,同时多路复用器会不停的轮询检查是否有通道准备就绪,准备就绪的通道就可以扔给tomcat线程池处理了。

  • tomcat线程池处理请求

    • 这里会根据协议创建不同的Processor处理,这里创建的是Http11Processor,Http11Processor会使用CoyoteAdapter去解析报文随后交给Container去处理请求

    • CoyoteAdapter解析报文随后交给Container去处理请求

    • Container会将Filter和Servlet组装成FilterChain依次调用

    • FilterChain会依次调用Filter#doFilter,然后调用Servlet#service方法

      至此会调用到Servlete#service方法,SpringMVC中的Dispatcher会反射调用我们controller的方法

2.2 AsyncContext 如何实现异步

2.2.1 request.startAsync() 修改异步状态机状态为Starting

AsycContext内部持有一个AsyncStateMachine来管理异步请求的状态(有点状态模式的意思)

状态机的初始状态是AsyncState.DISPATCHED,通过setStarted将状态机的状态更新成STARTING

2.2.2 AbstractProtocol启动定时任务处理超时异步请求

Connector启动的时候触发ProtocolHandler的start方法,如下

其中startAsyncTimeout方法会遍历waitingProcessors中每一个Processor的timeoutAsync方法,这里的Processor就是Http11Processor

那么waitProcessors中的Http11Processor是谁塞进去的昵?

tomcat线程在执行完我们的Servlet代码后,Http11NioProtocol会判断请求状态,如果为Long那么会塞到waitProcessors集合中。

如果发现请求超时,那么会调用Http11Processor#doTimeoutAsycn然后由封装的socket通道socketWrapper以TIMEOUT的事件类型重新提交到tomcat线程池中。

2.2.3 AsyncContext#complete触发OPEN_READ事件

可以看到其实和超时一样,只不过超时是由定时任务线程轮询来判断,而AsyncContext#complete则是我们业务线程触发processSocketEvent将后续处理提交到tomcat线程池中。

四丶长轮询的优点和缺点

本文学习了长轮询和tomcat长轮询的原理,可以看到这种方式的优点

  • 浏览器长轮询的过程中,请求并没有理解响应,而是等到超时或者有需要返回的数据(比如配置中心在这个超时事件内发送配置的变更)才返回,解决了短轮询频繁进行请求网络开销的问题,减少了读多写少业务情景下无意义请求。
  • 真是通过这种方式,减少了无意义的请求,而且释放了tomcat线程池中的线程,使得我们服务端可以支持更多的客户端(因为业务逻辑是放在其他的线程池执行的,而且对于配置中心来说,可以让多个客户端的长轮询请求由一个线程去处理,原本是一个请求一个tomcat线程处理,从而可以支持更多的请求)

当然这种方式也是有缺点的

  • hold住请求也是会消耗资源的,如果1w个请求同时到来,我们都需要hold住(封装成任务塞到队列)这写任务也是会占用内存的,而短轮询则会立马返回,从而时间资源的释放

  • 请求先后顺序无法保证,比如轮询第五个客户端的请求的时候,出现了配置的变更,这时候第五个请求会被提交到tomcat线程池中,从而早于前面四个请求得到响应,这对于需要严格有序的业务场景是有影响的

  • 多台实例监听配置中心实例,出现不一致的情况

    比如配置中心四台实例监听配置变更,前三台可能响应了得到V1的配置,但是轮询到第四台实例的请求的时候又发生了变更可能就得到了v2的配置,这时候这四台配置不一致了。需要保证这种一致性需要我们采取其他的策略,比如配置中心服务端主动udp推,或者加上版本号保证这四台配置一致。

Tomcat长轮询原理与源码解析的更多相关文章

  1. 机器学习实战(Machine Learning in Action)学习笔记————03.决策树原理、源码解析及测试

    机器学习实战(Machine Learning in Action)学习笔记————03.决策树原理.源码解析及测试 关键字:决策树.python.源码解析.测试作者:米仓山下时间:2018-10-2 ...

  2. Spring-Session实现Session共享实现原理以及源码解析

    知其然,还要知其所以然 ! 本篇介绍Spring-Session的整个实现的原理.以及对核心的源码进行简单的介绍! 实现原理介绍 实现原理这里简单说明描述: 就是当Web服务器接收到http请求后,当 ...

  3. Spring MVC工作原理及源码解析(三) HandlerMapping和HandlerAdapter实现原理及源码解析

    1.HandlerMapping实现原理及源码解析 在前面讲解Spring MVC工作流程的时候我们说过,前端控制器收到请求后会调⽤处理器映射器(HandlerMapping),处理器映射器根据请求U ...

  4. Tomcat系列(二)- EndPoint源码解析

    在上一节中我们描述了Tomcat的整体架构, 我们知道了Tomcat分为两个大组件,一个连接器和一个容器. 而我们这次要讲的 EndPoint的组件就是属于连接器里面的. 它是一个通信的端点,就是负责 ...

  5. Redux异步解决方案之Redux-Thunk原理及源码解析

    前段时间,我们写了一篇Redux源码分析的文章,也分析了跟React连接的库React-Redux的源码实现.但是在Redux的生态中还有一个很重要的部分没有涉及到,那就是Redux的异步解决方案.本 ...

  6. LinkedList原理及源码解析

    简介 LinkedList是一个双向线性链表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer).由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度, ...

  7. ORB原理与源码解析

    转载: http://blog.csdn.net/luoshixian099/article/details/48523267 CSDN-勿在浮沙筑高台 没有时间重新复制代码,只能一股脑的复制,所以代 ...

  8. Go中定时器实现原理及源码解析

    转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com 本文使用的go的源码15.7,需要注意的是由于timer是1.14版本进行改版,但是1. ...

  9. RocketMQ原理及源码解析

    RocketMQ原理深入: 一.定义: RocketMQ是一款分布式.队列模型的消息中间件,有以下部分组成: 1.NameServer: 一个几乎无状态的节点,可集群部署,节点之间无任何信息同步 2. ...

  10. IK分词器 原理分析 源码解析

    IK分词器在是一款 基于词典和规则 的中文分词器.本文讲解的IK分词器是独立于elasticsearch.Lucene.solr,可以直接用在java代码中的部分.关于如何开发es分词插件,后续会有文 ...

随机推荐

  1. 使用idea从零编写SpringCloud项目-Ribbo

    git:https://github.com/bmdcheng/product_server git:https://github.com/bmdcheng/order_server 1.需要创建两个 ...

  2. 基于airtest验证Android端app是否安装及自动化安装

    ​ 1.检测app是否安装: 使用check_app方法检测是否安装:为什么需要在封装一层做断言呢?主要check_app方法安装成功会返回True,但是未检测到安装时直接报异常了,停止执行.无法直接 ...

  3. 小米盒子TV变装魔法

    最近从一位台湾的朋友那里白嫖了一个 v2 节点, 恰好家里有一台家用的 小米盒子, 就寻思着能不能折腾一下, 共享上网 先将小米盒子开启adb调试, 参照这里: https://www.jb51.ne ...

  4. store数据仓库

    项目搭建 npm init vite-app GxShujukucd GxShujukunpm inpm i vue-router npm i vuex // 这一句是这节课的关键 新建store ① ...

  5. ASP.NET WEBAPI 获取微信ticket

    public static string GetTicket(string AccessToken) { //类型 string Type = "jsapi"; string st ...

  6. 使用IDEA的webservice工具解析生成的客户端调用远程接口

    由于这个接口的报文格式比较麻烦,是XML的请求头加上JSON格式的请求体,所以看起来比较复杂,也可以用RPC的方式调用,那样需要将请求头和请求体,响应头和响应体建实体.public JSONObjec ...

  7. ASP.NET的MVC模式中分布页和布局页的使用

    大概描述一下,分布页是布局页的一部分,分布页就相当于小图标,布局页就相当于PPT模板,PPT模板可以加入一些小图标(分布页),你只需要改改内容就好,视图创建的时候要选择包含布局页的 首先,去Contr ...

  8. tensorflow出现Failed to get convolution algorithm, cuDNN failed to initialize

    网上大多的教程是说tensorflow的版本过高,或者说cuda和cudnn的版本不对,需要降级,但这样会很麻烦!!! 以下值得推荐!!! 解决方法一:在代码前加上下面的代码 from tensorf ...

  9. Go_day01

    Go基础语法 注释 注释是为了增强代码的可读性,不会参与程序的一切功能, go语言注释分为单行注释与多行注释 单行注释 //双斜杠 每行都要添加 // 多行注释 /* */ 一次标记多行注释输入 pa ...

  10. 学习记录--C++作业3

    1.类是一个模板吗? 是:类模板是一个抽象的类,代表类的一般特性,可以用类模板来创建类,所有的类都有共有的特性. 4.函数模板的实例化是什么? 模板函数,即函数 3.关于cin和cout说法正确的: ...