Transaction ID wraparound: vì sao PostgreSQL phải freeze tuple cũ? Nâng cao
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 danh mục gần như không bao giờ được cập nhật, n_dead_tup = 0, nên nhóm vận hành cho rằng không cần VACUUM. Vậy mà PostgreSQL vẫn chạy autovacuum “to prevent wraparound” trên bảng đó.
Lý do: mỗi version row mang XID của transaction đã tạo nó, và XID thường chỉ có 32-bit.
Sau đủ nhiều transaction bộ đếm quay vòng; một tuple quá cũ vẫn được so sánh như XID thường sẽ bị hiểu nhầm là đến từ “tương lai” và trở nên vô hình.
PostgreSQL chống lại bằng cách freeze tuple đủ cũ. Đây là bảo trì an toàn dữ liệu, không phải tối ưu hiệu năng.
Bài chỉ có câu lệnh đọc trạng thái. Không mô phỏng hàng tỷ transaction, không hạ ngưỡng bảo vệ, không tắt autovacuum.
Thước đo tuổi XID
Phần tiêu đề “Thước đo tuổi XID”age() đếm số XID đã trôi qua, không phải thời gian. Các mốc dưới đây là mặc định của PostgreSQL 17 (đều từ tài liệu chính thức), đọc từ trên xuống khi tuổi tăng:
Tuổi XID (age) |
Mốc | Điều xảy ra |
|---|---|---|
| 0 | Vừa freeze xong | Không có gì phải làm |
| 50 triệu | vacuum_freeze_min_age |
Khi VACUUM chạy, freeze các tuple cũ hơn mốc này |
| 150 triệu | vacuum_freeze_table_age |
VACUUM tiếp theo chuyển sang quét aggressive để đẩy relfrozenxid lên |
| 200 triệu | autovacuum_freeze_max_age |
Autovacuum chống wraparound bắt đầu cho bảng đó, kể cả bảng không có dead tuple |
| 1,6 tỷ | vacuum_failsafe_age |
Failsafe: VACUUM bỏ cost-based delay và bỏ index vacuuming để chạy nhanh nhất có thể |
| Còn 40 triệu XID | (cách mốc quay vòng, tức tuổi khoảng 2,107 tỷ) | Server bắt đầu ghi WARNING ... must be vacuumed within N transactions |
| Còn 3 triệu XID | (tuổi khoảng 2,144 tỷ) | Server từ chối cấp XID mới: mọi lệnh ghi dừng |
Giới hạn tuyệt đối là 2^31 = 2.147.483.648, tức khoảng hai tỷ XID đang “ở quá khứ”; nửa vòng còn lại coi là tương lai. Lưu ý các mốc cấu hình bị chặn bởi nhau: ví dụ vacuum_freeze_min_age hiệu lực tối đa bằng một nửa autovacuum_freeze_max_age.
Multixact (dùng khi nhiều transaction cùng khoá một row, xem row-level locks) có bộ đếm riêng và mốc riêng: autovacuum_multixact_freeze_max_age mặc định 400 triệu, vacuum_multixact_freeze_table_age 150 triệu, failsafe 1,6 tỷ. Cảnh báo ở 40 triệu và từ chối ở 3 triệu còn lại áp dụng tương tự. Hệ thống nhiều foreign key và FOR SHARE cần theo dõi cả mxid_age().
Khái niệm cốt lõi
Phần tiêu đề “Khái niệm cốt lõi”- Freeze đánh dấu một tuple đủ cũ là luôn nhìn thấy, thay vì dựa vào so sánh XID. Từ PostgreSQL 9.4 đó là một bit trạng thái;
xminhiển thị có thể vẫn giữ nguyên số cũ. pg_class.relfrozenxidlà mốc XID cũ nhất có thể chưa freeze trong một relation;pg_database.datfrozenxidlà mốc thấp nhất của cả database. Đó là mốc bảo thủ do VACUUM duy trì, không thể thay bằngMIN(xmin); đừng quétxmintoàn bảng để điều tra.- Bảng chỉ insert vẫn cần aggressive VACUUM vì tuple cũ còn mang XID cũ. PostgreSQL 18 cho VACUUM thường freeze sớm một phần page all-visible (eager freezing, điều chỉnh bằng
vacuum_max_eager_freeze_failure_rate), giúp giảm bớt đợt freeze lớn, nhưng không bỏ nhu cầu freeze. - XID đang dùng là 32-bit trên PostgreSQL 17.
Cách hoạt động
Phần tiêu đề “Cách hoạt động”Mỗi transaction cần ID nhận một XID trong không gian 32-bit. ID không tăng vô hạn trong trường 32-bit.
- Transaction cần XID nhận một số trong không gian 32-bit.
- Tuple tạo ra mang XID đó; khi transaction mới xuất hiện,
age(xmin)của nó tăng. - VACUUM freeze tuple đủ điều kiện, visibility không còn phụ thuộc phép so sánh XID thường.
- Nếu bảng không được freeze kịp, autovacuum chống wraparound khởi động dù không có dead tuple; nếu vẫn thất bại, server cảnh báo rồi chặn lệnh cần XID mới.
Cảnh báo: truy vấn và ngưỡng
Phần tiêu đề “Cảnh báo: truy vấn và ngưỡng”SELECT datname, age(datfrozenxid) AS xid_age, 2147483648 - age(datfrozenxid) AS xid_remainingFROM pg_databaseORDER BY xid_age DESC;Đặt alert theo xid_age của từng database, và thêm age(relfrozenxid) lớn nhất trong từng database:
| Mức | Gợi ý ngưỡng (với mặc định 200M) | Hành động |
|---|---|---|
| Quan sát | Trên 200 triệu và đang giảm sau mỗi đợt autovacuum | Bình thường: autovacuum đang làm việc |
| Cảnh báo | Trên 500 triệu, hoặc không giảm sau một đợt autovacuum | Tìm thứ đang giữ horizon (bên dưới) |
| Page người trực | Trên 1 tỷ | Cách failsafe (1,6 tỷ) chưa xa; xử lý ngay trong ngày |
Các ngưỡng 500 triệu và 1 tỷ là gợi ý vận hành của bài này, không phải số của PostgreSQL. Đo thêm tốc độ tiêu thụ XID (lấy mẫu age(datfrozenxid) mỗi vài phút) để quy ra “còn bao nhiêu ngày”: tuổi 10 triệu có thể là nhiều tháng ở hệ ít ghi hoặc vài giờ ở hệ ghi dày.
Script trong repo in các ngưỡng cấu hình, tuổi từng database và 10 relation già nhất (có nối TOAST):
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-wraparound/check-xid-age.sqlKhi tuổi không giảm: tìm thứ giữ horizon
Phần tiêu đề “Khi tuổi không giảm: tìm thứ giữ horizon”Transaction mở lâu, prepared transaction, replication slot cũ (và replica bật hot_standby_feedback) giữ chân trời XID, nên VACUUM chạy xong tuổi vẫn không giảm. Tăng ngưỡng cấu hình chỉ dời cảnh báo. Bắt đầu bằng truy vấn quan sát:
SELECT pid, state, xact_start, age(backend_xid) AS xid_age, age(backend_xmin) AS xmin_age, queryFROM pg_stat_activityWHERE backend_xid IS NOT NULL OR backend_xmin IS NOT NULLORDER BY xact_start NULLS LAST;
SELECT gid, prepared, age(transaction) AS xid_ageFROM pg_prepared_xacts ORDER BY prepared;
SELECT slot_name, active, age(xmin) AS xmin_age, age(catalog_xmin) AS catalog_xmin_ageFROM pg_replication_slots;Các cột XID có thể NULL, age(NULL) cũng NULL: không phải lỗi. Xoá slot có thể buộc replica dựng lại, hãy xác nhận với người vận hành replica.
Nếu server đã phát cảnh báo wraparound, xử lý nguồn giữ horizon rồi chạy VACUUM trên đúng database với quyền đủ xử lý cả system catalogs, theo hướng dẫn của đúng phiên bản. VACUUM FULL không phải cứu hộ: nó rewrite bảng, lấy lock mạnh và bản thân có thể cần XID.
Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”Checklist production
Phần tiêu đề “Checklist production”- Alert
age(datfrozenxid)theo database: cảnh báo 500M, page 1B (vàmxid_age()tương ứng). - Giám sát
age(relfrozenxid)lớn nhất theo bảng, nhớ cả TOAST. - Ghi tốc độ tiêu thụ XID mỗi ngày để biết “còn bao lâu” thay vì chỉ con số tuyệt đối.
- Định kỳ rà prepared transaction, replication slot không hoạt động, session
idle in transaction. - Bảng khổng lồ insert-only: lên lịch
VACUUM (FREEZE)ngoài giờ cao điểm thay vì chờ autovacuum aggressive. - Không tắt autovacuum, không dùng
VACUUM FULLđể cứu hộ. - Có runbook cho trường hợp đã có WARNING: ai được phép dừng session, xoá slot, chạy VACUUM.
Trade-offs
Phần tiêu đề “Trade-offs”Freeze đúng nhịp
- Row sống lâu hơn vòng đời một XID 32-bit mà vẫn đúng visibility.
- Autovacuum chống wraparound tạo khoảng đệm lớn trước giới hạn thật.
age(relfrozenxid)vàage(datfrozenxid)giúp phát hiện xu hướng bằng truy vấn chỉ đọc.
Chi phí và rủi ro vận hành
- Aggressive VACUUM quét nhiều page, tốn I/O hơn VACUUM thường.
- Transaction, slot hoặc prepared transaction giữ horizon làm freeze không tiến.
- Tăng ngưỡng hoặc tắt autovacuum chỉ trì hoãn và giảm thời gian phản ứng.
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 bảng chỉ insert vẫn cần aggressive VACUUM? (2) age(datfrozenxid) = 10 triệu có cho biết còn bao nhiêu ngày an toàn không? (3) Failsafe ở 1,6 tỷ thay đổi hành vi VACUUM thế nào?