✦

Table phình dù đã xoá sạch — VACUUM trong PostgreSQL

•Phong Hy

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

  • DELETE không nhả đĩa, VACUUM cũng không nhả hết, chỉ VACUUM FULL/pg_repack mớ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_activity trướ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 -h rồ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
✦