Пейджер

🌍 Добрай раніцы! 🇧🇾

TL;DR
  • Три подхода к ретраю: простой, линейный и экспоненциальный
  • Экспоненциальный backoff — оптимальный баланс повторов и паузы
  • Jitter защищает от Thundering Herd четырьмя способами
  • Не все ошибки нужно ретраить: 404, 401 — бессмысленно
🌍 Добрай раніцы! 🇧🇾

Недавно пришлось имплементировать retry-логику. Один из сторонних сервисов, от которого зависит проект, к сожалению, нестабилен 😑.

Вариантов реализации куча — уверен, что кто-то из вас сделал бы по-другому (или вообще без ретраев). Но в рамках моей задачи мне нужен был retry, и, как в очерке Максима Горького "Простой, как правда".

🪣 Варианты реализации

✔️ Simple retry

Функция принимает:

- fn — функцию, которую нужно вызвать
- delay — задержку между попытками
- retries — количество попыток.

async function simpleRetry(fn, retries, delay) {
  for (let i = 0; i < retries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === retries - 1) throw error;
      await sleep(delay);
    }
  }
}

💡 Можно сделать ещё проще — убрать delay совсем и просто выполнять вызовы подряд (если это допустимо по логике).


✔️ Линейное увеличение задержки
(1 с → 2 с → 3 с → 4 с …)


✏️ Добавляем параметр step — шаг увеличения задержки.

async function retryWithLinearDelay(fn, retries, baseDelay, step) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === retries) throw error;
      const delay = baseDelay + step * (attempt - 1);
      await sleep(delay);
    }
  }
}


✔️ Экспоненциальный backoff
(1 с → 2 с → 4 с → 8 с …)


На мой взгляд — самый сбалансированный способ. Сначала быстрые ретраи, потом увеличение интервала, когда сервису нужно время "прийти в себя".

✏️ Базовая формула:
delay = baseDelay * (factor ^ retryNumber)

async function retryWithExponentialDelay(fn, retries, baseDelay, factor = 2) {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === retries - 1) throw error;
      const delay = baseDelay * Math.pow(factor, attempt);
      await sleep(delay);
    }
  }
}


Поговорили о вариантах реализации, есть еще бонус который сделает вашу систему более способной не завалить только поднятый сервер внешной системы - Jitter ⚡️.

🎲 Jitter — добавляем случайность

Jitter можно применить к любому из вышеперечисленных способов. Есть такое понятие Thundering herd, я про него не рассказывал, как-нибудь следующий раз. Если кратко, у системы к которой мы обращаемся возникли проблемы, чудом они скэйлятся и поднимается новый узел, и в этот прекрасный момент, клиенты с одинаковым таймингом на ретрае делают запрос и роняют свеже поднятый сервачок, там вылетает память, превышается RPS и как следствие снова бобик сдох.

🪣 Варианты реализации Jitter

✔️ Percentage jitter

Добавляем случайную погрешность к задержке (±10 %):
delay + (Math.random() * 2 - 1) * delay * 0.1;

baseDelay → with jitter

1 → 0.9–1.1
2 → 1.8–2.2
4 → 3.6–4.4
8 → 7.2–8.8


✔️ Full jitter

Полностью случайная задержка до рассчитанного значения:
Math.random() * delay;

1 → (0–1)
2 → (0–2)
4 → (0–4)
8 → (0–8)


✔️ Decorrelated jitter

Новая задержка зависит от предыдущей, чтобы избежать синхронных всплесков:

Math.min(maxDelay, Math.random() * (3 * prevDelay - delay) + delay);


delay → prevDelay → range
1     →     1     → (1–2)
2     →    1.8    → (2–3.4)
4     →    2.9    → (4–6.7)
8     →    5.8    → (8–10)


✔️ Equal jitter

Все значения лежат в верхней половине диапазона:

(delay / 2) + Math.random() * (delay / 2);

1 → (0.5–1)
2 → (1–2)
4 → (2–4)
8 → (4–8)


✏️ Полезности

🟠 Слишком частые или долгие ретраи могут превратиться в infinity loop и повесить ваш процесс.
🟠 Не все ошибки нужно ретраить: например, 404, 401.., их повтор не изменит результат.
🟠 Бэкграунд-задачи могут иметь более длительный retry.
🟠 Всегда ставьте maxRetries или общий timeout, чтобы не зависнуть навсегда.
🟠 Логируйте каждую попытку, потом проще понять что происходит.
🟠 Используйте circuit breaker — если внешний сервис падает, а вы продолжаете ретраить, то фактически помогаете его добить. Circuit breaker позволяет отключить вызовы на короткое время, дать сервису восстановиться и не создавать лавину запросов.

⬇️ Все примеры, наглядно, тута.

#BESTPRACTICES
Медиа 1
Хотите больше таких постов?
Подпишитесь на канал и читайте продолжение в Telegram.
Подписаться на @ivanchikovitclub Открыть пост в Telegram