Timeout và quan sát lock: tìm session đang chặn và giới hạn thời gian chờ Trung cấp
Thời gian đọc ước tính: 10 phút
Cần đọc trước
Vấn đề
Phần tiêu đề “Vấn đề”API đặt hàng trừ tồn kho bằng UPDATE products SET stock = stock - 1 WHERE id = 101. Pool của API có 10 connection. Một script admin chạy UPDATE cùng sản phẩm 101 trong transaction rồi dừng ở prompt, không COMMIT.
Mọi request đặt sản phẩm 101 chờ row-level lock, mỗi request giữ một connection. Mười request là đủ cạn pool; request đặt sản phẩm khác cũng chờ pool, nên p99 của toàn API tăng vọt dù CPU database gần như rảnh.
Bài này trả lời hai câu: timeout nào cắt được chuỗi chờ này, đặt ở đâu, và làm sao tìm ra session gốc đang chặn.
Chọn gì? (tóm tắt trước)
Phần tiêu đề “Chọn gì? (tóm tắt trước)”| Tình huống | Chọn | Điều kiện |
|---|---|---|
| Request không được chờ lock lâu | lock_timeout theo role app |
Nhỏ hơn statement_timeout và deadline request |
| Query chậm, không chờ lock | statement_timeout |
Theo query hợp lệ chậm nhất, cộng biên |
Code quên COMMIT/release() |
idle_in_transaction_session_timeout |
Pool chịu được connection bị đóng |
| Chặn transaction dài bất kể lý do | transaction_timeout (17+) |
Không áp cho job batch hợp lệ |
| Đang sự cố, cần tìm blocker | pg_stat_activity + pg_blocking_pids() |
Đi ngược cây tới gốc rồi mới kill |
| Cần dấu vết lần chờ lâu | log_lock_waits = on |
Ngưỡng là deadlock_timeout |
Khái niệm cốt lõi
Phần tiêu đề “Khái niệm cốt lõi”Bốn timeout trong PostgreSQL 17
Phần tiêu đề “Bốn timeout trong PostgreSQL 17”Cả bốn mặc định 0 (tắt); giá trị không ghi đơn vị tính bằng mili giây.
| Tham số | Đo cái gì | SQLSTATE | Huỷ gì |
|---|---|---|---|
lock_timeout |
Mỗi lần chờ lấy một lock | 55P03 lock_not_available |
Câu lệnh |
statement_timeout |
Từ lúc lệnh tới server đến khi xong, gồm cả chờ lock | 57014 query_canceled |
Câu lệnh |
idle_in_transaction_session_timeout |
Thời gian rảnh trong transaction đang mở | 25P03 |
Session |
transaction_timeout (mới ở 17) |
Tổng thời gian transaction, kể cả transaction ngầm của một câu | 25P04 |
Session |
Hệ quả:
- Huỷ câu lệnh trong transaction tường minh làm transaction hỏng; app phải
ROLLBACKtrước khi dùng lại connection. - Huỷ session là
FATAL: connection bị đóng, pool phải loại bỏ và tạo mới. statement_timeoutkhác 0 và ≤lock_timeoutthìlock_timeoutkhông bao giờ bắn.transaction_timeout≤ một trong hai timeout idle/statement thì timeout dài hơn bị bỏ qua. Prepared transaction không chịutransaction_timeout.
deadlock_timeout và log_lock_waits
Phần tiêu đề “deadlock_timeout và log_lock_waits”deadlock_timeout (mặc định 1s) là thời gian chờ trước khi server kiểm tra deadlock. Nó không huỷ lần chờ bình thường.
log_lock_waits ghi log khi một lần chờ vượt deadlock_timeout. Mặc định off ở PostgreSQL 17 và 18. Hai tham số này chỉ superuser hoặc role có quyền SET tương ứng đổi được.
Cách hoạt động
Phần tiêu đề “Cách hoạt động”Chuỗi chờ trong tình huống trên, với lock_timeout = 2s cho role API; idle timeout của role script rút còn 5 giây cho gọn hình:
admin-script UPDATE sản phẩm 101 rồi để transaction mở: idle in transaction, row lock vẫn giữ.
| Bước | Ai giữ lock | Ai chờ | Pool |
|---|---|---|---|
| 0 | admin-script, idle in transaction | Không ai | Bình thường |
| 1–3 | admin-script | order-api-1 (transactionid), order-api-2 (tuple) |
Cạn khi đủ 10 waiter |
| 4 | admin-script | Waiter nhận 55P03 sau 2 giây |
Connection quay về |
| 5–6 | Không ai: session admin bị đóng, rollback | Request mới lấy lock ngay | Hồi phục |
lock_timeout bảo vệ pool nhưng không gỡ blocker; idle_in_transaction_session_timeout gỡ blocker nhưng phải đợi đủ ngưỡng. Cần cả hai.
Chi tiết vận hành
Phần tiêu đề “Chi tiết vận hành”Đặt timeout ở đâu
Phần tiêu đề “Đặt timeout ở đâu”Tài liệu khuyên không đặt ba timeout huỷ lệnh/transaction trong postgresql.conf vì ảnh hưởng mọi session, gồm migration và bảo trì. Thứ tự ưu tiên từ thấp đến cao:
ALTER DATABASE ... SET.ALTER ROLE ... SET, rồiALTER ROLE ... IN DATABASE ... SET. Chỉ áp cho session mới.optionstrong connection string. Đã chạy thử: role đặt3s,options='-c lock_timeout=1s'cho1s.SETtrong session;SET LOCALchỉ đến hết transaction.
ALTER ROLE order_api SET lock_timeout = '2s';ALTER ROLE order_api SET statement_timeout = '5s';ALTER ROLE order_api SET idle_in_transaction_session_timeout = '10s';ALTER ROLE admin_ops SET idle_in_transaction_session_timeout = '5min';
BEGIN; -- migrationSET LOCAL lock_timeout = '500ms'; -- chỉ trong transaction này-- ALTER TABLE ...COMMIT;Qua PgBouncer chế độ transaction, SET cấp session có thể dính sang client khác; dùng ALTER ROLE hoặc SET LOCAL. Migration dùng lock_timeout ngắn kèm retry vì DDL đang chờ chặn mọi query sau nó (xem lock queue).
Quan hệ với timeout của app và driver
Phần tiêu đề “Quan hệ với timeout của app và driver”Theo timeout budget, timeout co lại khi đi vào trong: deadline request > statement_timeout > lock_timeout. Ví dụ deadline 8 giây: statement_timeout = 5s, lock_timeout = 2s.
Timeout phía app hết trước thì app ngừng chờ, nhưng server có thể vẫn chờ lock nếu driver không gửi cancel. Timeout phía server mới bảo đảm lần chờ dừng thật, kèm SQLSTATE để app map sang 503 hoặc retry.
Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”Demo: cây blocker → blocked
Phần tiêu đề “Demo: cây blocker → blocked”Chạy trên PostgreSQL 17.11 (examples/docker-compose.yml), schema demo_lock_timeouts. Ba session psql đặt application_name:
admin-script:BEGIN; UPDATE ... WHERE id = 101;rồi không gửi gì thêm.order-api-1, rồiorder-api-2:UPDATE ... SET stock = stock - 1 WHERE id = 101;.
Session thứ tư in cây chờ, gốc ở trên. Query gọi pg_blocking_pids() đúng một lần mỗi waiter vì hàm có chi phí:
WITH RECURSIVE waiting AS ( SELECT pid, pg_blocking_pids(pid) AS blockers FROM pg_stat_activity WHERE wait_event_type = 'Lock'), tree AS ( SELECT r.pid, 0 AS depth, ARRAY[r.pid] AS path FROM (SELECT DISTINCT unnest(blockers) AS pid FROM waiting) r WHERE r.pid NOT IN (SELECT pid FROM waiting) UNION ALL SELECT w.pid, t.depth + 1, t.path || w.pid FROM tree t JOIN waiting w ON t.pid = ANY (w.blockers) WHERE w.pid <> ALL (t.path))SELECT repeat(' ', t.depth) || t.pid AS pid_tree, a.application_name AS app, a.state, a.wait_event, round(extract(epoch FROM now() - a.xact_start)) AS xact_s, left(a.query, 45) AS queryFROM tree tJOIN pg_stat_activity a USING (pid)ORDER BY t.path; pid_tree | app | state | wait_event | xact_s | query-----------+--------------+---------------------+---------------+--------+----------------------------------------------- 17864 | admin-script | idle in transaction | ClientRead | 4 | UPDATE demo_lock_timeouts.products SET stock 17871 | order-api-1 | active | transactionid | 3 | UPDATE demo_lock_timeouts.products SET stock 17884 | order-api-2 | active | tuple | 2 | UPDATE demo_lock_timeouts.products SET stockCách đọc:
- Gốc không chờ gì (
ClientRead),state = idle in transaction,xact_startcũ nhất. querycủa session idle là câu cuối cùng đã chạy, không phải câu đang chạy.- Gốc có PID
0là prepared transaction. Waiter có nhiều blocker xuất hiện nhiều lần.
Output thật của các timeout
Waiter thứ ba với SET lock_timeout = '2s' và \set VERBOSITY verbose:
ERROR: 55P03: canceling statement due to lock timeoutLOCATION: ProcessInterrupts, postgres.c:3399Time: 2001.100 ms (00:02.001)Blocker đặt idle_in_transaction_session_timeout = '3s'; waiter vào sau khoảng 1 giây:
-- blockerFATAL: 25P03: terminating connection due to idle-in-transaction timeoutserver closed the connection unexpectedly-- waiterUPDATE 1Time: 1999.035 ms (00:01.999)SET statement_timeout = '1s'khi đang chờ lock:ERROR: canceling statement due to statement timeout(57014).SET transaction_timeout = '2s'; BEGIN; SELECT pg_sleep(3);:FATAL: 25P04: terminating connection due to transaction timeout, session bị đóng khi đang chạy.
Cancel hay terminate
Phần tiêu đề “Cancel hay terminate”Đang tải diagram…
pg_cancel_backend(pid): huỷ câu lệnh đang chạy; waiter bị cancel nhận57014.pg_terminate_backend(pid, timeout): đóng session, rollback, nhả mọi lock;timeout(ms) khiến hàm chờ process thoát thật.- Cần superuser, là thành viên role sở hữu session, hoặc có
pg_signal_backend.
Demo: sau pg_terminate_backend(pid, 5000) trên blocker, waiter kế tiếp UPDATE 1 ngay. PID và số giây đổi mỗi lần chạy.
Trade-offs
Phần tiêu đề “Trade-offs”Ưu điểm
-
lock_timeoutbiến chờ vô hạn thành lỗi nhanh có SQLSTATE riêng, giữ pool cho endpoint khác. -
Timeout cấp session tự gỡ blocker, không cần người trực.
< slot="cons"> >
-
Ngưỡng thấp gây lỗi giả lúc tải cao; app cần retry có giới hạn và idempotent.
-
Timeout session đóng connection: pool và driver phải chịu được
FATAL. -
Timeout chỉ cắt triệu chứng; transaction dài vẫn là lỗi thiết kế.
Nhược điểm
Checklist production
Phần tiêu đề “Checklist production”- Đặt
lock_timeout,statement_timeout,idle_in_transaction_session_timeoutbằngALTER ROLEcho từng role app; kiểm tra bằngSHOWtừ connection mới. - Giữ deadline request >
statement_timeout>lock_timeout. - Map
55P03,57014sang lỗi retry có giới hạn; coi25P03,25P04là connection chết. - Migration dùng
SET LOCAL lock_timeoutngắn kèm retry. - Bật
log_lock_waitstrên PostgreSQL 17/18. - Mọi connection đặt
application_nametheo service. - Cảnh báo theo số session
idle in transactionquá ngưỡng và số session chờLock. - Runbook: chạy query cây, lưu
queryvàxact_startcủa gốc, rồi mới terminate.
Sai lầm thường gặp
Phần tiêu đề “Sai lầm thường gặp”Lỗi hay gặp nhất lúc sự cố là xử lý nhầm session:
| Cách hiểu sai | Hệ quả | Cần làm |
|---|---|---|
deadlock_timeout giới hạn thời gian chờ |
Waiter vẫn chờ vô hạn | Dùng lock_timeout |
| Kill waiter đầu tiên thấy | Waiter sau lên đầu hàng | Đi ngược tới gốc |
query của session idle là câu đang chạy |
Đổ lỗi nhầm câu lệnh | Đó là câu cuối; xem state, xact_start |
lock_timeout bằng statement_timeout |
App chỉ thấy 57014, không biết là chờ lock |
Đặt lock_timeout nhỏ hơn rõ |
Tự kiểm tra
Phần tiêu đề “Tự kiểm tra”- Vì sao
lock_timeoutgiữ được pool nhưng không gỡ được blocker của sản phẩm 101? pg_blocking_pids()của order-api-2 trả về order-api-1. Kill order-api-1 thì sao?transaction_timeout = 30s,statement_timeout = 60s, một câu chạy 45 giây: lỗi gì, huỷ gì?- Vì sao timeout phía client hết hạn không chứng minh lần chờ lock trên server đã dừng?
Tóm tắt
Phần tiêu đề “Tóm tắt”Tham khảo
Phần tiêu đề “Tham khảo”- Client Connection Defaults: default và tương tác của bốn timeout.
- Error Codes:
55P03,57014,25P03,25P04. - Lock Management và Logging:
deadlock_timeout,log_lock_waits. - pg_stat_activity:
state,query,xact_start,wait_event_type. - Session Information Functions:
pg_blocking_pids(), hard/soft block, chi phí. - Server Signaling Functions: cancel, terminate, quyền.