Incident report for September 18, 2026: When Deletion Cascades Exhausted Our Database Connections

Bài tóm tắt Inngest 2026 3. Quy mô lớn Độ khó: Trung bình

Inngest · 2026 · 3. Quy mô lớn · Độ khó: Trung bình — bài gốc

Bối cảnh

  • Postgres chính của Inngest giữ dữ liệu quản lý account (account, workspace, cấu hình). Service hỏi nó định kỳ (quyền theo gói, account chạy trên phần hạ tầng nào) qua PgBouncer, và mọi service dùng chung các pool.
  • Đường gỡ cài đặt từ Vercel Marketplace được viết để hard delete (khác chỗ khác đều soft delete) để khách cài lại được. Nó xoá user trong một transaction rồi DELETE account; account và workspace được hàng chục bảng tham chiếu với ON DELETE CASCADE, nên một lần xoá lan ra phần lớn schema trong một transaction và giữ khoá tới khi commit.

Diễn biến (18/9/2026, UTC)

  • 15:39 bắt đầu xoá. 15:41–15:56 nhiều lần retry cho cùng account đến, xếp hàng sau khoá của transaction đầu thay vì fail nhanh.
  • Query bị chặn không trả kết nối: client connection lên khoảng 14 lần bình thường, gần như tất cả chỉ đang chờ. 16:18 PgBouncer đầy (no more connections allowed (max_client_conn)), lỗi lan ra cả nền tảng, kể cả service chẳng liên quan: lập lịch run giảm, event không được ack, nhiều service crash-loop vì không nối được DB lúc khởi động.
  • 16:26 restart PgBouncer: pool lại đầy ngay vì transaction chặn vẫn chạy. 16:39 huỷ các lệnh xoá; 16:42 khoá hết.
  • Lần hai: 17:45 tranh khoá quay lại (nguyên nhân kích hoạt vẫn đang điều tra), trong khi kill switch merge lúc 17:54 đang rollout. Đợt này lan rộng hơn (executor, batches, CDC... crash-loop, hai shard queue dừng). Postgres về bình thường lúc 18:00 nhưng PgBouncer bão hoà thêm khoảng 10 phút vì hàng client chờ phải tự xả. Hồi phục khoảng 18:12.
  • Event đã nhận được lưu bền; việc bị trễ chứ không mất.

Nguyên nhân (bốn yếu tố, theo Inngest)

  1. Lệnh xoá không giới hạn phạm vi: công việc và khoá tăng theo lượng dữ liệu của account, không chia nhỏ, không checkpoint.
  2. Retry khuếch đại thay vì được hấp thụ: không idempotent ở mức transaction, không có gì xếp hàng phía trước.
  3. Query chờ khoá chiếm chỗ trong pool, biến tranh khoá trên vài bảng thành cạn kết nối cho mọi service.
  4. PgBouncer không hồi cùng DB: thời gian phục hồi bị giới hạn bởi PgBouncer chứ không phải bởi lỗi gốc. Yếu tố 3 và 4 biến một job chậm thành sự cố toàn nền tảng có đuôi dài.

Khắc phục

  • Kill switch dừng xoá từ marketplace; yêu cầu xoá vẫn ghi thành event nội bộ có ID xác định để đối soát sau.
  • Chuyển sang soft delete, chỉ gỡ vài bản ghi unique cản việc cài lại.
  • Hard delete còn lại thì ra khỏi đường request, chia lô, rate limit, khử trùng để yêu cầu lặp không chồng lên nhau.
  • Tung bản giảm thiểu vào hệ đang yên, để fix và đợt tái phát không chồng nhau làm mất tín hiệu.
  • Headroom và cô lập trong PgBouncer để một workload không ăn hết kết nối của workload khác; tìm cách xả pool bão hoà chủ động.
  • Cảnh báo theo số khoá và thời gian client chờ, trước khi pool cạn.

Bài học cho SWE

  • ON DELETE CASCADE trên dữ liệu tenant là một transaction không đáy; xoá lớn nên là job nền chia lô (Sự cố và postmortem).
  • Pool kết nối dùng chung là một bulkhead bị thiếu: workload quản trị và workload chính cần pool tách (Hot partition và thundering herd).
  • Retry vào thao tác đang giữ khoá chỉ kéo dài sự cố; cần khử trùng hoặc xếp hàng phía trước.
  • Thời gian phục hồi có thể do thành phần trung gian quyết định; đo cả nó.

Liên quan