1.1Kпросмотров
88.8%от подписчиков
20 сентября 2025 г.
📷 ФотоScore: 1.3K
Jitter в ретраях: как не убить зависимый сервис при сбоях
Представьте, у вас есть система с сотнями микросервисов или клиентов, которые делают синхронные HTTP-запросы, например, в сервис уведомлений. Однажды этот сервис начинает отдавать 503 Service Unavailable. И все клиенты одновременно получают ошибку и запускают повторную попытку через фиксированное время (например, через 1 секунду).
- Через секунду все они шлют повторный запрос одновременно.
- Сервис, который и так упал, снова перегружается.
- Ретраи срабатывают синхронно → создаётся шквал запросов → сервис окончательно ложится. Это называется thundering herd problem — эффект "стада", когда множество клиентов одновременно "набегают" на ресурс. backoff и jitter
Чтобы смягчить нагрузку и дать сервису шанс на восстановление, применяются два подхода:
1. Exponential backoff — каждая новая попытка делает паузу длиннее (1s → 2s → 4s → 8s) (о нем мы уже говорили в постах выше)
2. Jitter — добавление случайной задержки в ожидание между попытками
Jitter размазывает повторные запросы по времени, и сервис получает не лавину, а дождь. Принцип работы jitter:
Без jitter:
все клиенты делают await sleep(2) → просыпаются одновременно → шлют запрос одновременно. С jitter (вариант full jitter):
вместо 2s задержки делается random.uniform(0, 2)
→ кто-то проснётся через 0.7s, кто-то через 1.5s, кто-то через 1.9s
→ запросы равномерно распределяются по времени Пример на Python: retry-декоратор с jitter
import asyncio
import random
import logging
from functools import wraps
from httpx import RequestError, HTTPStatusError logger = logging.getLogger(name) def retry_on_error_with_jitter( retries: int = 3, base_delay: float = 0.5, max_delay: float = 10.0, jitter: bool = True,
): """ Повторяет вызов async-функции с экспоненциальным backoff и jitter. """ def decorator(func): @wraps(func) async def wrapper(*args, **kwargs): attempt = 0 while True: try: return await func(*args, **kwargs) except (RequestError, HTTPStatusError) as exc: status = getattr(exc.response, "status_code", None) if status and 400 <= status < 500: logger.error(f"Ошибка клиента ({status}), не ретраим") raise attempt += 1 if attempt > retries: logger.error(f"Попытки исчерпаны. Ошибка: {exc}") raise delay = base_delay * (2 ** (attempt - 1)) if jitter: delay = random.uniform(0, min(delay, max_delay)) else: delay = min(delay, max_delay) logger.warning( f"Ошибка (попытка {attempt}/{retries}), повтор через {delay:.2f}с" ) await asyncio.sleep(delay) return wrapper return decorator Пример использования с httpx
@retry_on_error_with_jitter(retries=3, base_delay=1.0)
async def send_email(): response = await httpx.post("http://notifications/api/send", timeout=5.0) response.raise_for_status() return response.json() Что мы получили Повторные попытки при временных ошибках Распределение повторных запросов по времени Защиту зависимого сервиса от спама Jitter – простое решение, которое помогает сервисам выживать под нагрузкой. Особенно важно его использовать в случаях, когда: у вас сотни/тысячи клиентов или микросервисов есть ретраи после сбоев сервисы могут одновременно упасть или стать недоступными Без jitter: все клиенты ретраят через одно и то же время → лавина
С jitter: задержки у всех разные → нагрузка размазана → шанс на восстановление