題目與適用情境
一個使用 PostgreSQL 的訂單平台部署了 100 個無狀態 API 執行個體和 20 個後台 worker。資料庫的 max_connections 為 500,需要為遷移、故障處理和維運工具預留 50 個連線,因此應用程式連線總 預算是 450。流量尖峰為每秒 6,000 個請求,其中約 70% 會存取資料庫,應用程式追蹤顯示一次資料庫 操作平均持有連線 40 毫秒。API 請求的端到端 p99 目標是 800 毫秒。
目前每個程序都設定 10 個連線,理論上最多建立 100 × 10 + 20 × 10 = 1,200 個資料庫工作階段。 尖峰期開始出現連線取得逾時,但資料庫 CPU 有時只有 55%,也有時伴隨鎖定等待和查詢延遲上升。請 設計初始連線池上限、逾時與生命週期策略,並提出能區分連線池過小、連線洩漏、長交易、執行個體擴容 超出預算和資料庫本身過載的診斷方法。
題目中的執行個體數、吞吐量、40 毫秒和 500 個連線都是面試假設,不代表 PostgreSQL、HikariCP 或 PgBouncer 的通用效能。目前公開的後端面試資料仍把效能、可靠性和營運問題放在系統設計考察範圍, 2026 年也有專門的資料庫連線池設計題。它的核心考察能力是跨應用程式副本管理資料庫併發與資源 所有權,因此歸入 backend。
面試官考察重點
第一項訊號是把連線池視為併發准入。「連線越多,吞吐越高」並不成立。PostgreSQL 的 max_connections 是全資料庫限制,提高它還會增加資源配置。每個執行個體獨立設定的數字必須乘以 API、worker、排程任務和滾動發布期間的副本數,才能得到資料庫真正可能看到的總連線數。
第二項訊號是會用 Little's Law 做量級檢查,但不把平均值當成容量結論。題設中平均資料庫到達率是 6,000 × 70% = 4,200 次/秒,平均連線占用併發約為 4,200 × 0.04 = 168。168 只能說明穩態平均 量級;突發流量、p99 持有時間、鎖定等待、交易重試和副本傾斜仍需要壓測和執行資料。
第三項訊號是能依兩側證據定位排隊發生在哪裡。應用程式側要看 active、idle、pending、連線取得時間、 取得逾時和持有時間;資料庫側要看 pgstatactivity 的狀態、wait_event、活躍查詢、閒置交易和 連線來源。只看到「取得連線逾時」就提高池大小,可能只是把應用程式排隊轉移成資料庫排隊。
最後看故障邊界。高品質回答會涵蓋請求截止時間、連線洩漏、長交易、滾動發布、自動擴縮容、資料庫 重新啟動、失效連線和 PgBouncer 的工作階段池/交易池差異,也會說明哪些工作階段功能在交易池模式 下不安全。
回答前需要釐清的問題
- 500 個連線是整個叢集還是單一主資料庫的限制? 讀取副本、故障切換目標和維運連線可能有不同
預算。連線池只能按真正承載寫入或讀取的資料庫分別計算。
- 執行個體數的尖峰是多少? 100 個 API 是常態還是擴容上限?滾動發布可能短暫同時存在新舊兩批
執行個體。若只按常態副本數設定,發布或故障擴容會突破總預算。
- 請求的連線持有時間如何分布? 平均 40 毫秒不足以表達 p95、p99、交易內網路呼叫和鎖定等待。
長尾決定排隊與逾時,需要按路由、任務類型和交易標籤拆分。
- 後台任務能否排隊或限制併發? 長批次處理不應與短 API 請求爭搶同一個池。可以為不同工作負載
分池,但所有池仍共享 450 的全域預算。
- 逾時後業務會如何重試? 沒有退避的自動重試會在資料庫變慢時增加到達率。連線取得逾時必須受
請求總截止時間限制,並由上游限流、退避或失敗策略配合。
- 應用程式依賴哪些工作階段功能? 暫存資料表、
LISTEN、工作階段 advisory lock、跨交易的
SET 狀態或 prepared statement 行為,會影響能否使用 PgBouncer transaction pooling。
- 連線代理部署在哪裡? 單一 PgBouncer 執行個體會成為容量和可用性邊界;每節點 sidecar、獨立
代理層和託管代理的故障模式不同。
30 秒回答框架
「先按全域預算設定:500 個連線預留 50 個後,應用程式上限是 450;目前每程序 10 個會放大到 1,200。168 個平均併發只做量級檢查,初始可給 API 每執行個體 2 個、worker 每執行個體 4 個,總計 280,保留 170 餘量並用尖峰壓測調整。連線取得逾時要短於 800 毫秒請求截止時間。應用程式側看 pending、取得延遲和持有時間,資料庫側看 pgstatactivity 與 wait event。池排隊而資料庫有餘量 時檢查傾斜、洩漏和本地上限;查詢或鎖定等待已惡化時不要擴池。擴容要重算總預算,PgBouncer transaction pooling 前要稽核工作階段依賴。」
分步深入解答
先建立全域連線預算
把連線預算寫成明確不變量:
application_connection_cap
= max_connections
- operations_reserve
= 500 - 50
= 450所有 API、worker、遷移任務、管理端和滾動發布副本的池上限總和都不能超過 450。只要資料庫還有其他 應用程式,就要繼續從 450 中扣除它們的預算。預留連線不是平時拿來提高吞吐的彈性池;它保證故障時 仍能登入、觀察和修復。
題設的平均併發計算如下:
database_arrival_rate = 6,000 × 70% = 4,200 operations/second
average_in_flight = 4,200 × 0.04 seconds = 168這個結果能快速發現 1,200 個連線明顯沒有平均負載支持,也能判斷 20 個總連線大概不夠。它不能直接 給出最終池大小,因為平均值隱藏了突發、p99 持有時間、交易重試和排隊回饋。最終數字必須由服務 SLO、 資料庫可持續活躍併發和壓測共同決定。
提出可驗證的初始分配
一個保守起點是 API 每執行個體 2 個、worker 每執行個體 4 個:
API cap = 100 × 2 = 200
worker cap = 20 × 4 = 80
allocated = 280
app headroom = 450 - 280 = 170worker 的 4 個連線只是隔離後台負載的初始候選,不能從「任務較長」直接推導。真實設定要依各自 持有時間、到達率和併發限制調整。280 是一個滿足總預算、可開始壓測的候選,170 的餘量也不應自動 分完。若 API 執行個體擴到 200,維持每執行個體 2 個就會讓 API 使用 400,加上 worker 的 80 後 超過 450。因此擴容策略必須同時降低每執行個體上限、限制最大副本,或由連線代理執行更集中的資料庫 連線預算。
不要為了讓每個程序隨時有閒置連線而設定很高的 minimumIdle。閒置工作階段也占用全域名額。連線池 可以按需成長到硬上限;是否維持最小閒置量,要用連線建立成本和突發延遲驗證。
設定逾時與連線生命週期
連線取得逾時必須小於剩餘請求截止時間。例如 p99 目標是 800 毫秒時,不能讓執行緒在池中等待 30 秒。 可以先給取得階段 100 到 200 毫秒預算,再依排隊分布調整;這是本題的設計選擇,不是函式庫的通用 建議。逾時後回傳可辨識的過載錯誤,由上游限流或帶抖動退避處理,不能立即無限重試。
連線的最大生命週期應短於資料庫、代理或網路會強制回收連線的時間,並加入隨機抖動,避免大量連線 同時到期。keepalive 只用於保持本來應存活的閒置連線,間隔必須小於最大生命週期。每次借出前是否 檢測連線,要權衡額外往返;更重要的是正確處理資料庫重新啟動或故障切換後第一次操作的失敗,並在 業務層只重試冪等操作。
用應用程式側指標判斷池是否是瓶頸
每個池至少公開以下指標,並帶上服務、執行個體和工作負載標籤:
- configured max、active、idle、pending;
- 連線取得延遲的 p50、p95、p99 和逾時數;
- 連線持有時間及其路由、任務或交易標籤;
- 新建、關閉、失效驗證和重建連線的速率;
- 池上限乘以目前副本數得到的理論總連線數。
判斷時要組合多個數值。pending 和取得延遲上升、active 長期頂住上限,但資料庫活躍連線與 CPU 仍有餘量,可能是本地池太小、執行個體流量傾斜或少數請求持有連線過久。active 不下降、請求已結束 或執行緒堆疊停在業務程式碼,則更像連線未歸還。連線建立/關閉速率突然上升,可能是生命週期、代理 逾時或網路問題。
應為連線借用建立結構化作用域,確保成功、例外和取消路徑都歸還連線。洩漏檢測門檻可以協助定位, 但不能取代持有時間分布和程式碼審查;門檻太低還會把正常長交易報告為洩漏。
用 PostgreSQL 證據區分資料庫過載
PostgreSQL 的 pgstatactivity 每個伺服器程序一列,可依 application_name、用戶端位址、狀態和 等待事件彙總。排查時先看:
SELECT application_name, state, wait_event_type, wait_event, count(*)
FROM pg_stat_activity
WHERE datname = current_database()
GROUP BY application_name, state, wait_event_type, wait_event
ORDER BY count(*) DESC;再找持續閒置在交易中的工作階段:
SELECT pid, application_name, xact_start, state_change, wait_event_type, wait_event
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY xact_start;如果資料庫活躍工作階段、查詢延遲、鎖定等待或 I/O 已隨連線數上升,瓶頸在查詢、交易或資料庫資源; 繼續擴大池會增加併發爭用。如果資料庫接近 500 個工作階段,但大量是應用程式的 idle,工作階段預算 被各執行個體閒置池耗盡,應降低每執行個體池或引入代理複用。如果 idle in transaction 長時間 存在,要修正交易邊界,因為它既占連線,也可能妨礙 vacuum 或持有鎖定。
連線總數不等於平行執行數。處於 idle 的工作階段和正在等待鎖定的工作階段需要分別解釋。CPU 只有 55% 也不能證明資料庫有餘量:系統可能受鎖定、儲存延遲、單一熱點分割區或序列執行路徑限制。
處理工作負載隔離與擴縮容
短 API 與長 worker 共用一個池時,少量批次處理就能造成隊頭阻塞。可以給兩者獨立池和獨立併發佇列, 如本題的 200/80 分配;隔離用於保護延遲,不能創造額外連線。worker 的池若沒有工作,應允許縮小, 批次任務本身也要設定併發上限。
擴容控制器需要知道資料庫預算。最簡單的硬限制是:
max_replicas × pool_size_per_replica + other_pool_caps <= 450滾動發布的 maxSurge 也要計入。若執行個體數變化很大,每執行個體固定上限會在低副本時浪費容量、 高副本時超出預算。可以選擇較小固定池配合請求佇列,或透過 PgBouncer 把大量用戶端工作階段複用到 受控數量的伺服器端連線;無論哪種方式,都必須繼續限制資料庫側活躍併發。
判斷是否使用 PgBouncer
PgBouncer 的 session pooling 在一個用戶端工作階段結束前保留同一個伺服器端連線,兼容 PostgreSQL 的工作階段功能,但複用能力有限。transaction pooling 只在交易期間分配伺服器端連線,能顯著減少 閒置用戶端長期占用,卻改變了「下一次交易仍使用同一個伺服器端工作階段」的假設。
採用 transaction pooling 前,要檢查跨交易 SET/RESET、LISTEN、工作階段 advisory lock、暫存 資料表和其他工作階段狀態。業務需要這些語意時,可以繼續使用 session pooling、為特定路徑設定獨立 直連池,或重構為交易內狀態。不能只因連線數下降就宣布代理方案完成;還要壓測代理排隊、代理故障、 驗證、連線重建和資料庫故障切換。
用故障實驗完成容量閉環
驗證從階梯負載開始,逐步增加到 6,000 請求/秒,再加入突發和自動擴容。每輪同時記錄端到端 p95/p99、 連線取得延遲、pending、持有時間、資料庫活躍工作階段、鎖定等待、查詢延遲、CPU 和 I/O。比較 200/80 初始分配附近的多個候選點,找出吞吐不再成長、排隊或資料庫延遲開始惡化的位置。
對抗性實驗至少包括:注入一個連線未歸還路徑;讓少量交易持有連線數秒;製造鎖定競爭;同時啟動 滾動發布的新舊副本;重新啟動資料庫;使代理中斷既有伺服器端連線;讓 worker 突發積壓。通過標準 包括:總連線不超過預算,系統能快速失敗或降載,指標能指出瓶頸位置,恢復後沒有連線風暴。
高品質示範回答
「我先把 500 個資料庫連線拆成預算,再給每個執行個體設定上限。預留 50 個維運連線後,應用 程式上限是 450。現有 120 個程序各 10 個,理論值 1,200,發布或突發時一定存在超出預算風險。
按題設,資料庫操作到達率是每秒 4,200,平均持有 40 毫秒,依 Little's Law 平均併發約 168。我只 用它做量級檢查,不把它當 p99 容量。初始可給 100 個 API 每個 2 個連線、20 個 worker 每個 4 個, 總計 280,留下 170 個應用程式餘量。然後用真實持有時間分布、突發和鎖定競爭壓測 280 附近的候選值。 擴容上限和滾動發布副本必須進入公式,否則 200 個 API 執行個體加上 worker 會超過 450。
連線取得逾時要受 800 毫秒請求截止時間限制,我會先給 100 到 200 毫秒並測量。應用程式側監控 active、idle、pending、取得延遲、逾時、持有時間和連線重建;資料庫側按 application_name 彙總 pgstatactivity,查看 active、idle、idle in transaction 和 wait_event。若池排隊但資料庫還有 可持續餘量,檢查執行個體傾斜、本地上限和連線未歸還;若查詢、鎖定或 I/O 已惡化,擴大池只會增加 資料庫併發。若接近 max_connections 的大多是 idle,則降低每執行個體閒置池或用連線代理複用。
短 API 和長 worker 分池並各自限制併發,但總和仍受 450 限制。需要 PgBouncer 時,我會先選擇 pooling 模式:session pooling 兼容性高,transaction pooling 複用強,但必須清點 LISTEN、工作階段 advisory lock、暫存資料表和跨交易 SET 等依賴。最後用階梯負載、突發、連線洩漏、長交易、鎖定競爭、滾動 發布、資料庫重新啟動和代理斷線驗證 p99、排隊、總連線和恢復過程。」
常見錯誤
- 每個執行個體固定 20 個連線 → 副本數變化後總連線不可控 → **先定資料庫總預算,再分配到所有池
和最大副本數。**
- 用平均併發 168 直接設定 168 個連線 → 忽略突發、p99 持有時間、鎖定等待和分配粒度 → **把
Little's Law 當量級檢查,用壓測和分位數完成容量決定。**
- 取得逾時就擴大池 → 可能把應用程式排隊轉成資料庫鎖定、I/O 或 CPU 排隊 → **同時檢查池
pending 與資料庫 active、wait event 和查詢延遲。**
- 只看資料庫 CPU → 鎖定等待、儲存延遲和熱點也能在低 CPU 下阻塞連線 → **按狀態和等待事件
解釋工作階段。**
- 忽略 idle 連線 → 大量執行個體的閒置池會先耗盡 max_connections → **監控池上限乘副本數,
並控制 minimum idle。**
- 把 idle in transaction 當普通閒置 → 交易可能持有鎖定、保留舊快照並妨礙清理 → **定位交易
開始時間和程式碼邊界,設定合理逾時。**
- 所有任務共用一個池 → 長 worker 搶占短 API 的連線,造成隊頭阻塞 → **按延遲特性隔離池與
佇列,但不突破總預算。**
- 接入 PgBouncer 就預設使用 transaction pooling → 工作階段狀態可能跨交易遺失或落到另一個
連線 → 先稽核功能相容性,再選擇 pooling 模式。
- 連線錯誤後立即批次重試 → 資料庫恢復期會遭遇連線風暴和更高到達率 → **限制重試、加入退避
與抖動,並只重試冪等操作。**
- 只在穩態壓測 → 滾動發布、擴容、長交易和資料庫重新啟動才會暴露預算與生命週期問題 → **把
這些故障放入驗收矩陣。**
追問與應對
追問一:為什麼連線更多反而會更慢?
資料庫的 CPU、快取、鎖定和儲存頻寬有限。超過可持續活躍併發後,更多連線會增加上下文切換、快取 競爭和鎖定等待,單次查詢變慢,連線持有時間又進一步變長,形成正回饋。小池讓請求在應用程式側 有界排隊,通常比讓所有請求同時進入資料庫更可控,但池也不能小到浪費資料庫餘量。
追問二:API 擴到 200 個執行個體時怎麼辦?
維持每執行個體 2 個會產生 400 個 API 連線,加上 worker 的 80 後已超過 450。可以把每執行個體池 降到 1,並重新分配 worker 預算;限制最大副本與滾動發布 surge;或由 PgBouncer 複用用戶端連線。 決定要以突發排隊和資料庫可持續併發壓測,不能讓擴容器只看 CPU 而忽略資料庫下游容量。
追問三:怎樣區分連線洩漏和查詢真的很慢?
洩漏常表現為 active 借用數只升不降、池 pending 持續增加,但資料庫中對應工作階段可能已經 idle, 或應用程式請求已結束。把每次借用關聯到路由、任務和堆疊取樣,比較連線持有時間與請求生命週期, 並審查例外、取消和提前返回路徑。若資料庫工作階段持續 active 或等待鎖定,應先解釋查詢與交易, 不能直接定性為洩漏。
追問四:HikariCP 的連線數公式可以直接用嗎?
(corecount × 2) + effectivespindle_count 是 HikariCP 文件提供的起始啟發式,並明確要求圍繞它 做負載測試;SSD、快取命中、查詢類型和遠端資料庫都會改變結果。更重要的是,單一資料庫被多個應用 程式執行個體共享時,資料庫層級的候選數還要分配回所有副本。公式不能取代 450 的總預算和實際 SLO 資料。
追問五:PgBouncer transaction pooling 下 prepared statement 一定不能用嗎?
不能用一句絕對結論涵蓋所有版本和設定。PgBouncer 的功能矩陣需要與目前版本、協定層 prepared statement 支援和應用程式驅動設定一起驗證;交易池仍不保證跨交易綁定同一個伺服器端工作階段。 面試中應先列出應用程式真正依賴的工作階段語意,用整合測試確認,再決定特定路徑走交易池、工作階段 池或直連。
追問六:提高池大小後連線取得延遲下降,但端到端 p99 上升,怎麼解釋?
排隊從應用程式池移到了資料庫。更多查詢同時進入後,鎖定競爭、I/O 或 CPU 佇列使查詢執行時間變長; 連線取得指標變好不代表使用者延遲變好。應比較端到端 p99、資料庫查詢時間、wait event 和吞吐量, 選擇總延遲最低且保持穩定餘量的併發點。