Connection Leak(連線洩漏)
Connection Leak(連線洩漏)
連線洩漏(connection leak)是程式向 Connection Pool 借了一條連線,用完卻沒有還回池裡。那條連線在池的帳上一直是「使用中」,洩漏累積到池的上限之後,之後每一個要用資料庫的請求都排隊等連線,等到逾時為止。資料庫那一端這時負載多半很低:連線開著,上面沒有在執行的查詢。和它症狀相同、成因不同的是連線被長時間合法持有,最常見的是交易開著時去呼叫外部服務(長交易佔住連線的成本見 Transaction,外部呼叫要不要放進交易見 Transaction Boundary)。
概念位置
連線在程式碼上不一定以「連線」的樣子出現,查詢結果的讀取物件(Cursor)與交易(Transaction)都各自佔著一條,所以借出的位置常常不明顯。各語言最常見的借出點:
- Go 的
database/sql:查詢回傳的*sql.Rows在Close或讀到最後一列之前佔著一條連線;*sql.Tx在Commit或Rollback之前佔著一條連線。讀到一半就return、又沒有defer rows.Close()是最常見的洩漏(實測見 10.1 database/sql:連線池、查詢結果的讀取與交易〈查詢結果讀取物件(*sql.Rows)的關閉與連線洩漏〉)。QueryRowContext回傳的*sql.Row要呼叫Scan才歸還連線,db.Conn(ctx)借出的專用連線要Close。pgx 的pgxpool用Acquire明確借出時,要對應一次Release。 - Python:
psycopg_pool的getconn()要對應putconn(),SQLAlchemy 的engine.connect()與Session要關閉;寫成with區塊時,區塊結束就歸還,洩漏多半出在沒有用with的那幾處。 - PHP-FPM:沒有開持久連線(
PDO::ATTR_PERSISTENT)時,請求結束時 PHP 釋放這個請求的所有資源,連線跟著關閉,所以單一請求內的洩漏不會跨請求累積;改成常駐 worker(Laravel Octane、FrankenPHP 的 worker 模式),以及本來就常駐的佇列 worker(php artisan queue:work)之後,跨請求或跨工作留著的物件就可能一直握著連線;Python 的 Celery worker 同理。
可觀察訊號與例子
- 池的指標:使用中的連線數一直貼著上限、等待連線的次數持續上升,而查詢的執行時間沒有變長。Go 的
db.Stats()是InUse與WaitCount;SQLAlchemy 在等不到連線時拋出QueuePool limit of size 5 overflow 10 reached, connection timed out這類錯誤。 - 資料庫端:PostgreSQL 的
pg_stat_activity裡,來自應用程式的連線長時間停在idle(池裡正常閒置的連線也是idle,所以要對照池的帳:池說使用中、資料庫說閒置的那些才是借出後沒在用的)或idle in transaction(交易開著沒有結束)。寫過資料或跑在 repeatable read 以上的idle in transaction另外會擋住 VACUUM 清理舊版本的資料列,idle_in_transaction_session_timeout可以讓 PostgreSQL 主動切斷這種連線(擋住 VACUUM 的機制見 PostgreSQL MVCC + Lock Model:為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價)。 - 找出是哪一段程式:
pg_stat_activity的query欄是那條連線最後執行的語句,application_name(連線字串可以設定)標出是哪個服務;停在idle in transaction或池說使用中的連線,最後一句 SQL 通常就指向借出連線的那段程式。 - 只在流量高或長時間運行之後才出現:池的上限越大,洩漏要累積越久才浮現。本機開發把池上限設成 1 或 2,同樣的洩漏在第二個查詢就卡住,最容易重現。
設計責任
借出與歸還寫在同一個作用域裡,讓語言的機制保證歸還:Go 在取得 *sql.Rows 或 *sql.Tx 的下一行寫 defer Close() 或 defer Rollback(),Python 用 with。監控上要能看到池的使用中、閒置與等待次數;測試裡可以在每個測試結束時檢查池的使用中連線數回到 0,讓洩漏在測試階段就失敗。Java 的 HikariCP 這類連線池內建洩漏偵測(連線借出超過設定時間就記錄借出位置的 stack trace),Go 的 database/sql 沒有,要靠池的使用中連線數、等待次數,以及測試結束時的連線數檢查。