← PostgreSQL DBA← PostgreSQL DBA
PostgreSQL DBAPostgreSQL DBA19 Th7, 2026Jul 19, 202629 phút đọc23 min read

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:

ViewCho biết gì
pg_stat_activityMọi kết nối hiện tại, trạng thái, và câu query đang chạy
pg_stat_databaseTổng số theo từng database: commit, rollback, block đọc/hit, deadlock, sử dụng temp file
pg_stat_user_tablesSố 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_indexesSố lượt scan theo từng index — cách kinh điển để tìm index không được dùng
pg_stat_bgwriterHoạt động checkpoint và background writer
pg_statio_user_tablesTỷ lệ buffer cache hit so với đọc disk theo từng bảng
pg_stat_statementsThố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
%mTimestamp kèm mili-giây
%pProcess ID của backend
%uTên user database
%dTên database
%aApplication name (client set qua application_name trong connection string)
%qKhô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.)
%lSố thứ tự dòng log trong session này
%rRemote 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 transactionMộ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)
disabledtrack_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 = 1WHERE 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 PostgreSQLNhìn vào đâu
LatencyQuery 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)
TrafficCó bao nhiêu công việc đang đếnpg_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
ErrorsCông việc thất bạiLỗ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
SaturationTài nguyên còn cách giới hạn bao xaSố 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

pgcenterpgcluu 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_pgactivitycheck_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ểuPhù hợp nhất cho
pgBadgerParse log offline → báo cáo HTMLPhân tích định kỳ/hồi cứu pattern query, lỗi, query chậm
pgcenterTrực tiếp, terminal, kiểu topTriage thời gian thực khi đang giữa sự cố
pgcluuCollector 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_pgactivityPlugin NagiosTích hợp alert theo ngưỡng
check_pgbackrestPlugin NagiosAlert sức khỏe/độ mới của backup
temBoardUI web, dựa trên agentDashboard đ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ứngCông cụ kiểm tra đầu tiênCho biết gì
Query đột nhiên chậmpg_stat_activity + pg_stat_statementsCó 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ếnEXPLAIN (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 timeoutpg_stat_activity lọc theo wait_event/stateSession 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ậmtop/htop, rồi iotopMá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ườngpg_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ốiSố đếm pg_stat_activity so với max_connectionsBả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ử pgcluuLị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ồistrace -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 đượcgdb -p <pid>, btCall 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 gdbTrạng thái chính xác của tiến trình tại thời điểm crash

Best Practices

Tài liệu tham khảo

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:

ViewWhat it shows
pg_stat_activityEvery current connection, its state, and its running query
pg_stat_databasePer-database totals: commits, rollbacks, blocks read/hit, deadlocks, temp file usage
pg_stat_user_tablesPer-table scan counts (seq vs. index), rows inserted/updated/deleted, vacuum/analyze history
pg_stat_user_indexesPer-index scan counts — the classic way to find unused indexes
pg_stat_bgwriterCheckpoint and background writer activity
pg_statio_user_tablesPer-table buffer cache hits vs. disk reads
pg_stat_statementsAggregate 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 '
EscapeMeaning
%mTimestamp with milliseconds
%pProcess ID of the backend
%uDatabase user name
%dDatabase name
%aApplication name (set by the client, e.g. via application_name in the connection string)
%qNothing for backend messages; suppresses the following fields for non-session messages (checkpointer, etc.)
%lLog line number within this session
%rRemote 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:

StateMeaning
activeCurrently executing a query
idleWaiting for the client to send a new command — normal, connection-pool baseline
idle in transactionA 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 callExecuting a fast-path function call (rare, low-level)
disabledtrack_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.

SignalWhat it means for PostgreSQLWhere to look
LatencyHow long queries take to returnpg_stat_statements.mean_exec_time, application-side query timers, slow-query log (log_min_duration_statement)
TrafficHow much work is arrivingpg_stat_database.xact_commit + xact_rollback (transactions/sec), connections in pg_stat_activity, queries/sec from pg_stat_statements.calls deltas
ErrorsFailed workFailed authentication attempts and statement errors in the log, pg_stat_database.deadlocks, application-reported connection refusals
SaturationHow close resources are to their limitActive 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.

ToolStyleBest for
pgBadgerOffline log parser → HTML reportPeriodic/retrospective analysis of query patterns, errors, slow queries
pgcenterLive, terminal, top-likeReal-time triage during an active incident
pgcluuSampling collector + historical HTML reportTrend analysis over days/weeks
check_pgactivityNagios pluginThreshold-based alerting integration
check_pgbackrestNagios pluginBackup health/freshness alerting
temBoardWeb UI, agent-basedPersistent 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:

SymptomFirst tool to checkWhat it tells you
Queries suddenly slowpg_stat_activity + pg_stat_statementsWhether it’s one runaway query (single bad plan, missing index) or systemic load spread across many queries
One query is much slower than expectedEXPLAIN (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 outpg_stat_activity filtered on wait_event/stateWhether sessions are blocked on locks (and by whom), or genuinely idle-in-transaction
Server unresponsive / everything slowtop/htop, then iotopWhether the box is CPU-bound (one process, or all of them) or I/O-bound
Disk usage growing unexpectedlypg_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 refusedpg_stat_activity count vs. max_connectionsWhether the instance itself or the connection pooler in front of it is saturated
Historical “what happened last night”sar, pgBadger report, pgcluu historySystem resource and query-pattern history, since live views only show the present
A backend genuinely will not respondstrace -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 explaingdb -p <pid>, btThe actual internal call stack — last-resort, rarely needed
Server just crashedLog tail + core dump in gdbThe exact state of the process at the moment of the crash

Best Practices

References