сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · ETL и оркестрация

Надёжность пайплайна и ретраи

Надёжность: ретраи и таймауты

Пайплайн ходит по сети, а сеть падает, поэтому надёжность это дисциплина ретраев, таймаутов и идемпотентности, а не «обычно же работает». Собес проверяет, что и когда ты ретраишь и почему таймаут обязателен.

Стержень: ретраить с бэкоффом можно только идемпотентное и только транзиентные ошибки; таймаут - на каждый внешний вызов.

// Формулировки: «как сделать сетевой вызов надёжным?», «что можно ретраить?», «зачем circuit breaker?».

Ретрай: что и как

Ретрай с экспоненциальным бэкоффом и джиттером - стандарт для сетевого: пауза растёт (1с, 2с, 4с), а случайный сдвиг рассинхронизирует толпу клиентов, чтобы они не долбили сервис синхронно. Ретрай без бэкоффа в цикле это DDoS собственной зависимости.

Ретраить можно только идемпотентное: повтор GET безопасен, повтор «списать деньги» - нет. Неидемпотентные операции защищают идемпотентным ключом или проверкой состояния перед повтором.

// И только транзиентное: таймауты, 429, 5xx имеет смысл повторить; 4xx кроме 429 - ошибка самого запроса, повтор бессмыслен и лишь жжёт ресурсы.

экспоненциальный бэкофф
растущая пауза между попытками + джиттер
транзиентная ошибка
временная (таймаут/429/5xx) - имеет смысл повторить

Таймауты и circuit breaker

Таймаут обязателен на каждый внешний вызов: у requests дефолт - ждать вечно, и один зависший вызов морозит воркер навсегда, а за ним встаёт пайплайн. Таймаут плюс ретрай плюс лимит попыток дают предсказуемое поведение под сбоем.

Circuit breaker: после серии отказов перестаём долбить лежащий сервис и падаем быстро. Это даёт ему встать и не копит на своей стороне зависшие запросы.

// Логика проста: retry борется с редким сбоем, breaker - с устойчивым. Без breaker'а ретраи к мёртвому сервису только добивают его и топят собственные ресурсы в ожидании.

таймаут
предел ожидания вызова; обязателен, дефолт - вечность
circuit breaker
быстрый отказ после серии сбоев вместо долбёжки

Идемпотентность джобы и явные сбои

Надёжна не только отдельная попытка, но и джоба целиком: перезапуск за тот же период не должен дублировать данные. Достигается overwrite партиции или upsert по ключу, а не append - append при перезапуске упавшей джобы даёт дубли за период.

Пиши атомарно: во временный файл или таблицу, затем атомарный rename/swap. Читатель никогда не должен видеть полфайла.

// Fail loudly: битая запись уходит в dead-letter с логом и метрикой, а пайплайн падает по порогу брака. Тихий try/except continue без счётчика однажды выбросит треть данных, и никто не заметит.

dead-letter
отстойник необработанных записей для разбора
атомарная запись
temp + rename: читатель видит всё или ничего

Как отвечать: «Как сделать сетевой вызов в пайплайне надёжным?»

Собираю четыре вещи вместе. Первое - таймаут на каждый вызов, потому что дефолт клиента вроде requests это ждать вечно, и один зависший запрос морозит воркер, а за ним весь пайплайн. Второе - ретрай, но с экспоненциальным бэкоффом и джиттером и с лимитом попыток, чтобы не превратить ретраи в DDoS зависимости. Третье - ретраю строго избирательно: только транзиентные ошибки, таймауты, 429 и 5xx, и только идемпотентные операции; неидемпотентное, вроде списания, защищаю идемпотентным ключом. Четвёртое - circuit breaker, чтобы после серии отказов падать быстро и дать лежащему сервису встать, а не добивать его. Поверх этого делаю джобу идемпотентной целиком - перезапуск через overwrite или upsert, а не append, и пишу атомарно через temp плюс rename. И падаю громко: брак в dead-letter с метрикой, а не молчаливый пропуск.

Почему это сильный ответ: собраны все четыре механизма (таймаут, бэкофф-ретрай, избирательность, breaker), плюс идемпотентность джобы и атомарная запись - полный контур надёжности, а не только «сделаю ретрай».

На чём валят

  • Ретрай без бэкоффа в цикле - DDoS собственного сервиса-зависимости.
  • Ретрай POST-списания без идемпотентного ключа - двойное списание.
  • requests.get без timeout - воркер завис навсегда, пайплайн стоит.
  • Append при перезапуске упавшей джобы - дубли за перезапущенный период.
  • try/except continue на битых записях без счётчика - 30% данных молча выброшено.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 15, остальные разбираются в тренажёре.

  1. #reliability_retries1 / 5
    Почему опасно вслепую ретраить не-идемпотентный запрос (например, POST-платёж)?
    A)Повторно ретраить POST-запрос безопасно — сервер сам распознаёт и отбрасывает дубли
    B)Ретрай не-идемпотентного запроса задвоит эффект; нужен ключ идемпотентности
    C)POST-запросы не могут завершаться сетевой ошибкой при отправке
    D)Ретраи вообще не нужны в надёжных системах, если код написан аккуратно
    показать ответ и разбор
    +B)Ретрай не-идемпотентного запроса задвоит эффект; нужен ключ идемпотентности

    // разбор: Таймаут — не признак неуспеха: сервер мог принять и выполнить запрос, а ответ потерялся по пути. Слепой повтор создающего действия тогда задвоит его — два списания, два заказа. Решение — ключ идемпотентности (idempotency key): сервер распознаёт повтор по нему и не выполняет действие дважды.

  2. #reliability_retries2 / 5
    Зачем к экспоненциальному backoff добавляют случайный jitter?
    A)Случайный jitter в задержке добавляют ради читаемости логов попыток
    B)Экспоненциальный backoff без случайности оптимален и в jitter не нуждается
    C)Синхронные ретраи бьют залпами (thundering herd); jitter разносит попытки
    D)Jitter означает полный отказ от повторных попыток после первой же ошибки
    показать ответ и разбор
    +C)Синхронные ретраи бьют залпами (thundering herd); jitter разносит попытки

    // разбор: Если тысяча клиентов упала одновременно и все ждут одинаковый backoff, они повторят запрос ровно в один момент — и снова уронят сервис (thundering herd). Случайная добавка к задержке (jitter) размазывает повторы во времени, сглаживая пиковую нагрузку. Поэтому в проде почти всегда backoff + jitter, а не чистая экспонента.

  3. #reliability_retries3 / 5
    Что делает паттерн circuit breaker при обращении к нестабильному внешнему сервису?
    A)Повторяет один и тот же запрос к упавшему сервису без пауз между попытками
    B)Отключает обращения к сервису после первой же ошибки и больше их не возобновляет
    C)Обеспечивает, что внешний сервис не будет падать под большой нагрузкой
    D)После серии ошибок временно «размыкает цепь» — быстро отклоняет вызовы, давая сервису восстановиться
    показать ответ и разбор
    +D)После серии ошибок временно «размыкает цепь» — быстро отклоняет вызовы, давая сервису восстановиться

    // разбор: Circuit breaker считает ошибки и после порога «размыкается»: на время перестаёт слать запросы, сразу отклоняя вызовы (fail fast), — это бережёт клиента (не висит на таймаутах) и падающий сервис (не добивают штормом ретраев). Через паузу breaker переходит в half-open, пробует пропустить пару запросов и закрывается, если сервис ожил.

  4. #reliability_retries4 / 5
    Сообщение стабильно роняет обработчик на каждой попытке (poison message). Что с ним делать?
    A)Повторно ретраить именно его по кругу, блокируя остальную очередь
    B)После N неудачных попыток убрать его в dead-letter очередь для разбора, не блокируя поток
    C)Молча удалить это сообщение без логирования и следа
    D)Остановить обработку очереди до ручного исправления этого сообщения
    показать ответ и разбор
    +B)После N неудачных попыток убрать его в dead-letter очередь для разбора, не блокируя поток

    // разбор: Poison message — сообщение, которое стабильно валит обработчик (битый формат, невалидные данные). Бесконечный ретрай застопорит всю очередь. Правильно — ограничить число попыток и после исчерпания отправить сообщение в dead-letter очередь: поток идёт дальше, а «ядовитое» сообщение сохранено для ручного разбора и не потеряно.

  5. #reliability_retries5 / 5
    Как безопасно ретраить не-идемпотентную операцию (например, создание платежа)?
    A)Просто повторять запрос почаще и понадеяться, что дубль платежа никто не заметит
    B)Запретить ретраи — тогда операция выполнится ровно один раз
    C)Передавать ключ идемпотентности: сервер по нему распознаёт повтор и не выполняет операцию дважды
    D)Слепо ретраить: создание платежа по своей природе идемпотентно и повтор безвреден
    показать ответ и разбор
    +C)Передавать ключ идемпотентности: сервер по нему распознаёт повтор и не выполняет операцию дважды

    // разбор: Ключ идемпотентности (уникальный id операции в заголовке/теле) позволяет клиенту безопасно ретраить: сервер запоминает выполненные ключи и на повтор возвращает прежний результат, не создавая второй платёж. Это переносит ответственность за дедупликацию на принимающую сторону и делает небезопасную по природе операцию безопасной к повтору при таймаутах/сбоях сети.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.