SpringBoot内嵌Tomcat连接池分析

文章目录

  • 1 Tomcat连接池
    • 1.1 简介
    • 1.2 架构图
      • 1.2.1 JDK线程池架构图
      • 1.2.2 Tomcat线程架构
    • 1.3 核心参数
      • 1.3.1 AcceptCount
      • 1.3.2 MaxConnections
      • 1.3.3 MinSpareThread/MaxThread
      • 1.3.4 MaxKeepAliveRequests
      • 1.3.5 ConnectionTimeout
      • 1.3.6 KeepAliveTimeout
    • 1.4 核心内部线程
      • 1.4.1 Acceptor
      • 1.4.2 Poller
      • 1.4.3 TomcatThreadPoolExecutor
    • 1.5 测试

1 Tomcat连接池

每个Spring Boot版本和内置容器不同,结果也不同,这里以Spring Boot 2.6.11版本 + 内置Tomcat容器举例

1.1 简介

Spring Boot 2.6.11版本中内置Tomcat版本是 9.0.65SpringBoot 内置Tomcat 的默认设置如下:

  • Tomcat 的连接等待队列长度,默认是100
  • Tomcat 的最大连接数,默认是8192
  • Tomcat 的最小工作线程数,默认是10
  • Tomcat 的最大线程数,默认是200
  • Tomcat 的连接超时时间,默认是20s

在这里插入图片描述

相关配置及默认值如下

server:tomcat:# 当所有可能的请求处理线程都在使用中时,传入连接请求的最大队列长度accept-count: 100# 服务器在任何给定时间接受和处理的最大连接数。一旦达到限制,操作系统仍然可以接受基于“acceptCount”属性的连接。max-connections: 8192threads:# 工作线程的最小数量,初始化时创建的线程数min-spare: 10# 工作线程的最大数量 io密集型建议10倍的cpu数,cpu密集型建议cpu数+1,绝大部分应用都是io密集型max: 200# 连接器在接受连接后等待显示请求 URI 行的时间。connection-timeout: 20000# 在关闭连接之前等待另一个 HTTP 请求的时间。如果未设置,则使用 connectionTimeout。设置为 -1 时不会超时。keep-alive-timeout: 20000# 在连接关闭之前可以进行流水线处理的最大HTTP请求数量。当设置为0或1时,禁用keep-alive和流水线处理。当设置为-1时,允许无限数量的流水线处理或keep-alive请求。 max-keep-alive-requests: 100

1.2 架构图

在这里插入图片描述

当连接数大于maxConnections+acceptCount + 1 时,新来的请求不会收到服务器拒绝连接响应,而是不会和新的请求进行3次握手建立连接,一段时间后(客户端的超时时间或者Tomcat的20s后)会出现请求连接超时

Tomcat扩展了线程池增强了功能:

  • JDK 线程池流程:minThreads --> queue --> maxThreads --> Exception
  • Tomcat 增强后:minThreads --> maxThreads --> queue --> Exception

点击此处了解JDK线程池

1.2.1 JDK线程池架构图

在这里插入图片描述

1.2.2 Tomcat线程架构

在这里插入图片描述

1.3 核心参数

1.3.1 AcceptCount

连接等待队列容量,等同于backlog参数,与Linux中的系统参数somaxconn取较小值,Windows中没有系统参数。

NioEndpoint.java

serverSock = ServerSocketChannel.open();
socketProperties.setProperties(serverSock.socket());
InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
// 这里
serverSock.socket().bind(addr,getAcceptCount());

1.3.2 MaxConnections

最大连接数
Acccptor.java

// 线程的run方法。
public void run() {    while (!stopCalled) { // 如果我们已达到最大连接数,等待connectionLimitLatch.countUpOrAwait();// 接受来自服务器套接字的下一个传入连接socket = endpoint.serverSocketAccept()// socket.close 释放的时候 调用 connectionLimitLatch.countDown();          

1.3.3 MinSpareThread/MaxThread

工作线程最小/最大线程数
AbstractEndpoint.java

// tomcat 启动时
public void createExecutor() {internalExecutor = true;// 容量为Integer.MAX_VALUETaskQueue taskqueue = new TaskQueue();TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());// Tomcat扩展的线程池executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);taskqueue.setParent( (ThreadPoolExecutor) executor);
}

1.3.4 MaxKeepAliveRequests

长连接,在发送了maxKeepAliveRequests个请求后就会被服务器端主动断开连接。

在连接关闭之前可以进行流水线处理的最大HTTP请求数量。当设置为0或1时,禁用keep-alive和流水线处理。当设置为 -1 时,允许无限数量的流水线处理或 keep-alive 请求。

较大的 MaxKeepAliveRequests 值可能会导致服务器上的连接资源被长时间占用。根据具体需求,可以根据服务器的负载资源配置来调整 MaxKeepAliveRequests 的值,以平衡并发连接和服务器资源的利用率。

NioEndpoint.setSocketOptions socketWrapper.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());Http11Processor.service(SocketWrapperBase<?> socketWrapper)keepAlive = true;while(!getErrorState().isError() && keepAlive && !isAsync() && upgradeToken == null &&sendfileState == SendfileState.DONE && !protocol.isPaused()) {// 默认100  int maxKeepAliveRequests = protocol.getMaxKeepAliveRequests();if (maxKeepAliveRequests == 1) {keepAlive = false;} else if (maxKeepAliveRequests > 0 &&//    socketWrapper.decrementKeepAlive() <= 0) {keepAlive = false;}

1.3.5 ConnectionTimeout

连接的生存周期,当已经建立的连接,在 connectionTimeout 时间内,如果没有请求到来,服务端程序将会主动关闭该连接。

Tomcat 9中,ConnectionTimeout的默认值是20000毫秒,也就是20秒

如果该时间过长,服务器将要等待很长时间才会收到客户端的请求结果,从而导致服务效率低下。如果该时间过短,则可能会出现客户端在请求过程中网络慢等问题,而被服务器取消连接的情况。
由于某个交换机或者路由器出现了问题,导致某些post大文件的请求堆积在交换机或者路由器上,tomcat的工作线程一直拿不到完整的文件数据。

NioEndpoint.Poller#run()// Check for read timeoutif ((socketWrapper.interestOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {long delta = now - socketWrapper.getLastRead();long timeout = socketWrapper.getReadTimeout();if (timeout > 0 && delta > timeout) {readTimeout = true;}}// Check for write timeoutif (!readTimeout && (socketWrapper.interestOps() & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) {long delta = now - socketWrapper.getLastWrite();long timeout = socketWrapper.getWriteTimeout();if (timeout > 0 && delta > timeout) {writeTimeout = true;}}

1.3.6 KeepAliveTimeout

等待另一个 HTTP 请求的时间,然后关闭连接。当未设置时,将使用 connectionTimeout。当设置为 -1 时,将没有超时。

Http11InputBuffer.parseRequestLine// Read new bytes if needed
if (byteBuffer.position() >= byteBuffer.limit()) {if (keptAlive) {// 还没有读取任何请求数据,所以使用保持活动超时wrapper.setReadTimeout(keepAliveTimeout);}if (!fill(false)) {// A read is pending, so no longer in initial stateparsingRequestLinePhase = 1;return false;}//  至少已收到请求的一个字节 切换到套接字超时。wrapper.setReadTimeout(connectionTimeout);
}

1.4 核心内部线程

1.4.1 Acceptor

Acceptor:接收器,作用是接受scoket网络请求,并调用setSocketOptions()封装成为NioSocketWrapper,并注册到Pollerevents中。注意查看run方法org.apache.tomcat.util.net.Acceptor#run

public void run() {while (!stopCalled) {// 等待下一个请求进来socket = endpoint.serverSocketAccept();// 注册socket到Poller,生成PollerEvent事件endpoint.setSocketOptions(socket);// 向轮询器注册新创建的套接字- poller.register(socketWrapper);- (SynchronizedQueue(128))events.add(new PollerEvent(socketWrapper))  

1.4.2 Poller

Poller:轮询器,轮询是否有事件达到,有请求事件到达后,以NIO的处理方式,查询Selector取出所有请求,遍历每个请求的需求,分配给Executor线程池执行。查看org.apache.tomcat.util.net.NioEndpoint.Poller#run()

public void run() {while (true) {//查询selector取出所有请求事件Iterator<SelectionKey> iterator =keyCount > 0 ? selector.selectedKeys().iterator() : null;// 遍历就绪键的集合并调度任何活动事件。while (iterator != null && iterator.hasNext()) {SelectionKey sk = iterator.next();iterator.remove();NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();// 分配给Executor线程池执行处理请求keyif (socketWrapper != null) {processKey(sk, socketWrapper);- processSocket(socketWrapper, SocketEvent.OPEN_READ/SocketEvent.OPEN_WRITE)- executor.execute((Runnable)new SocketProcessor(socketWrapper,SocketEvent))}}

1.4.3 TomcatThreadPoolExecutor

真正执行连接读写操作的线程池,在JDK线程池的基础上进行了扩展优化。

AbstractEndpoint.java

public void createExecutor() {internalExecutor = true;TaskQueue taskqueue = new TaskQueue();TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());// tomcat自定义线程池executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);taskqueue.setParent( (ThreadPoolExecutor) executor);}

TomcatThreadPoolExecutor.java

// 与 java.util.concurrent.ThreadPoolExecutor 相同,但实现了更高效的getSubmittedCount()方法,用于正确处理工作队列。
// 如果未指定 RejectedExecutionHandler,将配置一个默认的,并且该处理程序将始终抛出 RejectedExecutionException
public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {// 已提交但尚未完成的任务数。这包括队列中的任务和已交给工作线程但后者尚未开始执行任务的任务。// 这个数字总是大于或等于getActiveCount() 。private final AtomicInteger submittedCount = new AtomicInteger(0);@Overrideprotected void afterExecute(Runnable r, Throwable t) {if (!(t instanceof StopPooledThreadException)) {submittedCount.decrementAndGet();}@Overridepublic void execute(Runnable command){// 提交任务的数量+1submittedCount.incrementAndGet();try {//  线程池内部方法,真正执行的方法。就是JDK线程池原生的方法。super.execute(command);} catch (RejectedExecutionException rx) {// 再次把被拒绝的任务放入到队列中。if (super.getQueue() instanceof TaskQueue) {final TaskQueue queue = (TaskQueue)super.getQueue();try {//强制的将任务放入到阻塞队列中if (!queue.force(command, timeout, unit)) {//放入失败,则继续抛出异常submittedCount.decrementAndGet();throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));}} catch (InterruptedException x) {//被中断也抛出异常submittedCount.decrementAndGet();throw new RejectedExecutionException(x);}} else {//不是这种队列,那么当任务满了之后,直接抛出去。submittedCount.decrementAndGet();throw rx;}}}
/*** 实现Tomcat特有逻辑的自定义队列*/
public class TaskQueue extends LinkedBlockingQueue<Runnable> {private static final long serialVersionUID = 1L;private transient volatile ThreadPoolExecutor parent = null;private static final int DEFAULT_FORCED_REMAINING_CAPACITY = -1;/*** 强制遗留的容量*/private int forcedRemainingCapacity = -1;/*** 队列的构建方法*/public TaskQueue() {}public TaskQueue(int capacity) {super(capacity);}public TaskQueue(Collection<? extends Runnable> c) {super(c);}/*** 设置核心变量*/public void setParent(ThreadPoolExecutor parent) {this.parent = parent;}/*** put:向阻塞队列填充元素,当阻塞队列满了之后,put时会被阻塞。* offer:向阻塞队列填充元素,当阻塞队列满了之后,offer会返回false。** @param o 当任务被拒绝后,继续强制的放入到线程池中* @return 向阻塞队列塞任务,当阻塞队列满了之后,offer会返回false。*/public boolean force(Runnable o) {if (parent == null || parent.isShutdown()) {throw new RejectedExecutionException("taskQueue.notRunning");}return super.offer(o);}/*** 带有阻塞时间的塞任务*/@Deprecatedpublic boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {if (parent == null || parent.isShutdown()) {throw new RejectedExecutionException("taskQueue.notRunning");}return super.offer(o, timeout, unit); //forces the item onto the queue, to be used if the task is rejected}/*** 当线程真正不够用时,优先是开启线程(直至最大线程),其次才是向队列填充任务。** @param runnable 任务* @return false 表示向队列中添加任务失败,*/@Overridepublic boolean offer(Runnable runnable) {if (parent == null) {return super.offer(runnable);}//若是达到最大线程数,进队列。if (parent.getPoolSize() == parent.getMaximumPoolSize()) {return super.offer(runnable);}//当前活跃线程为10个,但是只有8个任务在执行,于是,直接进队列。if (parent.getSubmittedCount() < (parent.getPoolSize())) {return super.offer(runnable);}//当前线程数小于最大线程数,那么直接返回false,去创建最大线程if (parent.getPoolSize() < parent.getMaximumPoolSize()) {return false;}//否则的话,将任务放入到队列中return super.offer(runnable);}/*** 获取任务*/@Overridepublic Runnable poll(long timeout, TimeUnit unit) throws InterruptedException {Runnable runnable = super.poll(timeout, unit);//取任务超时,会停止当前线程,来避免内存泄露if (runnable == null && parent != null) {parent.stopCurrentThreadIfNeeded();}return runnable;}/*** 阻塞式的获取任务,可能返回null。*/@Overridepublic Runnable take() throws InterruptedException {//当前线程应当被终止的情况下:if (parent != null && parent.currentThreadShouldBeStopped()) {long keepAliveTime = parent.getKeepAliveTime(TimeUnit.MILLISECONDS);return poll(keepAliveTime, TimeUnit.MILLISECONDS);}return super.take();}/*** 返回队列的剩余容量*/@Overridepublic int remainingCapacity() {if (forcedRemainingCapacity > DEFAULT_FORCED_REMAINING_CAPACITY) {return forcedRemainingCapacity;}return super.remainingCapacity();}/*** 强制设置剩余容量*/public void setForcedRemainingCapacity(int forcedRemainingCapacity) {this.forcedRemainingCapacity = forcedRemainingCapacity;}/*** 重置剩余容量*/void resetForcedRemainingCapacity() {this.forcedRemainingCapacity = DEFAULT_FORCED_REMAINING_CAPACITY;}
} 

1.5 测试

如下配置举例

server:port: 8080tomcat:accept-count: 3max-connections: 6threads:min-spare: 2max: 3

使用ss -nlt查看全连接队列容量。

ss -nltp
ss -nlt|grep 8080
- Recv-Q 表示客户端有多少个字节发送但还没有被服务端接收
- Send-Q 表示有多少个字节未被客户端接收

静默状态
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/106490.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

代理池在过程中一直运行

Hey&#xff0c;爬虫达人们&#xff01;在爬虫的过程中&#xff0c;要保持代理池的稳定性可不容易。今天就来和大家分享一些实用经验&#xff0c;教你如何让代理池在爬虫过程中一直运行&#xff01;方法简单易行&#xff0c;让你的爬虫工作更顺畅. 在进行爬虫工作时&#xff0…

在服务器上搭建Jenkins

目录 1.服务器要求 2.官方文档 3.在服务器上下载Jenkins 3.1 下载war包 3.2 将war包上传到服务器的一个目录下 3.3 启动jenkins 3.3.1 jdk版本升级 1&#xff09;下载jdk17 2&#xff09;解压到当前文件夹 3&#xff09;配置路径 4.jenkins配置 4.1 填写初始密码&a…

最新docker多系统安装技术

在Ubuntu操作系统中安装Docker 在Ubuntu操作系统中安装Docker的步骤如下。 1&#xff0e;卸载旧版本Docker 卸载旧版本Docker的命令如下&#xff1a; $ sudo apt-get remove docker docker-engine docker.io 2&#xff0e;使用脚本自动安装 在测试或开发环境中&#xff0…

MIUI 欧版刷机教程(操作篇)

文章目录 0 前置条件1 下载ROM包2 确定刷机方式3 线刷教程4 卡刷教程使用系统更新使用 TWRP 问题汇总 0 前置条件 必须先解除手机的 bootloader 锁。详细教程参见官网&#xff1a;申请解锁小米手机 (miui.com)。 1 下载ROM包 在 MIUI EU 官方论坛&#xff08;需要科学上网&a…

器件介绍TMP1826NGRR、TMP1826DGKR、TMP1827NGRR、TMP1075NDRLR数字温度传感器

一、TMP1826 具有 2Kb EEPROM 的 1-Wire、0.2C 精度温度传感器 器件介绍 TMP1826 是一款高精度、1-Wire 兼容的数字输出温度传感器&#xff0c;具有集成的 2Kb EEPROM 和 –55C 至150C 的宽工作温度范围。TMP1826 在 10C 至45C 的温度范围内提供 0.1C&#xff08;典型值&#…

Mac安装Docker

简简单单 目录 前言 一、安装步骤 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、安装步骤 第一种方式&#xff0c;直接去官网去下载 Docker官网 下载我勾出来的那个版本 针对于M1&#xff0c;M2用户 下载完之后 安装拖入Application中 打开…

thinkphp安装workman

需要加版本&#xff0c;版本太高了不行 composer require topthink/think-worker1.0.*

怎么查看小程序中的会员信息

商家通过查看会员信息&#xff0c;可以更好地了解用户&#xff0c;并为他们提供更个性化的服务和推荐。接下来&#xff0c;就将介绍如何查看会员信息。 商家在管理员后台->会员管理处&#xff0c;可以查看到会员列表。支持搜索会员的卡号、手机号和等级。还支持批量删除会员…

arm: day8

1.中断实验&#xff1a;按键控制led灯 流程&#xff1a; key.h /*************************************************************************> File Name: include/key.h> Created Time: 2023年08月21日 星期一 17时03分20秒***************************************…

Android 之 WindowManager (窗口管理服务)

本节引言&#xff1a; 本节给大家带来的Android给我们提供的系统服务中的——WindowManager(窗口管理服务)&#xff0c; 它是显示View的最底层&#xff0c;Toast&#xff0c;Activity&#xff0c;Dialog的底层都用到了这个WindowManager&#xff0c; 他是全局的&#xff01;该类…

c语言每日一练(11)

前言&#xff1a;每日一练系列&#xff0c;每一期都包含5道选择题&#xff0c;2道编程题&#xff0c;博主会尽可能详细地进行讲解&#xff0c;令初学者也能听的清晰。每日一练系列会持续更新&#xff0c;暑假时三天之内必有一更&#xff0c;到了开学之后&#xff0c;将看学业情…

MySQL 日志

目录 一、日志概述 二、二进制日志 1、开启二进制日志 2、查看二进制文件 3、删除二进制日志文件 4、恢复二进制日志 5、暂时停止二进制日志功能 三、错误日志 1、启动和设置错误日志 2、查看错误日志 3、删除错误日志 四、通用查询日志 五、慢查询日志 一、日志概…

LeetCode 138.复制带随机指针的链表

文章目录 &#x1f4a1;题目分析&#x1f4a1;解题思路&#x1f6a9;步骤一&#xff1a;拷贝节点插入到原节点的后面&#x1f369;步骤一代码 &#x1f6a9;步骤二&#xff1a;控制拷贝节点的random进行连接&#x1f369;步骤二代码 &#x1f6a9;步骤三&#xff1a;拷贝节点解…

【小沐学Unity3d】3ds Max 骨骼动画制作(Mixamo )

文章目录 1、简介2、基本操作2.1 Characters&#xff08;角色&#xff09;2.2 Animations&#xff08;动画&#xff09; 3、常见问题FAQ3.1 问题一3.2 问题二 结语 1、简介 官网地址&#xff1a; https://www.mixamo.com/#/ 使用 Mixamo 上传和装配 Adobe Fuse CC 3D 人物、自…

STM32 CAN 波特率计算分析

这里写目录标题 前言时钟分析时钟元到BIT 前言 CubeMX中配置CAN波特率的这个界面刚用的时候觉得非常难用&#xff0c;怎么都配置不到想要的波特率。接下来为大家做一下简单的分析。 时钟分析 STM32F4的CAN时钟来自APB1 在如下界面配置&#xff0c;最好配置为1个整一点的数。…

积跬步至千里 || 数学基础、算法与编程

数学基础、算法与编程 1. BAP 技能 BAP 技能是指基础(Basic)、算法(Algorithm)和编程(Programm)三种基本技能的深度融合。理工科以数学、算法与编程为根基&#xff0c;这三个相辅相成又各有区别。 &#xff08;1&#xff09;数学以线性代数为主要研究工具和部分微积分技术为手…

【QT】绘制旋转等待

很高兴在雪易的CSDN遇见你 ,给你糖糖 欢迎大家加入雪易社区-CSDN社区云 前言 程序中经常会遇到耗时的操作,需要提供等待的窗口,防止用户多次点击造成卡顿等问题。本文分享旋转等待技术,希望对各位小伙伴有所帮助!结果如下:

记录帖子-开发过程中遇到的问题和感悟记录

记录帖子1:2023年08月25日结束开发 前端规范 1.关于计算属性 计算属性关联的变量不可以过多&#xff0c;同时要保证关联的变量在代码中的变换次数不可过多 例如这段代码的this.options内部数据变化过多&#xff0c;导致计算属性调用次数过多导致页面卡顿 2.关于自定义v-mod…

求生之路2社区服务器sourcemod安装配置搭建教程centos

求生之路2社区服务器sourcemod安装配置搭建教程centos 大家好我是艾西&#xff0c;通过上文我们已经成功搭建了求生之路2的服务端。但是这个服务端是纯净的服务端&#xff0c;就是那种最纯粹的原版。如果想要实现插件、sm开头的命令等功能&#xff0c;需要安装这个sourcemod。…

【golang】for语句和switch语句

使用携带range子句的for语句时需要注意哪些细节&#xff1f; numbers1 : []int{1, 2, 3, 4, 5, 6} for i : range numbers1 {if i 3 {numbers1[i] | i} } fmt.Println(numbers1)这段代码执行后会打印出什么内容&#xff1f; 答案&#xff1a;[1 2 3 7 5 6] 当for语句被执行…