VACUUM, autovacuum và bloat: vì sao bảng vẫn lớn sau DELETE? Trung cấp
Thời gian đọc ước tính: 8 phút
Cần đọc trước
Vấn đề
Phần tiêu đề “Vấn đề”Bảng job được cập nhật liên tục: pending, running, done. Vài tuần sau count(*) vẫn quanh một triệu row nhưng file bảng lớn lên nhiều lần. Đội vận hành DELETE job cũ: row giảm, file trên đĩa gần như không đổi.
Đây là hành vi bình thường của MVCC: UPDATE/DELETE để lại version cũ cho các snapshot đang đọc, VACUUM dọn chúng và đánh dấu chỗ trống để tái sử dụng, nhưng thường không trả file về hệ điều hành.
Ba thứ hay bị trộn:
- Dead tuple: version không còn ai nhìn thấy.
- Free space: chỗ trống trong file, dùng lại được.
- Bloat: phần dung lượng dư đáng kể so với dữ liệu sống và nhu cầu ghi tiếp. File to mà sắp ghi nhiều thì chưa chắc là vấn đề; file và index phình đều kèm latency tăng thì là.
Bốn lệnh, bốn việc khác nhau
Phần tiêu đề “Bốn lệnh, bốn việc khác nhau”| Việc làm | Lock | Trả disk về OS? | |
|---|---|---|---|
VACUUM |
Dọn dead tuple và index entry tương ứng, cập nhật visibility map, freeze XID cũ | Không chặn đọc/ghi thông thường | Thường không. Ngoại lệ: các page trống ở cuối bảng có thể bị cắt nếu lấy được exclusive lock dễ dàng |
ANALYZE |
Lấy mẫu cập nhật statistics cho planner | Nhẹ | Không |
VACUUM FULL |
Viết lại cả bảng thành file mới, không còn chỗ trống | ACCESS EXCLUSIVE: chặn mọi truy cập; cần thêm disk bằng cỡ bảng |
Có |
| autovacuum | Tiến trình nền tự chạy VACUUM và ANALYZE theo ngưỡng; còn chạy chống wraparound |
Như VACUUM |
Như VACUUM |
Đừng tắt autovacuum để đỡ I/O: nó đang trả chi phí bắt buộc của MVCC và bảo vệ an toàn XID (xem wraparound).
Heap page trước và sau (minh hoạ)
Phần tiêu đề “Heap page trước và sau (minh hoạ)”Trước VACUUM (một page, 6 chỗ) [ live ][ DEAD ][ live ][ DEAD ][ DEAD ][ live ]
Sau VACUUM [ live ][ free ][ live ][ free ][ free ][ live ] file KHÔNG co; free dùng cho INSERT/UPDATE sau
Sau VACUUM FULL [ live ][ live ][ live ] file mới, nhỏ hơn; bảng bị khoá suốt quá trìnhNgưỡng autovacuum: số mặc định và phép tính
Phần tiêu đề “Ngưỡng autovacuum: số mặc định và phép tính”Autovacuum chạy VACUUM trên một bảng khi số dead tuple vượt:
threshold = autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor x reltuples| Tham số (PostgreSQL 17) | Mặc định |
|---|---|
autovacuum_naptime |
1min |
autovacuum_max_workers |
3 |
autovacuum_vacuum_threshold / scale_factor |
50 / 0.2 |
autovacuum_vacuum_insert_threshold / insert_scale_factor |
1000 / 0.2 |
autovacuum_analyze_threshold / scale_factor |
50 / 0.1 |
autovacuum_vacuum_cost_delay |
2ms |
Ví dụ tính: bảng 10 triệu row, mặc định. Ngưỡng là 50 + 0,2 x 10.000.000 = 2.000.050 dead tuple mới kích hoạt. Với bảng job bị update dày, autovacuum đến quá muộn, và mỗi lần chạy phải dọn một khối lớn.
Chỉnh riêng bảng nóng, không đổi mặc định toàn cụm:
ALTER TABLE jobs SET ( autovacuum_vacuum_scale_factor = 0.01, -- 10M row: ngưỡng 1.000 + 100.000 = 101.000 thay vì 2.000.050 autovacuum_vacuum_threshold = 1000, autovacuum_vacuum_cost_delay = 0 -- bớt nhường I/O, chỉ làm cho bảng nóng);PostgreSQL 18 thêm autovacuum_vacuum_max_threshold, đặt trần số dead tuple cố định để bảng cực lớn không phải đợi theo phần trăm. PostgreSQL 18 cũng thêm autovacuum_worker_slots để đổi autovacuum_max_workers mà không restart.
Thứ chặn VACUUM dọn: “xmin horizon”
Phần tiêu đề “Thứ chặn VACUUM dọn: “xmin horizon””VACUUM chỉ xoá version mà không snapshot nào còn cần. Một thứ giữ snapshot cũ sẽ chặn dọn cho mọi bảng, và dead tuple tích tụ dù autovacuum vẫn chạy:
| Nguồn | Cách phát hiện | Cách chặn |
|---|---|---|
Transaction dài, hoặc session idle in transaction |
pg_stat_activity theo xact_start |
idle_in_transaction_session_timeout (mặc định 0, tức tắt) và statement_timeout |
| Prepared transaction bị bỏ quên | pg_prepared_xacts |
Rollback hoặc commit |
| Replication slot cũ | pg_replication_slots |
Xoá slot chết (cẩn thận, replica có thể cần dựng lại) |
Replica bật hot_standby_feedback chạy query dài |
Kiểm tra trên replica | Rút ngắn query trên replica |
v1 → v2UPDATE tạo version mới; version cũ vẫn nằm trong heap vì snapshot đang mở có thể cần nó.
Quan sát
Phần tiêu đề “Quan sát”-- Bảng nào nhiều dead tuple nhất; n_dead_tup là ước lượng, dùng để xem xu hướngSELECT relname, n_live_tup, n_dead_tup, round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 1) AS dead_pct, last_autovacuumFROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;
-- VACUUM đang chạy (view này chỉ có dữ liệu trong lúc đang chạy)SELECT relid::regclass, phase, heap_blks_scanned, heap_blks_totalFROM pg_stat_progress_vacuum;
-- Ai đang giữ transaction cũ nhấtSELECT pid, state, now() - xact_start AS transaction_age, queryFROM pg_stat_activity WHERE xact_start IS NOT NULL ORDER BY xact_start;Cách đọc nhanh:
last_autovacuumcũ chưa chắc là autovacuum tắt: có thể bảng chưa đạt ngưỡng.pg_relation_size()không giảm sauVACUUMlà bình thường. Dùngpg_total_relation_size()nếu cần tính cả index và TOAST.- Index phình là chuyện riêng: chữa bằng
REINDEX CONCURRENTLY, không phảiVACUUM FULL. - Đo bloat chính xác theo byte cần extension
pgstattuple;n_dead_tupkhông thay được nó.
Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”Demo (tạo và xoá bảng vacuum_demo, không chạy trên database có bảng cùng tên):
docker compose -f examples/docker-compose.yml up -d postgresdocker compose -f examples/docker-compose.yml exec -T postgres psql -U hub -d hub < examples/src/postgres-vacuum/vacuum-demo.sqlScript tạo 10.000 row, UPDATE tất cả một lần, in kích thước heap trước và sau, chạy VACUUM (VERBOSE, ANALYZE) rồi in lại. Điều cần rút ra: count(*) vẫn 10.000 và file không bắt buộc co lại sau VACUUM. Dùng bảng thường, không dùng TEMP, vì bảng TEMP không được autovacuum xử lý.
Khi nào VACUUM FULL là chấp nhận được
Phần tiêu đề “Khi nào VACUUM FULL là chấp nhận được”- Đã xoá vĩnh viễn phần lớn dữ liệu và bảng sẽ không lớn lại.
- Có maintenance window, đủ disk cho bản sao mới, và chấp nhận chặn mọi truy cập vào bảng.
- Không dùng làm cứu hộ wraparound: nó rewrite bảng, lấy lock mạnh và bản thân có thể cần XID.
Muốn thu nhỏ bảng mà ít gián đoạn hơn, dùng công cụ ngoài như pg_repack hoặc pg_squeeze (cần kiểm tra khả dụng trên nền tảng của bạn).
Checklist production
Phần tiêu đề “Checklist production”- Theo dõi theo bảng:
n_dead_tup,dead_pct,last_autovacuum, kích thước heap và index theo thời gian. - Bảng update dày (queue, session, counter): đặt
autovacuum_vacuum_scale_factorthấp (cỡ 0.01-0.05) cho riêng bảng đó. - Đặt
idle_in_transaction_session_timeoutvàstatement_timeoutcho app; cảnh báo transaction dài hơn vài phút. - Kiểm tra định kỳ prepared transaction, replication slot không hoạt động và
hot_standby_feedback. - Xoá dữ liệu cũ theo partition thay vì
DELETEhàng loạt khi có thể. - Index phình:
REINDEX CONCURRENTLY.VACUUM FULLchỉ theo kế hoạch có window. - Không tắt autovacuum, kể cả cho bảng chỉ insert (vẫn cần freeze).
Trade-offs
Phần tiêu đề “Trade-offs”VACUUM thường
- Chạy song song với app, thu hồi chỗ cho lần ghi sau.
- Autovacuum tự theo dõi workload và bảo vệ khỏi wraparound.
VACUUM (ANALYZE)làm mới statistics cho planner sau thay đổi lớn.
Khi muốn thu nhỏ file
- Tốn I/O; chạy muộn hoặc bị horizon chặn thì bloat tích tụ.
- Thường không trả dung lượng về OS.
VACUUM FULLtrả disk nhưng rewrite bảng, cần thêm disk và chặn truy cập.
Sai lầm thường gặp
Phần tiêu đề “Sai lầm thường gặp”Tóm tắt
Phần tiêu đề “Tóm tắt”Tự kiểm tra: (1) Vì sao DELETE đã commit mà file chưa nhỏ? (2) Bảng 10 triệu row ở mặc định cần bao nhiêu dead tuple để autovacuum chạy, và chỉnh thế nào cho bảng nóng? (3) Một transaction chỉ đọc mở lâu liên quan gì đến dead tuple?