Phần 21 đã đo xmin và xmax — hai số hiệu giao dịch quyết định một dòng có hiển thị hay không. Phần này đo chuyện xảy ra khi những con số ấy chạy hết dãy, và vì sao đó là lý do PostgreSQL bắt buộc phải dọn dù bạn có tắt autovacuum đi chăng nữa.
Con số 32 bit
Mỗi giao dịch ghi nhận được một số hiệu tăng dần. Kiểu của nó là 32 bit, tức 4.294.967.296 giá trị — nhưng chỉ dùng được một nửa vòng, 2.147.483.647, vì PostgreSQL so sánh XID theo kiểu vòng tròn: một XID được coi là "cũ hơn" nếu nó nằm trong nửa vòng phía sau.
Nghe thì rất nhiều. Đo tốc độ tiêu thụ thật:
select txid_current();
-- chạy tải, đợi, gọi lại
| Tải | Tốc độ | Cạn 2,1 tỷ sau |
|---|---|---|
| Một kết nối chèn tuần tự | 328 XID/giây | 76 ngày |
pgbench -c 8 -j 4 |
5.053 XID/giây | 4,9 ngày |
Bốn ngày chín. Và pgbench với tám kết nối trên một container không phải là tải nặng — một hệ thống thật với hàng trăm kết nối có thể tiêu nhanh hơn nhiều lần.
Điều đáng chú ý: mỗi câu lệnh đơn lẻ cũng là một giao dịch và cũng ăn một XID. Không cần BEGIN để tiêu XID; một vòng lặp INSERT trong ứng dụng ăn đúng một XID mỗi dòng nếu không gộp giao dịch.
Chỉ có giao dịch ghi mới nhận XID. SELECT thuần không tốn — đó là lý do một máy chủ chỉ đọc gần như không bao giờ tới gần giới hạn.
Chuyện gì xảy ra khi cạn
Hai mốc, và không mốc nào chỉnh được:
| Còn lại | Hậu quả |
|---|---|
| 40.000.000 XID | Cảnh báo trong log ở mỗi lần kết nối |
| 3.000.000 XID | Máy chủ từ chối mọi giao dịch ghi |
Mốc thứ hai không phải là chậm đi hay suy giảm — nó là dừng hẳn. Ứng dụng nhận lỗi ở mọi câu INSERT, UPDATE, DELETE. Cách chữa duy nhất là dừng máy chủ, khởi động ở chế độ đơn người dùng, và chạy VACUUM — trên một cơ sở dữ liệu lớn thì đó là nhiều giờ hoặc nhiều ngày ngừng dịch vụ.
Lý do PostgreSQL chọn dừng thay vì để tràn: nếu XID quay vòng, các dòng cũ đột nhiên trông như được tạo bởi giao dịch ở tương lai và biến mất khỏi mọi truy vấn. Mất dữ liệu im lặng thì tệ hơn ngừng dịch vụ rất nhiều.
Cách tự cứu: đóng băng
PostgreSQL tránh chuyện đó bằng cách đóng băng các dòng đủ cũ. Một dòng được đóng băng nghĩa là xmin của nó được đánh dấu "cũ hơn mọi thứ" vĩnh viễn, nên nó không còn phụ thuộc vào số hiệu giao dịch nữa và XID của nó có thể tái sử dụng.
Mỗi bảng giữ một mốc relfrozenxid — mọi dòng cũ hơn mốc đó đều đã được đóng băng. Khoảng cách từ mốc đó tới hiện tại là age:
select relname, age(relfrozenxid)
from pg_class where relkind = 'r'
order by age(relfrozenxid) desc limit 10;
Khi age vượt autovacuum_freeze_max_age (mặc định 200.000.000), autovacuum chạy một lượt dọn khẩn cấp trên bảng đó để đóng băng và đẩy relfrozenxid lên.
Không thể chờ 200 triệu XID để đo, nên tôi hạ ngưỡng xuống mức tối thiểu PostgreSQL cho phép là 100.000 rồi cho pgbench chạy:
| Giây | age(relfrozenxid) |
Số lần dọn |
|---|---|---|
| 20 | 99.102 | 0 |
| 45 | 11.142 | 2 |
| 70 | 36.823 | 3 |
| 95 | 49.289 | 4 |
| 120 | 71.750 | 5 |
age leo tới sát 100.000 rồi bị kéo về 11.142. Sau đó nó lặp: leo lên, bị kéo về, leo lên. Cơ chế hoạt động đúng như thiết kế.
Điều quan trọng nhất: không tắt được
Đây là phần tôi muốn kiểm chứng kỹ nhất, vì nó là lý do bạn không bao giờ nên tắt autovacuum để "cho nhanh".
Dựng một bảng tắt hẳn autovacuum và không hề ghi vào nó một lần nào:
create table tat(id bigserial primary key, gc text)
with (autovacuum_enabled = off);
insert into tat(gc) select md5(i::text) from generate_series(1, 200000) i;
vacuum freeze tat;
Rồi cho pgbench tiêu XID trên các bảng khác, và theo dõi bảng tat:
| XID hiện tại | age(tat) |
Số lần dọn |
|---|---|---|
| 1.385.574 | 78.820 | 5 |
| 1.438.972 | 29.830 | 6 |
| 1.492.320 | 83.229 | 6 |
| 1.543.408 | 21.713 | 7 |
| 1.595.883 | 74.158 | 7 |
| 1.644.777 | 13.269 | 8 |
autovacuum_count tăng đều — trên một bảng có autovacuum_enabled = off và không có một dòng chết nào. age leo lên 83.229 rồi bị kéo về 21.713, đúng chu kỳ đóng băng.
Cờ autovacuum_enabled = off chỉ áp cho việc dọn thông thường. Dọn chống tràn số là bắt buộc, và PostgreSQL bỏ qua cờ đó. Cũng vậy với autovacuum = off ở mức toàn máy chủ — nó vẫn khởi động worker khi có bảng tới hạn.
Đây là thiết kế đúng, nhưng nó có mặt trái: nếu bạn tắt autovacuum vì bảng quá lớn và bạn sợ nó ăn tài nguyên, bạn không hề tránh được lượt dọn ấy — bạn chỉ dời nó tới lúc bạn không kiểm soát được, và khi đó nó phải quét toàn bảng trong một lần.
Một điều tôi không giải thích được
Máy chủ đặt log_autovacuum_min_duration = 0, tức là ghi log mọi lượt autovacuum. Log có 68 dòng cho pgbench_tellers, 42 dòng cho pgbench_branches, và không dòng nào cho bảng tat — dù autovacuum_count của nó rõ ràng tăng từ 5 lên 8 và last_autovacuum có mốc thời gian cụ thể.
Tôi không tìm ra lý do. Bộ đếm và mốc thời gian là bằng chứng đủ mạnh để tin rằng lượt dọn có xảy ra, nhưng tôi không muốn đoán bừa vì sao nó không xuất hiện trong log.
Nếu bạn theo dõi wraparound trên máy chủ thật, đây là lý do cụ thể để đừng dựa vào log: hãy đọc pg_class.relfrozenxid và pg_stat_user_tables.autovacuum_count trực tiếp.
Bảng tĩnh mới là bảng nguy hiểm
Trực giác thông thường là bảng ghi nhiều thì rủi ro cao. Thực tế ngược lại.
Bảng ghi nhiều sinh ra dòng chết liên tục, nên nó vượt ngưỡng dọn thông thường thường xuyên, và mỗi lượt dọn ấy cũng đóng băng luôn các dòng đủ cũ. age của nó không bao giờ leo cao.
Bảng lưu trữ thì ngược lại: ghi một lần rồi để đó nhiều năm. Nó không bao giờ có đủ dòng chết để vượt ngưỡng dọn thông thường, nên PostgreSQL không chạm tới nó — cho đến khi age chạm 200.000.000. Lúc đó nó phải quét toàn bộ bảng trong một lượt.
Trên bảng 2 TB, lượt quét đó kéo dài nhiều giờ hoặc nhiều ngày, chạy với vacuum_cost_delay hãm tốc (phần 23 đo được 15 MB/giây), và bạn không chọn được thời điểm.
Cách xử lý là chủ động đóng băng vào lúc bạn chọn:
vacuum freeze luu_tru_2019;
Hoặc chia nhỏ ra và làm dần theo lịch bảo trì. Điểm mấu chốt là lượt quét ấy sẽ xảy ra dù bạn có làm gì hay không — bạn chỉ chọn được là nó xảy ra lúc hai giờ sáng chủ nhật hay lúc chín giờ sáng thứ hai.
Các tham số liên quan
| Tham số | Mặc định | Ý nghĩa |
|---|---|---|
autovacuum_freeze_max_age |
200.000.000 | age vượt mức này thì dọn khẩn cấp |
vacuum_freeze_min_age |
50.000.000 | Dòng cũ hơn mức này thì được đóng băng khi dọn |
vacuum_freeze_table_age |
150.000.000 | VACUUM thường chuyển sang quét toàn bảng |
vacuum_failsafe_age |
1.600.000.000 | Bỏ hết hãm tốc, dọn bằng mọi giá |
vacuum_failsafe_age là lưới an toàn cuối cùng, thêm từ PostgreSQL 14: khi age vượt 1,6 tỷ, autovacuum bỏ qua cost_delay, bỏ qua việc dọn chỉ mục, và chạy hết tốc lực. Nếu bạn thấy nó kích hoạt trong log thì tình hình đã rất xấu — nhưng ít nhất máy chủ đang tự cứu.
Hạ autovacuum_freeze_max_age xuống có nghĩa là đóng băng thường xuyên hơn, mỗi lượt nhẹ hơn. Với bảng lưu trữ rất lớn thì đó là đánh đổi hợp lý:
alter table luu_tru set (autovacuum_freeze_max_age = 100000000);
Thử ba mươi giây
select datname,
age(datfrozenxid) as tuoi,
2147483647 - age(datfrozenxid) as con_lai,
round(100.0 * age(datfrozenxid) / 2147483647, 2) as phan_tram_da_dung
from pg_database
order by age(datfrozenxid) desc;
Và bảng nào đang kéo con số đó lên:
select relname,
age(relfrozenxid) as tuoi,
pg_size_pretty(pg_relation_size(oid)) as kich_thuoc
from pg_class
where relkind in ('r','m','t')
order by age(relfrozenxid) desc
limit 10;
Dưới 10% thì không cần nghĩ tới. Trên 50% thì nên xem bảng đứng đầu danh sách là bảng nào, nó lớn cỡ nào, và bạn có muốn tự chọn thời điểm VACUUM FREEZE cho nó hay không.
Phần sau chuyển sang 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.