每個Spring Boot版本和內置容器不同,結果也不同,這裡以Spring Boot 2.7.10版本 + 內置Tomcat容器舉例。 概序 在SpringBoot2.7.10版本中內置Tomcat版本是9.0.73,SpringBoot內置Tomcat的預設設置如下: Tomcat的連接等待隊列 ...
每個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: 8192
threads:
# 工作線程的最小數量,初始化時創建的線程數
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後)會出現請求連接超時。
推薦一個開源免費的 Spring Boot 實戰項目:
TCP的3次握手4次揮手
時序圖
核心參數
Spring Boot 基礎就不介紹了,推薦看這個實戰項目:
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());
MaxConnections
Acccptor.java
// 線程的run方法。
public void run() {
while (!stopCalled) {
// 如果我們已達到最大連接數,等待
connectionLimitLatch.countUpOrAwait();
// 接受來自伺服器套接字的下一個傳入連接
socket = endpoint.serverSocketAccept()
// socket.close 釋放的時候 調用 connectionLimitLatch.countDown();
MinSpareThread/MaxThread
AbstractEndpoint.java
// tomcat 啟動時
public void createExecutor() {
internalExecutor = true;
// 容量為Integer.MAX_VALUE
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);
}
「重點重點重點」
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;
} else if (maxKeepAliveRequests > 0 &&
//
socketWrapper.decrementKeepAlive() <= 0) {
keepAlive = false;
}
ConnectionTimeout
連接的生存周期,當已經建立的連接,在connectionTimeout時間內,如果沒有請求到來,服務端程式將會主動關閉該連接。
- 在Tomcat 9中,ConnectionTimeout的預設值是20000毫秒,也就是20秒。
- 如果該時間過長,伺服器將要等待很長時間才會收到客戶端的請求結果,從而導致服務效率低下。如果該時間過短,則可能會出現客戶端在請求過程中網路慢等問題,而被伺服器取消連接的情況。
- 由於某個交換機或者路由器出現了問題,導致某些post大文件的請求堆積在交換機或者路由器上,tomcat的工作線程一直拿不到完整的文件數據。
NioEndpoint.Poller#run()
// Check for read timeout
if ((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 timeout
if (!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 needed
if (byteBuffer.position() >= byteBuffer.limit()) {
if (keptAlive) {
// 還沒有讀取任何請求數據,所以使用保持活動超時
wrapper.setReadTimeout(keepAliveTimeout);
}
if (!fill(false)) {
// A read is pending, so no longer in initial state
parsingRequestLinePhase = 1;
return false;
}
// 至少已收到請求的一個位元組 切換到套接字超時。
wrapper.setReadTimeout(connectionTimeout);
}
內部線程
Acceptor
Acceptor: 接收器,作用是接受scoket網路請求,並調用setSocketOptions()封裝成為NioSocketWrapper,並註冊到Poller的events中。註意查看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))
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線程池執行處理請求key
if (socketWrapper != null) {
processKey(sk, socketWrapper);
- processSocket(socketWrapper, SocketEvent.OPEN_READ/SocketEvent.OPEN_WRITE)
- executor.execute((Runnable)new SocketProcessor(socketWrapper,SocketEvent))
}
}
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);
@Override
protected void afterExecute(Runnable r, Throwable t) {
if (!(t instanceof StopPooledThreadException)) {
submittedCount.decrementAndGet();
}
@Override
public void execute(Runnable command){
// 提交任務的數量+1
submittedCount.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);
}
/**
* 帶有阻塞時間的塞任務
*/
@Deprecated
public 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 表示向隊列中添加任務失敗,
*/
@Override
public 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);
}
/**
* 獲取任務
*/
@Override
public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException {
Runnable runnable = super.poll(timeout, unit);
//取任務超時,會停止當前線程,來避免記憶體泄露
if (runnable == null && parent != null) {
parent.stopCurrentThreadIfNeeded();
}
return runnable;
}
/**
* 阻塞式的獲取任務,可能返回null。
*/
@Override
public Runnable take() throws InterruptedException {
//當前線程應當被終止的情況下:
if (parent != null && parent.currentThreadShouldBeStopped()) {
long keepAliveTime = parent.getKeepAliveTime(TimeUnit.MILLISECONDS);
return poll(keepAliveTime, TimeUnit.MILLISECONDS);
}
return super.take();
}
/**
* 返回隊列的剩餘容量
*/
@Override
public 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;
}
}
JDK線程池架構圖
Tomcat線程架構
測試
如下配置舉例
server:
port: 8080
tomcat:
accept-count: 3
max-connections: 6
threads:
min-spare: 2
max: 3
使用ss -nlt查看全連接隊列容量。
ss -nltp
ss -nlt|grep 8080
- Recv-Q表示(acceptCount)全連接隊列目前長度
- Send-Q表示(acceptCount)全連接隊列的容量。
「靜默狀態」
「6個併發連接」
結果同上
「9個併發連接」
「10個併發連接」
「11個併發連接」
結果同上
使用ss -nt查看連接狀態。
ss -ntp
ss -nt|grep 8080
- Recv-Q表示客戶端有多少個位元組發送但還沒有被服務端接收
- Send-Q就表示為有多少個位元組未被客戶端接收。
「靜默狀態」
「6個併發連接」
「9個併發連接」
「補充個netstat」
「10個併發連接」
結果同上,隊列中多加了個
「11個併發連接」
超出連接後,會有個連接一直停留在SYN_RECV狀態,不會完成3次握手了。
超出連接後客戶端一直就停留在SYN-SENT狀態,服務端不會再發送SYN+ACK,直到客戶端超時(20s內核控制)斷開。
客戶端請求超時(需要等待一定時間(20s))。
這裡如果客戶端設置了超時時間,要和服務端3次握手超時時間對比小的為準。
「12個併發連接」
參考鏈接:
版權聲明:本文為CSDN博主「lakernote」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。原文鏈接:https://blog.csdn.net/abu935009066/article/details/130957301
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2022最新版)
4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!
覺得不錯,別忘了隨手點贊+轉發哦!