Надёжность пайплайна и ретраи
Пайплайн ходит по сети, а сеть падает, поэтому надёжность это дисциплина ретраев, таймаутов и идемпотентности, а не «обычно же работает». Собес проверяет, что и когда ты ретраишь и почему таймаут обязателен.
Стержень: ретраить с бэкоффом можно только идемпотентное и только транзиентные ошибки; таймаут - на каждый внешний вызов.
// Формулировки: «как сделать сетевой вызов надёжным?», «что можно ретраить?», «зачем 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, остальные разбираются в тренажёре.
- Почему опасно вслепую ретраить не-идемпотентный запрос (например, POST-платёж)?A)Повторно ретраить POST-запрос безопасно — сервер сам распознаёт и отбрасывает дублиB)Ретрай не-идемпотентного запроса задвоит эффект; нужен ключ идемпотентностиC)POST-запросы не могут завершаться сетевой ошибкой при отправкеD)Ретраи вообще не нужны в надёжных системах, если код написан аккуратно
показать ответ и разбор
+B)Ретрай не-идемпотентного запроса задвоит эффект; нужен ключ идемпотентности// разбор: Таймаут — не признак неуспеха: сервер мог принять и выполнить запрос, а ответ потерялся по пути. Слепой повтор создающего действия тогда задвоит его — два списания, два заказа. Решение — ключ идемпотентности (idempotency key): сервер распознаёт повтор по нему и не выполняет действие дважды.
- Зачем к экспоненциальному backoff добавляют случайный jitter?A)Случайный jitter в задержке добавляют ради читаемости логов попытокB)Экспоненциальный backoff без случайности оптимален и в jitter не нуждаетсяC)Синхронные ретраи бьют залпами (thundering herd); jitter разносит попыткиD)Jitter означает полный отказ от повторных попыток после первой же ошибки
показать ответ и разбор
+C)Синхронные ретраи бьют залпами (thundering herd); jitter разносит попытки// разбор: Если тысяча клиентов упала одновременно и все ждут одинаковый backoff, они повторят запрос ровно в один момент — и снова уронят сервис (thundering herd). Случайная добавка к задержке (jitter) размазывает повторы во времени, сглаживая пиковую нагрузку. Поэтому в проде почти всегда backoff + jitter, а не чистая экспонента.
- Что делает паттерн circuit breaker при обращении к нестабильному внешнему сервису?A)Повторяет один и тот же запрос к упавшему сервису без пауз между попыткамиB)Отключает обращения к сервису после первой же ошибки и больше их не возобновляетC)Обеспечивает, что внешний сервис не будет падать под большой нагрузкойD)После серии ошибок временно «размыкает цепь» — быстро отклоняет вызовы, давая сервису восстановиться
показать ответ и разбор
+D)После серии ошибок временно «размыкает цепь» — быстро отклоняет вызовы, давая сервису восстановиться// разбор: Circuit breaker считает ошибки и после порога «размыкается»: на время перестаёт слать запросы, сразу отклоняя вызовы (fail fast), — это бережёт клиента (не висит на таймаутах) и падающий сервис (не добивают штормом ретраев). Через паузу breaker переходит в half-open, пробует пропустить пару запросов и закрывается, если сервис ожил.
- Сообщение стабильно роняет обработчик на каждой попытке (poison message). Что с ним делать?A)Повторно ретраить именно его по кругу, блокируя остальную очередьB)После N неудачных попыток убрать его в dead-letter очередь для разбора, не блокируя потокC)Молча удалить это сообщение без логирования и следаD)Остановить обработку очереди до ручного исправления этого сообщения
показать ответ и разбор
+B)После N неудачных попыток убрать его в dead-letter очередь для разбора, не блокируя поток// разбор: Poison message — сообщение, которое стабильно валит обработчик (битый формат, невалидные данные). Бесконечный ретрай застопорит всю очередь. Правильно — ограничить число попыток и после исчерпания отправить сообщение в dead-letter очередь: поток идёт дальше, а «ядовитое» сообщение сохранено для ручного разбора и не потеряно.
- Как безопасно ретраить не-идемпотентную операцию (например, создание платежа)?A)Просто повторять запрос почаще и понадеяться, что дубль платежа никто не заметитB)Запретить ретраи — тогда операция выполнится ровно один разC)Передавать ключ идемпотентности: сервер по нему распознаёт повтор и не выполняет операцию дваждыD)Слепо ретраить: создание платежа по своей природе идемпотентно и повтор безвреден
показать ответ и разбор
+C)Передавать ключ идемпотентности: сервер по нему распознаёт повтор и не выполняет операцию дважды// разбор: Ключ идемпотентности (уникальный id операции в заголовке/теле) позволяет клиенту безопасно ретраить: сервер запоминает выполненные ключи и на повтор возвращает прежний результат, не создавая второй платёж. Это переносит ответственность за дедупликацию на принимающую сторону и делает небезопасную по природе операцию безопасной к повтору при таймаутах/сбоях сети.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.