Phần trước đã đo bảng phình lên. Phần này đo việc làm nó co lại — ba lệnh khác nhau, ba cái giá rất khác nhau, và một tham số mặc định khiến autovacuum chậm hơn 25 lần so với khi bạn gõ tay.
VACUUM thường: phình một lần rồi dừng
Bảng 400.000 dòng có khoá chính và một chỉ mục. Cập nhật toàn bảng rồi VACUUM, lặp ba vòng:
| Bước | Bảng | Khoá chính | Chỉ mục ma |
Chỗ trống bên trong |
|---|---|---|---|---|
| ban đầu | 45 MB | 8.792 kB | 8.792 kB | 184 kB |
| update lần 1 | 89 MB | 17 MB | 17 MB | 357 kB |
VACUUM |
89 MB | 17 MB | 17 MB | 45 MB |
| update lần 2 | 89 MB | 17 MB | 17 MB | 371 kB |
VACUUM |
89 MB | 17 MB | 17 MB | 45 MB |
| update lần 3 | 89 MB | 17 MB | 17 MB | 370 kB |
VACUUM |
89 MB | 17 MB | 17 MB | 45 MB |
Hai điều đáng nhớ nằm trong bảng này.
Thứ nhất, VACUUM không làm tệp nhỏ lại. Sau lần dọn đầu tiên, bảng vẫn 89 MB — nhưng 45 MB bên trong đã được đánh dấu là dùng lại được. Cột cuối lấy từ pg_freespace, cần extension pg_freespacemap:
create extension pg_freespacemap;
select pg_size_pretty(sum(avail)::bigint) from pg_freespace('bl');
Thứ hai, và quan trọng hơn: nó dừng ở đó. Vòng 2 và vòng 3 không làm bảng lớn thêm một byte nào. Chỗ trống 45 MB vừa đủ chứa phiên bản mới của lần cập nhật tiếp theo.
Đây là điều tôi nghĩ nhiều người hiểu sai về bloat. Với VACUUM chạy đều, bảng phình lên một lần tới mức tương ứng với khối lượng ghi giữa hai lần dọn, rồi ổn định ở đó vĩnh viễn. Một bảng 89 MB cho 45 MB dữ liệu không phải là hệ thống hỏng — đó là trạng thái cân bằng bình thường, và bạn nên coi đó là chi phí vận hành chứ không phải vấn đề cần sửa.
Bloat thật sự là khi con số đó không dừng: bảng 45 MB dữ liệu mà chiếm 3 GB. Nguyên nhân gần như luôn là VACUUM không chạy được — vì giao dịch dài (phần 21), hoặc vì autovacuum không theo kịp (mục cuối bài này).
Ba lệnh, ba cái giá
VACUUM
Chạy VACUUM trên bảng 808 MB, đồng thời từ phiên khác chạy select count(*):
select count(*) trả về 3.000.000 sau 0,16 s
Không đợi gì cả. VACUUM lấy khoá SHARE UPDATE EXCLUSIVE — khoá này chặn VACUUM khác và chặn phần lớn ALTER TABLE, nhưng không chặn SELECT, INSERT, UPDATE hay DELETE.
Một chi tiết nhỏ đáng biết: VACUUM không chạy được bên trong giao dịch.
begin; vacuum lk;
ERROR: VACUUM cannot run inside a transaction block
Phần 1 đã đo rằng gần như mọi DDL trong PostgreSQL đều có giao dịch — CREATE TABLE, DROP INDEX, ALTER TABLE đều hoàn tác được. VACUUM là một trong số ít ngoại lệ, cùng với CREATE INDEX CONCURRENTLY và CREATE DATABASE.
VACUUM FULL
trước : bảng 89 MB khoá chính 17 MB chỉ mục 17 MB
VACUUM FULL : bảng 45 MB khoá chính 8.792 kB chỉ mục 8.792 kB mất 0,47 s
Nó viết lại toàn bộ bảng vào một tệp mới rồi xoá tệp cũ, nên vừa co bảng vừa dựng lại mọi chỉ mục. Đổi lại hai cái giá.
Nó chặn mọi thứ. Cùng phép thử như trên, trong lúc VACUUM FULL chạy:
select count(*) trả về 3.000.000 sau 1,13 s
Câu SELECT đó bình thường mất 0,16 s. Nó không chạy chậm hơn — nó đợi hết VACUUM FULL rồi mới bắt đầu. Với bảng 808 MB thì đợi khoảng một giây; với bảng 500 GB thì đợi hàng giờ, và mọi kết nối tới bảng đó cũng xếp hàng theo.
Nó cần thêm đĩa. Đo mức dùng đĩa thật trong lúc VACUUM FULL co một bảng 809 MB:
trước: 105.442 MB đỉnh cao: 105.763 MB sau: 105.037 MB
Cần thêm 321 MB tạm thời — vì bản mới phải viết xong trước khi bản cũ bị xoá. Nếu đĩa còn ít hơn kích thước bảng, VACUUM FULL sẽ thất bại giữa chừng. Đây là tình huống trớ trêu hay gặp: bạn chạy nó vì đĩa sắp đầy, và chính vì đĩa sắp đầy mà nó không chạy được.
REINDEX CONCURRENTLY
VACUUM thường không sửa được bloat của chỉ mục:
ban đầu : bảng 50 MB chỉ mục 30 MB
sau 3 vòng update + VACUUM : bảng 100 MB chỉ mục 60 MB
sau REINDEX CONCURRENTLY : bảng 100 MB chỉ mục 30 MB mất 0,71 s
Chỉ mục gấp đôi và VACUUM để nguyên. REINDEX CONCURRENTLY dựng chỉ mục mới song song với chỉ mục cũ rồi hoán đổi, nên không chặn đọc ghi — đây là cách duy nhất gỡ bloat chỉ mục mà không khoá bảng.
Trên máy chủ thật, thứ tự nên là: REINDEX CONCURRENTLY trước (rẻ, không khoá), và chỉ dùng VACUUM FULL nếu chính bảng mới là chỗ phình.
Với bảng lớn không có cửa sổ bảo trì, công cụ ngoài như pg_repack làm được việc của VACUUM FULL mà chỉ khoá trong vài giây ở bước cuối. Tôi không đo nó ở đây vì nó không nằm trong bản cài đặt chuẩn.
Vì sao autovacuum không theo kịp
Đây là phép đo bất ngờ nhất trong bài. Cùng một bảng 416 MB, cùng một lệnh VACUUM, chỉ đổi một tham số:
vacuum_cost_delay |
Thời gian | Tốc độ |
|---|---|---|
0 ms — mặc định khi bạn gõ VACUUM |
1,11 s | 375 MB/giây |
| 2 ms — mặc định của autovacuum | 28,64 s | 15 MB/giây |
Chậm 25 lần.
PostgreSQL hãm tốc autovacuum có chủ ý: nó tích điểm cho mỗi trang đọc và mỗi trang ghi, và cứ tới vacuum_cost_limit điểm thì ngủ vacuum_cost_delay mili giây. Mục đích là để việc dọn nền không cướp hết băng thông đĩa của các truy vấn thật.
Ý định đúng, nhưng mặc định được chọn từ thời đĩa cứng quay. Trên SSD hiện đại, 15 MB/giây là quá thận trọng, và hậu quả là con số này: một bảng 500 GB cần khoảng chín tiếng cho một lượt autovacuum. Nếu bảng đó sinh dòng chết nhanh hơn thế, autovacuum vĩnh viễn không đuổi kịp và bảng phình không có giới hạn.
Khi bạn gõ VACUUM bằng tay thì mặc định vacuum_cost_delay là 0 — không hãm gì cả. Đó là lý do "chạy tay thì nhanh mà để tự động thì mãi không xong" không phải ảo giác.
Trên SSD, nới ra là hợp lý:
alter system set autovacuum_vacuum_cost_delay = '1ms';
alter system set autovacuum_vacuum_cost_limit = 1000;
select pg_reload_conf();
Cả hai đều áp dụng ngay, không cần khởi động lại.
Ngưỡng kích hoạt
ngưỡng dọn = 50 + 0,20 × số dòng
ngưỡng ANALYZE = 50 + 0,10 × số dòng
Chú ý hệ số 0,20 cho việc dọn, gấp đôi hệ số 0,10 của ANALYZE ở phần 20. Bảng 10 triệu dòng phải tích đủ 2.000.050 dòng chết mới được dọn một lần.
Hệ số theo tỷ lệ nghĩa là bảng càng lớn càng phải chờ lâu — đúng ngược với cái người ta muốn. Hạ riêng cho bảng lớn:
alter table don_hang set (autovacuum_vacuum_scale_factor = 0.05);
Và nhớ autovacuum_max_workers mặc định là 3. Nếu bạn có mười bảng lớn cùng cần dọn, bảy bảng phải xếp hàng chờ.
Theo dõi cái gì
select relname,
n_live_tup as dong_song,
n_dead_tup as dong_chet,
round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 1) as pt_chet,
pg_size_pretty(pg_relation_size(relid)) as kich_thuoc,
last_autovacuum,
autovacuum_count
from pg_stat_user_tables
where n_dead_tup > 10000
order by n_dead_tup desc limit 10;
Ba dấu hiệu, ba nguyên nhân khác nhau:
| Dấu hiệu | Nguyên nhân thường gặp |
|---|---|
pt_chet cao, last_autovacuum gần đây, autovacuum_count tăng đều |
autovacuum có chạy nhưng không dọn được — kiểm giao dịch dài (phần 21) |
pt_chet cao, last_autovacuum từ rất lâu |
chưa đạt ngưỡng, hoặc cả 3 worker đang bận |
pt_chet thấp mà bảng vẫn rất lớn |
bloat cũ đã được dọn nhưng tệp không co — cần VACUUM FULL hoặc pg_repack |
Vế thứ ba đáng nói riêng: một bảng đã ổn định ở mức phình gấp đôi thì không cần làm gì cả. Chạy VACUUM FULL để lấy lại 45 MB rồi để nó phình lại đúng 45 MB trong tuần sau là công không.
Thử ba mươi giây
Xem bảng nào đang phình và autovacuum có đang chạy trên nó không:
select relname,
pg_size_pretty(pg_relation_size(relid)) as kich_thuoc,
n_dead_tup,
coalesce(last_autovacuum::text, 'chưa bao giờ') as lan_don_gan_nhat
from pg_stat_user_tables
order by pg_relation_size(relid) desc
limit 5;
Nếu bảng lớn nhất của bạn hiện chưa bao giờ ở cột cuối, hãy kiểm ngay hai thứ: autovacuum_enabled trong reloptions của bảng đó, và pg_stat_activity xem có giao dịch nào đang mở lâu không. Hai nguyên nhân này chiếm gần hết các ca bloat thật.
Phần sau đo giao dịch và bốn mức cô lập: chính xác thì read committed cho phép chuyện gì xảy ra mà repeatable read thì không.