Retry một request thất bại nghe có vẻ đơn giản: đợi một chút rồi thử lại. Nhưng nếu hàng ngàn client cùng retry với delay cố định như nhau, bạn sẽ có một thundering herd — mọi client cùng thức dậy và dồn request vào server đúng một thời điểm, thường đánh sập thứ vừa mới bắt đầu hồi phục.
Exponential backoff giãn dần khoảng cách giữa các lần retry (200ms, 400ms, 800ms...), nhưng tự thân nó vẫn khiến mọi client đồng bộ theo cùng một lịch trình. Jitter khắc phục điều đó bằng cách ngẫu nhiên hóa delay, để các lần retry trải đều thay vì đến cùng lúc.
Dòng Thời Gian Retry
Mỗi bước gần như gấp đôi delay cơ sở, giới hạn ở một mức tối đa, cộng thêm một thành phần ngẫu nhiên để không có hai client nào retry đúng cùng một thời điểm.
Cố Định vs Exponential vs Có Jitter
Delay cố định (luôn đợi 1s) thì đơn giản nhưng khiến mọi client thất bại cùng lúc bị đồng bộ với nhau. Exponential backoff thuần túy (200ms, 400ms, 800ms, 1600ms) trải các lần thử xa nhau hơn theo thời gian, nhưng các client thất bại cùng lúc vẫn retry cùng lúc ở mỗi bước. Thêm jitter — chọn một delay ngẫu nhiên thay vì giá trị exponential cố định — phá vỡ hoàn toàn sự đồng bộ đó.
Full Jitter
Chiến lược full jitter (được chính AWS dùng nội bộ trong SDK của họ) tính delay exponential như một mức trần, rồi chọn một giá trị ngẫu nhiên đều giữa 0 và mức trần đó:
delay = random_between(0, min(cap, base * 2^attempt))
Cách này đơn giản hơn các biến thể "equal jitter" hay "decorrelated jitter" và hoạt động tốt trong thực tế: nó trải các lần retry ra toàn bộ khoảng thay vì chỉ thêm một chút nhiễu quanh giá trị exponential.
Triển Khai
function calculateBackoff(int $attempt, int $baseDelayMs = 200, int $maxDelayMs = 30000): int
{
$exponential = $baseDelayMs * (2 ** $attempt);
$capped = min($exponential, $maxDelayMs);
return random_int(0, $capped);
}
function retryWithBackoff(callable $operation, int $maxAttempts = 5, int $baseDelayMs = 200, int $maxDelayMs = 30000)
{
$attempt = 0;
while (true) {
try {
return $operation();
} catch (\Throwable $e) {
$attempt++;
if ($attempt >= $maxAttempts) {
throw $e;
}
$delayMs = calculateBackoff($attempt, $baseDelayMs, $maxDelayMs);
usleep($delayMs * 1000);
}
}
}import random
import time
from typing import Callable, TypeVar
T = TypeVar("T")
def calculate_backoff(attempt: int, base_delay_ms: int = 200, max_delay_ms: int = 30000) -> int:
exponential = base_delay_ms * (2 ** attempt)
capped = min(exponential, max_delay_ms)
return random.randint(0, capped)
def retry_with_backoff(
operation: Callable[[], T],
max_attempts: int = 5,
base_delay_ms: int = 200,
max_delay_ms: int = 30000,
) -> T:
attempt = 0
while True:
try:
return operation()
except Exception:
attempt += 1
if attempt >= max_attempts:
raise
delay_ms = calculate_backoff(attempt, base_delay_ms, max_delay_ms)
time.sleep(delay_ms / 1000)Chọn Tham Số
baseDelayMs nên khớp tương đối với tốc độ hồi phục thực tế của dịch vụ phía sau — quá thấp thì bạn retry vào lúc dịch vụ vẫn còn lỗi, quá cao thì người dùng phải chờ vô ích với những lỗi thoáng qua. maxDelayMs giới hạn trường hợp xấu nhất để client không phải chờ hàng phút giữa các lần thử. maxAttempts nên đủ thấp để một dependency thực sự hỏng sẽ fail nhanh thay vì treo caller qua một chuỗi delay tăng dần mãi.
Lỗi Thường Gặp
Retry mà không giới hạn delay khiến client có thể chờ hàng phút cho một lần thử rồi vẫn thất bại. Retry mà không có jitter thì phá vỡ hoàn toàn mục đích ban đầu — các client đồng bộ vẫn đồng bộ dù delay có xa đến đâu. Retry với mọi loại lỗi, kể cả lỗi 4xx phía client sẽ không bao giờ thành công, lãng phí ngân sách retry vào những thất bại mà delay không thể sửa được. Và retry vô thời hạn không giới hạn maxAttempts biến một trục trặc thoáng qua thành một request không bao giờ chịu dừng và không bao giờ báo lỗi cho caller.
Khi Nào Nên Dùng
Bất kỳ lời gọi nào tới một dependency có thể lỗi thoáng qua rồi tự hồi phục: gọi HTTP tới dịch vụ khác, kết nối database trong lúc failover, consumer của queue gọi vào một API bị giới hạn tốc độ. Nếu lỗi hiếm khi là thoáng qua — một request sai định dạng, một tài nguyên không tồn tại — thì backoff và retry chỉ trì hoãn một lỗi vốn dĩ sẽ không bao giờ thành công.
Thử thách
Luyện tập ngay điều vừa học. Viết lời giải, mở gợi ý nếu bí.
Đề bài
Cài đặt calculateBackoff(attempt, baseDelayMs, maxDelayMs) và retryWithBackoff(operation, maxAttempts, baseDelayMs, maxDelayMs). calculateBackoff() phải tính delay exponential (baseDelayMs * 2^attempt), giới hạn ở maxDelayMs, rồi trả về một giá trị ngẫu nhiên giữa 0 và giá trị đã giới hạn đó (full jitter). retryWithBackoff() phải gọi operation(), và khi thất bại thì đợi calculateBackoff() mili-giây trước khi thử lại, tối đa maxAttempts lần, ném lại exception cuối cùng nếu tất cả các lần thử đều thất bại. Điền vào các TODO.
Code khởi tạo
function calculateBackoff(int $attempt, int $baseDelayMs = 200, int $maxDelayMs = 30000): int
{
// TODO: compute the exponential delay: $baseDelayMs * 2^$attempt
// TODO: cap it at $maxDelayMs
// TODO: return a random integer between 0 and the capped value (full jitter)
}
function retryWithBackoff(callable $operation, int $maxAttempts = 5, int $baseDelayMs = 200, int $maxDelayMs = 30000)
{
$attempt = 0;
while (true) {
try {
return $operation();
} catch (\Throwable $e) {
// TODO: increment $attempt
// TODO: if $attempt >= $maxAttempts, rethrow $e
// TODO: otherwise sleep for calculateBackoff() milliseconds, then loop again
}
}
}
import random
import time
from typing import Callable, TypeVar
T = TypeVar("T")
def calculate_backoff(attempt: int, base_delay_ms: int = 200, max_delay_ms: int = 30000) -> int:
# TODO: compute the exponential delay: base_delay_ms * 2**attempt
# TODO: cap it at max_delay_ms
# TODO: return a random int between 0 and the capped value (full jitter)
pass
def retry_with_backoff(
operation: Callable[[], T],
max_attempts: int = 5,
base_delay_ms: int = 200,
max_delay_ms: int = 30000,
) -> T:
attempt = 0
while True:
try:
return operation()
except Exception:
# TODO: increment attempt
# TODO: if attempt >= max_attempts, re-raise
# TODO: otherwise sleep for calculate_backoff() milliseconds, then loop again
pass
Lời giải của bạn
Gợi ý
Đã mở hết gợi ýFull jitter chọn một delay ngẫu nhiên trong [0, cap] thay vì luôn đợi đủ giá trị đã giới hạn, giúp các lần retry trải đều thay vì bắn ra cùng lúc
Giới hạn tăng trưởng exponential (min(delay, maxDelayMs)) trước khi áp jitter, nếu không delay sẽ tăng vô hạn
function calculateBackoff(int $attempt, int $baseDelayMs = 200, int $maxDelayMs = 30000): int
{
$exponential = $baseDelayMs * (2 ** $attempt);
$capped = min($exponential, $maxDelayMs);
return random_int(0, $capped);
}
function retryWithBackoff(callable $operation, int $maxAttempts = 5, int $baseDelayMs = 200, int $maxDelayMs = 30000)
{
$attempt = 0;
while (true) {
try {
return $operation();
} catch (\Throwable $e) {
$attempt++;
if ($attempt >= $maxAttempts) {
throw $e;
}
$delayMs = calculateBackoff($attempt, $baseDelayMs, $maxDelayMs);
usleep($delayMs * 1000);
}
}
}
import random
import time
from typing import Callable, TypeVar
T = TypeVar("T")
def calculate_backoff(attempt: int, base_delay_ms: int = 200, max_delay_ms: int = 30000) -> int:
exponential = base_delay_ms * (2 ** attempt)
capped = min(exponential, max_delay_ms)
return random.randint(0, capped)
def retry_with_backoff(
operation: Callable[[], T],
max_attempts: int = 5,
base_delay_ms: int = 200,
max_delay_ms: int = 30000,
) -> T:
attempt = 0
while True:
try:
return operation()
except Exception:
attempt += 1
if attempt >= max_attempts:
raise
delay_ms = calculate_backoff(attempt, base_delay_ms, max_delay_ms)
time.sleep(delay_ms / 1000)

