iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Software Development

學校沒教的後端生存指南:30 天打造非同步 S3 微服務,部署 K8s 實現 HA 架構系列 第 9

[ Day 9 ] 效能大對決 : 同步Apache vs. 非同步Netty連線AWS S3 API 效能壓力測試

  • 分享至 

  • xImage
  •  

昨天我們用 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 執行緒分配的效率。

本次壓測檢測的參數:

  1. Throughput (QPS / 吞吐量):每秒成功處理完的 API 數量。數值越高,代表系統的承載能力越強
  2. Average Latency (平均回應時間):一個請求發出到收到回應的平均耗時(毫秒 ms)。數值越低越好
  3. P95 / P99 Latency (百分位數延遲):代表 95% 或 99% 的使用者體驗到的最大延遲。幫助抓出系統在併發臨界點時產生的長尾效應與排隊延遲
  4. 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) 即可啟動圖形介面
    https://ithelp.ithome.com.tw/upload/images/20260903/20183864trX5al9Ise.png

建立壓測腳本三部曲:

第一步:新增執行緒群組 (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 方便除錯)
https://ithelp.ithome.com.tw/upload/images/20260903/201838643VHqhsY7mf.png

硬體資源監測 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等資訊了。
    https://ithelp.ithome.com.tw/upload/images/20260903/20183864VkPouHG5TT.png

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
      https://ithelp.ithome.com.tw/upload/images/20260904/20183864HY5wfOzqnL.png

上傳檔案至s3出現500error

從jmeter的Results Tree可以看出來自500的錯誤
https://ithelp.ithome.com.tw/upload/images/20260904/20183864tfPAI04uaO.png

回到 Java console 會發現噴出了SdkClientException AWS S3 SDK 預設會重試3次,但最終依然宣告失敗
https://ithelp.ithome.com.tw/upload/images/20260904/20183864I3CR6xGgc9.png

非同步的錯誤率(44.4%)為什麼比同步(32.4%)還要高?!

  1. 同步模式:天然的客戶端限流(Client-side Throttling)保護傘
    • 在同步模式下,執行緒會被 putObject 的 I/O 操作死死阻塞。雖然我們將連線池上限設為 100,但因為檔案傳輸需要時間,這 50 個併發請求(乘上 5 迴圈)實際上是被「排隊卡在 Tomcat 執行緒池裡」慢慢消化的
    • 因為發送速度被實體執行緒的阻塞給拖慢了,這種慢反而無意間控制住了流量,保護了本地端到 AWS 之間有限的網路頻寬。
  2. 非同步模式:引發災難的緩衝區膨脹(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

https://ithelp.ithome.com.tw/upload/images/20260904/20183864j0qW8K6moT.png

負載控制後,非同步 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

  • JMeter 結果:Sync 與 Async 皆繳出 0% 錯誤率 的亮眼成績,系統吞吐量高達 422 ~ 426 req/sec。

    • Apache 同步 (Sync):樣本數 5000,平均回應時間 1255 ms,最小 186ms,最大 4305ms,標準差 518.96,錯誤率 0.00%,吞吐量 422.0 req/sec。
    • Netty 非同步 (Async):樣本數 5000,平均回應時間 1264 ms,最小 293ms,最大 2011ms,標準差 432.50,錯誤率 0.00%,吞吐量 426.6 req/sec。
  • JConsole 監控現形:

觀測指標 Apache Netty
活動執行緒數 (Live Threads) 瞬間飆升至 245 條(Peak 達到 253) 維持在 ~100(一條水平線)
堆記憶體 (Heap Memory) 一路衝破 260 MB 穩定在 110 MB 波動
CPU 使用率 (CPU Usage) 拉出一個 15% 的陡峭高峰 波動溫和,CPU 曲線平緩

https://ithelp.ithome.com.tw/upload/images/20260904/20183864aOFHGmOZ6v.png

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 其實只有那幾條在默默發力,因此呈現完美的一字橫線,記憶體也極度節省。印證了非同步模型能用極少的資源,安全撐過數倍於同步模型的併發!

總結

綜合上述三次極端測試,我們可以用兩張圖表來總結同步與非同步的真實面貌:
https://ithelp.ithome.com.tw/upload/images/20260904/20183864z0MK8D1iWZ.png

  1. JVM 實體執行緒數的黃金交叉: 在 1000 併發的 GET 壓力下,同步 Apache 為了應付排隊人潮,被迫將執行緒數量瘋狂拉升至 245 條,伴隨記憶體一路飆破 260MB。而 Netty 則以不變應萬變,始終將活動執行緒平穩控制在 100 條(大多為 Tomcat 基礎與背景執行緒),真正的 Netty Event Loop 用極少的資源就撐起了海量連線,沒有資源搶奪與頻繁的上下文切換,高下立判。

  2. 沒有限流的非同步,是一把雙面刃: 這是本次壓測最寶貴的發現。在實驗一中,當併發瞬間拉高,Netty 的非同步上傳錯誤率(44.4%)竟然反超了 Apache(32.4%)。因為非同步引擎處理得太快了!主執行緒在幾毫秒內就把 50 個上傳任務全部拋給 Netty,導致本地頻寬瞬間被塞爆(Bufferbloat)。反觀同步模型因為阻塞,無意間形成了一種天然的客戶端限流保護傘。

這證明了一個殘酷的現實:當我們利用非同步 Netty 以極高的速度向背景拋任務時,只要底層的物理通道(頻寬、記憶體、資料庫連線)遇到瓶頸,我們其實是在向未來的系統借債。

而我們今天測試的情境,還是建立在網路相對穩定的本地端。如果把場景搬到現實世界,遇到 4G/5G 網路抖動、Timeout、或是因為封包遺失導致狀態沒有同步到的狀況,這套微服務會迎來怎樣的狀態不一致災難?

明天開始,我們將面對真實世界的網路惡意,探討為什麼實務上不愛用效能極差的「強一致性三階段提交(3PC)」,並引出我們真正需要的輕量級分散式狀態解決方案!


上一篇
[ Day 8 ] 從引擎到微服務:用 Spring Boot 同時封裝 Apache 與 Netty S3 API
系列文
學校沒教的後端生存指南:30 天打造非同步 S3 微服務,部署 K8s 實現 HA 架構9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言