Java 後端底層 ch03 連線池:為什麼把池開大反而更慢
下一章→
CH 03 資源管理

連線池:為什麼把池開大反而更慢

建立連線有多貴池大小不是越大越好HikariCP 四個關鍵參數maxLifetime 與半夜斷線連線洩漏的診斷三層上限要對齊

先講一個幾乎每個後端工程師都做過的錯誤決策:

線上系統變慢,監控顯示大量請求卡在「等待資料庫連線」
→ 直覺反應:連線池不夠大,從 20 調到 100
→ 結果:更慢了,而且資料庫的 CPU 飆到 100%

這不是意外,是可以預測的結果。連線池的大小和系統吞吐量之間,不是「越大越好」的線性關係—— 它有一個最佳點,超過那個點之後,加得越多、跑得越慢。

這一章要處理的是後端最常見、卻最少被真正理解的一層資源管理:

  • 建立一個資料庫連線到底有多貴——先看成本,才知道池在省什麼。
  • 為什麼 10 個連線的池,往往比 100 個快:從資料庫端的瓶頸推導,不是玄學。
  • HikariCP 的四個關鍵參數分別在解決什麼問題,設錯會怎樣。
  • maxLifetime 為什麼一定要設,以及那些只在半夜出現的 Communications link failure。
  • 連線洩漏:徵狀、診斷、以及為什麼它常常根本不是洩漏。
  • 三層上限要對齊:Tomcat 執行緒數、連線池大小、資料庫 max_connections——這三個數字算錯,系統會在你最不想要的地方崩掉。

這一章會多次回頭用到前兩章:慢 SQL(ch01)和長事務(ch02)是「連線不夠用」最常見的兩個真正原因——而它們都不是靠調大池能解決的。

連線池存在的理由只有一個:建立連線太貴,貴到不能每次查詢都做一次。先看貴在哪。

一次「建立連線」實際發生的事

  1. TCP 三次握手——一個 RTT(同機房約 0.5ms,跨區可能 30ms 以上)
  2. TLS 握手(如果有加密)——再一到兩個 RTT,加上憑證驗證與金鑰交換的 CPU 成本
  3. 資料庫認證——帳密驗證、權限載入
  4. Session 初始化——設定字元集、時區、隔離等級、autocommit
  5. 資料庫端配置資源——MySQL 為每個連線開一條 thread,PostgreSQL 直接 fork 一個 process
同機房加上 TLS,一次建立連線通常是 10~100 毫秒。而一次走索引的查詢可能只要 1 毫秒。
換句話說:建立連線比查詢本身貴了幾十到上百倍。

連線不只是「一個物件」

它同時佔用三邊的資源:

  • 應用端:一個 socket、一個檔案描述符(fd)
  • 網路中間層:防火牆、負載平衡器的連線追蹤表都有一筆
  • 資料庫端:一條 thread 或 process,加上它的記憶體(PostgreSQL 每個 backend 幾 MB 起跳)

所以池做的事很單純

先建好一批連線放著,用完不關、還回池裡,下次直接拿。把「幾十毫秒的建立成本」攤平成「幾乎為零的借還成本」。

💡 但這也帶出下一個問題:既然連線這麼寶貴,是不是應該多開一點?——直覺說是,實際上剛好相反。下一張卡就在講這個。

直覺是這樣的:連線越多 → 能同時處理的請求越多 → 吞吐越高。這個直覺在超過某個點之後就完全錯了。

關鍵:真正在幹活的不是連線,是資料庫的 CPU 和磁碟

連線只是一條通道。資料庫實際能同時執行的工作量,受限於:

  • CPU 核心數——8 核就是同時只能真的跑 8 件事,其餘都在排隊
  • 磁碟 I/O 通道——能同時發出多少個有效的 I/O 請求
  • 鎖與 latch 競爭——連線越多,搶同一份資源的人就越多

超過瓶頸之後,多開連線會發生什麼

8 核的資料庫,100 個連線同時送查詢進來

→ 100 個 thread 搶 8 顆 CPU
→ 大量 context switch(每次切換都要換快取、換 TLB)
→ CPU 有相當比例花在「切換」而不是「幹活」
→ 鎖競爭同步放大:等鎖的人變多,持鎖時間也被拉長

結果:吞吐量下降,而且每個請求的延遲都變長
這就是為什麼「調大連線池」常常讓情況更糟:你沒有增加資料庫的處理能力,只是讓更多請求同時擠進去互相拖累。
原本 20 個人排隊、每人 10ms;現在 100 個人同時衝進去、每人 80ms——總時間更長,而且每個人的體感都更差。

HikariCP 官方建議的起算公式

連線數 = (CPU 核心數 × 2) + 有效磁碟數

一台 8 核、用 SSD(視為 1)的資料庫,算出來是 17。這個數字通常讓人很不安——「才 17 個連線怎麼撐得住幾百 QPS?」

撐得住,因為連線是「借用」不是「佔有」

一個請求只在真正執行 SQL 的那幾毫秒持有連線,其餘時間(處理 HTTP、跑業務邏輯、序列化 JSON)都不需要連線。

✅ 用 Little's Law 換算:需要的連線數 = 目標 TPS × 平均持有連線的時間。
500 TPS × 20ms = 10 個連線就夠了。這也解釋了為什麼小池能撐大流量。

排隊不是壞事。在池外面排 2ms,比擠進去讓所有人都慢 80ms 好得多——這是連線池設計的核心哲學。

Spring Boot 從 2.0 起預設用 HikariCP。它的參數不多,但每一個都對應一個具體問題。

maximumPoolSize —— 並行上限

spring.datasource.hikari.maximum-pool-size=10

池裡最多幾個連線。這是整個系統真正的資料庫並行上限,不是 Tomcat 的執行緒數。預設 10——不要因為「看起來太小」就調大,先看下一張卡的算法。

minimumIdle —— 保持待命的數量

閒置時至少留幾個連線不關掉,避免流量突然回來時要重新建立。HikariCP 官方建議不要設——不設的話它會等於 maximumPoolSize,變成固定大小的池。固定大小反而更好,因為「動態伸縮」在尖峰時剛好會讓你付出建立連線的成本。

connectionTimeout —— 等不到連線就放棄

spring.datasource.hikari.connection-timeout=3000   # 毫秒,預設 30000

池滿了、所有連線都被借走時,新的請求要等多久才放棄並丟 SQLTransientConnectionException。

預設 30 秒太長了。如果資料庫已經卡住,讓每個請求都等 30 秒只會讓應用程式的執行緒全部堆積,最後整個服務一起死。設短一點(2~5 秒)讓它快速失敗,比慢慢死掉好。

maxLifetime —— 連線的最長壽命

spring.datasource.hikari.max-lifetime=1500000   # 毫秒,預設 1800000(30 分)

一個連線活過這麼久就會被主動汰換掉(在閒置時,不會打斷使用中的連線)。這個參數的重要性遠超它的名氣——下一張卡專門講它。

順帶一提:idleTimeout

閒置多久就關掉多餘的連線。只有在 minimumIdle 小於 maximumPoolSize 時才有作用;照上面的建議不設 minimumIdle 的話,這個參數等於不存在。

這是連線池最經典的靈異現象,而且它的成因完全不靈異。

徵狀

  • 錯誤訊息是 Communications link failure、Connection reset、The last packet successfully received from the server was N milliseconds ago
  • 通常出現在深夜或清晨等低流量時段
  • 重試一次就成功了,所以很容易被當成「網路不穩」而忽略
  • 白天尖峰時反而幾乎不出現

成因:有人在你不知道的時候切斷了連線

連線閒置太久時,路徑上有三個角色可能主動把它切掉:

  1. 資料庫本身:MySQL 的 wait_timeout(預設 8 小時,但雲端服務常改成 5~30 分鐘)
  2. 防火牆 / NAT 閘道:連線追蹤表有 idle timeout,常見 5~30 分鐘,而且經常是直接丟棄封包,兩端都不會收到 FIN
  3. 負載平衡器 / Proxy:同樣有 idle timeout
致命的地方在第 2 種:連線被靜默切斷,應用端完全不知道。
池以為手上這條連線還好好的,借給下一個請求用——直到真的送出封包才發現對面早就不在了。這就是為什麼是「拿出來用的那一刻」才爆,而不是「被切斷的那一刻」。

低流量時段連線閒置更久,所以更容易超過 idle timeout——半夜才出現的原因就在這裡。

解法:讓池比所有中間設備更早汰換連線

maxLifetime  <  min(資料庫 wait_timeout, 防火牆 idle timeout, LB idle timeout)
  • HikariCP 官方建議:比最短的那個少幾秒(留出時間差,避免剛好在邊界上撞車)
  • 例:資料庫 wait_timeout 是 600 秒 → maxLifetime 設 570000(570 秒)
  • 先查清楚環境裡到底有哪些 timeout:SHOW VARIABLES LIKE 'wait_timeout',以及去問網路/雲端設定
💡 不要用「連線測試」當作主要解法。connectionTestQuery(例如 SELECT 1)會讓每次借用都多一次來回,而且現代 JDBC 驅動支援 Connection.isValid(),HikariCP 預設就會用。正確的做法是主動汰換,不是每次檢查。

典型徵狀

  • 服務剛啟動時完全正常
  • 跑了幾小時或幾天之後,所有請求開始卡在等連線
  • 最後大量 SQLTransientConnectionException: Connection is not available, request timed out after 30000ms
  • 重啟服務就好了,過幾天又發生——這是最典型的指紋

真正的洩漏:借了沒還

// ❌ 例外路徑漏掉 close
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ps.execute();          // 這裡丟例外的話,下面兩行永遠不會執行
ps.close();
conn.close();

// ✅ try-with-resources
try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.execute();
}

用 Spring 的 @Transactional、JdbcTemplate、JPA Repository 幾乎不會洩漏——框架保證會還。會漏的幾乎都是手寫 dataSource.getConnection() 的地方。

診斷:打開洩漏偵測

spring.datasource.hikari.leak-detection-threshold=20000   # 毫秒

連線被借走超過這個時間還沒還,HikariCP 會印出當初借走它的那段 stack trace——直接指到兇手那一行。正式環境可以長期開著(20~60 秒),成本很低。

但更常見的情況是:它根本不是洩漏,是「還得太慢」。
連線確實會還,只是被佔用太久:
① 慢 SQL(ch01)——一句查詢跑 5 秒,連線就被佔 5 秒
② 長事務(ch02)——@Transactional 裡呼叫第三方 API,交易多久連線就被佔多久

這兩種情況下,調大連線池只會讓更多請求同時卡住,並且把壓力轉嫁給資料庫。

怎麼分辨是哪一種

觀察真洩漏還得太慢
active 連線數單調上升,永不回落尖峰時衝高,離峰會回落
重啟後從零開始再慢慢漲流量一來馬上又滿
leak detection會印出 stack trace可能也會印,但指向的是慢查詢

一個請求要走完全程,會依序經過三個有上限的關卡。大多數線上事故都是因為這三個數字沒有對齊。

HTTP 請求
   ↓
① Tomcat 執行緒池      server.tomcat.threads.max     預設 200
   ↓
② HikariCP 連線池      maximum-pool-size             預設 10
   ↓
③ 資料庫連線上限        max_connections               MySQL 預設 151

觀念一:真正的並行上限是 ②,不是 ①

200 條 Tomcat 執行緒配 10 個連線,代表最多 190 條執行緒會同時在等連線。很多人看到這個數字會嚇到,覺得應該把池調大。

但這是正常且正確的設計。執行緒在等連線的時候幾乎不耗 CPU;讓它們排隊,總比全部擠進資料庫互相拖累好(回到第二張卡的推導)。

觀念二:③ 是全域的,要除以實例數

最容易算錯的地方:max_connections 是整個資料庫的總額,不是每個應用實例的額度。

假設 maximum-pool-size = 50,Kubernetes 上跑 4 個 Pod:
50 × 4 = 200 個連線,而 MySQL 預設 max_connections 只有 151。

徵狀:平常都好好的,一遇到自動擴容(HPA 加 Pod)或滾動更新(新舊 Pod 並存)就大量 Too many connections——而且擴容本來是為了救火,結果反而把資料庫打死。

正確的算法

單一實例池大小 × 最大實例數  +  維運保留額  ≤  max_connections

例:max_connections = 151
    保留 20 給 DBA 連線、監控、備份工具
    最大實例數 = 6(含滾動更新時的新舊並存)
    → 單一實例池大小 ≤ (151 - 20) / 6 ≈ 21
🚨 滾動更新時新舊 Pod 會同時存在,實例數要用「更新期間的峰值」而不是穩定狀態的數量。這是很多人算漏的一項。

要監控的指標

  • hikaricp_connections_active——正在被借走的數量
  • hikaricp_connections_pending——正在排隊等連線的執行緒數,這個持續大於 0 才是真的要調整
  • hikaricp_connections_acquire_seconds——拿到連線花的時間

不要用「感覺」設池大小,也不要直接抄別人的設定。有兩個可以實際算的方法。

方法一:從流量反推(Little's Law)

需要的連線數 = 目標 TPS × 每個請求持有連線的平均時間

關鍵是第二項——不是整個請求的耗時,只有真正在跑 SQL 的那一段。

假設:目標 500 TPS,每個請求平均跑 3 句 SQL、
      每句 5ms → 持有連線約 15ms

500 × 0.015 = 7.5  →  取 10(留一點餘裕)

方法二:從資料庫能力反推(HikariCP 公式)

連線數 = (CPU 核心數 × 2) + 有效磁碟數

兩個算法取比較小的那個——因為兩邊都是上限,較小的那個才是真正的瓶頸。

實務調整流程

  1. 從小開始(10 或算出來的值),不要從大開始往下調
  2. 壓測或觀察正式流量,看 hikaricp_connections_pending
  3. pending 持續大於 0 → 有人在排隊,可以考慮加
  4. 每次加一點(例如 +5),同時看資料庫的 CPU 與平均查詢延遲
  5. 如果加了池但吞吐沒上升、延遲反而變長 → 已經過了最佳點,退回去
判斷是否已達最佳點的訊號:增加連線數之後,總吞吐量(TPS)沒有提升,但 P99 延遲上升。這代表資料庫已經飽和,多出來的連線只是在裡面互相排隊。

不同用途要分開的池

如果同一個服務同時有「線上請求」和「後台批次」,應該用兩個 DataSource:

  • 線上請求池:小、connectionTimeout 短,快速失敗
  • 批次池:更小(2~3 個就夠)、timeout 長,允許慢慢跑

否則一個跑十分鐘的報表就能把線上請求的連線全部吃光。

連線池是一個很好用的調節閥,但它只能分配資料庫的處理能力,不能增加它。以下四種情況,動池是在治標。

1. 真正的瓶頸是 SQL 本身

一句沒走索引的查詢跑 5 秒(ch01),連線就被佔 5 秒。此時加大池只是讓更多請求同時慢,還會把資料庫 CPU 推更高。先去 EXPLAIN,不要先調池。

2. 交易邊界畫錯

@Transactional 包住第三方 API 呼叫(ch02),交易開多久連線就被佔多久。這種情況下池永遠不夠大——對方慢一點你就再爆一次。該修的是交易範圍。

3. 應用實例數量會大幅變動

Serverless、大量短生命週期的容器、每次擴容就多一組池——每個實例各自維護一個池的模型會直接壓垮資料庫。這時需要的是外部連線池:

  • PostgreSQL:PgBouncer
  • AWS RDS / Aurora:RDS Proxy
  • 應用端的池可以設得很小,由外部 pooler 統一收斂到資料庫

4. 讀多寫少但全部打在主庫

該做的是讀寫分離(讀走 replica),配兩組 DataSource,而不是把單一池撐大。

🚨 診斷順序:先看 SQL(ch01)→ 再看交易邊界(ch02)→ 再看池的參數 → 最後才考慮加大池。把順序倒過來做,通常會在事故報告上寫下「已將連線池由 20 調整為 100」,然後下週再出一次事。

一個反直覺的收尾

如果你的系統在池滿的時候優雅地排隊、快速失敗、然後自動恢復,那個池的大小很可能是對的。池的作用本來就包含「保護資料庫不被打死」——它擋住請求不是故障,是它正在做它的工作。

HikariCP 關鍵參數:解決什麼問題、設錯會怎樣
參數預設值解決什麼問題設錯的後果
maximumPoolSize10資料庫並行上限(真正的上限在這,不是 Tomcat 執行緒數)太大 → 資料庫 context switch 與鎖競爭暴增,吞吐反而下降
minimumIdle= maximumPoolSize閒置時保留多少連線待命官方建議不要設;動態伸縮會讓你在尖峰時付出建立連線的成本
connectionTimeout30000(30 秒)池滿了要等多久才放棄預設太長:資料庫卡住時所有執行緒堆積,整個服務一起死。建議 2~5 秒
maxLifetime1800000(30 分)主動汰換連線,避開資料庫/防火牆/LB 的 idle timeout沒設或設太長 → 半夜的 Communications link failure
leakDetectionThreshold0(關閉)連線借走太久時印出借走它的 stack trace不開就查不到洩漏源頭;建議正式環境長期開 20~60 秒
連線池問題診斷表:徵狀 → 原因 → 怎麼確認
徵狀最可能的原因怎麼確認
半夜/低流量時偶發 Communications link failure連線被防火牆或資料庫靜默切斷,池不知道比對 maxLifetime 與 wait_timeout/防火牆 idle timeout,看是不是 maxLifetime 比較長
跑幾天後所有請求卡住,重啟就好連線洩漏(借了沒還)看 active 連線數是否單調上升不回落;開 leakDetectionThreshold 抓 stack trace
尖峰時大量等待,離峰恢復正常不是洩漏,是「還得太慢」——慢 SQL 或長事務看慢查詢日誌、看 innodb_trx 的 trx_started;先修 SQL 與交易邊界
調大池之後更慢,資料庫 CPU 飆高已超過資料庫最佳並行度,多出來的連線在互相拖累看 TPS 有沒有跟著上升;沒上升但 P99 變長就是過頭了,退回去
擴容或滾動更新時 Too many connections池大小 × 實例數超過資料庫 max_connections算 池大小 × 更新期間峰值實例數 + 維運保留額,跟 max_connections 比
三層上限對照:一個請求會撞到哪些牆
層級參數常見預設算錯的後果
① Tomcat 執行緒池server.tomcat.threads.max200太小 → 請求連排隊的機會都沒有;太大 → 記憶體浪費,但不影響資料庫
② 連線池maximum-pool-size10這才是真正的資料庫並行上限;大量執行緒在此排隊是正常設計
③ 資料庫連線上限max_connectionsMySQL 151全域總額,要除以實例數;忘記算滾動更新的並存 Pod 是最常見的錯

練習題 點選選項查看解析

0 / 10
01 / 10
為什麼把連線池從 20 調到 100 之後,系統反而更慢了?
A 因為 HikariCP 有 bug,池太大時會有額外開銷
B 因為連線數沒有增加資料庫的處理能力,只是讓更多請求同時擠進去互相競爭 CPU 與鎖
C 因為建立 100 個連線太花時間
D 因為 JVM 記憶體不夠放這麼多連線物件
解析
資料庫真正的處理能力受限於 CPU 核心數、磁碟 I/O 與鎖競爭。超過最佳並行度之後,更多連線只會造成大量 context switch 與鎖競爭放大,吞吐下降、延遲上升。原本 20 個人排隊每人 10ms,變成 100 個人同時擠進去每人 80ms。
02 / 10
用 Little's Law 估算:目標 500 TPS,每個請求平均持有連線 20ms,需要多大的池?
A 100
B 50
C 10
D 500
解析
500 × 0.02 = 10。關鍵在於「持有連線的時間」只算真正在跑 SQL 的那一段,不是整個請求的耗時——處理 HTTP、跑業務邏輯、序列化 JSON 都不需要佔著連線。這也解釋了為什麼小池能撐大流量。
03 / 10
為什麼「Communications link failure」特別容易在半夜出現?
A 半夜資料庫在跑備份,負載比較高
B 低流量時連線閒置更久,更容易超過防火牆或資料庫的 idle timeout 而被靜默切斷
C 半夜網路品質比較差
D JVM 在半夜觸發 Full GC 導致連線中斷
解析
連線閒置太久會被資料庫的 wait_timeout、防火牆或 LB 的 idle timeout 切掉,而且防火牆常常是直接丟棄封包、兩端都收不到 FIN。池以為連線還活著,借出去後真正送封包才發現對面不在了——所以是「拿出來用的那一刻」才爆。
04 / 10
maxLifetime 應該怎麼設?
A 越長越好,減少重建連線的成本
B 設成跟資料庫 wait_timeout 一樣
C 比「資料庫 wait_timeout、防火牆 idle timeout、LB idle timeout」之中最短的那個再短幾秒
D 不用設,HikariCP 會自動偵測
解析
目標是讓池比所有中間設備更早主動汰換連線,這樣就不會拿到已經被別人切斷的連線。設成一樣長會剛好在邊界上撞車,所以要留幾秒的時間差。
05 / 10
服務跑幾天後所有請求卡住、重啟就好,最可能是什麼問題?怎麼確認?
A 資料庫效能衰退,需要重建索引
B 連線洩漏;看 active 連線數是否單調上升不回落,並開 leakDetectionThreshold 抓 stack trace
C JVM 記憶體洩漏,需要調大 heap
D 網路頻寬不足
解析
「重啟就好、過幾天再犯」是連線洩漏最典型的指紋。真洩漏的 active 連線數會單調上升永不回落;如果是尖峰衝高、離峰回落,那就不是洩漏而是「還得太慢」(慢 SQL 或長事務)。leakDetectionThreshold 會印出當初借走連線的 stack trace,直接指到兇手。
06 / 10
Tomcat 執行緒 200、連線池 10,代表最多 190 條執行緒會在等連線。這是問題嗎?
A 是,應該把連線池調到 200 才不會有人排隊
B 是,應該把 Tomcat 執行緒降到 10
C 不是,這是正常設計——執行緒等待時幾乎不耗 CPU,排隊比全部擠進資料庫互相拖累好
D 不是,因為 Tomcat 會自動調整連線池大小
解析
真正的資料庫並行上限是連線池,不是執行緒池。讓執行緒在池外排隊 2ms,比擠進資料庫讓所有人都慢 80ms 好得多。池擋住請求不是故障,是它在保護資料庫——這正是連線池設計的核心哲學。
07 / 10
maximum-pool-size 設 50,Kubernetes 上跑 4 個 Pod,MySQL max_connections 是 151。會發生什麼?
A 沒問題,50 < 151
B 50 × 4 = 200 > 151,擴容或滾動更新時會出現 Too many connections
C HikariCP 會自動把總數限制在 151 以內
D MySQL 會自動提高 max_connections
解析
max_connections 是整個資料庫的總額,不是每個實例的額度。最容易算漏的是滾動更新期間新舊 Pod 並存,實例數要用「更新期間的峰值」。正確算法:單實例池大小 × 最大實例數 + 維運保留額 ≤ max_connections。
08 / 10
哪一個指標最適合判斷「連線池是不是真的不夠大」?
A hikaricp_connections_active(正在被借走的數量)
B hikaricp_connections_pending(正在排隊等連線的執行緒數)
C 資料庫的 max_connections 使用率
D Tomcat 的執行緒使用率
解析
active 接近上限只代表池被用滿,不一定有問題。pending 持續大於 0 才代表真的有人在排隊等不到。但即使 pending > 0,也要先確認不是慢 SQL 或長事務造成的「還得太慢」,否則調大池只會讓更多請求同時慢。
09 / 10
connectionTimeout 預設 30 秒,為什麼建議調短到 2~5 秒?
A 為了節省記憶體
B 資料庫卡住時,讓每個請求等 30 秒會導致應用程式執行緒全部堆積,整個服務一起死;快速失敗比慢慢死好
C 因為 30 秒超過 HTTP 逾時時間,會造成資料不一致
D 因為 HikariCP 不支援超過 10 秒的等待
解析
這是「快速失敗」的取捨。資料庫已經有問題時,長時間等待會讓上游的執行緒、甚至更上游的呼叫方全部堆積,把局部故障擴散成全站故障。短 timeout 讓請求快速失敗,配合重試或降級,系統反而更容易恢復。
10 / 10
應用跑在 Serverless 或大量短生命週期容器上,每個實例各自維護連線池,最好的做法是?
A 把每個實例的池調到 1,避免佔用太多連線
B 改用外部連線池(PgBouncer、RDS Proxy)統一收斂,應用端的池可以很小
C 把資料庫的 max_connections 調到很大
D 改用 NoSQL 資料庫
解析
實例數量大幅變動時,「每個實例一個池」的模型會直接壓垮資料庫,因為總連線數不受控。外部 pooler 在資料庫前面統一收斂連線,應用端可以自由擴縮。把 max_connections 調很大只是把問題推到資料庫端——每個連線在 PostgreSQL 是一個 process,記憶體會先爆掉。

點擊卡片翻面查看答案,共 14 張。

QUESTION
建立一個資料庫連線大概多貴?
點擊翻面
ANSWER
同機房含 TLS 約 10~100 毫秒(TCP 握手+TLS 握手+認證+session 初始化+資料庫端配置 thread/process)。而一次走索引的查詢可能只要 1ms——建連線比查詢貴幾十到上百倍。
點擊翻回
QUESTION
為什麼池不是越大越好?
點擊翻面
ANSWER
連線只是通道,資料庫的處理能力受限於 CPU 核心數、磁碟 I/O 與鎖競爭。超過最佳並行度後,多開連線只會增加 context switch 與鎖競爭,吞吐下降、延遲上升。
點擊翻回
QUESTION
HikariCP 建議的池大小公式?
點擊翻面
ANSWER
(CPU 核心數 × 2) + 有效磁碟數。8 核配 SSD 算出來約 17。要跟 Little's Law 的結果取比較小的那個。
點擊翻回
QUESTION
Little's Law 怎麼算池大小?
點擊翻面
ANSWER
需要的連線數 = 目標 TPS × 每個請求「持有連線」的平均時間。關鍵:只算真正在跑 SQL 的那一段,不是整個請求耗時。500 TPS × 20ms = 10 個連線。
點擊翻回
QUESTION
半夜才出現 Communications link failure,為什麼?
點擊翻面
ANSWER
低流量時連線閒置更久,超過防火牆/資料庫的 idle timeout 被靜默切斷(防火牆常直接丟封包,兩端都收不到 FIN)。池以為還活著,借出去真正送封包才爆——所以是「拿出來用的那一刻」才失敗。
點擊翻回
QUESTION
maxLifetime 的設定規則?
點擊翻面
ANSWER
要比 min(資料庫 wait_timeout, 防火牆 idle timeout, LB idle timeout) 再短幾秒。留時間差是為了避免剛好在邊界上撞車。
點擊翻回
QUESTION
連線洩漏的典型指紋?
點擊翻面
ANSWER
剛啟動正常 → 幾小時/幾天後所有請求卡住 → 重啟就好 → 過幾天再犯。active 連線數單調上升永不回落。
點擊翻回
QUESTION
怎麼分辨「真洩漏」和「還得太慢」?
點擊翻面
ANSWER
真洩漏:active 單調上升不回落,重啟後從零慢慢漲。還得太慢:尖峰衝高、離峰回落,流量一來馬上又滿——原因是慢 SQL(ch01)或長事務(ch02)。
點擊翻回
QUESTION
怎麼抓出連線洩漏的兇手?
點擊翻面
ANSWER
leak-detection-threshold=20000。連線借走超過該時間未還,HikariCP 會印出當初借走它的 stack trace。成本很低,正式環境可長期開 20~60 秒。
點擊翻回
QUESTION
Tomcat 200 執行緒配 10 個連線,190 條在等——這是問題嗎?
點擊翻面
ANSWER
不是,這是正常設計。真正的資料庫並行上限是連線池不是執行緒池;執行緒等待時幾乎不耗 CPU。排隊 2ms 比全部擠進去每人 80ms 好。
點擊翻回
QUESTION
max_connections 的算法?最容易算漏什麼?
點擊翻面
ANSWER
單實例池大小 × 最大實例數 + 維運保留額 ≤ max_connections。最常算漏的是「滾動更新時新舊 Pod 並存」,實例數要用更新期間的峰值。
點擊翻回
QUESTION
判斷池是否真的不夠大,該看哪個指標?
點擊翻面
ANSWER
hikaricp_connections_pending(排隊等連線的執行緒數)持續大於 0。active 滿了不一定是問題。而且即使 pending > 0,也要先排除慢 SQL 與長事務。
點擊翻回
QUESTION
為什麼 connectionTimeout 預設 30 秒該調短?
點擊翻面
ANSWER
資料庫卡住時,讓每個請求等 30 秒會讓執行緒全部堆積,把局部故障擴散成全站故障。設 2~5 秒快速失敗,配合重試或降級反而更容易恢復。
點擊翻回
QUESTION
什麼時候「調整連線池」不是答案?
點擊翻面
ANSWER
① 瓶頸是慢 SQL(先 EXPLAIN)② 交易邊界畫錯(@Transactional 包住外部呼叫)③ 實例數大幅變動(該用 PgBouncer/RDS Proxy)④ 讀多寫少(該做讀寫分離)。診斷順序:SQL → 交易邊界 → 池參數 → 最後才是加大池。
點擊翻回