PostgreSQL bị ‘phình’ ổ cứng? Tuyệt chiêu xử lý Bloat và tối ưu Autovacuum

Database tutorial - IT technology blog
Database tutorial - IT technology blog

5 phút kiểm tra và “cấp cứu” ổ đĩa

Bạn nhận được cảnh báo ổ cứng server chạm ngưỡng 90-95% lúc nửa đêm. Kiểm tra thực tế cho thấy dữ liệu (rows) chỉ chiếm khoảng 20GB, nhưng file database trên đĩa lại nặng tới 100GB. Đây là dấu hiệu điển hình của Bloat. Đừng vội gõ VACUUM FULL ngay lập tức. Lệnh này sẽ khóa (lock) toàn bộ table, khiến mọi truy vấn từ ứng dụng bị treo cứng.

Hãy bắt đầu bằng việc xác định bảng nào đang chứa nhiều “rác” (dead tuples) nhất bằng câu lệnh SQL này:

SELECT 
    relname AS table_name, 
    n_dead_tup AS dead_tuples, 
    last_autovacuum, 
    last_vacuum 
FROM pg_stat_user_tables 
ORDER BY n_dead_tup DESC;

Nếu bảng có hàng triệu dead_tupleslast_autovacuum đã chạy từ… tuần trước, bạn đã tìm đúng thủ phạm. Để giải quyết tạm thời mà không làm gián đoạn hệ thống, hãy chạy:

VACUUM (ANALYZE) your_table_name;

Lưu ý: Lệnh này chỉ dọn dẹp để lấy chỗ trống cho dữ liệu mới bên trong table. Nó chưa trả lại dung lượng cho hệ điều hành ngay đâu.

Tại sao xóa dữ liệu mà ổ cứng không giảm?

Cơ chế MVCC (Multi-Version Concurrency Control) là linh hồn của PostgreSQL, nhưng cũng là lý do gây ra Bloat. Khi bạn UPDATE một dòng, Postgres không sửa trực tiếp. Nó đánh dấu dòng cũ là “hết hạn” (dead tuple) và chèn một dòng mới hoàn toàn. Lệnh DELETE cũng tương tự, chỉ đánh dấu chứ không xóa vật lý ngay lập tức.

Cách làm này giúp các transaction khác vẫn thấy dữ liệu cũ, đảm bảo tính nhất quán. Tuy nhiên, Bloat sẽ xuất hiện khi tốc độ tạo rác nhanh hơn tốc độ dọn dẹp. File dữ liệu phình to chứa đầy khoảng trống vô dụng. Hệ quả là câu lệnh SELECT chậm đi rõ rệt vì database phải quét qua cả “núi rác” để tìm dữ liệu thật.

Kinh nghiệm thực tế: Khi Autovacuum mặc định quá “nhát gan”

Tôi từng xử lý một hệ thống log với tần suất 3.000 TPS (Transactions Per Second). Dù Postgres có sẵn Autovacuum, nhưng cấu hình mặc định của nó thường quá an toàn. Nó chạy chậm và dừng sớm để tiết kiệm CPU. Kết quả là một bảng 60GB thì có tới 40GB là bloat.

Đừng giữ thông số mặc định nếu hệ thống của bạn có cường độ ghi cao. Hãy điều chỉnh file postgresql.conf để Autovacuum hoạt động quyết liệt hơn.

Cấu hình Autovacuum để dọn dẹp hiệu quả

Dưới đây là các thông số tôi thường ưu tiên tinh chỉnh:

# Tăng bộ nhớ cho việc dọn dẹp (Ví dụ: 512MB cho server 16GB RAM)
maintenance_work_mem = 512MB 

# Tăng số lượng worker chạy đồng thời
autovacuum_max_workers = 5 

# Kích hoạt dọn dẹp khi 5% dữ liệu thay đổi (mặc định là 20% - quá cao)
autovacuum_vacuum_scale_factor = 0.05 

# Giảm độ trễ để dọn rác nhanh hơn
autovacuum_vacuum_cost_limit = 1000

Sau khi lưu, hãy áp dụng cấu hình mới bằng lệnh:

SELECT pg_reload_conf();

Thu hồi dung lượng không gây downtime với pg_repack

Thông thường, VACUUM chỉ gom rác để dùng lại chỗ trống đó cho chính table đó. Để thực sự trả lại dung lượng cho OS, bạn cần VACUUM FULL. Nhưng như đã nói, lệnh này khóa bảng rất lâu. Nếu table nặng 200GB, hệ thống có thể “chết lâm sàng” vài tiếng đồng hồ.

Giải pháp tối ưu nhất là pg_repack. Đây là extension giúp tái cấu trúc table và index mà không cần lock lâu. Nó tạo một table tạm, copy dữ liệu sang, sau đó swap tên cực nhanh.

Cách cài đặt pg_repack (Ubuntu/Debian)

sudo apt-get install postgresql-15-repack
# Sau đó vào database tạo extension
CREATE EXTENSION pg_repack;

Để chạy dọn dẹp cho một bảng cụ thể:

pg_repack -d your_db_name -t your_table_name

Trong lúc pg_repack chạy, ứng dụng vẫn INSERT hay UPDATE bình thường. Đây là cứu cánh cho môi trường Production cần hoạt động 24/7.

Đừng quên Index Bloat

Không chỉ table, Index cũng bị phình. Thực tế, có những index phình to gấp 3 lần dữ liệu thật. Cách xử lý Index Bloat an toàn nhất là dùng REINDEX CONCURRENTLY:

REINDEX INDEX CONCURRENTLY idx_customer_email;

Lệnh này tạo một index mới chạy song song. Khi hoàn tất, nó sẽ tự động thay thế và xóa index cũ. Bạn sẽ thấy dung lượng ổ cứng giảm xuống đáng kể mà không làm gián đoạn người dùng.

Lời khuyên cho quản trị viên Database

  • Giám sát chủ động: Sử dụng Grafana hoặc Prometheus theo dõi n_dead_tup. Đừng đợi ổ cứng báo đỏ mới bắt đầu xử lý.
  • Tránh Transaction quá dài: Một transaction treo từ sáng đến tối sẽ chặn đứng Autovacuum. Rác không thể bị dọn dẹp nếu có một transaction cũ vẫn đang “nhìn” thấy chúng.
  • Ưu tiên công cụ ngoài: Luôn dự phòng pg_repack trong bộ công cụ bảo trì. Nó chuyên nghiệp và an toàn hơn nhiều so với các lệnh mặc định khi table quá lớn.
  • Kiểm tra kiểu dữ liệu: Đôi khi Bloat là hệ quả của thiết kế kém. Việc lạm dụng UUID hoặc TEXT không cần thiết sẽ làm tăng kích thước mỗi dòng dữ liệu.

Hiểu rõ cơ chế MVCC sẽ giúp bạn kiểm soát PostgreSQL tốt hơn. Xử lý Bloat không khó, quan trọng là bạn chọn đúng thời điểm và công cụ để không làm ảnh hưởng đến trải nghiệm người dùng.

Share: