Таймауты, ретраи и предохранитель
Посчитал, во что превращаются повторы в цепочке сервисов. Три слоя, каждый повторяет неудачный вызов трижды: на самый нижний сервис прилетает не 1000 запросов, а 64 000. В шестьдесят четыре раза больше - и ровно в тот момент, когда он и так лежит. Повторы, поставленные из лучших побуждений, добивают сервис вернее самой поломки.
Стержень: у любого сетевого вызова должен быть предел ожидания; повторы нужны с разбросом и с ограничением; а предохранитель не даёт занять все потоки ожиданием мёртвого соседа.
// Формулировки: «зачем таймауты?», «почему ретраи опасны?», «как работает circuit breaker?»
Предел ожидания и умножение повторов
Без предела ожидания вызов зависшего соседа занимает поток на минуты. Поток тут - одна из параллельных линий, в которых сервис обрабатывает запросы; их конечное число, и через некоторое время свободных не остаётся - сервис перестаёт отвечать даже тем, кто ходит совсем в другое место. Так одна медленная зависимость превращается в полный отказ.
Повторы усугубляют, если делать их наивно. Мой расчёт: один слой с тремя повторами даёт четырёхкратную нагрузку, два слоя - шестнадцатикратную, три - шестидесятичетырёхкратную. Даже два повтора в трёх слоях дают двадцать семь. Заметь: множитель это (1 + число повторов) в степени числа слоёв - растёт он не сложением, а умножением.
// Отсюда правила. Повторять только безопасные к повтору операции - те, которые можно выполнить дважды без вреда: списание денег к ним не относится. Ограничивать число повторов маленьким числом. Повторять только там, где повтор осмыслен, - как правило, на самом верхнем слое, а не на каждом. И вводить общий предел: если доля повторов в трафике превысила несколько процентов, повторы отключаются целиком, потому что это уже не помощь, а добивание.
- предел ожидания
- сколько ждём ответа; без него один медленный сосед занимает все потоки
- умножение повторов
- множитель равен (1 плюс число повторов) в степени числа слоёв
- бюджет повторов
- общий предел на долю повторов в трафике
Разброс: почему все возвращаются одновременно
Сервис лёг, две тысячи клиентов получили ошибку в одну и ту же секунду и все повторяют ровно через секунду. Смоделировал: без разброса в одну десятую секунды прилетает пик в 2000 запросов, из которых 1800 сверх ёмкости сервиса. Сервис, который только начал подниматься, ложится снова - и так по кругу.
Тот же опыт с разбросом, когда пауза перед повтором берётся случайно в диапазоне: пик 232 запроса вместо 2000, сверх ёмкости всего 43, а всплеск размазан по одиннадцати интервалам вместо одного. Изменение в одну строчку кода, эффект почти в десять раз.
// Обычно разброс сочетают с растущей паузой: первый повтор через полсекунды, второй через секунду, третий через две, и к каждой паузе добавляется случайная величина. Растущая пауза даёт сервису шанс подняться, разброс не даёт всем прийти одновременно. Работает это только вместе: одна растущая пауза без разброса просто переносит одинаковый пик на более поздний момент.
- разброс
- случайная добавка к паузе перед повтором
- растущая пауза
- каждая следующая попытка ждёт дольше предыдущей
Предохранитель и деградация
Предохранитель считает неудачи и, набрав их подряд, перестаёт вызывать соседа вовсе - какое-то время отвечает отказом сразу. Потом пропускает пробный вызов: ответил - открывается обратно, не ответил - снова закрывается.
Замерил выгоду. Двадцать вызовов мёртвого сервиса с пределом ожидания 200 миллисекунд занимают 4003 миллисекунды, и все двадцать заканчиваются ошибкой. С предохранителем, который срабатывает после трёх неудач подряд, те же двадцать вызовов занимают 600 миллисекунд, потому что 17 из них отказали мгновенно. В 6,7 раза быстрее - и, что важнее, столько же потоков не заняты бесполезным ожиданием.
// Рядом стоит деградация: заранее решить, что сервис делает, когда сосед недоступен. Отдать закэшированное. Отдать частичный ответ без блока рекомендаций. Принять заказ, но пометить его «оплата будет проверена позже». Плохой вариант тут один - отдать пятисотую ошибку и считать, что сделал всё возможное. Решение, чем именно жертвовать, принимается не в три часа ночи, а заранее и вместе с продуктом.
- предохранитель
- перестаёт вызывать соседа после серии неудач и отвечает отказом сразу
- пробный вызов
- одиночная попытка проверить, ожил ли сосед
- деградация
- заранее решённый упрощённый ответ при недоступности зависимости
Как отвечать: «Сервис упал, все клиенты ретраят. Что не так с ретраями?»
С ними не так три вещи, и все три считаются. Первая - умножение по слоям: три слоя, каждый повторяет трижды, дают на нижний сервис шестидесятичетырёхкратную нагрузку, я это считал. Множитель растёт в степени числа слоёв, поэтому повторы ставят на одном слое, а не на каждом. Вторая - синхронность: если все повторяют ровно через секунду, сервис получает разом весь пик и падает снова. Я моделировал: две тысячи клиентов без разброса дают пик в две тысячи запросов в десятую долю секунды, а со случайной добавкой к паузе - двести тридцать, то есть почти в десять раз меньше. Третья - отсутствие общего предела: если доля повторов в трафике перевалила за несколько процентов, их надо отключать целиком. И рядом обязателен предохранитель: двадцать вызовов мёртвого сервиса без него занимают четыре секунды и все потоки, а с ним - шестьсот миллисекунд, потому что большинство отказывает мгновенно.
Ответ разбирает проблему на три измеримых механизма и к каждому даёт лечение. Числа показывают, что человек это моделировал, а не пересказывал.
На чём валятся
- −Ставят повторы на каждом слое и получают умножение нагрузки в десятки раз.
- −Повторяют без разброса: все клиенты возвращаются одновременно и роняют сервис снова.
- −Повторяют небезопасные к повтору операции и создают дубли платежей.
- −Живут без предела ожидания: одна медленная зависимость занимает все потоки.
- −Не продумывают деградацию заранее и отдают пятисотую там, где хватило бы устаревших данных.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 6, остальные разбираются в тренажёре.
- Почему на каждый исходящий вызов ставят таймаут?A)Иначе внешний сервис сочтёт соединение мёртвым и разорвёт егоB)Таймаут снижает нагрузку на сеть за счёт коротких соединенийC)Без таймаута ответ приходит с задержкой и попадает не в тот запросD)Иначе медленный сосед забирает потоки и роняет сервис целиком
показать ответ и разбор
+D)Иначе медленный сосед забирает потоки и роняет сервис целиком// разбор: Ожидание — это занятый поток, соединение и место в пуле. Если зависимость отвечает по десять секунд, очередь на входе растёт, пул кончается, и сервис перестаёт обслуживать даже те запросы, которым чужой ответ не нужен. Таймаут превращает чужую медлительность в свою управляемую ошибку: можно вернуть заглушку, отдать кэш или честный отказ и освободить ресурсы.
- Клиенты повторяют неудачные запросы сразу и по три раза. Сервис прилёг и не поднимается. Что произошло?A)Ретраи утроили нагрузку и держат сервис внизу синхронной волнойB)Повторы забили пул соединений на балансировщике до истечения таймаутовC)Сервис принимает дубликаты и падает на конфликтах записиD)Кэш заполнился ошибочными ответами и отдаёт их новым клиентам
показать ответ и разбор
+A)Ретраи утроили нагрузку и держат сервис внизу синхронной волной// разбор: Мгновенные повторы превращают просадку в лавину: нагрузка растёт втрое ровно тогда, когда сервис слабее всего, а клиенты бьются синхронно, потому что стартовали от одного сбоя. Лечится экспоненциальной паузой со случайным разбросом, ограничением общего числа попыток и бюджетом ретраев на клиента. И отдельно: повторять можно только идемпотентные операции, иначе получите дубли платежей.
- Таймауты и ретраи уже есть. Что добавляет предохранитель (circuit breaker)?A)Он переключает трафик на резервный экземпляр зависимостиB)Он растягивает таймауты, давая зависимости больше времениC)После серии отказов он отвечает отказом сразуD)Он копит неудачные запросы и повторяет их после восстановления
показать ответ и разбор
+C)После серии отказов он отвечает отказом сразу// разбор: Таймаут защищает один запрос, но при массовом отказе каждый запрос всё равно ждёт своё время и тратит ресурсы. Предохранитель считает долю ошибок и размыкает цепь: следующие вызовы падают мгновенно, без похода в сеть, — это разгружает и нас, и лежащую зависимость. Через паузу он пропускает пробные запросы (полуоткрытое состояние) и, если те прошли, замыкается обратно.
- Клиент повторяет упавший запрос. Для каких операций ретрай безопасен, а для каких опасен?A)Ретрай безопасен для чтений и записей одинаковоB)Безопасен для идемпотентных операцийC)Опасность только в скорости повтора, а не в операцииD)Ретрай всех POST-запросов безопасен по спецификации HTTP
показать ответ и разбор
+B)Безопасен для идемпотентных операций// разбор: Ретрай безопасен для идемпотентных операций: повтор даёт тот же результат (GET, PUT по ключу, DELETE). Для неидемпотентных (списать деньги, создать заказ) повтор после неясного исхода проводит эффект дважды — двойное списание, дубль заказа. Решения: делать операции идемпотентными (idempotency key, дедуп на стороне сервера), ретраить только там, где это безопасно, и различать «точно не выполнилось» от «неизвестно».
- Ретраи есть, но при сбое апстрима клиенты дружно бьют по три раза подряд и добивают его. Как ретраить правильно?A)Экспоненциальный бэкофф с джиттером и потолком попытокB)Повторять сразу и почаще, чтобы быстрее пробитьсяC)Фиксированная пауза в одну секунду между попыткамиD)Убрать лимит попыток, повторять до успеха
показать ответ и разбор
+A)Экспоненциальный бэкофф с джиттером и потолком попыток// разбор: Немедленные синхронные ретраи ×3 превращаются в лавину, которая не даёт упавшему сервису встать (retry storm). Правильно: экспоненциальный бэкофф (пауза растёт: 1с, 2с, 4с…) + джиттер (случайный разброс, чтобы клиенты не били в такт) + потолок числа попыток и общий бюджет ретраев. Ещё лучше — в связке с circuit breaker, который на серии отказов вовсе перестаёт долбить апстрим.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.