昨天我們用 Spring Boot 框架將 Apache 同步與 Netty 非同步引擎封裝成了對外的微服務 API。
理論說得天花亂墜,但在本機電腦上,這兩套 API 跑起來真的有差嗎?
今天,我們要立刻進行壓力測試來證明Day 3 為什麼伺服器會卡死?圖解同步、非同步與阻塞的愛恨情仇 中說明的同步和非同步效能實務上的差異!
壓力測試環境與場景設計
我用本地開發環境(Mac筆電 8核 8GB Memory)進行測試 :
- 本機頻寬:約 74 Mbps(換算約 9.25 MB/s 的傳輸能力)
- 連線池上限:S3Client (Apache) 與 S3AsyncClient (Netty) 的最大連線數皆限制為 100
- 檔案大小:74 KB 的實體圖片檔案(模擬使用者頻繁上傳頭貼、發票截圖的真實場景)
因為筆電環境有限,一開始併發規模先設定在50,且為了將聚焦鎖定在I/O 執行緒模型與連線池的處理能力上,後續會使用Jmeter這款壓力測試工具,並規劃了以下兩個測試場景:
場景 A:檔案清單查詢(List Files API)
- API 路徑:
GET http://localhost:8080/api/s3/sync/files vs. GET http://localhost:8080/api/s3/async/files
- 目的:完全排除大檔案傳輸的頻寬干擾,純粹測試當高併發打進來時,同步 Servlet 執行緒卡死與非同步 Event Loop 的純效能對抗。
場景 B:小檔案上傳(Upload API)
- API 路徑:
POST http://localhost:8080/api/s3/sync/upload vs. POST http://localhost:8080/api/s3/async/upload
- 目的:高頻發送檔案,測試兩者在處理網路連線建立、IAM 簽章與 I/O 執行緒分配的效率。
本次壓測檢測的參數:
- Throughput (QPS / 吞吐量):每秒成功處理完的 API 數量。數值越高,代表系統的承載能力越強
- Average Latency (平均回應時間):一個請求發出到收到回應的平均耗時(毫秒 ms)。數值越低越好
- P95 / P99 Latency (百分位數延遲):代表 95% 或 99% 的使用者體驗到的最大延遲。幫助抓出系統在併發臨界點時產生的長尾效應與排隊延遲
- Error Rate (錯誤率):在高流量下系統連線失敗、超時或噴出 500 錯誤的比例。理想狀況下必須為0%
JMeter 本地快速架設指南
Apache JMeter 是後端工程師壓測的黃金標準工具。以下是本機最速建置流程:
安裝 JMeter:
- macOS (Homebrew):
brew install jmeter後直接在終端及執行jmeter即可
- Windows / Linux: 官方下載頁面, apache-jmeter-5.x.x.zip,解壓後進入 bin 目錄,雙擊 jmeter.bat (Windows) 或執行 ./jmeter (Mac/Linux) 即可啟動圖形介面
建立壓測腳本三部曲:
第一步:新增執行緒群組 (Thread Group)
- 在 Test Plan 按右鍵 -> Add -> Threads (Users) -> Thread Group。
- Number of Threads (Users):設定併發人數,我們分別測試 50
- Ramp-up period (seconds):設定在幾秒內將人數拉滿,我們設定 5 秒(平緩起跑)
- Loop Count:我們設定為 5(每個使用者重複打 5 次,所以 50 個使用者會產生 250 次請求)
第二步:設定 HTTP 請求 (HTTP Request)
- 在 Thread Group 按右鍵 -> Add -> Sampler -> HTTP Request
- Protocol:http
- Server Name or IP:localhost
- Port Number:8080
- Method & Path:
- 檔案清單測試:GET,路徑為 /api/s3/sync/files 或 /api/s3/async/files
- 小檔案上傳測試:POST,路徑為 /api/s3/sync/upload 或 /api/s3/async/upload
- 下方 Parameters 旁點選 Files Upload,新增檔案路徑填入要測試上傳的檔案
絕對路徑,Parameter Name:file,MIME Type:multipart/form-data
第三步:新增觀測儀表板 (Listeners)
- Test Plan 按右鍵 -> Add -> Listener -> Summary Report(看吞吐量與平均延遲)
- Test Plan 按右鍵 -> Add -> Listener -> View Results Tree(請求是否成功與Response內容)
完成的架構大致如下(也可以為每個 HTTP Request 獨立新增 View Results Tree 方便除錯)

硬體資源監測 JConsole
要觀察硬體I/O、CPU使用率、Memory等資源的方式有很多,但我們今天會用一個免安裝的JDK內建監控神器JConsole 來觀察本地伺服器內部資源,不需要寫複雜的監控 Script,也不需要大費周章在本地安裝 Docker 去跑 Prometheus 或是 Grafana。
- 在執行Spring Boot專案之後,對終端機(Terminal)直接輸入
jconsole 就會跳出一個連線視窗
- 直接選擇com.example.s3service.S3ServiceApplication,然後點選連線即可。
- 進入觀測面版後就可以看到Memory Usage、Threads、CPU usage、Classes等資訊了。
Netty vs Apache 壓力測試
先啟動 Spring Boot 服務,在打開jconsole,接著用JMeter分別對不同的Thread Group針對 50 人併發請求(測試同步上傳時,可以把其他三種Group Thread都先disable)
實驗一: 50 併發 × 5 迴圈
左側是Jmeter Summary Report的結果,右側則是jconsole的硬體監測數據,從CPU使用率來看就可以清楚看到四個高峰,就代表四次的執行測試,由於硬體資源尚未見頂,我們先聚焦於 JMeter 測出的外部指標 :
- GET (List Files):
- Apache 同步 (Sync):樣本數 250,平均回應時間 229 ms,最小 186ms,最大 724ms,標準差 99.04,錯誤率 0.00%,吞吐量 42.3 req/sec
- Netty 非同步 (Async):樣本數 250,平均回應時間 351 ms,最小 190ms,最大 1275ms,標準差 221.64,錯誤率 0.00%,吞吐量 39.2 req/sec
- POST (Upload 74KB):
- Apache 同步 (Sync):樣本數 250,平均回應時間 4477 ms,最小 445ms,最大 11960ms,標準差 4270.13,錯誤率 32.40%,吞吐量 6.5 req/sec
- Netty 非同步 (Async):樣本數 250,平均回應時間 5953 ms,最小 447ms,最大 12429ms,標準差 4464.08,錯誤率 44.40%,吞吐量 5.6 req/sec
上傳檔案至s3出現500error
從jmeter的Results Tree可以看出來自500的錯誤

回到 Java console 會發現噴出了SdkClientException AWS S3 SDK 預設會重試3次,但最終依然宣告失敗

非同步的錯誤率(44.4%)為什麼比同步(32.4%)還要高?!
- 同步模式:天然的客戶端限流(Client-side Throttling)保護傘
- 在同步模式下,執行緒會被 putObject 的 I/O 操作死死阻塞。雖然我們將連線池上限設為 100,但因為檔案傳輸需要時間,這 50 個併發請求(乘上 5 迴圈)實際上是被「排隊卡在 Tomcat 執行緒池裡」慢慢消化的
- 因為發送速度被實體執行緒的阻塞給拖慢了,這種慢反而無意間控制住了流量,保護了本地端到 AWS 之間有限的網路頻寬。
- 非同步模式:引發災難的緩衝區膨脹(Bufferbloat)
- 主執行緒是完全非阻塞的。當 JMeter 發動攻勢時,主執行緒在短短幾毫秒內,就愉快地把這數百個上傳任務全部拋給了 Netty。
- Netty 的 Event Loop 效率極高,試圖在同一瞬間發起海量的實體 HTTP 連線並把檔案推出去。由於我們還沒有實作「背壓(Backpressure)」或應用層限流機制,本地僅有 74 Mbps 的頻寬發送佇列瞬間膨脹塞車。導致封包大面積丟失、TCP 頻繁重傳,引發AWS SDK噴出大量的 S3 連線超時與異常(SdkClientException)。
實驗二:GET 200 併發× 5 迴圈 vs. POST 20 併發 × 5 迴圈
透過實驗一我們發現GET綽綽有餘,而上傳卻遇到瓶頸,所以第二次我們調高GET的併發,並減少post的併發
- GET (List Files):
- Apache 同步 (Sync):樣本數 1000,平均回應時間 293 ms,最小 234ms,最大 815ms,標準差 127.11,錯誤率 0.00%,吞吐量 158.8 req/sec
- Netty 非同步 (Async):樣本數 1000,平均回應時間 301 ms,最小 234ms,最大 830ms,標準差 129.32,錯誤率 0.00%,吞吐量 158.6 req/sec
- POST (Upload 74KB):
- Apache 同步 (Sync):樣本數 100,平均回應時間 2098 ms,最小 539ms,最大 10893ms,標準差 2934.10,錯誤率 9.00%,吞吐量 4.3 req/sec
- Netty 非同步 (Async):樣本數 100,平均回應時間 1811 ms,最小 554ms,最大 23582ms,標準差 2948.63,錯誤率 2.00%,吞吐量 3.3 req/sec

負載控制後,非同步 Netty 效能明顯高於 Apache
當我們把併發限制在本地頻寬74 Mbps內(20併發x 74 KB 最多只會有 )與 S3 連線池(100)可以安全負擔的範圍內時,非同步 Netty 的優點馬上出現!
- Netty 非同步上傳的錯誤率(2.0%)大幅低於同步 Apache(9.0%)
- 在平均回應時間上,Netty(1811ms)也比 Apache(2098ms)快了接近 300 毫秒
- 換言之只要不超過底層物理通道的臨界點,非同步引擎在應付併發時的穩定性與速度皆完勝同步阻塞!
實驗三:GET 1000 併發× 5
| 觀測指標 |
Apache |
Netty |
| 活動執行緒數 (Live Threads) |
瞬間飆升至 245 條(Peak 達到 253) |
維持在 ~100(一條水平線) |
| 堆記憶體 (Heap Memory) |
一路衝破 260 MB |
穩定在 110 MB 波動 |
| CPU 使用率 (CPU Usage) |
拉出一個 15% 的陡峭高峰 |
波動溫和,CPU 曲線平緩 |

50 併發時,兩者數據相差無幾,提高併發卻擴大差異
- 50 併發,你可能會發現 Apache 與 Netty 的 QPS 與平均 Latency 非常接近,錯誤率也都是 0%。是因為我們在 Day 8 配置 S3Config 時,將 Apache 連線池上限 (maxConnections) 設為了 100,且 Spring Boot (Tomcat) 預設的工作執行緒池高達 200。
- 50 個併發請求還沒有塞滿連線池,Apache 的執行緒雖然會阻塞等待 S3,但它隨時能從池子裡拿到多餘的執行緒和 HTTP 連線。此時硬體資源充裕,同步與非同步在體驗上幾乎沒有差別。
當併發拉到 200 甚至1000時,兩者的表現黃金交叉
- Apache 的慘狀:回應時間(尤其是 P95、P99 延遲)會出現極其恐怖的暴增
- 因為連線池上限是 100。一旦請求達到臨界點,再加上 S3 的網路傳輸需要時間,連線池會瞬間乾涸
- 第 101 個進來的 HTTP 請求執行緒,就必須卡在 Tomcat 的佇列中「死等」前面的請求傳完、釋放連線
- 這種佇列阻塞效能連鎖反應 (Queueing Delay),會讓後續使用者的等待時間呈指數級上升
- 從執行緒數量也可以看出1000併發JVM thread數量爆增到245條,且伴隨記憶體攀升
- Netty 的優雅:吞吐量 (QPS) 依然維持高檔,且回應時間曲線極度平滑,P99 延遲仍控制在極佳的範圍內
- Netty 不需要為了每一個連線配置一條實體執行緒。發起請求的 Tomcat 執行緒在將任務丟給 Netty 的 CompletableFuture 後,就立刻被釋放回去接客
- 底層所有的網路傳輸與 S3 回傳處理,全部由少數幾個 NIO Event Loop 執行緒以事件驅動、多路複用的方式處理。沒有人需要排隊等連線,自然不會發生 Apache 那種連鎖阻塞地獄
- JConsole 裡的執行緒數量平穩得維持在100條左右,這大部分是 Spring Boot 維持 Web 伺服器運作、GC (垃圾回收) 等基本盤,真正的 Netty S3 Event Loop 其實只有那幾條在默默發力,因此呈現完美的一字橫線,記憶體也極度節省。印證了非同步模型能用極少的資源,安全撐過數倍於同步模型的併發!
總結
綜合上述三次極端測試,我們可以用兩張圖表來總結同步與非同步的真實面貌:

-
JVM 實體執行緒數的黃金交叉: 在 1000 併發的 GET 壓力下,同步 Apache 為了應付排隊人潮,被迫將執行緒數量瘋狂拉升至 245 條,伴隨記憶體一路飆破 260MB。而 Netty 則以不變應萬變,始終將活動執行緒平穩控制在 100 條(大多為 Tomcat 基礎與背景執行緒),真正的 Netty Event Loop 用極少的資源就撐起了海量連線,沒有資源搶奪與頻繁的上下文切換,高下立判。
-
沒有限流的非同步,是一把雙面刃: 這是本次壓測最寶貴的發現。在實驗一中,當併發瞬間拉高,Netty 的非同步上傳錯誤率(44.4%)竟然反超了 Apache(32.4%)。因為非同步引擎處理得太快了!主執行緒在幾毫秒內就把 50 個上傳任務全部拋給 Netty,導致本地頻寬瞬間被塞爆(Bufferbloat)。反觀同步模型因為阻塞,無意間形成了一種天然的客戶端限流保護傘。
這證明了一個殘酷的現實:當我們利用非同步 Netty 以極高的速度向背景拋任務時,只要底層的物理通道(頻寬、記憶體、資料庫連線)遇到瓶頸,我們其實是在向未來的系統借債。
而我們今天測試的情境,還是建立在網路相對穩定的本地端。如果把場景搬到現實世界,遇到 4G/5G 網路抖動、Timeout、或是因為封包遺失導致狀態沒有同步到的狀況,這套微服務會迎來怎樣的狀態不一致災難?
明天開始,我們將面對真實世界的網路惡意,探討為什麼實務上不愛用效能極差的「強一致性三階段提交(3PC)」,並引出我們真正需要的輕量級分散式狀態解決方案!