сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · SRE и надёжность

Таймауты, ретраи и предохранитель

Отказоустойчивость: таймауты, повторы, предохранители

Посчитал, во что превращаются повторы в цепочке сервисов. Три слоя, каждый повторяет неудачный вызов трижды: на самый нижний сервис прилетает не 1000 запросов, а 64 000. В шестьдесят четыре раза больше - и ровно в тот момент, когда он и так лежит. Повторы, поставленные из лучших побуждений, добивают сервис вернее самой поломки.

Стержень: у любого сетевого вызова должен быть предел ожидания; повторы нужны с разбросом и с ограничением; а предохранитель не даёт занять все потоки ожиданием мёртвого соседа.

// Формулировки: «зачем таймауты?», «почему ретраи опасны?», «как работает circuit breaker?»

Предел ожидания и умножение повторов

Без предела ожидания вызов зависшего соседа занимает поток на минуты. Поток тут - одна из параллельных линий, в которых сервис обрабатывает запросы; их конечное число, и через некоторое время свободных не остаётся - сервис перестаёт отвечать даже тем, кто ходит совсем в другое место. Так одна медленная зависимость превращается в полный отказ.

Повторы усугубляют, если делать их наивно. Мой расчёт: один слой с тремя повторами даёт четырёхкратную нагрузку, два слоя - шестнадцатикратную, три - шестидесятичетырёхкратную. Даже два повтора в трёх слоях дают двадцать семь. Заметь: множитель это (1 + число повторов) в степени числа слоёв - растёт он не сложением, а умножением.

// Отсюда правила. Повторять только безопасные к повтору операции - те, которые можно выполнить дважды без вреда: списание денег к ним не относится. Ограничивать число повторов маленьким числом. Повторять только там, где повтор осмыслен, - как правило, на самом верхнем слое, а не на каждом. И вводить общий предел: если доля повторов в трафике превысила несколько процентов, повторы отключаются целиком, потому что это уже не помощь, а добивание.

предел ожидания
сколько ждём ответа; без него один медленный сосед занимает все потоки
умножение повторов
множитель равен (1 плюс число повторов) в степени числа слоёв
бюджет повторов
общий предел на долю повторов в трафике

Разброс: почему все возвращаются одновременно

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

Тот же опыт с разбросом, когда пауза перед повтором берётся случайно в диапазоне: пик 232 запроса вместо 2000, сверх ёмкости всего 43, а всплеск размазан по одиннадцати интервалам вместо одного. Изменение в одну строчку кода, эффект почти в десять раз.

// Обычно разброс сочетают с растущей паузой: первый повтор через полсекунды, второй через секунду, третий через две, и к каждой паузе добавляется случайная величина. Растущая пауза даёт сервису шанс подняться, разброс не даёт всем прийти одновременно. Работает это только вместе: одна растущая пауза без разброса просто переносит одинаковый пик на более поздний момент.

разброс
случайная добавка к паузе перед повтором
растущая пауза
каждая следующая попытка ждёт дольше предыдущей

Предохранитель и деградация

Предохранитель считает неудачи и, набрав их подряд, перестаёт вызывать соседа вовсе - какое-то время отвечает отказом сразу. Потом пропускает пробный вызов: ответил - открывается обратно, не ответил - снова закрывается.

Замерил выгоду. Двадцать вызовов мёртвого сервиса с пределом ожидания 200 миллисекунд занимают 4003 миллисекунды, и все двадцать заканчиваются ошибкой. С предохранителем, который срабатывает после трёх неудач подряд, те же двадцать вызовов занимают 600 миллисекунд, потому что 17 из них отказали мгновенно. В 6,7 раза быстрее - и, что важнее, столько же потоков не заняты бесполезным ожиданием.

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

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

Как отвечать: «Сервис упал, все клиенты ретраят. Что не так с ретраями?»

С ними не так три вещи, и все три считаются. Первая - умножение по слоям: три слоя, каждый повторяет трижды, дают на нижний сервис шестидесятичетырёхкратную нагрузку, я это считал. Множитель растёт в степени числа слоёв, поэтому повторы ставят на одном слое, а не на каждом. Вторая - синхронность: если все повторяют ровно через секунду, сервис получает разом весь пик и падает снова. Я моделировал: две тысячи клиентов без разброса дают пик в две тысячи запросов в десятую долю секунды, а со случайной добавкой к паузе - двести тридцать, то есть почти в десять раз меньше. Третья - отсутствие общего предела: если доля повторов в трафике перевалила за несколько процентов, их надо отключать целиком. И рядом обязателен предохранитель: двадцать вызовов мёртвого сервиса без него занимают четыре секунды и все потоки, а с ним - шестьсот миллисекунд, потому что большинство отказывает мгновенно.

Ответ разбирает проблему на три измеримых механизма и к каждому даёт лечение. Числа показывают, что человек это моделировал, а не пересказывал.

На чём валятся

  • Ставят повторы на каждом слое и получают умножение нагрузки в десятки раз.
  • Повторяют без разброса: все клиенты возвращаются одновременно и роняют сервис снова.
  • Повторяют небезопасные к повтору операции и создают дубли платежей.
  • Живут без предела ожидания: одна медленная зависимость занимает все потоки.
  • Не продумывают деградацию заранее и отдают пятисотую там, где хватило бы устаревших данных.

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

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

  1. #dvo_resilience1 / 5
    Почему на каждый исходящий вызов ставят таймаут?
    A)Иначе внешний сервис сочтёт соединение мёртвым и разорвёт его
    B)Таймаут снижает нагрузку на сеть за счёт коротких соединений
    C)Без таймаута ответ приходит с задержкой и попадает не в тот запрос
    D)Иначе медленный сосед забирает потоки и роняет сервис целиком
    показать ответ и разбор
    +D)Иначе медленный сосед забирает потоки и роняет сервис целиком

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

  2. #dvo_resilience2 / 5
    Клиенты повторяют неудачные запросы сразу и по три раза. Сервис прилёг и не поднимается. Что произошло?
    A)Ретраи утроили нагрузку и держат сервис внизу синхронной волной
    B)Повторы забили пул соединений на балансировщике до истечения таймаутов
    C)Сервис принимает дубликаты и падает на конфликтах записи
    D)Кэш заполнился ошибочными ответами и отдаёт их новым клиентам
    показать ответ и разбор
    +A)Ретраи утроили нагрузку и держат сервис внизу синхронной волной

    // разбор: Мгновенные повторы превращают просадку в лавину: нагрузка растёт втрое ровно тогда, когда сервис слабее всего, а клиенты бьются синхронно, потому что стартовали от одного сбоя. Лечится экспоненциальной паузой со случайным разбросом, ограничением общего числа попыток и бюджетом ретраев на клиента. И отдельно: повторять можно только идемпотентные операции, иначе получите дубли платежей.

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

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

  4. #dvo_resilience4 / 5
    Клиент повторяет упавший запрос. Для каких операций ретрай безопасен, а для каких опасен?
    A)Ретрай безопасен для чтений и записей одинаково
    B)Безопасен для идемпотентных операций
    C)Опасность только в скорости повтора, а не в операции
    D)Ретрай всех POST-запросов безопасен по спецификации HTTP
    показать ответ и разбор
    +B)Безопасен для идемпотентных операций

    // разбор: Ретрай безопасен для идемпотентных операций: повтор даёт тот же результат (GET, PUT по ключу, DELETE). Для неидемпотентных (списать деньги, создать заказ) повтор после неясного исхода проводит эффект дважды — двойное списание, дубль заказа. Решения: делать операции идемпотентными (idempotency key, дедуп на стороне сервера), ретраить только там, где это безопасно, и различать «точно не выполнилось» от «неизвестно».

  5. #dvo_resilience5 / 5
    Ретраи есть, но при сбое апстрима клиенты дружно бьют по три раза подряд и добивают его. Как ретраить правильно?
    A)Экспоненциальный бэкофф с джиттером и потолком попыток
    B)Повторять сразу и почаще, чтобы быстрее пробиться
    C)Фиксированная пауза в одну секунду между попытками
    D)Убрать лимит попыток, повторять до успеха
    показать ответ и разбор
    +A)Экспоненциальный бэкофф с джиттером и потолком попыток

    // разбор: Немедленные синхронные ретраи ×3 превращаются в лавину, которая не даёт упавшему сервису встать (retry storm). Правильно: экспоненциальный бэкофф (пауза растёт: 1с, 2с, 4с…) + джиттер (случайный разброс, чтобы клиенты не били в такт) + потолок числа попыток и общий бюджет ретраев. Ещё лучше — в связке с circuit breaker, который на серии отказов вовсе перестаёт долбить апстрим.

дальше

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

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