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=GlobalRequestProcessorrequestCount總請求數
errorCount錯誤數
processingTime總處理時間
maxTime最大回應時間
Catalina:type=ThreadPoolcurrentThreadCount當前線程數
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) -->
           />
參數默認值說明
maxThreads200最大工作線程數
minSpareThreads10最小空閒線程數
maxConnections10000 (NIO)最大連線數
acceptCount100連線等待佇列長度
connectionTimeout20000連線逾時時間(毫秒)

如何確定 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 利用率下降

建議從較小的值開始,通過壓力測試逐步調整。

調校步驟

  1. 使用公式估算理論值
  2. 設置較小的初始值
  3. 進行壓力測試
  4. 觀察 CPU 使用率和回應時間
  5. 逐步增加線程數
  6. 找到 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.2Windows 平台大資料量傳輸
APRTLS 加密場景,效能要求極高
<!-- 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 + acceptCount

Keep-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+ 分代 ZGC

GC 日誌組態#

# 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);

減少物件創建#

  1. 複用 StringBuilder
// 不推薦
String result = str1 + str2 + str3;

// 推薦
StringBuilder sb = threadLocalStringBuilder.get();
sb.setLength(0);
sb.append(str1).append(str2).append(str3);
String result = sb.toString();
  1. 使用基本類型
// 不推薦
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 日誌

效能測試#

  • 進行基準測試,建立效能基線
  • 定期進行壓力測試
  • 在調校後重新測試驗證