Câu hỏi "query nào chậm" chỉ trả lời được một nửa. Nửa còn lại, thường quan trọng hơn, là vì sao nó chậm — nó đang chờ cái gì? Chờ một khoá? Chờ đĩa trả dữ liệu? Chờ ghi WAL? PostgreSQL trả lời câu này qua cột wait_event trong pg_stat_activity. Bài trước ta dùng nó để tìm phiên bị chặn; bài này khai thác nó thành một công cụ mạnh hơn: lấy mẫu wait event nhiều lần để thấy toàn hệ thống đang chờ ở đâu — một profiler nghèo nhưng cực hữu dụng.

Ý tưởng: lấy mẫu để biết thời gian đi đâu

Một ảnh chụp wait_event chỉ cho biết một khoảnh khắc. Nhưng nếu bạn lấy mẫu liên tục (mỗi 50–100 ms) và đếm phân bố, bức tranh hiện ra: nhóm wait_event nào chiếm nhiều mẫu nhất chính là nơi hệ thống tiêu tốn thời gian chờ — tức nút thắt.

-- Lấy mẫu này nhiều lần rồi đếm:
SELECT coalesce(wait_event_type || '/' || wait_event, 'CPU')
FROM pg_stat_activity
WHERE state = 'active' AND backend_type = 'client backend';
-- wait_event NULL nghĩa là đang thật sự chạy trên CPU, không chờ gì

Ảnh chụp đoạn mã SQL nền tối minh hoạ wait events hệ thống đang chờ ở đâu không phải chạy ở đâu PostgreSQL 16 lấy mẫu wait_event nhiều lần là một profiler nghèo cho database, ý tưởng lấy mẫu wait_event liên tục để thấy thời gian đi đâu chạy nhiều lần mỗi 50 100ms rồi đếm phân bố SELECT coalesce wait_event_type wait_event CPU FROM pg_stat_activity WHERE state active AND backend_type client backend wait_event NULL đang thật sự chạy trên CPU không chờ gì, các nhóm wait_event_type quan trọng Lock chờ khoá nặng hàng bảng Lock transactionid LWLock chờ khoá nhẹ nội bộ buffer WAL ánh xạ buffer tranh chấp IO chờ đĩa DataFileRead đọc heap WALWrite WALSync Client chờ client gửi lệnh ClientRead thường là rảnh không phải nút thắt IPC Timeout BufferPin chờ tiến trình khác hẹn giờ pin trang, đọc kết quả nhóm nào chiếm nhiều là nút thắt LWLock WALWrite cộng IO WALSync nhiều nghẽn ghi WAL tải write IO DataFileRead nhiều thiếu RAM cache đọc đĩa nhiều Lock nhiều tranh chấp khoá giữa giao dịch CPU nhiều nghẽn tính toán cần tối ưu query, bỏ qua Client ClientRead khi tìm nút thắt phiên đang chờ ứng dụng gửi câu lệnh tiếp theo cao không có nghĩa database chậm thường chỉ là kết nối rảnh

Hình 1: Lấy mẫu wait_event liên tục để thấy thời gian đi đâu. Các nhóm chính: Lock (khoá nặng), LWLock (khoá nhẹ nội bộ), IO (đĩa), Client (chờ client). Nhóm chiếm nhiều mẫu = nút thắt; bỏ qua ClientRead.

Đo thật: một tải ghi bị nghẽn ở WAL

Tôi chạy pgbench ghi nặng (16 client) rồi lấy mẫu wait_event khoảng 40 lần trong vài giây. Phân bố thật:

Ảnh chụp bảng kết quả đo thật nền tối lấy mẫu wait_event dưới tải PostgreSQL 16 shared_buffers 128MB, tải ghi nặng pgbench 16 client lấy mẫu khoảng 40 lần wait_event Client ClientRead 273 mẫu chờ client gửi lệnh bỏ qua LWLock WALWrite 117 tranh chấp ghi WAL CPU không chờ 62 đang thật sự chạy IO WALSync 31 fsync WAL xuống đĩa Lock transactionid 30 chờ khoá hàng IO DataFileRead 2 đọc heap từ đĩa, bỏ ClientRead ra nút thắt là WAL LWLock WALWrite cộng IO WALSync đúng bản chất của tải ghi nặng muốn nhanh hơn chỉnh WAL gộp commit đĩa WAL nhanh hơn, quét bảng 925 MB lớn hơn shared_buffers 128MB đọc đĩa wait_event xuất hiện IO DataFileRead đọc trang heap từ đĩa vì không đủ cache, kịch bản khoá hai phiên UPDATE cùng hàng id 1 phiên thứ hai bị chặn wait_event Lock transactionid đang chờ giao dịch kia nhả khoá hàng nút thắt do tranh chấp khoá, cốt lõi wait_event trả lời đang chờ gì nhóm nào chiếm nhiều mẫu chính là nút thắt cách chẩn đoán mạnh hơn query nào chậm nó chỉ ra chờ ở đâu

Hình 2: Phân bố wait_event dưới tải ghi 16 client: sau khi bỏ Client/ClientRead (273, chỉ là chờ client), nút thắt là WAL — LWLock/WALWrite 117 và IO/WALSync 31. Cùng IO/DataFileRead khi quét bảng lớn và Lock/transactionid khi hai phiên tranh khoá.

Con số thật, sau khi loại Client/ClientRead (273 mẫu — chỉ là chờ ứng dụng gửi lệnh, không phải nút thắt):

  • LWLock/WALWrite (117) + IO/WALSync (31): đây là nút thắt thật — hệ thống dành phần lớn thời gian chờ ghi WAL. Đúng bản chất của một tải ghi nặng.
  • CPU (62): đang thật sự làm việc.
  • Lock/transactionid (30): tranh chấp khoá hàng giữa các giao dịch đồng thời.
  • IO/DataFileRead (2): đọc heap từ đĩa (ít, vì dữ liệu phần lớn đã trong cache).

Bức tranh rõ ngay: tải này nghẽn ở WAL. Muốn tăng tốc, hướng đúng là chỉnh cấu hình WAL, gộp commit, hoặc dùng đĩa WAL nhanh hơn — chứ không phải tối ưu từng query hay thêm index. Wait events cho bạn hướng tối ưu, thay vì đoán mò.

Đọc từng nhóm wait event

IO/DataFileRead — đọc trang dữ liệu từ đĩa. Tôi quét bảng pgbench_accounts 925 MB (lớn hơn shared_buffers 128 MB nhiều), và IO/DataFileRead xuất hiện — vì cache không chứa nổi cả bảng, PostgreSQL phải đọc đĩa. Nhóm này chiếm nhiều nghĩa là thiếu RAM/cache, hướng tối ưu là tăng shared_buffers hoặc RAM.

Lock/transactionid — chờ khoá hàng nặng. Tôi dựng hai phiên cùng UPDATE hàng id=1; phiên thứ hai hiện wait_event = Lock/transactionid, đang chờ giao dịch kia nhả khoá. Nhóm này nhiều nghĩa là tranh chấp khoá — hướng tối ưu là giảm giao dịch dài, đổi thứ tự truy cập, hoặc thiết kế lại để bớt đụng cùng hàng.

LWLock — khoá nhẹ nội bộ (buffer WAL, ánh xạ buffer). Nhiều nghĩa là tranh chấp cấu trúc nội bộ, thường do tải rất cao trên một điểm nóng.

Đánh đổi cần cân nhắc

Lấy mẫu là gần đúng, không chính xác tuyệt đối. Đây là phương pháp thống kê: mẫu càng nhiều càng đáng tin, nhưng vẫn có nhiễu. Với chẩn đoán nghiêm túc trên production, dùng extension pg_wait_sampling (lấy mẫu tự động ở tần số cao, lưu lịch sử) thay vì tự vòng lặp psql — nó cho phân bố chính xác hơn nhiều và không tốn công thủ công.

Bỏ Client/ClientRead khỏi phân tích. Đây là bẫy phổ biến: ClientRead thường chiếm nhiều nhất, nhưng nó chỉ nghĩa là phiên đang chờ ứng dụng gửi câu lệnh tiếp theo — kết nối rảnh, không phải database chậm. Đưa nó vào làm lệch hẳn kết luận. Luôn loại nó (và các trạng thái idle) khi tìm nút thắt của database.

Wait event chỉ ra hướng, không phải giải pháp cụ thể. Biết "nghẽn WAL" là bước đầu; giải pháp còn tuỳ (đĩa nhanh hơn, wal_compression, gộp commit, synchronous_commit phù hợp — các bài trước đã đo). Wait events thu hẹp không gian tìm kiếm, giúp bạn không tối ưu nhầm chỗ, nhưng vẫn cần hiểu cơ chế để chọn cách chữa.

Ba ý mang về

  1. Wait events cho biết mỗi phiên đang CHỜ gì, và lấy mẫu nhiều lần rồi đếm phân bố biến chúng thành một profiler cho database: nhóm chiếm nhiều mẫu nhất chính là nút thắt — mạnh hơn câu hỏi "query nào chậm" vì nó chỉ ra chờ ở đâu.
  2. Đo thật một tải ghi 16 client cho thấy nút thắt là WAL (LWLock/WALWrite 117 + IO/WALSync 31 sau khi loại Client/ClientRead) — hướng tối ưu đúng là chỉnh WAL, không phải thêm index; cùng đó IO/DataFileRead xuất hiện khi quét bảng lớn hơn cache, và Lock/transactionid khi hai phiên tranh khoá hàng.
  3. Luôn bỏ Client/ClientRead khi tìm nút thắt (nó chỉ là kết nối rảnh chờ ứng dụng), và với chẩn đoán nghiêm túc dùng pg_wait_sampling thay vì tự vòng lặp — wait events chỉ ra hướng, còn giải pháp cụ thể vẫn cần hiểu cơ chế bên dưới.

Phần sau ta chuyển sang một view thống kê giúp phát hiện thiếu index: Phần sau mổ xẻ pg_stat_user_tables — cách đọc tỷ lệ seq scan so với index scan để tìm bảng đang bị quét tuần tự quá nhiều và cần thêm index.