Bỏ qua để đến nội dung
Knowledge Hub

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

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.

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
Một attempt thất bại trước khi quyết định có thử lại
Một attempt thất bại trước khi quyết định có thử lạiBước 1/4: Nhiều client cùng gặp lỗi tạm thời khi service quá tải.ClientsGatewayService503
Gateway → Service503
Bước 1/4

Nhiều client cùng gặp lỗi tạm thời khi service quá tải.

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 calls

Demo 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.

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.

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.

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).

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.

  1. Ghi rõ maxAttempts, không nhầm với maxRetries.
  2. Danh sách lỗi retry được rõ ràng (mạng, 502/503/504, 429); không retry 4xx còn lại.
  3. Full jitter, có cap; tôn trọng Retry-After.
  4. Retry budget: tỷ lệ retry/request (ví dụ 10% theo SRE book) hoặc token bucket.
  5. Chỉ một tầng retry trên mỗi đường gọi; tính worst case nếu không thể.
  6. Mọi attempt nằm trong một deadline tổng.
  7. Thao tác ghi có idempotency key giữ nguyên qua attempt.
  8. Metric: retry_attempts, retry ratio, lý do và delay của từng attempt; log không chứa payload nhạy cảm.

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

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?