DuckDev
#postgresql#database#disaster-recovery#dba#devops

Cuộc Chạy Đua Với 2 Tỷ Giao Dịch: Giải Cứu Database PostgreSQL Khỏi Thảm Họa Transaction ID Wraparound

21 tháng 2, 2026

Cuộc Chạy Đua Với 2 Tỷ Giao Dịch: Giải Cứu Database PostgreSQL Khỏi Thảm Họa Transaction ID Wraparound

Một case study chi tiết về cách chẩn đoán và phục hồi sau sự cố Transaction ID wraparound suýt gây sập hệ thống trên database PostgreSQL production dung lượng 2TB, và một cú twist bất ngờ đã biến một thao tác 60 giây thành 50 phút downtime.

Lời Mở Đầu: Quả Bom Hẹn Giờ Ít Ai Để Ý

Trong thế giới quản trị database, có những mối nguy hiểm rõ ràng như mất điện, lỗi phần cứng, hay các cuộc tấn công DDoS. Nhưng cũng có những kẻ thù thầm lặng, những quả bom hẹn giờ được cài cắm sâu bên trong kiến trúc hệ thống, và một trong những quả bom đáng sợ nhất đối với người dùng PostgreSQL chính là Transaction ID (XID) Wraparound.

Nếu bạn làm việc với PostgreSQL, bạn sẽ biết quy tắc vàng: giới hạn của XID là khoảng 2.147 tỷ. Khi vượt qua con số này, để ngăn chặn nguy cơ hỏng dữ liệu (các transaction trong quá khứ bỗng dưng có thể trông như ở tương lai), PostgreSQL sẽ tự động chuyển toàn bộ database sang chế độ chỉ đọc (read-only). Việc khôi phục đòi hỏi phải chạy VACUUM ở chế độ offline, single-user, một quá trình có thể mất từ vài giờ đến vài ngày tùy thuộc vào kích thước database. Đối với một table 2TB đang phục vụ traffic trực tiếp, "vài ngày downtime" không phải là một kịch bản giả định, nó là một mối đe dọa sống còn đối với doanh nghiệp.

Câu chuyện dưới đây là một cuộc chiến có thật. Một cuộc chạy đua với thời gian để cứu lấy một database production khổng lồ đang trên bờ vực thẳm.

Cảnh Báo Đầu Tiên: Khi Sự Tự Tin Trở Thành Mối Họa

Vào một buổi sáng thứ Ba yên tĩnh, một cảnh báo từ hệ thống giám sát vang lên: MaximumUsedTransactionIDs trên database PostgreSQL production của chúng tôi đã vượt ngưỡng 500 triệu.

Database này là xương sống của một nền tảng SaaS có lưu lượng truy cập cao. Trọng tâm của nó là một bảng tên events dung lượng 800GB, cùng với nhiều index lớn, nâng tổng dung lượng lên hơn 2TB. Nó xử lý hàng trăm truy vấn mỗi giây, 24/7.

Với con số 500 triệu, chúng tôi không quá lo lắng. Cơ chế autovacuum hàng tuần vẫn luôn giữ mọi thứ trong tầm kiểm soát. Chúng tôi tin rằng lần chạy VACUUM theo lịch trình tiếp theo sẽ tự động dọn dẹp và đặt lại bộ đếm XID.

Giả định đó, như chúng tôi sắp nhận ra, là một sai lầm nguy hiểm.

Diễn Biến Sự Cố: Cơn Bão Đang Kéo Đến

Ngày 4 — 700 triệu XIDs

Tiến trình autovacuum đã khởi động nhưng vẫn chưa hoàn thành trên bảng events. Điều này không quá lạ — trên một table lớn với traffic dày đặc, VACUUM có thể mất hơn một ngày. Chúng tôi quyết định chờ đợi. Đôi khi, lượng ghi lớn có thể khiến VACUUM chạy chậm hơn do phải cạnh tranh tài nguyên với các truy vấn production.

Nhưng bộ đếm XID vẫn tiếp tục tăng.

Ngày 9 — Vượt Mốc 1 Tỷ XIDs

VACUUM vẫn chưa chạy xong. Chúng tôi đã phá vỡ ngưỡng an toàn nội bộ là 1 tỷ. Lúc này, chúng tôi phải can thiệp thủ công và khởi chạy VACUUM FREEZE — một dạng VACUUM mạnh mẽ hơn, buộc phải đóng băng tất cả các tuple (hàng) để đặt lại tuổi transaction ID của chúng. Đây là phương án leo thang được đề xuất bởi đối tác tư vấn database của chúng tôi.

Chúng tôi kỳ vọng VACUUM FREEZE sẽ giải quyết tình hình trong vòng vài ngày. Nó chưa bao giờ làm chúng tôi thất vọng.

Ngày 14 — Lời Cảnh Báo Từ AWS

Sáng sớm ngày 14, chúng tôi nhận được một email tự động từ AWS RDS:

"Instance đang tiến gần đến ngưỡng transaction-wraparound. Cần phải có hành động khẩn cấp để tránh downtime trong tương lai. Khi tuổi XID đạt 2,146,483,647, database sẽ ngừng chấp nhận các transaction mới."

Con số lúc này đã là 1.5 tỷ XIDs. Chúng tôi ngay lập tức họp khẩn với support của AWS và đối tác tư vấn. Tình hình lúc đó:

  • VACUUM FREEZE đã chạy được 5 ngày.
  • Trong số 10 index trên bảng events, 7 đã hoàn thành, nhưng 3 index lớn nhất vẫn đang xử lý mà không có thời gian dự kiến hoàn thành.
  • Dựa trên tốc độ tăng của XID, chúng tôi ước tính chỉ còn 5 ngày nữa là chạm mốc 2 tỷ.

Năm ngày. Đó là quỹ thời gian còn lại của chúng tôi.

Phương Án Cuối Cùng: pg_repack

Ngày 15 — Quyết Định Sống Còn

Sáng ngày 15, VACUUM FREEZE không cho thấy bất kỳ tiến triển nào. Ba index còn lại đã bị kẹt. Sau 6 ngày chạy, rõ ràng VACUUM FREEZE sẽ không thể cứu chúng tôi. Chúng tôi buộc phải chuyển sang kế hoạch dự phòng cuối cùng: pg_repack.

Tại sao lại là pg_repack?

pg_repack là một extension của PostgreSQL cho phép xây dựng lại hoàn toàn một bảng và các index của nó, loại bỏ toàn bộ "bloat" (dữ liệu rác) trong quá trình này. Không giống như VACUUM FULL (khóa toàn bộ bảng trong suốt thời gian chạy), pg_repack hoạt động online. Nó tạo ra một bản sao của bảng, sử dụng trigger để đồng bộ các thay đổi, và cuối cùng thực hiện một thao tác hoán đổi (swap) chỉ trong thời gian rất ngắn. Thời gian downtime dự kiến chỉ là hai lần khóa ngắn, thường dưới 60 giây mỗi lần.

Đối với một table 2TB đang cận kề thảm họa XID, pg_repack là lựa chọn tốt nhất: downtime tối thiểu, loại bỏ triệt để bloat, và reset hoàn toàn bộ đếm XID.

Nhưng trước tiên, chúng tôi cần chuẩn bị mọi thứ để đảm bảo pg_repack thành công.

Dọn Đường Cho "Vị Cứu Tinh"

Bước 1: Cấp phát thêm dung lượng lưu trữ. pg_repack cần tạo một bản sao đầy đủ của table trước khi hoán đổi. Chúng tôi đã tăng dung lượng lưu trữ từ 8.5TB lên 11TB để đảm bảo không bị hết dung lượng giữa chừng. Hết dung lượng trong khi pg_repack đang chạy sẽ là một thảm họa thực sự.

Bước 2: Loại bỏ gánh nặng không cần thiết. Chúng tôi phân tích các index trên bảng events và phát hiện ra một số index lớn không còn được sử dụng. Việc loại bỏ chúng không chỉ giải phóng hàng trăm GB dung lượng mà còn giúp pg_repack chạy nhanh hơn đáng kể.

-- Việc xóa index trên một bảng lớn và đang hoạt động cần được thực hiện cẩn thận
-- Sử dụng CONCURRENTLY để tránh khóa bảng trong thời gian dài
DROP INDEX CONCURRENTLY unused_index_1;
DROP INDEX CONCURRENTLY unused_index_2;

Sau khi loại bỏ khoảng 800GB index không dùng đến, chúng tôi đã sẵn sàng cho trận chiến cuối cùng.

Cú Twist Bất Ngờ: Khi Người Hùng Trở Thành Kẻ Ngáng Đường

Chúng tôi khởi chạy pg_repack. Mọi thứ diễn ra hoàn hảo. Tiến trình sao chép dữ liệu, tạo index mới, và đồng bộ thay đổi diễn ra suôn sẻ. Chúng tôi nín thở chờ đợi đến giai đoạn cuối cùng: hoán đổi bảng cũ và bảng mới.

Đây là lúc pg_repack cần lấy một khóa ACCESS EXCLUSIVE trên bảng events để thực hiện việc hoán đổi một cách an toàn. Thời gian khóa dự kiến: dưới 60 giây.

Nhưng 60 giây trôi qua... rồi 5 phút... rồi 10 phút. Hệ thống báo động réo lên inh ỏi. Ứng dụng của chúng tôi đã ngừng hoạt động. Downtime đã bắt đầu.

Chúng tôi nhanh chóng kiểm tra các tiến trình đang bị khóa:

SELECT 
    activity.pid,
    activity.usename,
    activity.query,
    blocking.pid AS blocking_id,
    blocking.query AS blocking_query
FROM pg_stat_activity AS activity
JOIN pg_stat_activity AS blocking ON blocking.pid = ANY(pg_blocking_pids(activity.pid));

Kết quả khiến chúng tôi sững sờ. Tiến trình pg_repack đang bị chặn bởi... một tiến trình autovacuum! Trớ trêu thay, chính cơ chế bảo vệ của PostgreSQL, nhận thấy tuổi XID của database đã quá cao, đã tự động khởi chạy một tiến trình autovacuum khẩn cấp để chống wraparound. Tiến trình này cũng đang giữ một lock trên bảng, ngăn không cho pg_repack lấy được khóa ACCESS EXCLUSIVE mà nó cần.

Kẻ thù của chúng tôi lại chính là vị cứu tinh bất đắc dĩ. Một cuộc đối đầu giữa hai cơ chế bảo vệ.

Sau 50 phút downtime căng thẳng, chúng tôi không còn lựa chọn nào khác. Chúng tôi phải "giết" tiến trình autovacuum đang chặn đường.

-- Lấy PID của tiến trình autovacuum đang chặn từ câu lệnh trên
SELECT pg_terminate_backend(<blocking_pid>);

Ngay khi tiến trình autovacuum bị ngắt, pg_repack lập tức lấy được khóa, hoàn thành việc hoán đổi trong vài giây. Hệ thống hoạt động trở lại. Thảm họa đã được ngăn chặn.

Kết Quả và Bài Học Xương Máu

Sau khi pg_repack hoàn tất:

  • Tuổi XID của database giảm mạnh từ 1.6 tỷ xuống chỉ còn 440 triệu.
  • Dung lượng bảng được giải phóng đáng kể.
  • Tiến trình VACUUM mà trước đây mất nhiều ngày để chạy, giờ chỉ mất 2.5 giờ để hoàn thành.

Sự cố này đã dạy cho chúng tôi những bài học vô giá:

  1. Đừng bao giờ xem nhẹ cảnh báo XID: Giám sát tuổi XID (age(datfrozenxid)) phải là ưu tiên hàng đầu. Khi con số bắt đầu tăng, hãy hành động ngay lập tức.
  2. Hiểu rõ công cụ của bạn: VACUUM, VACUUM FREEZE, và pg_repack đều có những cơ chế khóa và trường hợp sử dụng khác nhau. Hiểu rõ chúng hoạt động như thế nào là chìa khóa để xử lý sự cố.
  3. Lường trước những tương tác không mong muốn: Kể cả khi bạn đang thực hiện một thao tác cứu hộ, các cơ chế tự động của database vẫn có thể chạy và gây ra xung đột. Hãy chuẩn bị cho những kịch bản này.
  4. Bảo trì chủ động luôn tốt hơn chữa cháy: Việc thường xuyên dọn dẹp bloat, loại bỏ index không sử dụng và theo dõi hiệu suất VACUUM có thể đã ngăn chặn được sự cố này ngay từ đầu.

Kết Luận

Thảm họa Transaction ID Wraparound là một mối đe dọa có thật và cực kỳ nguy hiểm trong PostgreSQL. Câu chuyện của chúng tôi là một minh chứng cho thấy ngay cả những đội ngũ có kinh nghiệm cũng có thể bị bất ngờ. Vận hành một hệ thống database lớn không chỉ là việc giữ cho nó hoạt động, mà còn là việc hiểu sâu sắc các cơ chế bên trong nó, lường trước các kịch bản tồi tệ nhất và luôn có một kế hoạch B (và cả C).


## Giải thích thuật ngữ

  • Transaction ID (XID): Một số nhận dạng duy nhất, có độ dài 32-bit, được gán cho mỗi giao dịch (thêm, sửa, xóa) trong PostgreSQL.
  • XID Wraparound: Hiện tượng khi bộ đếm XID 32-bit sử dụng hết ~4 tỷ giá trị và quay vòng trở lại số 0. Nếu không được quản lý, nó có thể làm hỏng dữ liệu do các giao dịch cũ đột nhiên trông như mới.
  • VACUUM: Một quy trình bảo trì thiết yếu trong PostgreSQL để thu hồi không gian lưu trữ bị chiếm dụng bởi các hàng dữ liệu đã chết (dead tuples).
  • autovacuum: Một tiến trình chạy nền tự động thực thi các lệnh VACUUMANALYZE để duy trì hiệu suất và sự ổn định của database.
  • VACUUM FREEZE: Một phiên bản mạnh mẽ hơn của VACUUM, nó quét qua bảng và đánh dấu các hàng cũ là "frozen" (đóng băng), có nghĩa là XID của chúng được coi là vô hạn tuổi và sẽ không gây ra sự cố wraparound.
  • pg_repack: Một extension của PostgreSQL cho phép xây dựng lại các bảng và index online (trong khi hệ thống đang chạy), giúp loại bỏ dữ liệu rác (bloat) với thời gian khóa tối thiểu.
  • ACCESS EXCLUSIVE Lock: Mức khóa nghiêm ngặt nhất trong PostgreSQL, nó chặn tất cả các hoạt động khác (bao gồm cả việc đọc) trên một bảng. Cần thiết cho các thao tác thay đổi cấu trúc bảng.
Cuộc Chạy Đua Với 2 Tỷ Giao Dịch: Giải Cứu Database PostgreSQL Khỏi Thảm Họa Transaction ID Wraparound | Tien Nguyen