Độ tin cậy18 tháng 4, 20267 phút đọc

Exponential backoff và jitter cho việc thử lại webhook

Vì sao exponential backoff kiểu ngây thơ gây ra thundering herd, jitter khắc phục nó thế nào, và lịch thử lại chuẩn production thực sự trông ra sao.

KHOẢNG CÁCH GIỮA CÁC LẦN THỬ · TĂNG THEO CẤP SỐ NHÂNbây giờ5 phút30 phút3 giờ18 giờbỏ cuộcstatus=failed · payload vẫn nằm trong nhật ký · vẫn phát lại được
Một lịch thử lại giãn rộng sau mỗi lần gửi. Lần gửi cuối không phải cơ hội cuối: một sự kiện đã hết lượt thử lại vẫn nằm trong nhật ký và vẫn phát lại được.

TL;DR

  • Exponential backoff (khoảng chờ tăng theo cấp số nhân) kiểu ngây thơ (delay = base * 2^attempt) tạo ra thundering herd (cả đàn request ập tới cùng lúc), vì mọi client thử lại (retry) cùng một lúc
  • Thêm jitter (độ lệch ngẫu nhiên) để dàn trải các lần thử lại. Equal jitter (base/2 + random(base/2)) là điểm cân bằng tốt nhất
  • Giới hạn độ trễ tối đa, giới hạn tổng thời gian thử lại, và tách 4xx (không thử lại) khỏi 5xx (có thử lại)
  • Dùng circuit breaker (cơ chế ngắt mạch) ở cấp đích sau khoảng 20 lần thất bại liên tiếp. Thử lại không sửa được một endpoint đã hỏng
  • Phát lại (replay) dễ dàng tốt hơn thử lại dồn dập: nếu phát lại chỉ tốn một cú bấm, bạn chỉ cần thử lại để xử lý các lỗi tạm thời

Phần lớn code thử lại tôi thấy ngoài thực tế trông như thế này:

for (let i = 0; i < maxRetries; i++) {
  try {
    return await deliver(event);
  } catch {
    await sleep(1000 * Math.pow(2, i));  // 1s, 2s, 4s, 8s...
  }
}

Đây là exponential backoff, và nó tốt hơn thử lại theo khoảng cố định. Nhưng nó cũng sai theo một kiểu chỉ lộ ra trên production, khi hàng trăm webhook cùng thất bại và cùng thử lại vào đúng một thời điểm, quật server đang hồi phục của bạn ngã lăn ra lần nữa.

Cách sửa là jitter. Dưới đây là lý do, và cách làm cho đúng.

Thundering herd

Hãy hình dung endpoint của bạn ngừng hoạt động lúc T+0. Trong 60 giây tiếp theo, 500 webhook của Stripe đến và đều thất bại. Mọi hàng đợi thử lại giờ đều có 500 lần thử lại thứ nhất được lên lịch lúc T+5min (hoặc bất kể độ trễ cơ sở của bạn là bao nhiêu), 500 lần thử lại thứ hai lúc T+10min, và cứ thế.

Endpoint của bạn hoạt động trở lại lúc T+4min. Xin chúc mừng: giờ bạn nhận 500 lần thử lại đồng thời ập tới lúc T+5min, đẩy server vẫn đang hồi phục của bạn quá ngưỡng chịu tải lần nữa. Đó là thundering herd.

KHÔNG CÓ JITTER · MỌI LẦN THỬ LẠI ĐỀU CHỌN CÙNG MỘT THỜI ĐIỂM500 lần thử lại trong một mili giâyEQUAL JITTER · CÙNG SỐ LẦN, CÙNG ĐƯỜNG CONGrải đều trên cả khoảng thời gianT+0T+5minT+10min
Cùng 500 lần thử lại, cùng đường cong backoff. Khác biệt duy nhất là độ trễ là một con số cố định hay một khoảng.

Backoff tất định mắc vấn đề này ngay từ thiết kế. Mọi client hay sự kiện thất bại cùng lúc sẽ thử lại cùng lúc.

Jitter

Jitter thêm yếu tố ngẫu nhiên vào thời điểm thử lại để các sự kiện được thử lại dàn trải thay vì dồn thành một đỉnh. Ba chiến lược phổ biến:

Full jitter

const base = 1000 * Math.pow(2, attempt);  // 1s, 2s, 4s, 8s
const sleep = Math.random() * base;         // 0 to base

Mỗi lần thử lại chờ một khoảng bất kỳ từ 0 đến trọn khoảng backoff. Độ dàn trải tối đa, nhưng có những lần thử lại được bắn gần như ngay, và điều đó có thể có vẻ kém hiệu quả.

Equal jitter

const base = 1000 * Math.pow(2, attempt);
const sleep = base / 2 + Math.random() * (base / 2);

Mỗi lần thử lại chờ trong khoảng từ một nửa đến trọn khoảng backoff. Có mức tối thiểu, mà vẫn dàn trải tốt.

Decorrelated jitter

let sleep = baseDelay;
// on each retry:
sleep = Math.min(maxDelay, baseDelay + Math.random() * (sleep * 3 - baseDelay));

Độ trễ của mỗi lần thử lại là một bội số ngẫu nhiên của lần trước. Phù hợp khi bạn muốn backoff tăng dần mà không theo một công thức cố định.

Với webhook, equal jitter là điểm cân bằng tốt nhất. Nó giữ các lần thử lại tăng xấp xỉ theo cấp số nhân mà vẫn giải tán được đàn request dồn dập.

Lịch thử lại chuẩn production trông như thế nào

Ngoài jitter, đây là những gì các team giàu kinh nghiệm làm.

Giới hạn độ trễ tối đa. Đừng để lần gửi thứ 10 phải chờ 17 phút. Đặt một mức trần, chẳng hạn 60 giây, và khi chạm trần thì cứ giữ ở đó cho đến khi bỏ cuộc.

Giới hạn tổng thời gian thử lại, không chỉ số lần gửi. “Thử lại trong 24 giờ” là một SLA rõ ràng hơn “thử lại 8 lần”, vì cái thứ hai phụ thuộc vào phép tính độ trễ mà không ai nhớ.

Tách lỗi tạm thời khỏi lỗi vĩnh viễn. Một mã 4xx từ đích, đặc biệt là 401, 403 hoặc 404, sẽ không thành công khi thử lại. Server đang báo cho bạn biết request bị sai. Chỉ thử lại với 5xx, 429, timeout và lỗi kết nối.

Hiển thị trạng thái thử lại. Người dùng của bạn cần biết một sự kiện đang ở lần gửi thứ 3 trên 5, với lần thử lại tiếp theo vào một thời điểm cụ thể. Nếu không nhìn thấy được, debug sẽ rất khổ.

Ngắt mạch ở cấp đích. Nếu một đích đã thất bại 20 lần liên tiếp, hãy ngừng thử lại nó một thời gian. Có gì đó sai mà thử lại không sửa được, và bạn chỉ đang đốt tài nguyên tính toán.

AnyHook xử lý chuyện này thế nào

Chính sách thử lại của AnyHook được điều chỉnh theo gói và chạy trên hạ tầng thử lại được quản lý của QStash. Số lần gửi là 3 với gói Free, 5 với gói Pro và 10 với gói Scale, theo một thang cấp số nhân cố định: 5 phút, 30 phút, 3 giờ, 18 giờ, rồi 24 giờ cho mỗi lần gửi sau đó. Không có jitter nào được thêm vào các độ trễ này. Mức trần timeout lần lượt là 60 giây, 120 giây và 5 phút. Sau 20 lần thất bại liên tiếp, circuit breaker tạm dừng việc gửi tới đích đó (10 phút, rồi 30 phút, 2 giờ và 24 giờ ở các lần ngắt tiếp theo) và gửi email cho bạn, nên không có cơn bão thử lại nào. Mã 4xx, trừ 408 và 429, hoàn toàn không được thử lại, và cảnh báo lỗi sẽ nói rõ lý do, vì thử lại không giúp được gì ở đó và bạn cần sửa cấu trúc request.

Hết lượt thử lại không có nghĩa là mất sự kiện. Nó được lưu trong nhật ký của bạn với status=failed, và bạn có thể phát lại thủ công từ dashboard hoặc API bất cứ lúc nào trong thời hạn phát lại của gói (3 ngày với gói Free, 30 ngày với gói Pro, 90 ngày với gói Scale).

Bí kíp

Điều thay đổi cách bạn nghĩ về thử lại: bạn không cần thử lại dồn dập nếu phát lại dễ dàng.

Nếu phát lại chỉ tốn một cú bấm, thử lại 3 lần trong một giờ là đủ. Bạn tự động bắt được phần lớn lỗi tạm thời, còn với những sự cố kéo dài hiếm hoi, một người sẽ phát lại loạt sự kiện thất bại khi server đã ổn định.

So với các hệ thống chỉ dựa vào thử lại, nơi bạn kéo dài thời gian thử lại tới 72 giờ cho chắc, nghĩa là 72 giờ gửi phí công tới một endpoint mà bạn đã biết là đang ngừng hoạt động.

Phát lại dễ dàng tốt hơn hẳn thử lại dồn dập, và đó là điều AnyHook được xây dựng xoay quanh.

Điều cần nhớ

Hãy coi thử lại là lối đi nhanh cho các lỗi tạm thời, không phải thứ thay thế cho việc lưu trữ và phát lại. Những hệ thống trụ vững qua các tuần tồi tệ là những hệ thống mà một lượt gửi thất bại luôn khôi phục được, vì sự kiện đã được ghi nhật ký trước khi được gửi đi.

Lịch thực tế của từng bên gửi có trong tài liệu tham khảo về các nhà cung cấp webhook của chúng tôi. Thử lại cũng có một mức trần cứng mà không ai kiểm soát: cứ thất bại mãi thì Shopify có thể xóa subscription, còn Stripe sẽ vô hiệu hóa endpoint. Và chính thử lại là lý do bản trùng và sự kiện đến sai thứ tự là điều chắc chắn xảy ra chứ không hiếm: Bạn nhận cùng một webhook hai lần.

Tất cả bài viết18 tháng 4, 2026 · 7 phút

Đừng để mất webhook nữa.

Đổi một URL. Có ngay cơ chế thử lại, nhật ký sự kiện và phát lại bằng một cú bấm. Hoặc xem tận mắt trước: đăng ký, chạy bài diễn tập sự cố 30 giây và xem một sự kiện vẫn còn nguyên khi server của bạn ngừng hoạt động. Không cần có server.