Web 容器的調校直接影回應用的效能和穩定性。本章涵蓋線程池組態、連線器調校、JVM 組態、內存最佳化以及常見效能問題的排查方法。
效能指標監控#
關鍵指標#
Tomcat 的關鍵效能指標
類型 指標 說明 業務指標 吞吐量 (TPS) 每秒處理的請求數 回應時間 請求處理耗時 錯誤數 錯誤請求的數量 資源指標 線程池 活躍線程數、等待佇列長度 CPU 使用率、上下文切換 JVM 內存 堆內存、GC 頻率
通過 JMX 監控#
開啟 JMX 遠程監控:
# 在 setenv.sh 中添加
export JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote"
export JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote.port=9001"
export JAVA_OPTS="${JAVA_OPTS} -Djava.rmi.server.hostname=your.server.ip"
export JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote.ssl=false"
export JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote.authenticate=false"JConsole 連線:
jconsole your.server.ip:9001關鍵 MBean 指標#
| MBean 路徑 | 指標 | 說明 |
|---|---|---|
| Catalina:type=GlobalRequestProcessor | requestCount | 總請求數 |
| errorCount | 錯誤數 | |
| processingTime | 總處理時間 | |
| maxTime | 最大回應時間 | |
| Catalina:type=ThreadPool | currentThreadCount | 當前線程數 |
| currentThreadsBusy | 忙碌線程數 | |
| maxThreads | 最大線程數 |
命令行監控#
# 查看 Tomcat 程序
ps -ef | grep tomcat
# 查看程序狀態
cat /proc/<pid>/status
# 監控 CPU 和內存
top -p <pid>
# 查看網路連線
netstat -an | grep 8080
# 統計連線狀態
netstat -an | grep 8080 | awk '{print $6}' | sort | uniq -c
# 查看網路流量
ifstat線程池組態#
Tomcat 線程池參數#
<Connector port="8080" protocol="HTTP/1.1"
maxThreads="200" <!-- 最大線程數 -->
minSpareThreads="25" <!-- 最小空閒線程數 -->
maxConnections="10000" <!-- 最大連線數 -->
acceptCount="100" <!-- 等待佇列長度 -->
connectionTimeout="20000" <!-- 連線逾時時間(ms) -->
/>| 參數 | 默認值 | 說明 |
|---|---|---|
| maxThreads | 200 | 最大工作線程數 |
| minSpareThreads | 10 | 最小空閒線程數 |
| maxConnections | 10000 (NIO) | 最大連線數 |
| acceptCount | 100 | 連線等待佇列長度 |
| connectionTimeout | 20000 | 連線逾時時間(毫秒) |
如何確定 maxThreads#
利特爾法則(Little’s Law)
系統中的請求數 = 請求到達速率 × 平均請求處理時間例如:每秒 100 個請求,平均處理時間 2 秒,則需要 200 個線程。
考慮 I/O 阻塞的公式:
線程數 = (I/O 等待時間 + CPU 時間) / CPU 時間 × CPU 核數例如:8 核 CPU,I/O 時間 80ms,CPU 時間 20ms:
線程數 = (80 + 20) / 20 × 8 = 40實際調校策略#
線程數並非越多越好
- 線程太少:請求排隊,回應時間增加
- 線程太多:上下文切換開銷大,CPU 利用率下降
建議從較小的值開始,通過壓力測試逐步調整。
調校步驟:
- 使用公式估算理論值
- 設置較小的初始值
- 進行壓力測試
- 觀察 CPU 使用率和回應時間
- 逐步增加線程數
- 找到 TPS 最大且 CPU 不飽和的點
理想狀態:
- CPU 使用率 70-80%
- 線程池沒有大量等待佇列
- 回應時間穩定Jetty 線程池組態#
// 嵌入式組態
QueuedThreadPool threadPool = new QueuedThreadPool();
threadPool.setMinThreads(10);
threadPool.setMaxThreads(200);
threadPool.setIdleTimeout(60000);
Server server = new Server(threadPool);<!-- jetty.xml 組態 -->
<New id="threadPool" class="org.eclipse.jetty.util.thread.QueuedThreadPool">
<Set name="minThreads">10</Set>
<Set name="maxThreads">200</Set>
<Set name="idleTimeout">60000</Set>
</New>連線器調校#
I/O 模型選擇#
| I/O 模型 | 適用場景 |
|---|---|
| NIO | 默認選擇,適合大多數場景 |
| NIO.2 | Windows 平台大資料量傳輸 |
| APR | TLS 加密場景,效能要求極高 |
<!-- NIO (默認) -->
<Connector port="8080" protocol="HTTP/1.1" />
<!-- NIO.2 -->
<Connector port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol" />
<!-- APR -->
<Connector port="8080" protocol="org.apache.coyote.http11.Http11AprProtocol" />Linux 平台建議使用 NIO
Linux 內核的非同步 I/O 支持不完善,Java NIO.2 在 Linux 上仍是通過 epoll 模擬的,因此 NIO 更加簡單高效。
連線數組態#
<Connector port="8080" protocol="HTTP/1.1"
maxConnections="10000" <!-- NIO 最大連線數 -->
acceptCount="100" <!-- 等待佇列長度 -->
/>maxConnections vs acceptCount:
maxConnections:Tomcat 能處理的最大連線數acceptCount:當 maxConnections 達到上限時,操作系統能排隊的連線數
總最大連線數 = maxConnections + acceptCountKeep-Alive 組態#
<Connector port="8080" protocol="HTTP/1.1"
keepAliveTimeout="20000" <!-- Keep-Alive 逾時時間(ms) -->
maxKeepAliveRequests="100" <!-- 單個連線最大請求數 -->
/>keepAliveTimeout:空閒連線保持時間maxKeepAliveRequests:單個連線最多處理的請求數,-1 表示無限制
壓縮組態#
<Connector port="8080" protocol="HTTP/1.1"
compression="on"
compressionMinSize="2048"
compressibleMimeType="text/html,text/xml,text/plain,text/css,
text/javascript,application/javascript,
application/json,application/xml"
/>何時開啟壓縮
- 網路帶寬是瓶頸時開啟
- CPU 是瓶頸時關閉
- 大文本回應(HTML、JSON)適合壓縮
- 圖片、視頻等已壓縮格式不需要
JVM 組態#
內存組態#
# setenv.sh
export JAVA_OPTS="-Xms2g -Xmx2g" # 堆內存(建議 min=max)
export JAVA_OPTS="$JAVA_OPTS -Xmn512m" # 新生代大小
export JAVA_OPTS="$JAVA_OPTS -XX:MetaspaceSize=256m" # 元空間初始大小
export JAVA_OPTS="$JAVA_OPTS -XX:MaxMetaspaceSize=256m" # 元空間最大大小| 參數 | 說明 | 建議 |
|---|---|---|
| -Xms | 初始堆大小 | 與 -Xmx 相同,避免動態擴展 |
| -Xmx | 最大堆大小 | 不超過物理內存的 70% |
| -Xmn | 新生代大小 | 堆的 1/3 到 1/4 |
| -XX:MetaspaceSize | 元空間初始大小 | 256m 起步 |
GC 組態#
G1 GC(Java 9+ 推薦):
export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC"
export JAVA_OPTS="$JAVA_OPTS -XX:MaxGCPauseMillis=200" # 目標停頓時間
export JAVA_OPTS="$JAVA_OPTS -XX:G1HeapRegionSize=16m" # Region 大小ZGC(Java 11+,低延遲場景):
export JAVA_OPTS="$JAVA_OPTS -XX:+UseZGC"
export JAVA_OPTS="$JAVA_OPTS -XX:+ZGenerational" # Java 21+ 分代 ZGCGC 日誌組態#
# Java 9+
export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*:file=/var/log/tomcat/gc.log:time,uptime:filecount=5,filesize=10m"
# Java 8
export JAVA_OPTS="$JAVA_OPTS -XX:+PrintGCDetails"
export JAVA_OPTS="$JAVA_OPTS -XX:+PrintGCDateStamps"
export JAVA_OPTS="$JAVA_OPTS -Xloggc:/var/log/tomcat/gc.log"內存最佳化#
物件池技術#
物件池的適用場景
- 物件創建代價高
- 物件生命週期短
- 物件數量大
Tomcat 使用 SynchronizedStack 作為物件池,Jetty 使用 ByteBufferPool。
Tomcat 的 SynchronizedStack:
// 獲取物件
SocketProcessor processor = socketProcessorPool.pop();
if (processor == null) {
processor = new SocketProcessor(wrapper);
}
// 使用完歸還
socketProcessorPool.push(processor);減少物件創建#
- 複用 StringBuilder
// 不推薦
String result = str1 + str2 + str3;
// 推薦
StringBuilder sb = threadLocalStringBuilder.get();
sb.setLength(0);
sb.append(str1).append(str2).append(str3);
String result = sb.toString();- 使用基本類型
// 不推薦
Map<Integer, Integer> map = new HashMap<>();
// 推薦(使用 Eclipse Collections 等庫)
IntIntMap map = new IntIntHashMap();延遲加載#
Tomcat 採用延遲解析策略,HTTP 請求體直到呼叫 getInputStream() 或 getParameter() 時才解析。
// 不呼叫這些方法,請求體不會被讀取
request.getInputStream();
request.getParameter("name");啟動速度最佳化#
清理不必要的內容#
# 刪除不需要的 Web 應用
rm -rf $CATALINA_HOME/webapps/docs
rm -rf $CATALINA_HOME/webapps/examples
rm -rf $CATALINA_HOME/webapps/host-manager
rm -rf $CATALINA_HOME/webapps/manager
# 清理日誌
rm -rf $CATALINA_HOME/logs/*
# 清理工作目錄
rm -rf $CATALINA_HOME/work/Catalina禁用 TLD 掃描#
如果不使用 JSP:
<!-- conf/context.xml -->
<Context>
<JarScanner>
<JarScanFilter defaultPluggabilityScan="false"/>
</JarScanner>
</Context>禁用 WebSocket 和 JSP#
<!-- conf/context.xml -->
<Context containerSciFilter="
org.apache.tomcat.websocket.server.WsSci|
org.apache.jasper.servlet.JasperInitializer">
</Context>禁用 Servlet 註解掃描#
<!-- WEB-INF/web.xml -->
<web-app metadata-complete="true">
<!-- ... -->
</web-app>並行啟動多個應用#
<!-- conf/server.xml -->
<Host name="localhost" appBase="webapps"
startStopThreads="0"> <!-- 0 表示自動選擇線程數 -->
</Host>最佳化熵源#
# setenv.sh
export JAVA_OPTS="$JAVA_OPTS -Djava.security.egd=file:/dev/./urandom"常見效能問題排查#
問題診斷流程圖#
回應慢
│
├── CPU 高?
│ ├── 是 → 分析線程堆疊,找熱點程式碼
│ └── 否 → 繼續 ↓
│
├── 線程數高?
│ ├── 是 → 下游服務回應慢
│ └── 否 → 繼續 ↓
│
├── 內存使用高?
│ ├── 是 → 分析 Heap Dump,找內存洩漏
│ └── 否 → 繼續 ↓
│
├── GC 頻繁?
│ ├── 是 → 調整 GC 參數或增加內存
│ └── 否 → 繼續 ↓
│
└── 網路 I/O?
└── 是 → 檢查網路延遲、帶寬場景 1:CPU 使用率高#
診斷步驟:
# 1. 找到 CPU 高的線程
top -Hp <pid>
# 2. 將線程 ID 轉為 16 進制
printf '%x\n' <thread_id>
# 3. 查看線程堆疊
jstack <pid> | grep -A 30 <hex_thread_id>常見原因:
- 死循環
- 正則表達式回溯
- 大量字串拼接
- 序列化/反序列化
場景 2:線程數飆升但 CPU 不高#
典型場景:下游服務回應慢
當下游服務(資料庫、微服務)回應時間變長時,線程會阻塞等待,導致線程池中的線程被佔滿,但 CPU 使用率不高。
診斷方法:
# 查看線程狀態
jstack <pid> | grep -c "BLOCKED"
jstack <pid> | grep -c "WAITING"
jstack <pid> | grep -c "TIMED_WAITING"解決方案:
- 增加逾時設置
- 增加線程池大小(臨時方案)
- 使用熔斷器(Hystrix、Resilience4j)
- 最佳化下游服務
場景 3:頻繁 Full GC#
診斷方法:
# 查看 GC 情況
jstat -gcutil <pid> 1000
# 分析 GC 日誌
# 關注 Full GC 頻率和耗時常見原因:
- 堆內存設置過小
- 內存洩漏
- 大物件直接進入老年代
解決方案:
- 增加堆內存
- 分析 Heap Dump 找洩漏點
- 調整新生代大小
場景 4:內存洩漏#
獲取 Heap Dump:
# 手動獲取
jmap -dump:format=b,file=heap.hprof <pid>
# OOM 時自動獲取
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/tomcat/分析工具:
- Eclipse MAT (Memory Analyzer)
- VisualVM
- JProfiler
常見洩漏點:
- 未關閉的連線(資料庫、HTTP)
- 靜態集合持有物件引用
- ThreadLocal 未清理
- 監聽器未移除
場景 5:連線數耗盡#
診斷方法:
# 查看 TIME_WAIT 連線
netstat -an | grep TIME_WAIT | wc -l
# 查看 Tomcat 連線數
netstat -an | grep 8080 | grep ESTABLISHED | wc -l解決方案:
- 增加 maxConnections
- 調整 Keep-Alive 參數
- 最佳化連線池組態
- 調整操作系統 TCP 參數
調校清單#
啟動前檢查#
- 刪除不需要的默認應用
- 組態適當的 JVM 內存參數
- 開啟 GC 日誌
- 組態 OOM 時自動 Dump
線程池組態#
- 根據業務特點設置 maxThreads
- 設置合理的 minSpareThreads
- 組態連線逾時時間
連線器組態#
- 選擇適當的 I/O 模型
- 組態 maxConnections 和 acceptCount
- 根據需要開啟壓縮
監控告警#
- 組態 JMX 監控
- 設置關鍵指標告警閾值
- 定期檢查 GC 日誌
效能測試#
- 進行基準測試,建立效能基線
- 定期進行壓力測試
- 在調校後重新測試驗證