Disk của Postgres báo đỏ. Mình vào xoá 20 triệu dòng cũ trong bảng events, chờ chạy xong, gõ df -h lại — dung lượng gần như không nhúc nhích. Lúc đó mới ngộ ra một chuyện cơ bản: trong PostgreSQL, DELETE không trả đất về cho hệ điều hành. Nó chỉ đánh dấu dòng là đã chết, còn chỗ thì vẫn nằm nguyên đó, chờ VACUUM tới dọn.
Vì sao xoá rồi mà bảng vẫn phình
PostgreSQL dùng MVCC — mỗi giao dịch thấy một bản dữ liệu riêng, nên khi bạn UPDATE hay DELETE, dòng cũ không bị ghi đè hay cắt bỏ ngay. Nó được giữ lại làm "dead tuple", để mấy giao dịch đang chạy dở vẫn đọc được trạng thái cũ.
Chỉ khi không còn giao dịch nào cần tới nó nữa, dead tuple mới đủ điều kiện bị thu hồi. Công việc đó giao cho VACUUM.
Vấn đề là VACUUM thường (không phải VACUUM FULL) chỉ đánh dấu chỗ trống là tái sử dụng được cho lần ghi sau. File trên đĩa không nhỏ đi, chỉ là không phình thêm. Muốn trả đất thật cho OS thì phải VACUUM FULL — và nó khoá bảng.
Kiểm tra bảng nào đang có nhiều dead tuple
Đừng đoán. Xem số liệu trước, Postgres có sẵn:
SELECT relname,
n_live_tup,
n_dead_tup,
ROUND(n_dead_tup * 100.0
/ GREATEST(n_live_tup + n_dead_tup, 1), 1) AS dead_pct,
last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;
dead_pct vượt 20% là dấu hiệu autovacuum đang đuối. Còn last_autovacuum để trống trên bảng ghi liên tục thì càng đáng lo.
Một thủ phạm thứ hai hay bị bỏ quên: giao dịch mở quá lâu. Một session idle in transaction giữ xmin cũ sẽ chặn luôn việc dọn dẹp của cả hệ thống:
SELECT pid, now() - xact_start AS age, state, query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY age DESC
LIMIT 5;
Thấy giao dịch sống vài tiếng thì đó không phải chuyện của VACUUM nữa, mà là chuyện connection của app.
Dọn dẹp đúng cách
-- Dọn thường: chạy song song với ghi, không chặn ai
VACUUM (VERBOSE, ANALYZE) events;
-- Dọn trả đất về OS: KHOÁ bảng, chỉ chạy giờ thấp điểm
VACUUM FULL events;
Kinh nghiệm xương máu: đừng bao giờ VACUUM FULL trên bảng nóng trong giờ cao điểm. Nó lấy khoá ACCESS EXCLUSIVE, mọi truy vấn đọc ghi đều xếp hàng chờ. Bảng 80GB thì thời gian khoá tính bằng phút, và app coi như đứng.
Trường hợp buộc phải nén lại giữa giờ làm việc, dùng pg_repack — nó làm việc gần tương đương nhưng chỉ giữ khoá chốt hạ trong tích tắc.
Autovacuum mặc định không đủ cho bảng lớn
Mặc định autovacuum_vacuum_scale_factor = 0.2, nghĩa là phải có 20% số dòng chết mới chịu chạy. Bảng 200 triệu dòng thì phải chết 40 triệu dòng mới tới lượt — lúc đó đĩa đã căng từ lâu.
Với bảng sự kiện ghi nhiều xoá nhiều, mình chỉnh riêng theo bảng:
ALTER TABLE events SET (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_vacuum_threshold = 5000,
autovacuum_vacuum_cost_limit = 2000
);
Ba dòng đó đổi hẳn cục diện: 1% là đã dọn, nền tảng 5000 dòng cho bảng nhỏ, và cost_limit cao hơn để autovacuum chạy mạnh tay hơn thay vì ngủ hoài. Đổi xong theo dõi n_dead_tup vài ngày là biết ngay.
Nên nhớ autovacuum không thay thế được VACUUM FULL. Nó giữ bloat ở mức chấp nhận được, không làm đĩa nhỏ lại. Nếu bảng phình vì UPDATE liên tục trên cùng vài dòng, vấn đề nằm ở thiết kế — có thể là hàng đợi trạng thái bị ghi quá nhiều.
Vài thứ rút ra
DELETEkhông nhả đĩa,VACUUMcũng không nhả hết, chỉVACUUM FULL/pg_repackmới nhả.- Bảng lớn cần chỉnh autovacuum theo bảng, đừng xài mặc định rồi ngồi than.
- Kiểm tra
pg_stat_activitytrước khi đổ tội cho autovacuum — giao dịch treo mới là gốc rễ. - Đo bằng số liệu, đừng dựa vào
df -hrồi phán.
Bạn có từng bị đĩa đầy vì bảng phình mà tìm mãi không ra nguyên nhân chưa? Nếu hệ thống của bạn ghi nhiều hơn xoá, hãy đặt lịch xem n_dead_tup mỗi tuần — đỡ mệt hơn nhiều so với việc nửa đêm ngồi VACUUM FULL thủ công.
📋 Phụ lục thuật ngữ
| Thuật ngữ | Giải thích ngắn |
|---|---|
| MVCC | Cơ chế cho nhiều giao dịch đọc dữ liệu song song mà không chặn nhau |
| Dead tuple | Phiên bản dòng cũ đã bị xoá/sửa, còn giữ trên đĩa chờ dọn |
| Bloat | Phần dung lượng trống không dùng được bên trong file của bảng/index |
| VACUUM | Tiến trình thu hồi dead tuple để tái sử dụng không gian |
| VACUUM FULL | Viết lại toàn bộ bảng, trả đất về OS, có khoá bảng |
| Autovacuum | Tiến trình nền tự động chạy VACUUM/ANALYZE |
| xmin | Mốc giao dịch nhỏ nhất còn thấy được; giao dịch cũ giữ xmin sẽ chặn dọn dẹp |
| pg_repack | Công cụ nén bảng trực tuyến, chỉ khoá trong khoảnh khắc cuối |