Trung bình DevOps

Exponential Backoff với Jitter: Retry Đúng Cách

Vì sao exponential backoff thuần túy gây ra retry storm, và jitter ngẫu nhiên giúp trải đều tải ra sao, kèm cách triển khai full-jitter retry bằng PHP và Python.

03 Th09, 2026 6 phút 4 Lượt xem 3 Khối code
Sơ đồ
graph LR A[Attempt 1] -->|delay ~200ms| B[Attempt 2] B -->|delay ~400ms plus jitter| C[Attempt 3] C -->|delay ~800ms plus jitter| D[Attempt 4] D -->|delay ~1600ms plus jitter, capped| E[Give up or succeed]

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

graph LR A[Lần thử 1] -->|delay ~200ms| B[Lần thử 2] B -->|delay ~400ms cộng jitter| C[Lần thử 3] C -->|delay ~800ms cộng jitter| D[Lần thử 4] D -->|delay ~1600ms cộng jitter, giới hạn| E[Bỏ cuộc hoặc thành công]

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 tương tác

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

php
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
        }
    }
}
python
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 ý
Gợi ý 1

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

Gợi ý 2

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

Lời giải · php
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);
        }
    }
}
Lời giải · python
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)

Cùng chủ đề DevOps