文章目录
每个Spring Boot版本和内置容器不同,结果也不同,这里以Spring Boot 2.7.10版本 + 内置Tomcat容器举例。
概序
在SpringBoot2.7.10版本中内置Tomcat版本是9.0.73,SpringBoot内置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
架构图
当连接数大于maxConnections+acceptCount + 1时,新来的请求不会收到服务器拒绝连接响应,而是不会和新的请求进行3次握手建立连接,一段时间后(客户端的超时时间或者Tomcat的20s后)会出现请求连接超时。
TCP的3次握手4次挥手
时序图
核心参数
AcceptCount
全连接队列容量,等同于
backlog
参数,与
Linux
中的系统参数
somaxconn
取较小值,
Windows
中没有系统参数。
NioEndpoint.java
serverSock =ServerSocketChannel.open();
socketProperties.setProperties(serverSock.socket());InetSocketAddress addr =newInetSocketAddress(getAddress(),getPortWithOffset());// 这里
serverSock.socket().bind(addr,getAcceptCount());
MaxConnections
Acccptor.java
// 线程的run方法。publicvoidrun(){while(!stopCalled){// 如果我们已达到最大连接数,等待
connectionLimitLatch.countUpOrAwait();// 接受来自服务器套接字的下一个传入连接
socket = endpoint.serverSocketAccept()// socket.close 释放的时候 调用 connectionLimitLatch.countDown();
MinSpareThread/MaxThread
AbstractEndpoint.java
// tomcat 启动时publicvoidcreateExecutor(){
internalExecutor =true;// 容量为Integer.MAX_VALUETaskQueue taskqueue =newTaskQueue();TaskThreadFactory tf =newTaskThreadFactory(getName()+"-exec-", daemon,getThreadPriority());// Tomcat扩展的线程池
executor =newThreadPoolExecutor(getMinSpareThreads(),getMaxThreads(),60,TimeUnit.SECONDS,taskqueue, tf);
taskqueue.setParent((ThreadPoolExecutor) executor);}
重点重点重点
Tomcat扩展了线程池增强了功能。
- JDK线程池流程:minThreads --> queue --> maxThreads --> Exception
- Tomcat增强后: minThreads --> maxThreads --> queue --> Exception
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;}elseif(maxKeepAliveRequests >0&&//
socketWrapper.decrementKeepAlive()<=0){
keepAlive =false;}
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;}}
KeepAliveTimeout
等待另一个 HTTP 请求的时间,然后关闭连接。当未设置时,将使用 connectionTimeout。当设置为 -1 时,将没有超时。
Http11InputBuffer.parseRequestLine
// Read new bytes if neededif(byteBuffer.position()>= byteBuffer.limit()){if(keptAlive){// 还没有读取任何请求数据,所以使用保持活动超时
wrapper.setReadTimeout(keepAliveTimeout);}if(!fill(false)){// A read is pending, so no longer in initial state
parsingRequestLinePhase =1;returnfalse;}// 至少已收到请求的一个字节 切换到套接字超时。
wrapper.setReadTimeout(connectionTimeout);}
内部线程
Acceptor
Acceptor
: 接收器,作用是接受scoket网络请求,并调用
setSocketOptions()
封装成为
NioSocketWrapper
,并注册到Poller的events中。注意查看run方法
org.apache.tomcat.util.net.Acceptor#run
publicvoidrun(){while(!stopCalled){// 等待下一个请求进来
socket = endpoint.serverSocketAccept();// 注册socket到Poller,生成PollerEvent事件
endpoint.setSocketOptions(socket);// 向轮询器注册新创建的套接字- poller.register(socketWrapper);-(SynchronizedQueue(128))events.add(newPollerEvent(socketWrapper))
Poller
Poller
:轮询器,轮询是否有事件达到,有请求事件到达后,以NIO的处理方式,查询Selector取出所有请求,遍历每个请求的需求,分配给Executor线程池执行。查看
org.apache.tomcat.util.net.NioEndpoint.Poller#run()
publicvoidrun(){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)newSocketProcessor(socketWrapper,SocketEvent))}}
TomcatThreadPoolExecutor
真正执行连接读写操作的线程池,在JDK线程池的基础上进行了扩展优化。
AbstractEndpoint.java
publicvoidcreateExecutor(){
internalExecutor =true;TaskQueue taskqueue =newTaskQueue();TaskThreadFactory tf =newTaskThreadFactory(getName()+"-exec-", daemon,getThreadPriority());// tomcat自定义线程池
executor =newThreadPoolExecutor(getMinSpareThreads(),getMaxThreads(),60,TimeUnit.SECONDS,taskqueue, tf);
taskqueue.setParent((ThreadPoolExecutor) executor);}
TomcatThreadPoolExecutor.java
// 与 java.util.concurrent.ThreadPoolExecutor 相同,但实现了更高效的getSubmittedCount()方法,用于正确处理工作队列。// 如果未指定 RejectedExecutionHandler,将配置一个默认的,并且该处理程序将始终抛出 RejectedExecutionExceptionpublicclassThreadPoolExecutorextendsjava.util.concurrent.ThreadPoolExecutor{// 已提交但尚未完成的任务数。这包括队列中的任务和已交给工作线程但后者尚未开始执行任务的任务。// 这个数字总是大于或等于getActiveCount() 。privatefinalAtomicInteger submittedCount =newAtomicInteger(0);@OverrideprotectedvoidafterExecute(Runnable r,Throwable t){if(!(t instanceofStopPooledThreadException)){
submittedCount.decrementAndGet();}@Overridepublicvoidexecute(Runnable command){// 提交任务的数量+1
submittedCount.incrementAndGet();try{// 线程池内部方法,真正执行的方法。就是JDK线程池原生的方法。super.execute(command);}catch(RejectedExecutionException rx){// 再次把被拒绝的任务放入到队列中。if(super.getQueue()instanceofTaskQueue){finalTaskQueue queue =(TaskQueue)super.getQueue();try{//强制的将任务放入到阻塞队列中if(!queue.force(command, timeout, unit)){//放入失败,则继续抛出异常
submittedCount.decrementAndGet();thrownewRejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));}}catch(InterruptedException x){//被中断也抛出异常
submittedCount.decrementAndGet();thrownewRejectedExecutionException(x);}}else{//不是这种队列,那么当任务满了之后,直接抛出去。
submittedCount.decrementAndGet();throw rx;}}}
/**
* 实现Tomcat特有逻辑的自定义队列
*/publicclassTaskQueueextendsLinkedBlockingQueue<Runnable>{privatestaticfinallong serialVersionUID =1L;privatetransientvolatileThreadPoolExecutor parent =null;privatestaticfinalint DEFAULT_FORCED_REMAINING_CAPACITY =-1;/**
* 强制遗留的容量
*/privateint forcedRemainingCapacity =-1;/**
* 队列的构建方法
*/publicTaskQueue(){}publicTaskQueue(int capacity){super(capacity);}publicTaskQueue(Collection<?extendsRunnable> c){super(c);}/**
* 设置核心变量
*/publicvoidsetParent(ThreadPoolExecutor parent){this.parent = parent;}/**
* put:向阻塞队列填充元素,当阻塞队列满了之后,put时会被阻塞。
* offer:向阻塞队列填充元素,当阻塞队列满了之后,offer会返回false。
*
* @param o 当任务被拒绝后,继续强制的放入到线程池中
* @return 向阻塞队列塞任务,当阻塞队列满了之后,offer会返回false。
*/publicbooleanforce(Runnable o){if(parent ==null|| parent.isShutdown()){thrownewRejectedExecutionException("taskQueue.notRunning");}returnsuper.offer(o);}/**
* 带有阻塞时间的塞任务
*/@Deprecatedpublicbooleanforce(Runnable o,long timeout,TimeUnit unit)throwsInterruptedException{if(parent ==null|| parent.isShutdown()){thrownewRejectedExecutionException("taskQueue.notRunning");}returnsuper.offer(o, timeout, unit);//forces the item onto the queue, to be used if the task is rejected}/**
* 当线程真正不够用时,优先是开启线程(直至最大线程),其次才是向队列填充任务。
*
* @param runnable 任务
* @return false 表示向队列中添加任务失败,
*/@Overridepublicbooleanoffer(Runnable runnable){if(parent ==null){returnsuper.offer(runnable);}//若是达到最大线程数,进队列。if(parent.getPoolSize()== parent.getMaximumPoolSize()){returnsuper.offer(runnable);}//当前活跃线程为10个,但是只有8个任务在执行,于是,直接进队列。if(parent.getSubmittedCount()<(parent.getPoolSize())){returnsuper.offer(runnable);}//当前线程数小于最大线程数,那么直接返回false,去创建最大线程if(parent.getPoolSize()< parent.getMaximumPoolSize()){returnfalse;}//否则的话,将任务放入到队列中returnsuper.offer(runnable);}/**
* 获取任务
*/@OverridepublicRunnablepoll(long timeout,TimeUnit unit)throwsInterruptedException{Runnable runnable =super.poll(timeout, unit);//取任务超时,会停止当前线程,来避免内存泄露if(runnable ==null&& parent !=null){
parent.stopCurrentThreadIfNeeded();}return runnable;}/**
* 阻塞式的获取任务,可能返回null。
*/@OverridepublicRunnabletake()throwsInterruptedException{//当前线程应当被终止的情况下:if(parent !=null&& parent.currentThreadShouldBeStopped()){long keepAliveTime = parent.getKeepAliveTime(TimeUnit.MILLISECONDS);returnpoll(keepAliveTime,TimeUnit.MILLISECONDS);}returnsuper.take();}/**
* 返回队列的剩余容量
*/@OverridepublicintremainingCapacity(){if(forcedRemainingCapacity > DEFAULT_FORCED_REMAINING_CAPACITY){return forcedRemainingCapacity;}returnsuper.remainingCapacity();}/**
* 强制设置剩余容量
*/publicvoidsetForcedRemainingCapacity(int forcedRemainingCapacity){this.forcedRemainingCapacity = forcedRemainingCapacity;}/**
* 重置剩余容量
*/voidresetForcedRemainingCapacity(){this.forcedRemainingCapacity = DEFAULT_FORCED_REMAINING_CAPACITY;}}
JDK线程池架构图
Tomcat线程架构
测试
如下配置举例
server:port:8080tomcat:accept-count:3max-connections:6threads:min-spare:2max:3
使用
ss -nlt
查看全连接队列容量。
ss -nltp
ss -nlt|grep8080
- Recv-Q表示(acceptCount)全连接队列目前长度
- Send-Q表示(acceptCount)全连接队列的容量。
静默状态
6个并发连接
结果同上
9个并发连接
10个并发连接
11个并发连接
结果同上
使用
ss -nt
查看连接状态。
ss -ntp
ss -nt|grep8080
- Recv-Q表示客户端有多少个字节发送但还没有被服务端接收
- Send-Q就表示为有多少个字节未被客户端接收。
静默状态
6个并发连接
9个并发连接
补充个netstat
10个并发连接
结果同上,队列中多加了个
11个并发连接
超出连接后,会有个连接一直停留在
SYN_RECV
状态,不会完成3次握手了。
超出连接后客户端一直就停留在
SYN-SENT
状态,服务端不会再发送
SYN+ACK
,直到客户端超时(20s内核控制)断开。
客户端请求超时(需要等待一定时间(20s))。
这里如果客户端设置了超时时间,要和服务端3次握手超时时间对比小的为准。
12个并发连接
参考
版权归原作者 lakernote 所有, 如有侵权,请联系我们删除。