{"slug": "lock-monitoring-production-lock-analysis", "title": "Lock Monitoring — Production Lock Analysis", "summary": "A developer outlines a method for monitoring lock contention in PostgreSQL by joining pg_stat_activity and pg_locks views and using pg_blocking_pids() to build a blocking tree. The approach helps quickly identify root causes of production freezes, such as idle-in-transaction sessions holding AccessExclusiveLock, reducing incident resolution from 30 minutes to 3 minutes.", "body_md": "`pg_stat_activity`\n\nmột mình không đủ, và join với `pg_locks`\n\nmới ra root cause\nLock contention trong Postgres hiếm khi báo bằng error — nó báo bằng `wait_event_type = 'Lock'`\n\nở `pg_stat_activity`\n\nvà bằng latency tăng từ phía application. Khi một incident xảy ra (\"API treo, không ai biết tại sao\"), thứ team cần trong 60 giây đầu là một bức tranh: **PID nào đang đợi, đợi lock loại gì trên object nào, bị block bởi PID nào, PID block đó đang chạy query gì và đã giữ transaction bao lâu**. `pg_stat_activity`\n\nmột mình chỉ trả lời được nửa câu hỏi (\"ai đang đợi\"); `pg_locks`\n\nmột mình chỉ trả lời nửa còn lại (\"ai giữ gì\"). Phải join hai view này — và bám theo `pg_blocking_pids()`\n\n— để dựng được blocking tree. Không có dashboard cho luồng dữ liệu này là lý do điển hình một production freeze kéo dài 30 phút thay vì 3 phút: incident commander phải mò ad-hoc bằng `psql`\n\n, gõ sai query, miss `idle in transaction`\n\nđang giữ `AccessExclusiveLock`\n\ncủa một migration nửa đời trước.\n\n`pg_locks`\n\nlà một view phơi nội dung trực tiếp của shared lock manager trong shared memory. Mỗi dòng là một **lock request** (đã granted hoặc đang chờ) thuộc một backend. Theo Postgres docs phần \"System Views → pg_locks\", các column then chốt: `locktype`\n\n(`relation`\n\n, `transactionid`\n\n, `tuple`\n\n, `virtualxid`\n\n, `advisory`\n\n...), `relation`\n\n(OID — join `pg_class`\n\n), `transactionid`\n\n, `virtualtransaction`\n\n, `pid`\n\n(backend PID), `mode`\n\n(`AccessShareLock`\n\n, `RowExclusiveLock`\n\n, `ShareUpdateExclusiveLock`\n\n, `AccessExclusiveLock`\n\n...), `granted`\n\n(bool), `fastpath`\n\n(lock đi qua fast-path tránh shared lock manager), và `waitstart`\n\n(timestamp bắt đầu chờ — bổ sung sau v14, hữu ích để đo lock wait time mà không cần snapshot diff).\n\n`pg_stat_activity`\n\nlà view phơi trạng thái runtime của mỗi backend: `pid`\n\n, `usename`\n\n, `datname`\n\n, `application_name`\n\n, `client_addr`\n\n, `backend_start`\n\n, `xact_start`\n\n, `query_start`\n\n, `state`\n\n(`active`\n\n, `idle`\n\n, `idle in transaction`\n\n, `idle in transaction (aborted)`\n\n), `wait_event_type`\n\n, `wait_event`\n\n, `backend_xid`\n\n, `backend_xmin`\n\n, `query`\n\n, `backend_type`\n\n. Đây là nguồn duy nhất biết một PID **đang chạy query gì** và **transaction sống bao lâu**.\n\nFunction `pg_blocking_pids(pid)`\n\n(docs phần \"System Information Functions → Session Information\") trả về array PID đang block PID truyền vào. Nó là wrapper trên lock manager: cho mỗi lock request `granted=false`\n\ncủa PID đó, tìm các lock granted xung đột (theo lock conflict matrix) và trả về PID giữ chúng. Function này quan trọng vì self-join thủ công trên `pg_locks`\n\nrất dễ sai cho `transactionid`\n\nlock, advisory lock, hay tuple lock — `pg_blocking_pids`\n\nđã encode đúng conflict matrix.\n\nQuery nền tảng để dựng blocking tree (gần như identical với mẫu nổi tiếng trong Postgres Wiki \"Lock Monitoring\"):\n\n```\nSELECT\n  blocked.pid                      AS blocked_pid,\n  blocked.usename                  AS blocked_user,\n  blocked.application_name         AS blocked_app,\n  blocked.wait_event_type,\n  blocked.wait_event,\n  now() - blocked.query_start      AS blocked_for,\n  left(blocked.query, 200)         AS blocked_query,\n  blocking.pid                     AS blocking_pid,\n  blocking.usename                 AS blocking_user,\n  blocking.application_name        AS blocking_app,\n  blocking.state                   AS blocking_state,\n  now() - blocking.xact_start      AS blocking_xact_age,\n  left(blocking.query, 200)        AS blocking_query\nFROM pg_stat_activity AS blocked\nJOIN LATERAL unnest(pg_blocking_pids(blocked.pid)) AS bpid ON true\nJOIN pg_stat_activity AS blocking ON blocking.pid = bpid\nWHERE blocked.wait_event_type = 'Lock'\nORDER BY blocked_for DESC;\n```\n\nMỗi dòng output là một cạnh trong wait-for graph live. Sort theo `blocked_for`\n\nđể đẩy người đợi lâu nhất lên đầu — gần như luôn là root cause cần kill hoặc cần điều tra.\n\n**Failure mode 1: idle in transaction giữ lock vô thời hạn.** Một backend\n\n`BEGIN; UPDATE ...; -- (quên COMMIT)`\n\nrồi để connection nằm im trong pool. Row-level lock (`xmax`\n\n+ heavyweight `transactionid`\n\nlock) nó nắm không bao giờ nhả; mọi `UPDATE`\n\n/`DELETE`\n\nchạm row đó đều treo. Triệu chứng phía app: vài endpoint cụ thể timeout, các endpoint khác vẫn ổn. Không có dashboard lock thì khó tìm — connection đứng ở `state = 'idle in transaction'`\n\nkhông xuất hiện trong query log, không có error nào ném. Code điển hình gây ra:\n\n``` python\n# Sai: workflow phía app gọi external API trong transaction\ndef update_order(order_id, payload):\n    conn = pool.getconn()\n    try:\n        conn.execute(\"BEGIN\")\n        conn.execute(\"UPDATE orders SET status='processing' WHERE id=%s\", (order_id,))\n        # Lock row order_id, sau đó gọi external API có thể treo vài phút\n        resp = http_client.post(\"https://payment.example/charge\", json=payload, timeout=120)\n        conn.execute(\"UPDATE orders SET status=%s WHERE id=%s\", (resp.status, order_id))\n        conn.execute(\"COMMIT\")\n    finally:\n        pool.putconn(conn)\n```\n\nKhi `http_client.post`\n\ntreo vì DNS chậm hay upstream rate-limit, transaction sống suốt thời gian đó, row `orders.id = order_id`\n\nlock chặt. Sửa: tách network I/O ra khỏi transaction (commit trạng thái intermediate `'pending'`\n\nrồi mở transaction mới sau khi external call xong), và bật `idle_in_transaction_session_timeout`\n\n(mặc định 0 = không bao giờ kill) ở cấp database hoặc role:\n\n```\nALTER ROLE app_user SET idle_in_transaction_session_timeout = '30s';\nALTER ROLE app_user SET statement_timeout = '10s';\nALTER ROLE app_user SET lock_timeout = '3s';\n```\n\n**Failure mode 2: DDL kẹt giữa hai DML, block toàn bộ traffic trên bảng.** Postgres lock queue là FIFO: khi `ALTER TABLE ... ADD COLUMN ... NOT NULL DEFAULT ...`\n\nxin `AccessExclusiveLock`\n\ntrên một bảng mà một `SELECT`\n\ndài đang giữ `AccessShareLock`\n\n, DDL nằm trong queue chờ. Đây là chỗ đau: mọi `SELECT`\n\nmới đến **sau** DDL cũng phải xếp hàng (vì conflict với `AccessExclusiveLock`\n\npending), kể cả khi pending lock chưa được granted. Một transaction analytics 5 phút trên bảng `orders`\n\n+ một migration vô tình chạy giữa peak = toàn bộ traffic chạm `orders`\n\ntreo 5 phút. Phía app thấy \"tất cả endpoint dùng `orders`\n\ncùng đơ\" — dễ nhầm với DB down.\n\n```\n-- Sai: chạy migration không có lock_timeout vào giờ peak\nALTER TABLE orders ADD COLUMN refund_reason text;\n-- Treo vì SELECT analytics đang chạy, đồng thời block mọi request mới\n```\n\nSửa: trước mọi DDL, set `lock_timeout`\n\nngắn để fail nhanh thay vì giữ queue:\n\n```\nSET lock_timeout = '2s';\nSET statement_timeout = '30s';\nALTER TABLE orders ADD COLUMN refund_reason text;\n-- Nếu không lấy được lock trong 2s, ném ERROR 55P03 (lock_not_available)\n-- queue được giải phóng, DML mới tiếp tục\n```\n\nPattern này được nhắc trong nhiều engineering blog (Braintree, GitLab, PayPal) như \"zero-downtime migration\" rule số một: không bao giờ chạy DDL không có `lock_timeout`\n\ntrên production.\n\n**Failure mode 3: lock contention không ai biết vì không monitor.** Workload \"chỉ chậm chứ không lỗi\" — p99 latency tăng từ vài chục ms lên gần một giây, nhưng error rate vẫn 0. Team chỉ thấy \"API hơi chậm\". Không có dashboard lock thì nguyên nhân thường bị quy nhầm cho \"CPU cao\", \"network chậm\", \"GC pause\". Sau hàng tuần, root cause hoá ra là một hot row (counter, balance, inventory) bị tất cả request `UPDATE`\n\ntheo kiểu `SELECT ... FOR UPDATE`\n\nrồi update — backend xếp hàng tuần tự trên row đó. `pg_stat_activity.wait_event = 'transactionid'`\n\n(chờ lock chuyển sang granted khi transaction kia commit) là dấu hiệu — không có dashboard thì không thấy.\n\n**Failure mode 4: autovacuum bị cancel/skip vì DDL pending.** Autovacuum xin `ShareUpdateExclusiveLock`\n\ntrên bảng để vacuum. Khi gặp một DDL pending (`AccessExclusiveLock`\n\n), autovacuum tự cancel để nhường — nhưng dưới load DDL chạy đều, autovacuum không bao giờ chạy được, bloat dồn, query plan chậm dần. Triệu chứng: `pg_stat_user_tables.last_autovacuum`\n\nngày càng cũ trên các bảng nóng. Đây là class incident không \"freeze\" tức thì nhưng vỡ chậm — chỉ có lock dashboard kết hợp vacuum dashboard mới bắt được.\n\n**Bật log trước.** Trên production luôn bật:\n\n```\nlog_lock_waits = on\ndeadlock_timeout = 1s\nlog_line_prefix = '%m [%p] %a %u@%d '\nlog_min_duration_statement = 1000\n```\n\nKhi một backend đợi lock vượt `deadlock_timeout`\n\n, Postgres in một dòng `LOG: process N still waiting for ... after X ms ... DETAIL: Process holding the lock: M`\n\n. Đây là nguồn truth nhất cho post-mortem khi không có ai snapshot `pg_locks`\n\nđúng thời điểm.\n\n**Query blocking tree** (xem trong \"Cơ chế hoạt động\") là query xương sống. Lưu nó thành view trong schema `ops`\n\nđể incident commander chỉ cần `SELECT * FROM ops.blocking_tree`\n\n. Variant tổng hợp theo PID giữ lock — đếm số PID bị block và tổng thời gian chờ — phơi ra như Grafana panel \"top blockers\".\n\n**Snapshot pg_locks thô khi cần độ sâu**:\n\n```\nSELECT l.locktype, l.mode, l.granted, l.pid,\n       c.relname, l.transactionid, l.virtualtransaction,\n       a.state, a.wait_event_type, a.wait_event,\n       now() - a.xact_start AS xact_age,\n       left(a.query, 120) AS query\nFROM pg_locks l\nLEFT JOIN pg_class c ON c.oid = l.relation\nLEFT JOIN pg_stat_activity a ON a.pid = l.pid\nWHERE l.database = (SELECT oid FROM pg_database WHERE datname = current_database())\nORDER BY l.granted, c.relname;\n```\n\nXem cả granted lẫn pending trên cùng object để hiểu thứ tự queue.\n\n**Metric phơi ra Grafana.** `postgres_exporter`\n\n(Prometheus Community) phơi sẵn các series quan trọng: `pg_locks_count`\n\n(gauge theo `mode`\n\n, `database`\n\n), `pg_stat_activity_count`\n\n(theo `state`\n\n), `pg_stat_database_xact_commit`\n\n/`_rollback`\n\n, `pg_stat_database_deadlocks`\n\n. Bổ sung custom query trong file YAML của exporter để phơi blocking tree counter — ví dụ một metric đếm số session đang đợi lock, và max thời gian chờ. Hai metric này lên Grafana làm cảnh báo:\n\n`idle in transaction`\n\nvượt ngưỡng → leak transaction từ app.**Rule phòng ngừa.** Mọi role app set `statement_timeout`\n\n, `lock_timeout`\n\n, `idle_in_transaction_session_timeout`\n\nmặc định. Mọi migration tự động set `lock_timeout`\n\nngắn. Mọi connection pool config `idle timeout`\n\n< `idle_in_transaction_session_timeout`\n\ncủa Postgres để pool reset connection trước khi Postgres kill. Lock dashboard luôn-on, không phải \"dựng khi cần\" — vì khi cần thì đã trễ.\n\n`pg_locks`\n\nđược phơi từ shared lock manager — đọc nó tốn một short-lived shared lock trên lock manager partition, không free. Polling 1Hz từ exporter là rẻ; polling tần số cao từ nhiều exporter song song có thể tạo contention nhẹ trên chính lock manager trên cluster lớn có hàng chục nghìn lock đồng thời. Trade: tần số scrape thấp = phát hiện trễ; tần số cao = overhead nhỏ nhưng có. Rule thực tế: 5–15s scrape interval cho cluster production size vừa, không cần dưới 1s trừ khi đang truy một incident.\n\n`log_lock_waits = on`\n\nviết thêm vào server log mỗi lần lock wait vượt `deadlock_timeout`\n\n. Trên cluster hot, log có thể phình nhanh; nhưng tắt nó = mất khả năng post-mortem. Cách giữ là kèm log rotation/pipeline (rsyslog → Loki/CloudWatch) thay vì để file local phình. Đừng giảm `deadlock_timeout`\n\nxuống vài chục ms chỉ để có nhiều log hơn — Postgres docs cảnh báo: mọi lock wait vượt ngưỡng kích hoạt graph algorithm dò deadlock, CPU lãng phí.\n\nCó dashboard không bằng có alert đúng. Một dashboard đẹp mà không ai nhìn lúc 3 giờ sáng vô nghĩa. Ngược lại, alert quá nhạy (mỗi lock wait vài trăm ms cũng page) tạo alert fatigue, ops mute alert, incident thật miss. Quy tắc: alert trên **sustained** blocking (đợi > N giây trong > M giây), không alert trên transient lock wait — Postgres luôn có lock wait ngắn dưới load bình thường.\n\nQuy tắc thực tế: **dashboard lock + alert blocking tree là bắt buộc cho mọi production Postgres, ngang với CPU/disk/replication lag — đặt statement_timeout/lock_timeout/idle_in_transaction_session_timeout mặc định ở mọi role app, bật log_lock_waits = on, scrape pg_locks qua exporter mỗi 5–15s, và alert theo blocking duration chứ không theo lock count tức thời**.\n\nMột service Postgres production bỗng treo, latency mọi endpoint tăng vọt nhưng CPU và disk vẫn thấp. Debug thế nào, dùng view/function nào, và sửa ra sao?\n\nTriệu chứng \"latency tăng đều mà CPU thấp\" gần như chắc chắn là lock contention chứ không phải resource saturation. Bước đầu: vào psql với superuser, chạy query join `pg_stat_activity`\n\nvới `pg_blocking_pids()`\n\nđể dựng blocking tree — mỗi backend có `wait_event_type = 'Lock'`\n\nđược unnest qua `pg_blocking_pids(pid)`\n\nđể lấy danh sách PID block nó, rồi join lại `pg_stat_activity`\n\nđể biết PID block đang chạy query gì, `state`\n\n, và `xact_start`\n\ncách bây giờ bao lâu. Sort theo `now() - query_start DESC`\n\nđể root cause ở đầu.\n\nCác pattern hay gặp: (1) PID block ở `state = 'idle in transaction'`\n\nvới `xact_age`\n\nnhiều phút — một backend `BEGIN ... UPDATE ...`\n\nrồi gọi external API treo, không bao giờ commit, lock row hot bị mọi request mới đụng vào treo theo. Fix tức thì: `SELECT pg_terminate_backend(pid)`\n\ncái idle-in-tx; fix lâu dài: bật `idle_in_transaction_session_timeout`\n\nở role, không bao giờ gọi network I/O trong transaction. (2) Một DDL pending `AccessExclusiveLock`\n\ntrên bảng nóng làm mọi `SELECT`\n\nmới đến sau cũng phải xếp hàng dù DDL chưa granted (Postgres lock queue FIFO) — kill DDL hoặc đợi nó xong; sau đó luôn chạy DDL với `SET lock_timeout = '2s'`\n\n. (3) Hot row contention — nhiều backend đợi `transactionid`\n\ncủa một transaction đang `UPDATE`\n\ncùng row; fix bằng chia hot row (counter sharding), batch update, hoặc đổi data model.\n\nĐể không lặp lại incident, dashboard lock phải tồn tại sẵn: `postgres_exporter`\n\nscrape `pg_locks_count by (mode)`\n\n, `pg_stat_activity_count by (state)`\n\n, và custom query đếm số session đang chờ lock + thời gian chờ dài nhất. Alert trên `idle_in_transaction count > N`\n\n, `longest blocked seconds > 30s`\n\n, không alert trên lock count tức thời (luôn dao động). Bật `log_lock_waits = on`\n\nđể mọi lock wait > `deadlock_timeout`\n\nđẩy vào log, ship sang Loki/CloudWatch để post-mortem. Điểm ăn điểm là gọi đúng tên `pg_blocking_pids`\n\n, phân biệt được `wait_event = 'transactionid'`\n\n(đợi xmax) vs `wait_event = 'relation'`\n\n(đợi heavyweight relation lock), nêu được lock queue FIFO làm DDL pending block luôn DML mới, và nhớ `idle_in_transaction_session_timeout`\n\n/`lock_timeout`\n\nlà defense in depth bắt buộc.\n\nMục tiêu: dựng Postgres + postgres_exporter + Prometheus + Grafana, tái hiện ba kịch bản (idle-in-transaction giữ row lock, DDL block traffic, hot row contention), viết view `ops.blocking_tree`\n\n, scrape qua exporter, dựng Grafana panel + alert.\n\n**Bước 1 — dựng stack monitoring đầy đủ với docker-compose.**\n\nTạo thư mục lab:\n\n```\nmkdir -p ~/pg-lock-lab && cd ~/pg-lock-lab\n```\n\nFile `docker-compose.yml`\n\n:\n\n```\nservices:\n  postgres:\n    image: postgres:16\n    environment:\n      POSTGRES_PASSWORD: pw\n    command:\n      - \"postgres\"\n      - \"-c\"\n      - \"log_lock_waits=on\"\n      - \"-c\"\n      - \"log_line_prefix=%m [%p] %a %u@%d \"\n      - \"-c\"\n      - \"log_min_duration_statement=500\"\n      - \"-c\"\n      - \"deadlock_timeout=1s\"\n    ports:\n      - \"5440:5432\"\n  exporter:\n    image: quay.io/prometheuscommunity/postgres-exporter\n    environment:\n      DATA_SOURCE_NAME: \"postgresql://postgres:pw@postgres:5432/postgres?sslmode=disable\"\n    ports:\n      - \"9187:9187\"\n    depends_on:\n      - postgres\n  prometheus:\n    image: prom/prometheus\n    volumes:\n      - \"./prometheus.yml:/etc/prometheus/prometheus.yml\"\n    ports:\n      - \"9090:9090\"\n  grafana:\n    image: grafana/grafana\n    environment:\n      GF_AUTH_ANONYMOUS_ENABLED: \"true\"\n      GF_AUTH_ANONYMOUS_ORG_ROLE: Admin\n    ports:\n      - \"3000:3000\"\n```\n\nFile `prometheus.yml`\n\n:\n\n```\nglobal:\n  scrape_interval: 5s\nscrape_configs:\n  - job_name: postgres\n    static_configs:\n      - targets:\n          - \"exporter:9187\"\n```\n\nChạy stack: `docker compose up -d && sleep 8`\n\n.\n\n**Bước 2 — schema và view monitoring.**\n\nVào psql: `docker compose exec postgres psql -U postgres`\n\n, rồi:\n\n```\nCREATE SCHEMA ops;\n\nCREATE OR REPLACE VIEW ops.blocking_tree AS\nSELECT\n  blocked.pid                    AS blocked_pid,\n  blocked.application_name       AS blocked_app,\n  blocked.wait_event_type,\n  blocked.wait_event,\n  EXTRACT(EPOCH FROM (now() - blocked.query_start)) AS blocked_for_s,\n  left(blocked.query, 200)       AS blocked_query,\n  blocking.pid                   AS blocking_pid,\n  blocking.application_name      AS blocking_app,\n  blocking.state                 AS blocking_state,\n  EXTRACT(EPOCH FROM (now() - blocking.xact_start)) AS blocking_xact_age_s,\n  left(blocking.query, 200)      AS blocking_query\nFROM pg_stat_activity blocked\nJOIN LATERAL unnest(pg_blocking_pids(blocked.pid)) AS bpid ON true\nJOIN pg_stat_activity blocking ON blocking.pid = bpid\nWHERE blocked.wait_event_type = 'Lock';\n\nCREATE TABLE accounts (\n  id bigint PRIMARY KEY,\n  owner text,\n  balance numeric(12,2) NOT NULL DEFAULT 0\n);\nINSERT INTO accounts SELECT g, 'u'||g, 1000 FROM generate_series(1, 10000) g;\n```\n\n**Bước 3 — tái hiện idle-in-transaction giữ lock.**\n\nTerminal A (mở session, BEGIN + UPDATE, không commit):\n\n```\ndocker compose exec postgres psql -U postgres\n```\n\nTrong psql:\n\n```\nBEGIN;\nUPDATE accounts SET balance = balance - 1 WHERE id = 42;\n-- để psql nằm đây, không gõ gì thêm\n```\n\nTerminal B (cùng lúc, request mới đụng row 42):\n\n```\ndocker compose exec postgres psql -U postgres -c \"SET lock_timeout = '5s'; UPDATE accounts SET balance = balance + 1 WHERE id = 42;\"\n# Treo, sau 5s ném ERROR 55P03 lock_not_available\n```\n\nTerminal C — quan sát blocking tree live:\n\n```\ndocker compose exec postgres psql -U postgres -c \"SELECT * FROM ops.blocking_tree;\"\n# Thấy blocked_pid (terminal B), blocking_pid (terminal A) với blocking_state = idle in transaction\n```\n\n**Bước 4 — fix bằng idle_in_transaction_session_timeout.**\n\n```\ndocker compose exec postgres psql -U postgres -c \"ALTER ROLE postgres SET idle_in_transaction_session_timeout = '10s';\"\n# Tạo lại session A như Bước 3, đợi 10s — Postgres tự terminate, lock nhả\ndocker compose logs postgres 2>&1 | grep -i idle-in-transaction\n```\n\n**Bước 5 — tái hiện DDL block traffic.**\n\nTerminal A — chạy SELECT dài giả lập analytics:\n\n```\ndocker compose exec postgres psql -U postgres -c \"BEGIN; SELECT pg_sleep(60), count(*) FROM accounts;\"\n```\n\nTerminal B — chạy DDL không có lock_timeout:\n\n```\ndocker compose exec postgres psql -U postgres -c \"ALTER TABLE accounts ADD COLUMN note text;\"\n# Treo — đợi terminal A\n```\n\nTerminal C — request mới đến sau DDL cũng kẹt:\n\n```\ndocker compose exec postgres psql -U postgres -c \"SELECT count(*) FROM accounts;\"\n# Cũng treo, dù chỉ cần AccessShareLock — vì AccessExclusiveLock của DDL đang pending phía trước\n```\n\nTerminal D — xem blocking tree để confirm:\n\n```\ndocker compose exec postgres psql -U postgres -c \"SELECT * FROM ops.blocking_tree;\"\n```\n\nSửa: kill DDL (terminal B Ctrl-C) và chạy lại đúng cách:\n\n```\ndocker compose exec postgres psql -U postgres -c \"SET lock_timeout='2s'; ALTER TABLE accounts ADD COLUMN note text;\"\n# Fail nhanh sau 2s với ERROR 55P03, queue được giải phóng, traffic chạy tiếp\n```\n\n**Bước 6 — Grafana dashboard.**\n\nMở `http://localhost:3000`\n\n, add datasource Prometheus `http://prometheus:9090`\n\n. Tạo dashboard với các panel:\n\n`sum by (mode) (pg_locks_count)`\n\n— stack chart, thấy spike `AccessExclusiveLock`\n\nkhi có DDL.`sum by (state) (pg_stat_activity_count)`\n\n— thấy `idle in transaction`\n\nleak ngay.`rate(pg_stat_database_deadlocks_total[5m])`\n\n.`SELECT * FROM ops.blocking_tree ORDER BY blocked_for_s DESC`\n\n.Alert quan trọng nhất là `longest blocked seconds`\n\n. Thêm vào file `queries.yml`\n\nrồi mount vào exporter với flag `--extend.query-path=/etc/postgres_exporter/queries.yml`\n\n:\n\n```\npg_blocking:\n  query: |\n    SELECT count(*) AS blocked_sessions,\n           COALESCE(max(EXTRACT(EPOCH FROM (now() - query_start))), 0) AS longest_blocked_seconds\n    FROM pg_stat_activity WHERE wait_event_type = 'Lock'\n  metrics:\n    - blocked_sessions:\n        usage: GAUGE\n        description: Sessions waiting on a lock\n    - longest_blocked_seconds:\n        usage: GAUGE\n        description: Longest lock wait in seconds\n```\n\nAlert `pg_blocking_longest_blocked_seconds > 30`\n\ncho ra page khi có session kẹt > 30s — đây là alert có giá trị nhất trong toàn bộ stack lock monitoring.\n\n**Dọn dẹp:** `docker compose down -v`\n\n.", "url": "https://wpnews.pro/news/lock-monitoring-production-lock-analysis", "canonical_source": "https://dev.to/nguoidungai/lock-monitoring-production-lock-analysis-4301", "published_at": "2026-07-07 15:49:43+00:00", "updated_at": "2026-07-07 15:58:41.125736+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["PostgreSQL", "pg_stat_activity", "pg_locks", "pg_blocking_pids"], "alternates": {"html": "https://wpnews.pro/news/lock-monitoring-production-lock-analysis", "markdown": "https://wpnews.pro/news/lock-monitoring-production-lock-analysis.md", "text": "https://wpnews.pro/news/lock-monitoring-production-lock-analysis.txt", "jsonld": "https://wpnews.pro/news/lock-monitoring-production-lock-analysis.jsonld"}}