Sự cố hết dung lượng ổ đĩa lúc 2 giờ sáng
Gần đây tôi đã xử lý một cảnh báo PagerDuty mà bất kỳ DBA nào cũng e ngại: một database production chạm ngưỡng 95% dung lượng đĩa. Các con số không khớp nhau. Chúng tôi không hề import thêm tập dữ liệu mới nào, và số lượng row trong các bảng chính vẫn ổn định. Tuy nhiên, ổ đĩa EBS 500GB lại mất vài gigabyte dung lượng trống mỗi giờ.
Thủ phạm chính là “Table Bloat” (phình bảng). Dù tôi đã từng quản trị MySQL và MongoDB, PostgreSQL xử lý cập nhật dữ liệu khác hẳn do kiến trúc Multi-Version Concurrency Control (MVCC). Thiết kế này tuyệt vời cho hiệu năng, nhưng nó tạo ra một thách thức đặc thù: các dead tuple chiếm dụng không gian và không chịu rời đi.
MVCC thực sự tạo ra Bloat như thế nào
Để khắc phục sự tăng trưởng này, bạn phải hiểu cách PostgreSQL thực hiện ghi dữ liệu. Hầu hết các database sẽ ghi đè dữ liệu tại chỗ. PostgreSQL thì không. Để cho phép nhiều người dùng truy cập dữ liệu cùng lúc mà không cần khóa (locking) nặng nề, nó tạo ra các phiên bản khác nhau của các row.
Khi bạn thực hiện lệnh UPDATE, PostgreSQL để lại row gốc trên đĩa và đánh dấu nó là “hết hạn” (dead tuple). Sau đó, nó chèn một phiên bản hoàn toàn mới của row đó. Lệnh DELETE cũng hoạt động tương tự; nó không xóa các bit ngay lập tức mà chỉ gắn cờ row đó là không hiển thị với các transaction trong tương lai.
Những dead tuple này giống như những “bóng ma”. Chúng tồn tại trên ổ đĩa cho đến khi một tiến trình gọi là VACUUM dọn dẹp chúng. Nếu khối lượng ghi dữ liệu cao—chẳng hạn 5.000 update mỗi giây—và các thiết lập vacuum của bạn quá thận trọng, những bóng ma này sẽ tích tụ cho đến khi chúng chiếm nhiều không gian hơn cả dữ liệu thực tế.
Xác định điểm nghẽn
Đừng thay đổi cấu hình một cách mù quáng. Bạn cần xem bảng nào đang gặp vấn đề dọn dẹp. PostgreSQL theo dõi điều này trong view pg_stat_user_tables.
Tôi sử dụng query này để tìm những bảng “tệ” nhất trong cluster:
SELECT
relname AS table_name,
n_live_tup AS live_rows,
n_dead_tup AS dead_rows,
last_vacuum,
last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
Nếu bạn thấy một bảng có dead_rows chiếm hơn 20% live_rows, nghĩa là autovacuum của bạn không theo kịp. Nếu last_autovacuum trống hoặc đã từ nhiều ngày trước, bạn đang gặp vấn đề về cấu hình.
Bộ công cụ: VACUUM, VACUUM FULL và ANALYZE
Bạn có ba công cụ chính để quản lý việc này, nhưng chọn sai công cụ trong giờ cao điểm có thể làm ứng dụng của bạn ngừng hoạt động.
1. VACUUM thông thường
Đây là công cụ dùng hàng ngày. Nó tìm các dead tuple và đánh dấu không gian của chúng là có sẵn cho dữ liệu PostgreSQL mới. Nó không trả lại dung lượng cho hệ điều hành. Bạn có thể chạy lệnh này khi ứng dụng đang hoạt động vì nó không chặn việc đọc hay ghi.
VACUUM (VERBOSE, ANALYZE) orders;
2. VACUUM FULL
Đây là “phương án hạt nhân”. Nó ghi lại toàn bộ bảng vào một file mới trên đĩa, loại bỏ hoàn toàn phần bloat. Đây là cách duy nhất để thu nhỏ kích thước database vật lý trên ổ đĩa EBS của bạn. Tuy nhiên, nó yêu cầu một khóa ACCESS EXCLUSIVE. Ứng dụng của bạn sẽ không thể đọc hoặc ghi vào bảng cho đến khi lệnh hoàn tất. Với một bảng 100GB, việc này có thể gây downtime cả tiếng đồng hồ.
3. ANALYZE
Trong khi VACUUM quản lý không gian, ANALYZE cập nhật các số liệu thống kê được sử dụng bởi Query Planner. Nếu thống kê bị cũ, PostgreSQL có thể bỏ qua một index nhanh và thực hiện quét tuần tự (sequential scan) chậm chạp. Luôn chạy ANALYZE sau khi load một lượng lớn dữ liệu.
Tối ưu Autovacuum cho lưu lượng truy cập cao
Các thiết lập mặc định của PostgreSQL thường quá thụ động đối với phần cứng hiện đại. Ví dụ, autovacuum_vacuum_scale_factor mặc định là 0.2. Điều này có nghĩa là một bảng có 100 triệu row sẽ không kích hoạt dọn dẹp cho đến khi nó có 20 triệu dead tuple. Đó là mức bloat quá lớn đối với môi trường production.
Cập nhật postgresql.conf của bạn với các giá trị quyết liệt hơn sau đây:
# Kích hoạt vacuum khi 5% bảng thay đổi
autovacuum_vacuum_scale_factor = 0.05
# Kích hoạt analyze khi 2% bảng thay đổi
autovacuum_analyze_scale_factor = 0.02
# Tăng số lượng worker nếu bạn có nhiều bảng đang hoạt động
autovacuum_max_workers = 5
# Cấp thêm "ngân sách" để các worker làm việc nhanh hơn
autovacuum_vacuum_cost_limit = 1000
Đối với các bảng có biến động cao (high-churn) cụ thể, như bảng sessions, bạn có thể đặt giới hạn chặt chẽ hơn mà không ảnh hưởng đến toàn bộ database:
ALTER TABLE sessions SET (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_vacuum_cost_limit = 500
);
Bẫy “Transaction thây ma”
Đôi khi Autovacuum chạy nhưng không giải phóng được dung lượng. Điều này thường xảy ra do một transaction chạy quá lâu. PostgreSQL không thể xóa một dead tuple nếu có một transaction đang hoạt động được bắt đầu trước khi row đó bị xóa. Một lệnh BEGIN bị bỏ quên trong script Python hoặc một báo cáo BI bị treo có thể chặn việc dọn dẹp hàng triệu row.
Tìm các tác nhân gây nghẽn này bằng query sau:
SELECT pid, now() - xact_start AS duration, query, state
FROM pg_stat_activity
WHERE state != 'idle'
AND (now() - xact_start) > interval '10 minutes';
Nếu một query đã chạy trong nhiều giờ, hãy kill nó. Khi transaction kết thúc, Autovacuum cuối cùng sẽ có thể thực hiện công việc của mình.
Danh sách kiểm tra cuối cùng cho Production
Sức khỏe của database không phải là việc xử lý một lần. Đó là một thói quen. Để giữ cho instance PostgreSQL của bạn gọn nhẹ và nhanh chóng, hãy tuân thủ các quy tắc sau:
- Theo dõi
n_dead_tupvà thiết lập cảnh báo khi chúng vượt quá 15% kích thước bảng. - Giảm
scale_factorkhi dữ liệu của bạn tăng lên. 20% của 1GB thì ổn; nhưng 20% của 1TB là một thảm họa. - Tránh sử dụng
VACUUM FULLtrừ khi bạn có lịch bảo trì định kỳ. - Kill các transaction nhàn rỗi (idle) đã mở hơn 30 phút.
Hiểu về MVCC không chỉ giúp tiết kiệm dung lượng đĩa; nó còn giúp các truy vấn nhanh hơn và ứng dụng của bạn hoạt động tin cậy hơn.

