Monitoring, Logging & Chẩn đoánMonitoring, Logging & Diagnostics
Thuộc bộ kiến thức PostgreSQL DBA Roadmap.
Tổng quan
Mọi sự cố production đều bắt đầu bằng cùng một câu hỏi: “database đang thực sự làm gì ngay lúc này?” PostgreSQL trả lời câu hỏi đó ở ba lớp khác nhau, và một DBA giỏi cần thông thạo cả ba. Lớp trong cùng là statistics collector — tập hợp các bộ đếm và trạng thái phiên mà PostgreSQL tự duy trì nội bộ, phơi bày qua họ view hệ thống pg_stat_* — đây là nơi bạn nhìn vào đầu tiên, vì không cần công cụ ngoài và phản ánh đúng thời điểm bạn query. Lớp giữa là file log, nơi ghi lại những thứ mà các view thống kê không có (nội dung SQL gốc của một câu chậm, thông báo lỗi chi tiết, sự kiện kết nối/ngắt kết nối, hoạt động checkpoint) và các công cụ chuyên dụng như pgBadger sẽ biến nó thành báo cáo dễ đọc. Lớp ngoài cùng là hệ điều hành, bởi vì suy cho cùng PostgreSQL cũng chỉ là một tập hợp các tiến trình Unix bình thường tranh giành CPU, bộ nhớ, và disk I/O — top, iotop, và sar cho bạn biết những thứ mà không câu SQL nào có thể cho biết, ví dụ như chính cái máy có còn dư tài nguyên hay không.
Note này đi qua cả ba lớp và thêm một lớp thứ tư: các công cụ dùng khi cùng đường (strace, gdb, core dump, perf) mà bạn chỉ cần đến khi ba lớp trên không giải thích được một backend đang bị treo hoặc crash đang làm gì. Mục tiêu không phải là học thuộc mọi flag của mọi công cụ, mà là xây dựng phản xạ chẩn đoán — với một triệu chứng cho trước, biết ngay nên nhìn vào lớp nào trước và lớp đó trả lời câu hỏi gì. Note này giả định bạn đã quen với ./09-transactions-and-concurrency-control.md (lock, snapshot, transaction dài) và ./10-query-planning-and-performance-tuning.md (EXPLAIN, index, pg_stat_statements), vì chẩn đoán chính là nơi lý thuyết đó được áp dụng dưới áp lực thời gian thực. Nó cũng phản chiếu, ở phạm vi database, mô hình monitoring SRE tổng quát trong ../devops/en/15-monitoring-and-observability.md — khung “four golden signals” áp dụng gần như nguyên vẹn vào những gì một DBA theo dõi.
Kiến thức nền tảng
Statistics collector và các view pg_stat_*
PostgreSQL liên tục theo dõi hoạt động — bao nhiêu dòng được insert vào mỗi bảng, bao nhiêu index scan đã chạy, bao nhiêu transaction commit hay rollback, mỗi backend hiện tại đang làm gì — và phơi bày tất cả qua các view hệ thống có tiền tố pg_stat_ (các bộ đếm tích lũy, gần như luôn tăng dần) và pg_statio_ (bộ đếm riêng cho I/O: block hit so với block đọc từ disk). Trước đây, cơ chế này được thực hiện bởi một tiến trình nền riêng gọi là statistics collector mà các backend gửi thông điệp UDP tới; từ PostgreSQL 15, các bộ đếm này nằm trực tiếp trong shared memory, loại bỏ tiến trình collector cũ cùng các vấn đề độ trễ/độ tin cậy đi kèm — nhưng tên view và ngữ nghĩa bạn query thì không đổi.
Một vài view trong số này đáng nhớ tên:
| View | Cho biết gì |
|---|---|
pg_stat_activity | Mọi kết nối hiện tại, trạng thái, và câu query đang chạy |
pg_stat_database | Tổng số theo từng database: commit, rollback, block đọc/hit, deadlock, sử dụng temp file |
pg_stat_user_tables | Số lượt scan theo từng bảng (seq vs. index), số dòng insert/update/delete, lịch sử vacuum/analyze |
pg_stat_user_indexes | Số lượt scan theo từng index — cách kinh điển để tìm index không được dùng |
pg_stat_bgwriter | Hoạt động checkpoint và background writer |
pg_statio_user_tables | Tỷ lệ buffer cache hit so với đọc disk theo từng bảng |
pg_stat_statements | Thống kê hiệu năng tổng hợp theo từng query đã chuẩn hóa (cần cài extension) |
Vì hầu hết các bộ đếm này tích lũy kể từ lần reset stats gần nhất hoặc từ khi server khởi động lại, một con số thô hiếm khi có ý nghĩa gì nhiều — bạn cần so sánh tốc độ thay đổi theo thời gian (chụp snapshot bây giờ, chụp lại sau năm phút, trừ đi), hoặc reset bộ đếm tại một mốc đã biết bằng SELECT pg_stat_reset(); (toàn database) hoặc SELECT pg_stat_reset_single_table_counters(oid); trước khi bắt đầu một bài test có kiểm soát.
Cấu hình logging
Log output của PostgreSQL được cấu hình trong postgresql.conf (hoặc bằng ALTER SYSTEM, hoặc theo từng role/database với ALTER ROLE ... SET), và trên thực tế đây là artifact chẩn đoán giàu thông tin nhất bạn có cho bất cứ điều gì các view thống kê không nắm bắt được — nội dung SQL gốc của một câu chậm, chi tiết đầy đủ của một deadlock, một lỗi xác thực thất bại, những dòng cuối cùng trước khi một tiến trình chết.
Setting quan trọng nhất cho công việc tối ưu hiệu năng là log_min_duration_statement, ghi lại nội dung của bất kỳ câu lệnh nào chạy lâu hơn ngưỡng cấu hình (tính bằng mili-giây):
# postgresql.conf
log_min_duration_statement = 250 # log mọi câu lệnh chạy >= 250 ms
Đặt quá thấp (ví dụ 0, ghi mọi câu lệnh) sẽ nhấn chìm log và thêm chi phí I/O cho mọi query; đặt quá cao thì bỏ lỡ những query thực sự quan trọng. Cách làm phổ biến là chạy ở ngưỡng vừa phải (100–500 ms) thường trực trên production, rồi tạm thời đặt về 0 cho một session hoặc role khi cần điều tra tập trung:
-- chỉ log mọi thứ cho session này, trong vài phút tới
SET log_min_duration_statement = 0;
-- ... tái hiện lại sự cố ...
RESET log_min_duration_statement;
log_statement là một setting thô hơn, bổ sung, kiểm soát loại câu lệnh nào được log bất kể thời gian chạy — none (mặc định), ddl (chỉ thay đổi schema — baseline thường trực tốt cho mục đích audit), mod (DDL cộng thêm INSERT/UPDATE/DELETE), hoặc all (mọi thứ, rất ồn, hiếm khi phù hợp ngoài lúc debug ngắn hạn).
log_line_prefix kiểm soát metadata nào đứng trước mỗi dòng log, và thiết lập đúng ngay từ đầu sẽ tiết kiệm rất nhiều đau đầu về sau khi bạn cần đối chiếu một query chậm với một session, user, hoặc application cụ thể. Một baseline production vững chắc:
log_line_prefix = '%m [%p] %q%u@%d/%a '
| Escape | Ý nghĩa |
|---|---|
%m | Timestamp kèm mili-giây |
%p | Process ID của backend |
%u | Tên user database |
%d | Tên database |
%a | Application name (client set qua application_name trong connection string) |
%q | Không có gì cho message của backend; ẩn các trường phía sau đối với message không thuộc session (checkpointer, v.v.) |
%l | Số thứ tự dòng log trong session này |
%r | Remote host và port |
Sau khi thay đổi các setting này, pg_ctl reload hoặc SELECT pg_reload_conf(); là đủ — không cái nào cần restart, vì tất cả đều được phân loại context = sighup trong pg_settings. Nơi log thực sự được ghi ra được kiểm soát riêng bởi logging_collector, log_destination (stderr, csvlog, hoặc cả hai — csvlog là định dạng pgBadger và hầu hết công cụ parse ưa thích, vì nó không mơ hồ khi parse), và log_directory/log_filename.
pg_stat_activity: view cần kiểm tra đầu tiên
Nếu chỉ được nhớ một câu query chẩn đoán, hãy chọn câu này. pg_stat_activity có một dòng cho mỗi tiến trình server (backend), và nó đồng thời là danh sách kết nối, monitor query hiện tại, và chỉ báo chờ lock:
SELECT pid, usename, datname, application_name, client_addr,
state, wait_event_type, wait_event,
backend_start, xact_start, query_start,
state_change, query
FROM pg_stat_activity
WHERE datname = 'myapp'
ORDER BY query_start;
Cột state là thứ đầu tiên cần đọc, và mỗi giá trị mang một ý nghĩa cụ thể:
| State | Ý nghĩa |
|---|---|
active | Đang thực thi một query |
idle | Đang chờ client gửi lệnh mới — bình thường, là baseline của connection pool |
idle in transaction | Một transaction đang mở (BEGIN đã được gọi) nhưng không có câu lệnh nào đang chạy |
idle in transaction (aborted) | Giống trên, nhưng câu lệnh cuối cùng bị lỗi — client phải gọi ROLLBACK trước khi bất cứ điều gì khác được chấp nhận |
fastpath function call | Đang thực thi một fast-path function call (hiếm, mức thấp) |
disabled | track_activities bị tắt cho session này |
idle in transaction xứng đáng được chú ý đặc biệt vì đây là một trong những nguyên nhân phổ biến nhất gây suy giảm hiệu năng production một cách bí ẩn. Một kết nối nằm ở trạng thái này đang giữ một transaction mở — và do đó một MVCC snapshot mở — vô thời hạn, dù nó không làm gì cả. Hai tác hại cụ thể xuất phát trực tiếp từ đây: thứ nhất, bất kỳ row lock nào transaction đó đã lấy trước đó (từ một UPDATE, SELECT ... FOR UPDATE, v.v.) vẫn được giữ, nên các session khác cố chạm vào những dòng đó chỉ đơn giản xếp hàng chờ, thường vô hình cho đến khi ai đó nhận ra một đống query bị block. Thứ hai, và nguy hiểm hơn, VACUUM không thể dọn các dead tuple mới hơn snapshot cũ nhất mà bất kỳ session nào còn giữ — đây chính là vấn đề “xmin horizon” đã bàn trong ./09-transactions-and-concurrency-control.md. Một kết nối idle in transaction bị quên duy nhất, do một bug trong cách application xử lý connection pool (trường hợp kinh điển: ORM mở transaction, một exception được ném ra trước khi commit/rollback, và kết nối được trả về pool trong khi vẫn “đang mở”) có thể âm thầm ngăn autovacuum tiến triển trên một bảng nóng suốt nhiều giờ, và triệu chứng đầu tiên nhìn thấy được thường là bloat trông có vẻ không liên quan, hoặc chi phí sequential scan tăng đột ngột — chứ không phải một cảnh báo “transaction bị treo” rõ ràng.
Để tìm ra thủ phạm, lọc riêng theo state đó và sắp xếp theo thời gian đã mở:
SELECT pid, usename, datname, application_name, client_addr,
state, now() - xact_start AS xact_age, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY xact_start ASC;
Tổng quát hơn, để tìm các query active chạy lâu (không chỉ session idle-in-transaction):
SELECT pid, usename, datname, now() - query_start AS running_for, state, query
FROM pg_stat_activity
WHERE state != 'idle'
AND query_start < now() - interval '5 minutes'
ORDER BY query_start ASC;
Sau khi xác định được một backend có vấn đề qua pid, PostgreSQL cho bạn hai cách can thiệp theo mức độ tăng dần. pg_cancel_backend(pid) gửi thứ tương đương một yêu cầu hủy — nó ngắt câu query đang chạy nhưng giữ nguyên session và trạng thái transaction (nên nếu đang giữa transaction, transaction đó giờ đã bị abort và chờ ROLLBACK, chứ không biến mất). pg_terminate_backend(pid) là công cụ mạnh tay hơn: nó đóng cưỡng bức toàn bộ kết nối, như thể client đã ngắt kết nối, rollback bất kỳ transaction mở nào theo đó.
-- lịch sự: hủy câu lệnh đang chạy, giữ kết nối
SELECT pg_cancel_backend(12345);
-- mạnh tay: giết toàn bộ session/kết nối
SELECT pg_terminate_backend(12345);
Cả hai đều yêu cầu quyền superuser, hoặc là cùng role sở hữu backend mục tiêu (trừ một số tiến trình background/replication từ chối bị terminate vì lý do an toàn). Một biện pháp bảo vệ thực tế nên thiết lập ở production thay vì trông chờ vào can thiệp thủ công là idle_in_transaction_session_timeout, khiến PostgreSQL tự động terminate các session nằm ở idle in transaction quá một ngưỡng:
ALTER SYSTEM SET idle_in_transaction_session_timeout = '10min';
SELECT pg_reload_conf();
pg_stat_statements: điểm dừng đầu tiên cho câu hỏi “cái gì đang chậm”
pg_stat_activity cho biết cái gì đang chạy ngay bây giờ; nó không nói gì về việc query nào chậm tính trung bình trên toàn bộ workload. Đó là việc của extension pg_stat_statements, tổng hợp thống kê thực thi (số lần gọi, tổng/trung bình/max thời gian, số dòng, số lần hit shared buffer) theo từng query đã chuẩn hóa (các hằng số bị loại bỏ, nên WHERE id = 1 và WHERE id = 2 được tính là cùng một entry). Nó được trình bày chi tiết, bao gồm cả cách cài đặt và câu query kinh điển “top N query chậm nhất”, trong ./10-query-planning-and-performance-tuning.md; phiên bản ngắn gọn cho note này là khi ai đó nói “database cảm giác chậm” mà không có triệu chứng cụ thể, pg_stat_statements sắp theo total_exec_time gần như luôn là câu query đầu tiên bạn chạy, vì nó cho biết ngay vấn đề là một query bị chạy loạn hay thực sự là tải hệ thống dàn trải trên nhiều query.
Khái niệm chính
Bốn golden signal, áp dụng vào database
Vốn từ vựng monitoring SRE tổng quát — latency, traffic, errors, saturation — ánh xạ gần như chính xác vào một instance PostgreSQL, và tổ chức dashboard/alert của bạn quanh bốn hạng mục này (thay vì một đống metric không có cấu trúc) là cách nhanh nhất để đánh giá “database có khỏe không” chỉ bằng một cái nhìn thoáng qua. Xem ../devops/en/15-monitoring-and-observability.md cho khung tổng quát mà phần này chuyên biệt hóa.
| Signal | Ý nghĩa với PostgreSQL | Nhìn vào đâu |
|---|---|---|
| Latency | Query mất bao lâu để trả kết quả | pg_stat_statements.mean_exec_time, đồng hồ đo query phía application, slow-query log (log_min_duration_statement) |
| Traffic | Có bao nhiêu công việc đang đến | pg_stat_database.xact_commit + xact_rollback (transaction/giây), số kết nối trong pg_stat_activity, số query/giây từ delta của pg_stat_statements.calls |
| Errors | Công việc thất bại | Lỗi xác thực và lỗi câu lệnh trong log, pg_stat_database.deadlocks, kết nối bị từ chối do application báo lại |
| Saturation | Tài nguyên còn cách giới hạn bao xa | Số kết nối active so với max_connections, thời gian chờ của connection pool, tần suất checkpoint từ pg_stat_bgwriter, áp lực CPU/disk/memory ở mức OS (xem bên dưới) |
Một kiểm tra saturation cụ thể mọi DBA nên có trên dashboard là dư địa kết nối, vì cạn max_connections gây ra lỗi cứng (FATAL: sorry, too many clients already) chứ không suy giảm dần dần:
SELECT count(*) AS current_connections,
(SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max_connections
FROM pg_stat_activity;
Trên thực tế, hầu hết các team không để kết nối application tiến sát max_connections trực tiếp — một connection pooler (PgBouncer, hoặc pool tích hợp sẵn trong driver của application) đứng phía trước, và saturation của pool (client chờ một kết nối trong pool) là dấu hiệu cảnh báo sớm hơn, dễ hành động hơn so với giới hạn kết nối của bản thân PostgreSQL.
Công cụ phân tích log
Log thô của PostgreSQL đầy đủ nhưng không thân thiện để đọc ở quy mô lớn — một server bận rộn tạo ra hàng gigabyte mỗi ngày, và nhìn bằng mắt để tìm pattern không hiệu quả. Có một số công cụ chuyên dụng chỉ để biến luồng dữ liệu khổng lồ đó thành thứ con người (hoặc hệ thống alert) có thể hành động được.
pgBadger là công cụ phân tích log tiêu chuẩn trong hệ sinh thái PostgreSQL: một script Perl duy nhất parse các file log PostgreSQL (tốt nhất ở định dạng csvlog, mà nó parse không mơ hồ) và tạo ra một báo cáo HTML độc lập bao gồm slow query (xếp hạng theo tổng thời gian, theo số lần, theo thời gian trung bình), phân loại theo loại query, pattern kết nối và session theo thời gian, tỷ lệ lỗi và hủy query, hoạt động checkpoint và vacuum, cùng lock wait. Nó không cần extension phía server và không cần daemon chạy liên tục — bạn chỉ cần trỏ nó vào một file log (hoặc cả một thư mục rotate) sau khi sự việc đã xảy ra.
# đảm bảo csvlog đã được bật trước (postgresql.conf):
# log_destination = 'csvlog'
# logging_collector = on
pgbadger /var/lib/postgresql/data/log/postgresql-*.csv -o /tmp/pgbadger-report.html
# chế độ incremental: chạy hằng ngày qua cron, xây dựng một báo cáo theo thời gian
pgbadger --incremental --outdir /var/www/pgbadger /var/log/postgresql/postgresql-*.csv
pgcenter và pgcluu phủ một mảng khác — hiển thị trực tiếp kiểu top thay vì parse log sau sự việc. pgcenter top cho một dashboard dựa trên curses, tự động refresh, hiển thị song song pg_stat_activity, I/O theo bảng/index, và mức sử dụng tài nguyên hệ thống — chính xác là thứ bạn muốn mở khi đang giữa một sự cố. pgcluu chạy theo hai phần: một collector lấy mẫu nhẹ bạn để chạy liên tục (cron hoặc daemon) định kỳ chụp snapshot các view thống kê và metric OS, và một bộ tạo báo cáo riêng biến lịch sử thu thập được thành biểu đồ HTML có thể duyệt được — hữu ích khi bạn cần trả lời “số kết nối / tỷ lệ cache hit / dung lượng disk trông như thế nào trong hai tuần qua” thay vì chỉ ngay bây giờ.
check_pgactivity và check_pgbackrest là các plugin check tương thích Nagios — chúng không render dashboard, mà trả về một exit code kiểu Nagios (OK/WARNING/CRITICAL) cộng với một dòng trạng thái, được thiết kế để gắn vào bất kỳ hệ thống alert nào team bạn đang dùng (Nagios, Icinga, hoặc bất cứ thứ gì nói theo Nagios plugin API). check_pgactivity bao phủ một danh mục rộng các kiểm tra (số kết nối, bloat, replication lag, trạng thái backend, tình trạng autovacuum, và hàng chục thứ khác, mỗi cái cấu hình ngưỡng riêng độc lập); check_pgbackrest kiểm tra riêng sức khỏe và độ mới của backup pgBackRest, lấp khoảng trống phổ biến nơi một job backup âm thầm ngừng thành công mà không ai nhận ra cho đến khi cần restore.
temBoard lại có hình dạng khác: một UI web thường trực (agent trên mỗi instance PostgreSQL, ứng dụng web trung tâm tổng hợp chúng) kết hợp giám sát hoạt động trực tiếp, biểu đồ lịch sử, quản lý cấu hình, và các kiểm tra tư vấn tích hợp sẵn (index không dùng, thiếu primary key, v.v.) trong một nơi, hướng đến các team muốn một sản phẩm dashboard hơn là một tập hợp script rời rạc.
| Công cụ | Kiểu | Phù hợp nhất cho |
|---|---|---|
| pgBadger | Parse log offline → báo cáo HTML | Phân tích định kỳ/hồi cứu pattern query, lỗi, query chậm |
| pgcenter | Trực tiếp, terminal, kiểu top | Triage thời gian thực khi đang giữa sự cố |
| pgcluu | Collector lấy mẫu + báo cáo HTML lịch sử | Phân tích xu hướng theo ngày/tuần |
| check_pgactivity | Plugin Nagios | Tích hợp alert theo ngưỡng |
| check_pgbackrest | Plugin Nagios | Alert sức khỏe/độ mới của backup |
| temBoard | UI web, dựa trên agent | Dashboard đa instance thường trực + quản lý cấu hình |
Đưa metric PostgreSQL vào hệ thống monitoring tổng quát
Các công cụ chuyên dụng cho PostgreSQL rất tốt cho chiều sâu riêng của PostgreSQL, nhưng hầu hết tổ chức cũng muốn sức khỏe database hiển thị cùng nơi với mọi thứ khác — cùng dashboard Grafana, cùng pipeline paging on-call. Prometheus là đích đến phổ biến nhất: postgres_exporter chạy cùng với PostgreSQL (hoặc như một sidecar), tự query định kỳ các view pg_stat_*, và phơi bày kết quả trên một endpoint HTTP /metrics theo định dạng text của Prometheus để scrape — cùng mô hình exporter pull-based được dùng cho mọi _exporter khác trong hệ sinh thái đó. Zabbix đóng vai trò tích hợp tương tự qua template monitoring PostgreSQL riêng của nó, phổ biến hơn ở các nơi đã chuẩn hóa trên Zabbix từ trước khi Prometheus xuất hiện. Dù theo cách nào, cơ chế scrape, lưu trữ, vẽ biểu đồ, và alert trên các metric này là mối quan tâm chung của monitoring stack, không phải đặc thù PostgreSQL — xem ../devops/en/15-monitoring-and-observability.md để biết Prometheus, exporter, và pipeline alert khớp với nhau như thế nào nói chung.
Công cụ chẩn đoán ở mức hệ điều hành
Suy cho cùng, các backend PostgreSQL cũng chỉ là các tiến trình OS bình thường, và một phần lớn các sự cố “database chậm” hóa ra là do tranh chấp tài nguyên mà không câu SQL nào sẽ cho bạn thấy trực tiếp.
top (hoặc phiên bản thân thiện hơn htop) là lệnh đầu tiên chạy khi có gì đó cảm giác sai lệch trên diện rộng, vì nó trả lời “một tiến trình đang ngốn CPU, hay mọi thứ đều tải chung”:
top -c # -c hiện đầy đủ command line, để bạn thấy backend nào là backend nào
Mỗi backend PostgreSQL hiển thị như một tiến trình OS riêng (postgres: user dbname client_addr(port)), nên một backend chạy loạn ngốn 100% CPU (một query plan tệ đang sort hoặc hash khối lượng lớn, một recursive CTE chạy loạn, một vòng lặp chặt trong hàm PL/pgSQL) hiện ra ngay lập tức theo tiến trình, và bạn có thể đối chiếu trực tiếp PID của nó với pg_stat_activity.pid để biết chính xác nó đang chạy query nào.
iotop thu hẹp cùng ý tưởng đó vào disk I/O — thiết yếu khi triệu chứng là “mọi thứ đều chậm” nhưng CPU trông ổn, thường có nghĩa là có gì đó đang bị nghẽn I/O:
iotop -o # -o: chỉ hiện các tiến trình thực sự đang làm I/O
Một backend chạy sequential scan lớn, một VACUUM trên bảng bị bloat, hoặc một checkpoint đang flush một loạt lớn dirty buffer sẽ hiện ra ở đây với I/O cao, giúp bạn phân biệt “một query đang nặng I/O” với “toàn bộ subsystem disk đang bão hòa” (trường hợp này mọi tiến trình đều hiện I/O wait cao, không chỉ một tiến trình).
sysstat (cung cấp sar, iostat, và mpstat) là thứ bạn tìm đến khi cần dữ liệu tài nguyên hệ thống lịch sử thay vì một snapshot trực tiếp — cực kỳ quan trọng vì đến lúc bạn điều tra thì đợt tăng đột biến gây ra vấn đề có thể đã qua rồi.
# mức sử dụng CPU trong 24 giờ qua (sar mặc định xoay vòng giữ lịch sử)
sar -u
# lịch sử thống kê disk I/O
sar -d
# thống kê I/O theo thiết bị trực tiếp, refresh mỗi 2 giây
iostat -dx 2
Chẩn đoán chuyên sâu: strace, gdb, và core dump — phương án cuối cùng
Khi pg_stat_activity cho thấy một backend active với một query đơn giản là không bao giờ tiến triển, và log không ghi thêm gì mới, bạn đã dùng hết bộ công cụ thông thường và cần hỏi trực tiếp hệ điều hành xem tiến trình đó đang làm gì.
strace trace mọi system call mà một tiến trình đang chạy thực hiện, đúng là công cụ phù hợp cho câu hỏi “backend này có thực sự bị treo không, và treo ở đâu”:
# attach vào một PID backend cụ thể và theo dõi syscall trực tiếp
strace -p 12345
# chỉ tập trung vào các call liên quan I/O, kèm timestamp
strace -T -e trace=read,write,pread64,pwrite64,fsync -p 12345
Một backend thực sự bị block chờ một heavyweight lock thường hiện một syscall (thường là semop hoặc futex wait) đơn giản là không bao giờ trả về — xác nhận nó thực sự bị block bởi lock của session khác, chứ không lặng lẽ chạy vòng lặp trong userspace. Một backend bị block bởi disk I/O thay vào đó hiện một luồng các call read/pread64/fsync, chỉ hướng bạn về phía storage thay vì locking là thủ phạm. Lưu ý strace làm chậm đáng kể tiến trình bị trace, nên dùng có chủ đích và ngắn gọn trên một backend thực sự bị treo, không phải như thói quen giám sát thường xuyên.
gdb đi sâu hơn một tầng nữa: attach một debugger trực tiếp vào một tiến trình backend PostgreSQL đang sống để kiểm tra call stack thực tế và trạng thái nội bộ của nó.
gdb -p 12345
(gdb) bt # in call stack hiện tại của backend
(gdb) detach # để nó tiếp tục chạy sau đó
(gdb) quit
Đây thực sự là địa hạt nâng cao — bạn thường cần cài debug symbol của PostgreSQL, và đọc backtrace kết quả đòi hỏi quen thuộc với mã nguồn C của PostgreSQL (hàm nào đang chờ bên trong LWLockAcquire, hay bị kẹt trong một executor node cụ thể). Hầu hết DBA sẽ trải qua hàng tháng hoặc hàng năm mà không cần đến nó, nhưng đây là công cụ cuối đường khi một backend bị kẹt theo cách mà strace một mình không giải thích được, và giá trị của nó nằm ở việc biết nó tồn tại và dùng để làm gì hơn là thông thạo nó hằng ngày.
Core dump là tương đương hậu kiểm: một bản chụp toàn bộ bộ nhớ của một tiến trình được ghi ra disk tại thời điểm nó crash (thường do segfault hoặc assertion failure), mà sau đó bạn có thể nạp vào gdb để tái dựng chính xác tiến trình đang làm gì lúc chết — vô giá khi báo cáo một bug PostgreSQL thực sự lên upstream, hoặc khi chẩn đoán một crash gây ra bởi một extension lỗi. Core dump thường bị tắt hoặc giới hạn kích thước theo mặc định và cần được bật rõ ràng:
# nâng giới hạn kích thước core dump cho shell chạy tiến trình postgres (và duy trì qua limits.conf / systemd unit)
ulimit -c unlimited
# báo kernel ghi core file ở đâu (Linux)
echo '/var/crash/core.%e.%p.%t' | sudo tee /proc/sys/kernel/core_pattern
# sau một lần crash, nạp file core kết quả với đúng binary postgres tương ứng
gdb /usr/lib/postgresql/16/bin/postgres /var/crash/core.postgres.12345.1700000000
(gdb) bt
perf: profiling CPU dưới tải
Với các vấn đề hiệu năng có thật nhưng quá tản mạn để strace hay EXPLAIN xác định được — “server bị nghẽn CPU dưới workload này, nhưng hàm nào thực sự đang đốt cycle” — perf của Linux có thể lấy mẫu một tiến trình PostgreSQL đang chạy (hoặc cả hệ thống) và tạo ra một profile cho biết thời gian CPU thực sự đi đâu, xuống đến mức hàm nếu có debug symbol:
# lấy mẫu một backend cụ thể trong 30 giây ở tần số 99 Hz
perf record -p 12345 -F 99 -g -- sleep 30
perf report
Đây là công việc hiệu năng nâng cao, mức thấp — hầu hết vấn đề query chậm được giải quyết sớm hơn nhiều bằng EXPLAIN (ANALYZE, BUFFERS) và index (xem ./10-query-planning-and-performance-tuning.md) — nhưng perf là bước leo thang đúng đắn khi plan của một query trông ổn nhưng vẫn đốt CPU bất tương xứng, và bạn cần biết thời gian đó đi vào sort, hash, đánh giá biểu thức, hay thứ gì đó bất ngờ như lock spinning.
Quy trình xử lý sự cố: triệu chứng → công cụ đầu tiên → cho biết gì
Gộp tất cả những điều trên lại, đây là đường quyết định thực tế mà hầu hết sự cố đi theo, đại khái theo thứ tự bạn sẽ thực sự tìm đến mỗi công cụ:
| Triệu chứng | Công cụ kiểm tra đầu tiên | Cho biết gì |
|---|---|---|
| Query đột nhiên chậm | pg_stat_activity + pg_stat_statements | Có phải một query chạy loạn (plan tệ, thiếu index) hay tải hệ thống dàn trải trên nhiều query |
| Một query chậm hơn hẳn dự kiến | EXPLAIN (ANALYZE, BUFFERS) | Planner có chọn plan tệ không (thiếu stats, sai index, sai loại join) — xem ./10-query-planning-and-performance-tuning.md |
| Nhiều session “bị treo”, app timeout | pg_stat_activity lọc theo wait_event/state | Session có bị block bởi lock không (và bởi ai), hay thực sự idle-in-transaction |
| Server không phản hồi / mọi thứ chậm | top/htop, rồi iotop | Máy có bị nghẽn CPU (một tiến trình, hay tất cả) hay nghẽn I/O |
| Dung lượng disk tăng bất thường | pg_stat_user_tables (dead tuple), pg_stat_activity (transaction cũ) | Bloat bảng do autovacuum bị chặn, thường do một snapshot bị giữ lâu |
| Kết nối bị từ chối | Số đếm pg_stat_activity so với max_connections | Bản thân instance hay connection pooler phía trước đang bão hòa |
| ”Đêm qua đã xảy ra chuyện gì” (lịch sử) | sar, báo cáo pgBadger, lịch sử pgcluu | Lịch sử tài nguyên hệ thống và pattern query, vì view trực tiếp chỉ cho thấy hiện tại |
| Một backend thực sự không phản hồi | strace -p <pid> | Nó bị block bởi lock (syscall không bao giờ trả về) hay bởi disk I/O (luồng call read/write) |
| Backend bị kẹt theo cách strace không giải thích được | gdb -p <pid>, bt | Call stack nội bộ thực sự — phương án cuối cùng, hiếm khi cần |
| Server vừa crash | Đuôi log + core dump trong gdb | Trạng thái chính xác của tiến trình tại thời điểm crash |
Best Practices
- Bật
csvlogở production, không chỉ log dạng text thuầnstderr. Đây là định dạng pgBadger và hầu hết công cụ parse mong đợi, loại bỏ sự mơ hồ khi chính câu query được log chứa ký tự xuống dòng hoặc ký tự đặc biệt. - Đặt
log_min_duration_statementthường trực ở mức vừa phải (100–500 ms là điểm bắt đầu phổ biến) thay vì tắt hoàn toàn hoặc log mọi thứ — bạn muốn tín hiệu slow-query luôn bật, mà không nhấn chìm log. - Đầu tư vào một
log_line_prefixtốt ngay từ ngày đầu. Bao gồm timestamp, PID, user, database, và application name; thêm vào sau khi đã cần đối chiếu một sự cố cũ khó hơn nhiều so với thiết lập từ trước. - Coi
pg_stat_activitylà dashboard thường trực, không chỉ là công cụ khi có sự cố. Theo dõi số kết nối so vớimax_connections, và đặc biệt theo dõi các sessionidle in transactionnhư một kiểm tra sức khỏe liên tục, vì chúng là chỉ báo sớm của cả tranh chấp lock lẫn vấn đề vacuum. - Đặt
idle_in_transaction_session_timeoutnhư một lưới bảo vệ tự động thay vì trông chờ bắt được transaction bị treo bằng tay mỗi lần. - Chạy pgBadger theo lịch (báo cáo incremental hằng ngày/tuần), không chỉ phản ứng sau sự cố — nhiều dịch chuyển query chậm chỉ nhìn thấy được khi so sánh báo cáo theo thời gian.
- Đưa metric PostgreSQL vào cùng monitoring stack với phần còn lại của hạ tầng (Prometheus qua
postgres_exporter, hoặc Zabbix) để sức khỏe database nằm cạnh sức khỏe application và hạ tầng trên cùng dashboard và pipeline paging, thay vì sống trong một silo chỉ riêng PostgreSQL. - Chỉ tìm đến
strace/gdb/core dump sau khipg_stat_activityvà log đã bó tay — chúng mạnh nhưng gây gián đoạn (chi phí hiệu năng, cần cẩn thận với tiến trình production) và hiếm khi là con đường nhanh nhất đến câu trả lời. - Luyện tập đường leo thang trước khi cần đến nó dưới áp lực: biết trước công cụ nào trả lời câu hỏi nào, để một sự cố lúc 3 giờ sáng là một lần tra cứu, không phải một dự án nghiên cứu.
Tài liệu tham khảo
- PostgreSQL Docs — The Cumulative Statistics System
- PostgreSQL Docs — Viewing Statistics (pg_stat_activity and friends)
- PostgreSQL Docs — Error Reporting and Logging
- pgBadger — GitHub project and documentation
- pgcenter — GitHub project
- pgCluu — official site
- temBoard — official site
- postgres_exporter — Prometheus community project
- PostgreSQL Docs — pg_stat_statements
Part of the PostgreSQL DBA Roadmap knowledge base.
Overview
Every production incident starts with the same question: “what is the database actually doing right now?” PostgreSQL answers that question at three different layers, and a competent DBA needs fluency in all three. The innermost layer is the statistics collector, a set of counters and per-session state that PostgreSQL maintains internally and exposes through the pg_stat_* family of system views — this is where you look first, because it requires no extra tooling and reflects the exact instant you query it. The middle layer is the log file, which captures things the statistics views don’t (the literal slow query text, error messages, connection/disconnection events, checkpoint activity) and which specialized tools like pgBadger turn into readable reports. The outermost layer is the operating system, because PostgreSQL is, after all, a collection of ordinary Unix processes competing for CPU, memory, and disk I/O — top, iotop, and sar tell you things no amount of SQL querying can, such as whether the box itself is out of headroom.
This note walks through all three layers and adds a fourth: the last-resort tools (strace, gdb, core dumps, perf) you reach for only when the first three don’t explain what a stuck or crashing backend is doing. The goal is not to memorize every flag of every tool, but to build the diagnostic reflex — given a symptom, know which layer to check first and what question that layer answers. This note assumes you’re already familiar with ./09-transactions-and-concurrency-control.md (locks, snapshots, long transactions) and ./10-query-planning-and-performance-tuning.md (EXPLAIN, indexes, pg_stat_statements), since diagnostics is where that theory gets applied under time pressure. It also mirrors, at database scope, the general SRE monitoring model covered in ../devops/en/15-monitoring-and-observability.md — the “four golden signals” framing translates almost one-to-one onto what a DBA watches.
Fundamentals
The statistics collector and the pg_stat_* views
PostgreSQL continuously tracks activity — how many rows were inserted into each table, how many index scans ran, how many transactions committed or rolled back, what every current backend is doing — and exposes all of it through system views prefixed pg_stat_ (cumulative, mostly monotonically increasing counters) and pg_statio_ (I/O-specific counters: block hits vs. reads). Historically this was implemented by a separate statistics collector background process that backends sent UDP messages to; as of PostgreSQL 15 the counters live directly in shared memory, which removed the old collector process and its associated latency/reliability quirks, but the view names and semantics you query are unchanged.
A few of these views matter enough to know by name:
| View | What it shows |
|---|---|
pg_stat_activity | Every current connection, its state, and its running query |
pg_stat_database | Per-database totals: commits, rollbacks, blocks read/hit, deadlocks, temp file usage |
pg_stat_user_tables | Per-table scan counts (seq vs. index), rows inserted/updated/deleted, vacuum/analyze history |
pg_stat_user_indexes | Per-index scan counts — the classic way to find unused indexes |
pg_stat_bgwriter | Checkpoint and background writer activity |
pg_statio_user_tables | Per-table buffer cache hits vs. disk reads |
pg_stat_statements | Aggregate performance stats per normalized query (requires the extension) |
Because most of these counters are cumulative since the last stats reset or server restart, a raw number rarely means much on its own — you either compare a rate over time (snapshot now, snapshot again in five minutes, subtract) or reset the counters at a known point with SELECT pg_stat_reset(); (whole database) or SELECT pg_stat_reset_single_table_counters(oid); before starting a controlled test.
Logging configuration
PostgreSQL’s log output is configured in postgresql.conf (or with ALTER SYSTEM, or per-role/per-database with ALTER ROLE ... SET) and is, in practice, the single richest diagnostic artifact you have for anything the stats views don’t capture — the literal SQL text of a slow statement, a deadlock’s full detail, an authentication failure, a crash’s last words before the process died.
The setting that matters most for performance work is log_min_duration_statement, which logs the text of any statement that takes longer than the configured threshold (in milliseconds):
# postgresql.conf
log_min_duration_statement = 250 # log any statement taking >= 250 ms
Set it too low (e.g. 0, which logs every statement) and you’ll drown the log and add I/O overhead to every query; set it too high and you’ll miss the queries that matter. A common pattern is to run it at a moderate threshold (100–500 ms) permanently in production, then temporarily set it to 0 on a session or role for a focused investigation:
-- log everything for this session only, for the next few minutes
SET log_min_duration_statement = 0;
-- ... reproduce the issue ...
RESET log_min_duration_statement;
log_statement is a coarser, complementary setting that controls which kinds of statements get logged regardless of duration — none (default), ddl (schema changes only — a good permanent baseline for audit purposes), mod (DDL plus INSERT/UPDATE/DELETE), or all (everything, very noisy, rarely appropriate outside short-lived debugging).
log_line_prefix controls what metadata precedes each log line, and getting it right up front saves enormous pain later when you’re trying to correlate a slow query back to a specific session, user, or application. A solid production baseline:
log_line_prefix = '%m [%p] %q%u@%d/%a '
| Escape | Meaning |
|---|---|
%m | Timestamp with milliseconds |
%p | Process ID of the backend |
%u | Database user name |
%d | Database name |
%a | Application name (set by the client, e.g. via application_name in the connection string) |
%q | Nothing for backend messages; suppresses the following fields for non-session messages (checkpointer, etc.) |
%l | Log line number within this session |
%r | Remote host and port |
After changing any of these, pg_ctl reload or SELECT pg_reload_conf(); is enough — none of them require a restart, since they’re all classified context = sighup in pg_settings. Where the log actually lands is controlled separately by logging_collector, log_destination (stderr, csvlog, or both — csvlog is what pgBadger and most parsers prefer, since it’s unambiguous to parse), and log_directory/log_filename.
pg_stat_activity: the one view to check first
If you can only remember one diagnostic query, make it this one. pg_stat_activity has one row per server process (backend), and it is simultaneously the connection list, the current-query monitor, and the lock-wait indicator:
SELECT pid, usename, datname, application_name, client_addr,
state, wait_event_type, wait_event,
backend_start, xact_start, query_start,
state_change, query
FROM pg_stat_activity
WHERE datname = 'myapp'
ORDER BY query_start;
The state column is the first thing to read, and each value means something specific:
| State | Meaning |
|---|---|
active | Currently executing a query |
idle | Waiting for the client to send a new command — normal, connection-pool baseline |
idle in transaction | A transaction is open (BEGIN was issued) but no statement is currently running |
idle in transaction (aborted) | Same as above, but the last statement errored — the client must issue ROLLBACK before anything else will be accepted |
fastpath function call | Executing a fast-path function call (rare, low-level) |
disabled | track_activities is off for this session |
idle in transaction deserves special attention because it is one of the most common causes of mysterious production degradation. A connection sitting in this state is holding an open transaction — and therefore an open MVCC snapshot — indefinitely, even though it isn’t doing anything. Two concrete harms follow directly from this: first, any row locks that transaction acquired earlier (from an UPDATE, SELECT ... FOR UPDATE, etc.) stay held, so other sessions trying to touch those rows simply queue up and wait, often invisibly until someone notices a pile of blocked queries. Second, and more insidiously, VACUUM cannot remove dead tuples that are newer than the oldest snapshot any session still holds — this is the xmin horizon problem covered in ./09-transactions-and-concurrency-control.md. A single forgotten idle in transaction connection from a bug in application connection-pool handling (a classic case: an ORM opens a transaction, an exception is thrown before commit/rollback, and the connection is returned to the pool still “open”) can silently prevent autovacuum from making progress on a hot table for hours, and the first visible symptom is often unrelated-looking bloat or a sudden increase in sequential scan cost — not an obvious “transaction stuck” alert.
To find the offenders, filter specifically on that state and sort by how long it’s been open:
SELECT pid, usename, datname, application_name, client_addr,
state, now() - xact_start AS xact_age, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY xact_start ASC;
More generally, to find long-running active queries (not just idle-in-transaction sessions):
SELECT pid, usename, datname, now() - query_start AS running_for, state, query
FROM pg_stat_activity
WHERE state != 'idle'
AND query_start < now() - interval '5 minutes'
ORDER BY query_start ASC;
Once you’ve identified a problem backend by its pid, PostgreSQL gives you two escalating ways to intervene. pg_cancel_backend(pid) sends the equivalent of a cancel request — it interrupts the currently running query but leaves the session itself connected and its transaction state intact (so if it was mid-transaction, that transaction is now aborted and waiting for a ROLLBACK, not gone). pg_terminate_backend(pid) is the blunter tool: it forcibly closes the entire connection, as if the client had disconnected, rolling back any open transaction with it.
-- polite: cancel the running statement, keep the connection
SELECT pg_cancel_backend(12345);
-- forceful: kill the whole session/connection
SELECT pg_terminate_backend(12345);
Both require superuser, or being the same role that owns the target backend (with the exception of certain background/replication processes, which refuse termination for safety). A practical guardrail worth setting in production rather than relying on manual intervention is idle_in_transaction_session_timeout, which makes PostgreSQL automatically terminate sessions that sit in idle in transaction past a threshold:
ALTER SYSTEM SET idle_in_transaction_session_timeout = '10min';
SELECT pg_reload_conf();
pg_stat_statements: the first stop for “what’s slow”
pg_stat_activity tells you what’s running right now; it says nothing about which queries are slow on average across the whole workload. That’s the job of the pg_stat_statements extension, which aggregates execution statistics (calls, total/mean/max time, rows, shared buffer hits) per normalized query (constants stripped out, so WHERE id = 1 and WHERE id = 2 count as the same entry). It is covered in depth, including setup and the canonical “top N slow queries” query, in ./10-query-planning-and-performance-tuning.md; the short version for this note is that when someone says “the database feels slow” without a specific symptom, pg_stat_statements ordered by total_exec_time is almost always the first query you run, because it immediately tells you whether the problem is one runaway query or genuinely systemic load spread across many.
Key Concepts
The four golden signals, applied to a database
The general SRE monitoring vocabulary — latency, traffic, errors, saturation — maps onto a PostgreSQL instance almost exactly, and organizing your dashboards and alerts around these four categories (rather than an unstructured pile of metrics) is the fastest way to reason about “is the database healthy” at a glance. See ../devops/en/15-monitoring-and-observability.md for the general framing this specializes.
| Signal | What it means for PostgreSQL | Where to look |
|---|---|---|
| Latency | How long queries take to return | pg_stat_statements.mean_exec_time, application-side query timers, slow-query log (log_min_duration_statement) |
| Traffic | How much work is arriving | pg_stat_database.xact_commit + xact_rollback (transactions/sec), connections in pg_stat_activity, queries/sec from pg_stat_statements.calls deltas |
| Errors | Failed work | Failed authentication attempts and statement errors in the log, pg_stat_database.deadlocks, application-reported connection refusals |
| Saturation | How close resources are to their limit | Active connections vs. max_connections, connection-pool wait time, pg_stat_bgwriter checkpoint frequency, OS-level CPU/disk/memory (see below) |
A concrete saturation check every DBA should have on a dashboard is connection headroom, since exhausting max_connections produces a hard failure (FATAL: sorry, too many clients already) rather than graceful degradation:
SELECT count(*) AS current_connections,
(SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max_connections
FROM pg_stat_activity;
In practice, most teams don’t let application connections get anywhere near max_connections directly — a connection pooler (PgBouncer, or the pool built into the application driver) sits in front, and pool saturation (clients waiting for a pooled connection) is the earlier, more actionable warning sign than PostgreSQL’s own connection limit.
Log analysis tooling
Raw PostgreSQL logs are complete but not friendly to read at scale — a busy server produces gigabytes a day, and picking out patterns by eye doesn’t work. A handful of purpose-built tools exist specifically to turn that firehose into something a human (or an alerting system) can act on.
pgBadger is the standard log-analysis tool in the PostgreSQL ecosystem: a single Perl script that parses PostgreSQL log files (ideally in csvlog format, which it parses unambiguously) and produces a self-contained HTML report covering slow queries (ranked by total time, by count, by average duration), query type breakdown, connection and session patterns over time, error and cancellation rates, checkpoint and vacuum activity, and lock waits. It requires no server-side extension and no ongoing daemon — you just point it at a log file (or a whole rotated directory) after the fact.
# ensure csvlog is enabled first (postgresql.conf):
# log_destination = 'csvlog'
# logging_collector = on
pgbadger /var/lib/postgresql/data/log/postgresql-*.csv -o /tmp/pgbadger-report.html
# incremental mode: run daily via cron, building one report over time
pgbadger --incremental --outdir /var/www/pgbadger /var/log/postgresql/postgresql-*.csv
pgcenter and pgcluu cover a different niche — live, top-like visibility rather than after-the-fact log parsing. pgcenter top gives a curses-based, auto-refreshing dashboard of pg_stat_activity, per-table/index I/O, and system resource usage side by side, which is exactly what you want open during an active incident. pgcluu runs as a two-part tool: a lightweight sampling collector you leave running (cron or daemon) that periodically snapshots the stats views and OS metrics, and a separate report generator that turns the collected history into browsable HTML graphs — useful when you need to answer “what did connection count / cache hit ratio / disk usage look like over the last two weeks” rather than just right now.
check_pgactivity and check_pgbackrest are Nagios-compatible check plugins — they don’t render dashboards, they return a Nagios-style exit code (OK/WARNING/CRITICAL) plus a status line, designed to be wired into whatever alerting system your team already runs (Nagios, Icinga, or anything that speaks the Nagios plugin API). check_pgactivity covers a broad catalog of checks (connection count, bloat, replication lag, backend states, autovacuum status, and dozens more, each independently configurable with thresholds); check_pgbackrest specifically checks the health and freshness of pgBackRest backups, closing the common gap where a backup job silently stops succeeding and nobody notices until a restore is needed.
temBoard takes a different shape again: a persistent, web-based UI (agent on each PostgreSQL instance, central web application aggregating them) that combines live activity monitoring, historical graphs, configuration management, and built-in advisory checks (unused indexes, missing primary keys, etc.) in one place, aimed at teams who want a dashboard product rather than a collection of separate scripts.
| Tool | Style | Best for |
|---|---|---|
| pgBadger | Offline log parser → HTML report | Periodic/retrospective analysis of query patterns, errors, slow queries |
| pgcenter | Live, terminal, top-like | Real-time triage during an active incident |
| pgcluu | Sampling collector + historical HTML report | Trend analysis over days/weeks |
| check_pgactivity | Nagios plugin | Threshold-based alerting integration |
| check_pgbackrest | Nagios plugin | Backup health/freshness alerting |
| temBoard | Web UI, agent-based | Persistent multi-instance dashboard + config management |
Feeding PostgreSQL metrics into general-purpose monitoring
Purpose-built PostgreSQL tools are excellent for PostgreSQL-specific depth, but most organizations also want database health visible in the same place as everything else — the same Grafana dashboard, the same on-call paging pipeline. Prometheus is the most common target: the postgres_exporter runs alongside PostgreSQL (or as a sidecar), periodically queries the pg_stat_* views itself, and exposes the results on a /metrics HTTP endpoint in Prometheus’s text format for scraping — the same pull-based exporter pattern used for every other _exporter in that ecosystem. Zabbix plays a similar integration role via its own PostgreSQL monitoring template, more common in shops that standardized on Zabbix before Prometheus existed. Either way, the mechanics of scraping, storing, graphing, and alerting on these metrics are general monitoring-stack concerns, not PostgreSQL-specific ones — see ../devops/en/15-monitoring-and-observability.md for how Prometheus, exporters, and alerting pipelines fit together in general.
OS-level diagnostic tools
PostgreSQL backends are, at the end of the day, ordinary OS processes, and a large fraction of “the database is slow” incidents turn out to be resource contention that no SQL query will ever show you directly.
top (or the friendlier htop) is the first command to run when something feels globally wrong, because it answers “is one process hogging the CPU, or is everything just generally loaded”:
top -c # -c shows the full command line, so you can see which is which backend
Each PostgreSQL backend shows up as its own OS process (postgres: user dbname client_addr(port)), so a runaway backend chewing 100% CPU (a bad query plan doing a huge sort or hash, a runaway recursive CTE, a tight loop in a PL/pgSQL function) is immediately visible by process, and you can cross-reference its PID directly against pg_stat_activity.pid to see exactly which query it’s running.
iotop narrows the same idea to disk I/O — essential when the symptom is “everything is slow” but CPU looks fine, which usually means something is disk-bound:
iotop -o # -o: only show processes actually doing I/O
A backend running a large sequential scan, a VACUUM on a bloated table, or a checkpoint flushing a large batch of dirty buffers will show up here as heavy I/O, letting you distinguish “one query is I/O-heavy” from “the whole disk subsystem is saturated” (in which case everything shows elevated I/O wait, not just one process).
sysstat (which provides sar, iostat, and mpstat) is what you reach for when you need historical system resource data rather than a live snapshot — critical because by the time you’re investigating, the spike that caused the problem may already be over.
# CPU utilization over the last 24 hours (sar keeps rotating history by default)
sar -u
# disk I/O stats history
sar -d
# live device I/O stats, refreshed every 2 seconds
iostat -dx 2
Deep diagnostics: strace, gdb, and core dumps — the last resort
When pg_stat_activity shows a backend active with a query that simply never advances, and the logs show nothing new being written, you’ve exhausted the normal toolkit and need to ask the operating system directly what that process is doing.
strace traces every system call a running process makes, which is exactly the right tool for “is this backend actually stuck, and on what”:
# attach to a specific backend PID and watch its syscalls live
strace -p 12345
# focus on I/O-related calls only, with timestamps
strace -T -e trace=read,write,pread64,pwrite64,fsync -p 12345
A backend truly blocked waiting on a heavyweight lock typically shows a syscall (often a semop or futex wait) that simply never returns — confirming it’s genuinely blocked on another session’s lock, not silently spinning in userspace. A backend blocked on disk I/O instead shows a stream of read/pread64/fsync calls, pointing you toward storage rather than locking as the culprit. Note that strace measurably slows the traced process, so use it deliberately and briefly on a genuinely stuck backend, not as a routine monitoring habit.
gdb goes a level deeper still: attaching a debugger directly to a live PostgreSQL backend process to inspect its actual call stack and internal state.
gdb -p 12345
(gdb) bt # print the backend's current call stack
(gdb) detach # let it continue running afterward
(gdb) quit
This is genuinely advanced territory — you generally need PostgreSQL debug symbols installed, and reading the resulting backtrace requires familiarity with PostgreSQL’s C source (which function is waiting inside LWLockAcquire, or stuck in a particular executor node). Most DBAs will go months or years without needing it, but it’s the tool of last resort when a backend is stuck in a way strace alone doesn’t explain, and it’s valuable simply to know it exists and what it’s for rather than to be fluent in it day to day.
Core dumps are the post-mortem equivalent: a full memory snapshot of a process written to disk at the moment it crashes (typically on a segfault or assertion failure), which you can later load into gdb to reconstruct exactly what the process was doing when it died — invaluable for reporting a genuine PostgreSQL bug upstream, or for diagnosing a crash caused by a broken extension. Core dumps are usually disabled or size-limited by default and need explicit enabling:
# raise the core dump size limit for the postgres process's shell (and persist via limits.conf / systemd unit)
ulimit -c unlimited
# tell the kernel where to write core files (Linux)
echo '/var/crash/core.%e.%p.%t' | sudo tee /proc/sys/kernel/core_pattern
# after a crash, load the resulting core file with the matching postgres binary
gdb /usr/lib/postgresql/16/bin/postgres /var/crash/core.postgres.12345.1700000000
(gdb) bt
perf: CPU profiling under load
For performance problems that are real but too diffuse for strace or EXPLAIN to pin down — “the server is CPU-bound under this workload, but which function is actually burning the cycles” — Linux’s perf can sample a running PostgreSQL process (or the whole system) and produce a profile of where CPU time is actually going, down to the function level if debug symbols are available:
# sample a specific backend for 30 seconds at 99 Hz
perf record -p 12345 -F 99 -g -- sleep 30
perf report
This is advanced, low-level performance work — most slow-query problems are solved far earlier by EXPLAIN (ANALYZE, BUFFERS) and indexing (see ./10-query-planning-and-performance-tuning.md) — but perf is the right escalation when a query’s plan looks fine yet it still burns disproportionate CPU, and you need to know whether that time is going into sorting, hashing, expression evaluation, or something unexpected like lock spinning.
Troubleshooting workflow: symptom → first tool → what it tells you
Tying all of the above together, here’s the practical decision path most incidents follow, roughly in the order you’d actually reach for each tool:
| Symptom | First tool to check | What it tells you |
|---|---|---|
| Queries suddenly slow | pg_stat_activity + pg_stat_statements | Whether it’s one runaway query (single bad plan, missing index) or systemic load spread across many queries |
| One query is much slower than expected | EXPLAIN (ANALYZE, BUFFERS) | Whether the planner is choosing a bad plan (missing stats, bad index, wrong join type) — see ./10-query-planning-and-performance-tuning.md |
| Many sessions “stuck”, app times out | pg_stat_activity filtered on wait_event/state | Whether sessions are blocked on locks (and by whom), or genuinely idle-in-transaction |
| Server unresponsive / everything slow | top/htop, then iotop | Whether the box is CPU-bound (one process, or all of them) or I/O-bound |
| Disk usage growing unexpectedly | pg_stat_user_tables (dead tuples), pg_stat_activity (old transactions) | Table bloat from a stalled autovacuum, usually caused by a long-held snapshot |
| Connections being refused | pg_stat_activity count vs. max_connections | Whether the instance itself or the connection pooler in front of it is saturated |
| Historical “what happened last night” | sar, pgBadger report, pgcluu history | System resource and query-pattern history, since live views only show the present |
| A backend genuinely will not respond | strace -p <pid> | Whether it’s blocked on a lock (syscall never returns) or on disk I/O (stream of read/write calls) |
| A backend is stuck in a way strace can’t explain | gdb -p <pid>, bt | The actual internal call stack — last-resort, rarely needed |
| Server just crashed | Log tail + core dump in gdb | The exact state of the process at the moment of the crash |
Best Practices
- Enable
csvlogin production, not just plain-textstderrlogs. It’s what pgBadger and most parsers expect, and it eliminates ambiguity when a logged query itself contains newlines or special characters. - Set a permanent, moderate
log_min_duration_statement(100–500 ms is a common starting point) rather than leaving it off entirely or logging everything — you want the slow-query signal always on, without drowning the log. - Invest in a good
log_line_prefixfrom day one. Include timestamp, PID, user, database, and application name; retrofitting this after you need to correlate an old incident is much harder than setting it up up front. - Treat
pg_stat_activityas a routine dashboard, not just an incident tool. Watch connection count againstmax_connections, and specifically watch foridle in transactionsessions as an ongoing health check, since they’re a leading indicator of both lock contention and vacuum problems. - Set
idle_in_transaction_session_timeoutas an automatic safety net rather than relying on catching stuck transactions manually every time. - Run pgBadger on a schedule (daily/weekly incremental reports), not only reactively after an incident — a lot of slow-query drift is only visible when you compare reports over time.
- Feed PostgreSQL metrics into the same monitoring stack as the rest of your infrastructure (Prometheus via
postgres_exporter, or Zabbix) so database health sits next to application and infrastructure health on the same dashboards and paging pipeline, rather than living in a PostgreSQL-only silo. - Reach for
strace/gdb/core dumps only afterpg_stat_activityand the logs have failed you — they’re powerful but disruptive (performance overhead, requires care with production processes) and rarely the fastest path to an answer. - Practice the escalation path before you need it under pressure: know in advance which tool answers which question, so a 3 a.m. incident is a lookup, not a research project.
References
- PostgreSQL Docs — The Cumulative Statistics System
- PostgreSQL Docs — Viewing Statistics (pg_stat_activity and friends)
- PostgreSQL Docs — Error Reporting and Logging
- pgBadger — GitHub project and documentation
- pgcenter — GitHub project
- pgCluu — official site
- temBoard — official site
- postgres_exporter — Prometheus community project
- PostgreSQL Docs — pg_stat_statements