Retry, backoff và jitter: thử lại mà không làm lỗi nặng hơn Trung cấp
Thời gian đọc ước tính: 7 phút
Vấn đề
Phần tiêu đề “Vấn đề”Một dependency quá tải trả 503 cho 1.000 request. Nếu mọi caller thử lại ngay, nó nhận thêm 1.000 request đúng lúc yếu nhất. Nếu tất cả cùng đợi cố định 1 giây, đợt thứ hai lại đến đồng loạt. Retry vượt qua lỗi ngắn, nhưng cũng biến sự cố nhỏ thành vòng lặp tự khuếch đại.
Một policy retry trả lời đủ năm câu: lỗi nào retry, tối đa bao nhiêu attempts, cách nhau bao lâu, trong deadline nào, và side effect có an toàn khi lặp không.
Các điều khoản cần phân biệt
Phần tiêu đề “Các điều khoản cần phân biệt”| Thuật ngữ | Nghĩa |
|---|---|
| Attempt | Một lần gọi thật. maxAttempts = 3 nghĩa là 1 lần đầu cộng 2 retry |
| Exponential backoff | Chờ min(cap, base × 2^n) sau lần lỗi thứ n |
| Jitter | Thêm ngẫu nhiên vào thời gian chờ để các client không thức dậy cùng lúc |
| Retry budget | Giới hạn số retry như tỷ lệ của request gốc |
503Nhiều client cùng gặp lỗi tạm thời khi service quá tải.
Ba biến thể jitter (AWS Architecture Blog)
Phần tiêu đề “Ba biến thể jitter (AWS Architecture Blog)”Với base, cap, attempt bắt đầu từ 0:
| Biến thể | Công thức sleep | Đặc điểm |
|---|---|---|
| Full jitter | random(0, min(cap, base × 2^attempt)) |
Trải đều nhất; lần chờ có thể gần 0 |
| Equal jitter | t = min(cap, base × 2^attempt); t/2 + random(0, t/2) |
Luôn giữ ít nhất một nửa backoff |
| Decorrelated jitter | min(cap, random(base, prevSleep × 3)) |
Phụ thuộc lần chờ trước, không phụ thuộc số attempt |
Theo AWS:
- Không jitter là lựa chọn tệ nhất cả về số lần gọi lẫn thời gian hoàn tất.
- Full và Equal tạo số lần gọi xấp xỉ nhau nhưng Equal hoàn tất lâu hơn nhiều.
- Decorrelated gọi nhiều hơn Full, còn Full tốn ít việc hơn nhưng chậm hơn một chút.
Bài blog nói chọn giữa Full và Decorrelated “không rõ ràng” và không chỉ định mặc định; bài này chọn Full jitter làm mặc định vì công thức đơn giản nhất (không cần nhớ lần chờ trước).
function backoffMs(attempt: number, baseMs = 200, capMs = 800): number { const ceiling = Math.min(capMs, baseMs * 2 ** attempt); // attempt 0,1,2 -> 200, 400, 800 return Math.random() * ceiling; // full jitter}Không jitter và jitter, bốn client cùng lỗi
Phần tiêu đề “Không jitter và jitter, bốn client cùng lỗi”Output thực tế của examples/src/retry-jitter/jitter-demo.ts:
without jitter: [ 800, 800, 800, 800 ]full jitter: [ 296, 592, 88, 384 ]retry amplification for 4 clients × 3 attempts: 12 callsDemo dùng dãy cố định để minh hoạ phân tán, không phải bộ random của production. Vẽ lại trên trục thời gian (ms):
không jitter: 0 200 400 600 800 ████ 4 client cùng lúc tại 800 ms
full jitter: 0 200 400 600 800 █(88) █(296) █(384) █(592) mỗi client một thời điểmĐỉnh tải 4 request tại một điểm biến thành 4 điểm riêng. Jitter giảm đồng bộ; nó không thêm capacity.
Retry budget: con số
Phần tiêu đề “Retry budget: con số”| Nguồn | Quy tắc |
|---|---|
| Google SRE book | Tối đa 3 attempts cho một request; và client chỉ retry khi tỷ lệ retry/request dưới 10%. Worst case tăng tải còn khoảng 1,1 lần thay vì gần 3 lần |
| Envoy | retry_budget: budget_percent mặc định 20%: số retry đồng thời tối đa bằng 20% tổng request đang hoạt động và đang chờ; min_retry_concurrency mặc định 3 |
| gRPC | Retry throttling bằng token bucket: maxTokens, tokenRatio; mỗi lỗi trừ 1 token, mỗi thành công cộng tokenRatio; dưới một nửa maxTokens thì ngừng retry |
SRE book còn nêu quy tắc: chỉ tầng ngay trên dependency lỗi được retry, các tầng trên nữa trả lỗi luôn; thêm tín hiệu “overloaded, đừng retry” để báo cho các tầng trên.
Khuếch đại theo số tầng
Phần tiêu đề “Khuếch đại theo số tầng”Mỗi tầng retry tối đa 3 attempts; số lời gọi worst case xuống tầng dưới cùng cho một request ở trên cùng:
| Số tầng cùng retry | Lời gọi xuống tầng dưới cùng |
|---|---|
| 1 | 3 |
| 2 | 9 |
| 3 | 27 |
| 4 | 81 |
SDK retry ngầm, wrapper của app retry, gateway retry: tổng attempts khác hẳn con số trong một file cấu hình. Chọn một tầng chịu trách nhiệm retry (tầng gần dependency nhất, theo SRE book) và tắt các tầng khác.
Khi nào retry và không
Phần tiêu đề “Khi nào retry và không”| Retry | Không retry |
|---|---|
| Lỗi mạng trước khi có response, reset connection | 400, 401, 403, 404, 422 |
| 503, 502, 504 | Lỗi nghiệp vụ, validation |
429 kèm Retry-After (chờ theo header) |
Timeout của thao tác có side effect nhưng thiếu idempotency |
| 408 theo contract của dependency | Khi deadline tổng sắp hết |
Status code một mình chưa đủ: endpoint có thể trả 500 sau khi đã commit. Thao tác ghi cần idempotency key giữ nguyên qua mọi attempt (xem eight fallacies và idempotent consumer). Mọi attempt, kể cả thời gian ngủ, phải nằm trong deadline ban đầu (timeout budget).
Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”Test: dùng fake timer; lần đầu 503, lần sau 200; xác nhận số attempts, delay nằm trong khoảng, cùng idempotency key qua mọi attempt. Test thêm 400 không retry, deadline hết giữa backoff, 429 có Retry-After, và trường hợp server đã commit nhưng response mất. Inject bộ random thay vì so một giá trị ngẫu nhiên.
Checklist production
Phần tiêu đề “Checklist production”- Ghi rõ
maxAttempts, không nhầm vớimaxRetries. - Danh sách lỗi retry được rõ ràng (mạng, 502/503/504, 429); không retry 4xx còn lại.
- Full jitter, có cap; tôn trọng
Retry-After. - Retry budget: tỷ lệ retry/request (ví dụ 10% theo SRE book) hoặc token bucket.
- Chỉ một tầng retry trên mỗi đường gọi; tính worst case nếu không thể.
- Mọi attempt nằm trong một deadline tổng.
- Thao tác ghi có idempotency key giữ nguyên qua attempt.
- Metric:
retry_attempts, retry ratio, lý do và delay của từng attempt; log không chứa payload nhạy cảm.
Trade-offs
Phần tiêu đề “Trade-offs”Khi hữu ích
- Vượt qua lỗi mạng hoặc overload ngắn mà người dùng không phải thao tác lại
- Backoff và jitter giảm retry đồng loạt sau sự cố chung
- Giới hạn attempts, budget và deadline kiểm soát latency và tải
Đánh đổi
- Tăng traffic đúng lúc downstream yếu; policy sai khuếch đại sự cố
- Kéo dài thời gian phản hồi, khó truy lỗi nếu không log từng attempt
- Side effect cần idempotency thật, không chỉ thêm delay
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) maxAttempts = 3 là mấy retry? (2) Vì sao backoff cố định vẫn gây đỉnh tải? (3) 3 tầng retry 3 attempts mỗi tầng tạo tối đa bao nhiêu lời gọi?