Day 27:壓力測試,驗證系統在高併發下真的撐得住 讓訂單服務實戰整合版在受控的測試環境裡,真正經過一次考驗。節流、資料正確性、監控機制,大致都表現得符合預期。但那篇結尾也誠實留了一句話:這是在受控的測試環境下驗證的,真正部署上線時,還有一些跟協程與非阻塞架構相關、容易被忽略的環境設定細節。今天要把這件事攤開來講清楚。
先把問題定位清楚。這裡談的不是程式邏輯本身有沒有寫對,Day 27 的壓力測試已經驗證過邏輯層面的行為。今天要處理的是另一種容易被忽略的風險,同一套程式碼、同一套邏輯,換一個環境跑,某些數值設定可能就不再適用。
原因並不難理解。協程相關的數值設定,散落在應用程式組態的各個角落,Dispatcher 背後的執行緒資源、逾時秒數、連線池大小、節流上限,這幾項數字通常是在開發過程中,為了方便本機測試或符合當時測試環境的資源規模而隨手訂下的。專案一路開發下去,這些數字很容易被視為理所當然,沒有人會特地在部署前回頭檢視,正式環境的硬體規格、流量規模、網路拓樸,跟當初訂這些數字時的假設是否還一致。今天要做的,就是把這幾項數值攤開來,逐一列出部署前該確認的重點,整理成一份可以直接拿來對照的清單。
Day 07:Dispatchers,協程實際跑在哪條執行緒上 定案了 Dispatchers.Default 與 Dispatchers.IO 這兩個內建選項,背後各自維護著一組數量有限的執行緒資源。Dispatchers.Default 對同時可並行執行的協程數量設定的上限,通常與 CPU 核心數相關;Dispatchers.IO 則預設是 64 或 CPU 核心數兩者取大,用來容納阻塞式呼叫。
這件事到了部署階段就變成一個具體的檢查項目:正式環境實際配置的 CPU 核心數量,是否跟當初開發或測試機器的核心數相同?如果開發機器是四核心,正式環境的容器規格卻只分配到兩核心,Dispatchers.Default 背後可用的執行緒資源,實際上比開發階段觀察到的還要少,原本在開發環境跑起來游刃有餘的併發運算,搬到正式環境可能提早出現排隊。這不是程式碼的問題,純粹是執行環境的硬體規格假設,跟著容器換了地方就跟著變了,部署前應該重新確認這件事。
另一個該重新檢視的地方,是程式碼裡有沒有把明顯屬於阻塞式操作的邏輯,誤放在不適合的 Dispatcher 上執行。Day 7 已經講過這個風險,選錯 Dispatcher 會讓少數幾條執行緒被阻塞操作占滿,連帶拖累同一個 Dispatcher 底下其他原本該被快速排程的協程。開發階段可能因為流量小,這類誤用不容易被察覺,部署前值得重新掃過一次程式碼,確認有沒有這種埋在細節裡的誤用情況。
Day 16:逾時與取消,讓卡住的協程別拖垮整個系統 定案了 withTimeout 這個逾時與取消機制,示範程式碼裡設定的秒數,是依照當時示範情境的假設訂出來的。到了實際專案裡,這個秒數通常是在開發或測試環境下,依照內部網路的低延遲狀況決定的。
問題在於,正式環境的下游服務或資料庫,未必跟應用程式部署在同一個機房甚至同一個地區,額外的網路延遲、跨可用區的傳輸時間,都可能讓一次原本正常的呼叫,花費的時間比開發階段觀察到的還要長。如果逾時秒數還是沿用開發階段那組貼著低延遲假設訂出來的數字,正式環境裡完全正常、只是稍慢一點的回應,就可能被誤判為逾時,TimeoutCancellationException 提早被拋出,使用者收到的是一個原本不該發生的失敗結果。
這裡要提醒的態度很明確:逾時秒數該設多長,沒有一個能套用到所有專案的標準答案,它高度依賴正式環境實際的網路拓樸、下游服務所在位置、預期的延遲範圍。部署前應該重新盤點這些條件,重新檢視這個數值是否還站得住腳,而不是直接把開發階段測試方便訂下的數字原封不動搬過去。
Day 18:協程遇上 R2DBC 連線池,小心連線被偷偷耗盡 定案了非阻塞連線池的等待行為是暫停而非阻塞,但連線供需失衡依然會造成排隊,這件事跟執行緒有沒有被卡住無關。組態範例裡的 max-size 與 max-acquire-time,決定了連線池能同時提供的連線上限,以及協程等待連線的時間上限。
spring:
r2dbc:
url: r2dbc:postgresql://prod-db-host:5432/orders
pool:
enabled: true
initial-size: 10
max-size: 50
max-acquire-time: 3s
這組數字部署前需要重新對照兩件事。第一,max-size 設定的連線池大小,是否依據正式環境資料庫伺服器實際能承受的連線數量上限決定的,而不是沿用開發環境資料庫的預設值。開發環境的資料庫可能是一台規格陽春的測試機,正式環境的資料庫可能是規格完全不同的正式主機,兩者能承受的連線數量未必相同。連線池設定得過小,正式環境的高併發流量容易造成連線排隊;設定得過大,又可能超過資料庫伺服器本身能承受的連線數量上限,反而拖累資料庫效能。第二,max-acquire-time 這個逾時上限,同樣需要依照正式環境的實際情況重新檢視,道理跟檢查項目二一致。
Day 27:壓力測試,驗證系統在高併發下真的撐得住 的壓測結果,正好是這裡最實用的參考依據。如果那場測試過程中,觀察到連線池相關的排隊或效能瓶頸,這就是重新調整這個數值時最直接的線索,不需要憑空猜測,直接對照測試當下觀察到的現象即可。
Day 15:併發限制,用 Semaphore 保護下游別被打爆 定案了用 Semaphore 限制同時呼叫下游服務的協程數量,Day 23:把併發控制手段實際裝進訂單服務 把 permits 設為 20,這個數字是依據當時對確認庫存內部服務處理能力的評估訂出來的。
這個評估的前提,到了部署階段同樣需要重新檢視。正式環境實際串接的下游服務,規模、版本、甚至部署架構,都可能跟開發或測試階段假設的不同。如果正式環境的下游服務其實升級過、規格提升了,還沿用原本偏保守的節流數值,等於白白浪費了下游其實能承受的處理能力;反過來,如果正式環境的下游服務規模其實比開發階段假設的更小,沿用原本的數值可能還是會把下游打爆,Semaphore 節流形同虛設。這個數值不是訂一次就能一勞永逸,它綁定的是對下游能力的評估,評估的對象換了環境,數值就該跟著重新確認。
四個檢查項目分別對應系列不同天數建立的機制,整理成一份總覽,方便部署前逐項對照。
| 檢查項目 | 系列錨點來源 | 部署前應確認的重點 |
|---|---|---|
| Dispatcher 執行緒資源 | Day 07:Dispatchers,協程實際跑在哪條執行緒上 | 正式環境的 CPU 核心數是否符合 Default Dispatcher 的執行緒數量假設,程式碼中有無阻塞式操作誤放在不適合的 Dispatcher 上 |
| 逾時秒數 | Day 16:逾時與取消,讓卡住的協程別拖垮整個系統 | withTimeout 秒數是否依照正式環境實際的網路延遲重新檢視,而非沿用開發階段低延遲假設下的數字 |
| R2DBC 連線池大小 | Day 18:協程遇上 R2DBC 連線池,小心連線被偷偷耗盡 | max-size 與 max-acquire-time 是否依據正式環境資料庫的實際承載能力設定,並對照 Day 27 壓測結果 |
| Semaphore 節流數值 | Day 15:併發限制,用 Semaphore 保護下游別被打爆 | permits 是否依照正式環境實際串接的下游服務規模與處理能力重新評估 |
這份清單刻意不附上任何建議數值,理由前面幾節都已經反覆說明過:部署設定高度依賴實際環境,任何寫死的數字放到不同專案、不同硬體規格、不同下游服務,都可能是錯的建議。這份清單真正的用途,是提醒團隊在部署前,針對這四個協程情境下特別容易被忽略的設定,做一次刻意的重新檢視,而不是提供一組可以照抄的標準答案。
這份清單涵蓋了協程情境下最容易被忽略、但影響深遠的幾項設定,Dispatcher 背後的執行緒資源、逾時秒數、R2DBC 連線池大小、Semaphore 節流數值,四項各自源自系列不同天數建立的機制,部署前逐一確認過這些項目,能大幅降低上線後才發現問題的風險。
檢查清單確認完畢,理論上系統已經準備好上線。但清單畢竟是清單,逐項對照過設定值是否合理,跟系統實際從請求進來到回應結束的完整行為是否真的如預期運作,還是兩件事。在真正交付前,值得完整跑一次從頭到尾的驗收,把 Day 22 到今天陸續建立的骨架、併發控制、資料正確性機制、可觀測性,以及今天剛確認過的這幾項部署設定,全部放進同一個真實情境裡看一次。
下一篇會進行全系列的整合驗收。
《Day 29:全系列整合驗收,跑一次完整的高併發情境》 見。