![]()
很多性能問題,第一眼看起來都像 JVM 的問題。
CPU 沒打滿,吞吐量上不去,延遲開始抖,錯誤率慢慢冒頭。這個時候,團隊最容易做的一件事,就是打開 JVM 參數開始調:堆內存是不是小了?GC 選型是不是不對?容器內存限制是不是卡住了?線程棧要不要改?G1、ZGC 要不要對比一下?
這些動作看起來很專業。
但有些時候,真正把系統卡住的,不是 JVM。
而是 Spring Boot Web 服務里一個你可能從來沒認真看過的默認值。
server:Tomcat:threads:max: 200如果你的項目還是舊版本 Spring Boot,可能見過的是:
server.tomcat.max-threads=200這里要注意一下:Spring Boot 2.3 之后,更推薦的配置名是 server.tomcat.threads.max。當前 Spring Boot 官方配置里也能看到,這個值的默認值就是 200,并且如果啟用了虛擬線程,這個配置不會生效。(Home)
Tomcat 自己的文檔也說得很直接:每個非異步請求在處理期間都需要占用一個線程,maxThreads 決定了最多能創建多少個請求處理線程;如果沒有顯式配置,默認就是 200。(tomcat.apache.org)
這行配置平時很不起眼。
但在某些業務里,它就是吞吐量的天花板。
![]()
最容易誤判的地方:CPU 沒滿,不代表系統沒到瓶頸
壓測時最怕看到一種圖。
QPS 卡住了,比如一直在 18k 左右上下浮動。
CPU 卻只有 50% 多。
GC 很干凈。
內存也不緊張。
數據庫連接池沒有爆。
Redis 沒有慢。
下游接口也沒有大面積超時。
看起來所有指標都不像“資源耗盡”。但請求就是上不去,一加壓,延遲開始升,p99 越來越難看。
這時候很多人會下意識懷疑 JVM:
是不是堆太小?
是不是 GC 參數不合適?
是不是容器 CPU quota 有問題?
是不是 JIT 還沒熱起來?
于是壓測一輪,調一輪。
結果可能是這樣:
配置
最大吞吐量
CPU
p99
默認 JVM
18k req/s
54%
310ms
調整 G1 參數
19k req/s
58%
295ms
嘗試 ZGC
20k req/s
60%
290ms
看起來有提升,但很有限。
這種提升不是結構性變化,更像是在邊角上刮了一層油。
真正的問題是:CPU 明明還有余量,為什么請求跑不起來?
答案通常藏在線程里。
同步 Web 服務里,線程池就是并發預算
很多 Spring Boot 項目用的是典型的同步 Servlet 模型:
客戶端網關 / NginxSpring Boot 服務Redis / DB / 下游 HTTP 接口一個請求進來,Tomcat 分配一個工作線程。
這個線程執行認證、查緩存、查數據庫、調下游接口、組裝響應。
在純 CPU 計算場景里,線程主要是在干活。
但在企業系統里,絕大多數請求不是純計算。
它可能有:
權限校驗:3msRedis 查詢:5ms數據庫查詢:10ms下游 HTTP 調用:40msJSON 組裝:5ms總耗時 60 多毫秒,其中真正占 CPU 的時間可能只有十幾毫秒,剩下大量時間都在等網絡、等 IO、等下游返回。
這時候線程不是 CPU 的同義詞。
線程更像一張“入場券”。
你有 200 個線程,就意味著同一時刻最多只有 200 個請求真正進入業務處理流程。其他請求不是 CPU 沒能力處理,而是排在門口,等工作線程空出來。
所以你會看到一個很奇怪的現象:
CPU 只有 50% 多。
GC 很正常。
機器好像還很閑。
但吞吐量就是上不去。
不是服務器沒力氣,而是入口的通道太窄。
一個最該看的指標:busy threads
![]()
很多團隊壓測時盯著 CPU、內存、GC、數據庫、Redis,卻很少第一時間去看 Tomcat 線程池。
如果你打開 Actuator、JMX 或監控系統,重點看這幾個指標:
tomcat.threads.currenttomcat.threads.busytomcat.threads.config.maxtomcat.connections.currenttomcat.connections.config.max一旦你發現:
tomcat.threads.busy ≈ tomcat.threads.config.max并且 CPU 還沒打滿,那就要非常警惕。
這說明請求線程已經耗盡了。
服務不是算不過來,而是沒有足夠的線程去接住更多請求。
線程 dump 里也經常能看到類似狀態:
"http-nio-8080-exec-183" WAITING"http-nio-8080-exec-184" WAITING"http-nio-8080-exec-185" WAITING這些線程不一定在死鎖,也不一定在 CPU 忙等。
它們可能只是卡在下游 HTTP 調用、數據庫 IO、Redis 訪問、遠程服務響應上。
問題就出在這里。
一個同步請求占住一個工作線程,只要請求生命周期里有大量等待,線程池大小就會直接決定系統的并發上限。
改一個值,吞吐量可能立刻變樣
![]()
假設某次壓測中,服務默認是 200 個 Tomcat 工作線程。
調 JVM 沒有明顯效果。
把線程數逐步調大之后,結果可能會變成這樣:
Tomcat 最大線程數
最大吞吐量
CPU
p99
200
18k req/s
54%
310ms
350
23k req/s
71%
270ms
500
25.6k req/s
82%
255ms
吞吐量從 18k 到 25.6k,提升大約 42%。
JVM 沒換。
機器沒換。
代碼沒大改。
只是把入口處能同時干活的線程數放大了。
配置大概是這樣:
server:tomcat:threads:max: 500min-spare: 50accept-count: 300max-connections: 10000這里別只改 threads.max。
accept-count 也要一起看。Spring Boot 官方配置說明里,server.tomcat.accept-count 是當所有請求處理線程都在使用時,傳入連接請求的最大隊列長度,默認值是 100。(Home)
max-connections 控制 Tomcat 能接受和處理的最大連接數,當前 Spring Boot 文檔里的默認值是 8192。(Home)
也就是說,Tomcat 的并發能力不是只看一個 maxThreads,而是幾個參數一起決定的:
threads.max 決定最多多少請求線程參與處理accept-count 決定線程滿了之后,門口最多排多少連接max-connections 決定最多接受多少連接connection-timeout / keep-alive-timeout 影響連接占用時間很多線上問題,壞就壞在只調了一個參數。
線程放大了,連接和隊列沒看。
或者連接放大了,線程沒放大。
再或者線程和連接都放大了,下游連接池卻還是原來的幾十個。
這樣調出來的系統,只是把瓶頸從 A 挪到了 B。
但這不是讓你無腦把線程數改成 1000
這里有個很重要的邊界。
Tomcat 線程數不是越大越好。
線程多了,會帶來幾個代價:
線程棧內存增加上下文切換增加調度開銷增加鎖競爭變復雜尾延遲可能變差如果你的服務是 CPU 密集型,比如大量加密、壓縮、圖片處理、復雜計算,把線程數從 200 調到 800,未必會提升吞吐量,甚至可能讓系統更慢。
因為 CPU 就那么多核,線程太多只是讓操作系統忙著切來切去。
所以這類配置調整,一定要先判斷業務模型。
如果你的請求大部分時間在等 IO,比如:
調用下游接口訪問 Redis訪問數據庫調用第三方 API等待文件服務等待消息中間件響應適當增加 Tomcat 工作線程,可能明顯提升吞吐。
如果你的請求大部分時間都在 CPU 上跑,比如:
大批量 JSON 解析復雜規則計算圖片處理報表生成加解密壓縮解壓大對象排序盲目加線程就很危險。
性能優化最怕的不是參數不會調,而是不知道自己到底是什么類型的負載。
更好的排查順序,不該一上來就 JVM
我現在更傾向于用這條鏈路排查 Spring Boot 吞吐瓶頸。
先看容量圖。
QPS 到哪里開始平臺化?
CPU 是否同步升高?
p95、p99 是否開始拐頭?
錯誤率是在吞吐平臺之后出現,還是一開始就出現?
再看線程。
Tomcat busy threads 是否貼近 max?
業務線程池、異步線程池、數據庫連接池、HTTP 客戶端連接池是否打滿?
有沒有大量線程卡在 WAITING / TIMED_WAITING?
再看下游。
下游接口 RT 是否變長?
連接池有沒有等待?
Redis、DB、MQ 是否出現排隊?
再回來看 JVM。
GC 暫停是否異常?
堆是否持續上漲?
對象分配速率是否過高?
JIT、Safepoint、容器 CPU quota 有沒有異常?
這樣排查,不是說 JVM 不重要。
而是不要把 JVM 當成所有性能問題的垃圾桶。
很多系統不是 JVM 跑得不好,而是應用層并發預算太小。
可以加一個小工具,啟動時把關鍵配置打印出來
企業項目里,我建議把這類“默認值”顯式化。
不要讓生產系統靠框架默認值裸奔。
可以在配置文件里明確寫出來:
server:tomcat:threads:max: 300min-spare: 30accept-count: 200max-connections: 10000connection-timeout: 20skeep-alive-timeout: 15s然后啟動時打印關鍵參數,至少讓團隊知道當前服務到底跑在什么容量邊界上。
示例:
@Component@RequiredArgsConstructorpublic class TomcatConfigPrinter implements ApplicationRunner {private final ServerProperties serverProperties;@Overridepublic void run(ApplicationArguments args) {var tomcat = serverProperties.getTomcat();System.out.println("===== Tomcat Runtime Config =====");System.out.println("maxThreads = " + tomcat.getThreads().getMax());System.out.println("minSpareThreads= " + tomcat.getThreads().getMinSpare());System.out.println("acceptCount = " + tomcat.getAcceptCount());System.out.println("maxConnections = " + tomcat.getMaxConnections());System.out.println("connectionTimeout = " + serverProperties.getConnectionTimeout());System.out.println("=================================");這段代碼不一定非要上生產。
但在壓測環境里,它很有用。
因為很多性能事故,不是參數配錯,而是沒人知道它現在到底是多少。
真正應該沉淀的是容量基線
一次壓測發現 server.tomcat.threads.max=200 不夠,并不代表所有服務都要改成 500。
更靠譜的做法,是給每類服務沉淀容量基線。
比如:
服務類型
請求特點
建議關注點
純查詢接口
DB / Redis IO 多
Tomcat 線程、DB 連接池、慢 SQL
聚合接口
下游 HTTP 調用多
Tomcat 線程、HTTP 連接池、超時、熔斷
報表接口
CPU + DB 都重
慢 SQL、分頁、異步化、限流
文件上傳
帶寬和磁盤 IO 重
請求大小、連接占用、上傳超時
AI 調用接口
外部模型 RT 長
異步化、隊列、超時、并發隔離
對一個同步 Spring Boot 服務來說,吞吐量不是單靠 CPU 決定的。
它取決于整條鏈路中最窄的那一段。
有時候是數據庫連接池。
有時候是 HTTP 客戶端連接池。
有時候是 Redis。
有時候是下游接口。
也有時候,就是 Tomcat 那 200 個線程。
到了 JDK 21 以后,還要重新看虛擬線程
如果項目已經在用 JDK 21,并且開啟了 Spring Boot 對虛擬線程的支持,那這個問題又會變得不一樣。
因為 Spring Boot 當前文檔已經明確提示:server.tomcat.threads.max 在啟用虛擬線程時不會生效。(Home)
這也是為什么不能機械照搬調參經驗。
傳統平臺線程模型下,Tomcat 工作線程是很重要的并發預算。
但到了虛擬線程模型里,瓶頸可能會轉移到別的地方:
數據庫連接池大小HTTP 客戶端連接池下游限流鎖競爭阻塞驅動是否真的適配虛擬線程線程本地變量濫用虛擬線程可以緩解“平臺線程不夠用”的問題,但不會讓數據庫連接、下游接口、外部 API 免費變快。
它只是把并發的成本降低了,不是把系統邊界抹掉了。
![]()
這次踩坑真正值錢的地方
這類問題最容易讓人尷尬。
因為它不是高深問題。
不是 JVM 黑科技。
不是復雜的 GC 日志分析。
不是某個特別刁鉆的內核參數。
它就是一個普通配置。
可工程里最貴的,往往不是復雜東西沒人會,而是簡單東西沒人查。
我們太容易相信框架默認值。
默認值看起來像是“官方幫我選好了”。
但框架默認值追求的是普適、安全、不要太激進,而不是為你的業務流量、請求模型、下游等待時間、機器規格做過專門優化。
一個后臺管理系統,200 個線程可能綽綽有余。
一個內部 CRM,200 個線程可能也沒問題。
但一個高并發網關后面的聚合服務,每個請求還要等一兩個下游接口,200 個線程很可能就是天花板。
真正的性能優化,不是看到 CPU 沒滿就去調 JVM。
而是先問一句:
這個請求,到底卡在哪里?
如果線程都在等,那就看線程池。
如果連接都在等,那就看連接池。
如果數據庫在等,那就看 SQL 和索引。
如果下游在等,那就看超時、熔斷和隔離。
別把所有問題都推給 JVM。
有時候 JVM 只是安靜地坐在那里,等你給它足夠的請求線程。
而那道門,可能已經被一個默認的 200,關了很久。
如果這篇內容對你有幫助,歡迎點贊 、收藏 ?、轉發給需要的朋友
我會持續分享:
● ? Java 核心與高階實戰
● AI / Agent / 前沿技術落地
● 真實項目經驗 & 架構思考
● ? 企業數字化與產品實踐
關注我,一起把“技術”真正用在項目和業務里。
你的每一次支持,都是我持續輸出高質量內容的最大動力。
AI 確實能幫我們更快地寫代碼,但越是這樣,越要把基本功打牢。會生成只是起點,能識別問題、看清邊界、幫AI 優化和指導,會指揮 AI、理解業務、承擔結果的人,才是重新洗牌后的“護城河”
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.