How we stopped xmin horizon blocking Postgres vacuuming

Bài tóm tắt Trigger.dev 2024 2. Tăng trưởng Độ khó: Trung bình

Trigger.dev · 2024 · 2. Tăng trưởng · Độ khó: Trung bình — bài gốc

Bối cảnh

  • Trigger.dev (nền tảng chạy background job) dùng Postgres làm DB chính. Queue chính cho run (v3) nằm trên Redis; còn tác vụ nội bộ (lịch, email cảnh báo...) chạy bằng Graphile Worker, một job queue trên chính Postgres, tìm job tiếp theo bằng sequential scan (rất nhanh khi bảng dưới khoảng 10.000 dòng).
  • 15/8/2024, 13:45 → 19:25 UTC: CPU của primary nhảy từ 5–30% lên 95–99% và đứng đó. Queue dài ra, một số run hỏng vì API timeout, một số queue kẹt do lệch giữa Redis và Postgres.

Diễn biến chẩn đoán

  • Lượng run và log bình thường (có rate limit), nên không phải do tải tăng.
  • Query của Graphile thường rất nhanh nay mất 2 giây (25 lần/giây). Một query INSERT ... ON CONFLICT UPDATE vào bảng TaskQueue mất 46 giây; huỷ transaction đó cũng không đỡ. Đây là manh mối dẫn đúng hướng nhưng không phải gốc.
  • Dead tuple: bảng job của Graphile có 600.000 dead tuple và đang tăng. Bảng có tỷ lệ xoá cao, sequential scan phải lướt qua toàn bộ dòng chết. Họ đã kiểm tra bảng nhỏ, nhưng chưa kiểm tra dead tuple.
  • Vacuum không dọn được vì xmin horizon (ID transaction cũ nhất còn sống) bị kẹt. Thử mọi cách thường được khuyên đều không tác dụng: kill transaction dài, tắt hot_standby_feedback, replication slot thừa, prepared transaction treo.
  • 19:00: failover (promote một read replica trên RDS, đã thêm replica dự phòng trước). Mất dưới 60 giây; primary mới chạy ở 4% CPU, vacuum chạy lại bình thường. Nguyên nhân kẹt xmin không bao giờ được giải thích.

Rút ra (theo Trigger.dev)

  • Phải giám sát dead tuple và vacuum, nhất là với bảng xoá nhiều và truy vấn kiểu sequential scan. Họ thêm pganalyze.
  • Failover sớm hơn: tưởng là cách "thô" nhưng vài chục giây gián đoạn lại là lựa chọn tốt nhất.
  • Queue trên Redis cho throughput cao và concurrency theo tenant, nhưng khó nhất quán với Postgres; cần bảo đảm giao dịch mạnh hơn giữa hai bên.

Bài học cho SWE

  • Dùng Postgres làm queue được, cho tới khi vacuum không theo kịp: bảng có tỷ lệ xoá cao biến dead tuple thành nút cổ chai. Cùng họ vấn đề vacuum với Notion và Figma (Notion 2021, Chọn database theo workload).
  • Khi đã thử hết cách chẩn đoán, failover sang máy khác là một công cụ khôi phục hợp lệ; quyết sớm hơn là đỡ hàng giờ (Sự cố và postmortem).
  • Hai kho cho cùng một trạng thái (Redis queue, Postgres) sẽ lệch nhau lúc sự cố; cần cơ chế phát hiện và bù.

Liên quan