awesome-everything EN
↑ Обратно к восхождению

Архитектура бэкенда

Таймауты и фолбэки: что вернуть, когда он open

Суть Брейкер работает, лишь если таймаут вообще делает зависание похожим на отказ, а open-брейкер помогает, лишь если у вызывающего готов ответ. Сеньорный взгляд: таймаут — триггер, фолбэк — ответ, грациозная деградация должна быть редкой, а сброс нагрузки — последний рубеж.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 16 min

Команда добавляет circuit breaker к флакающему сервису рекомендаций и выкатывает его. Следующий инцидент — брейкер никогда не срабатывает: вызовы рекомендаций не ошибаются, они просто берут 30 секунд, а брейкер считал только ошибки. Без таймаута зависание невидимо брейкеру, поэтому он сидел closed сквозь весь сбой. Команда добавляет таймаут в 1 секунду; теперь брейкер срабатывает правильно. Но главная страница начинает возвращать жёсткий 500 в момент размыкания брейкера, потому что никто не решил, что показывать, когда рекомендаций нет. Два недостающих куска, оба существенны: таймаут — это то, что превращает зависание в считаемый отказ, а фолбэк — это то, что вызывающий возвращает, когда брейкер сработал. Брейкер без обоих — это театр.

Таймаут — это триггер

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

Так что таймаут — это то, что превращает зависание в считаемый отказ. Hystrix вшивает это по умолчанию — execution.isolation.thread.timeoutInMilliseconds = 1000 — так что любой вызов сверх 1 с провален и скормлен брейкеру. Таймаут надо ставить осознанно: достаточно долго, чтобы дать легитимный медленный-но-нормальный ответ, достаточно коротко, чтобы реальное зависание поймали до того, как оно припрёт ресурсы. Брейкер поверх зависимости без таймаута — самый частый способ, которым брейкер молча ничего не делает.

Фолбэк — это ответ

Раз брейкер open, каждый вызов отклоняется мгновенно — но отклоняется во что? Сырое исключение, всплывающее к пользователю, — жёсткий отказ; брейкер лишь превратил медленный отказ в быстрый. Ценность приходит, если дать вызывающему фолбэк: полезный ответ, когда настоящий недоступен. Частые фолбэки, грубо от лучшего к худшему:

  • Деградированный-но-корректный ответ. Спрячь карусель рекомендаций и отрисуй остальную страницу. Пользователь теряет фичу, не страницу.
  • Последний-известный-хороший кэш. Отдай чуть устаревшее значение (вчерашние рекомендации, кэшированную цену) вместо ничего.
  • Статический дефолт. Универсальный список «популярного», пустой массив, разумный ноль.
  • В очередь на потом. Для записей прими запрос и обработай его асинхронно, когда зависимость восстановится (паттерн outbox из юнита про идемпотентность).
  • Быстрый отказ с ясной ошибкой. Когда осмысленного фолбэка нет, чистый 503 с Retry-After лучше зависшего 30-секундного запроса.

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

Деградация должна быть редкой, а сброс — это пол

Тонкий сеньорный пункт из практики Google SRE: грациозная деградация не должна срабатывать часто. Фолбэк-путь, который бежит постоянно, недотестирован, скрывает реальную долю отказов и может маскировать хроническую проблему, пока она не станет острой. Деградация — для настоящих инцидентов, и ты должен алертить, когда слишком много серверов входит в деградированный или фолбэк-режим — эта частота сама по себе сигнал.

Когда даже фолбэки не справляются — весь сервис перегружен, не только одна зависимость — последний рубеж это сброс нагрузки: намеренно отклонять долю запросов с 503, чтобы защитить остальное, вместо того чтобы дать всему деградировать в таймауты. Книга Google SRE парует это с LIFO/CoDel-стилем очередей, что отбрасывает запросы, простоявшие в очереди достаточно долго (~10 с), чтобы всё равно промахнуться мимо своего дедлайна — нет смысла тратить ёмкость на запрос, от которого пользователь уже отказался.

Почему это работает

Почему фолбэк, который бежит всё время, это проблема, а не фича? Потому что постоянно срабатывающий фолбэк тихо переопределяет «работает». Если главная страница отдаёт устаревшие рекомендации из кэша на каждый запрос, потому что живой сервис сломан уже неделю, страница выглядит нормально, дашборды выглядят нормально, и никого не пейджат — отказ отмыт в нормальную работу. Три вещи гниют под этим. Первое: реальная доля отказов зависимости теперь невидима, поэтому маленькая проблема невыявленно растёт в большую. Второе: сам фолбэк-путь теперь несущий, но редко рассматриваемый; в день, когда кэш тоже падает, ты обнаруживаешь, что в фолбэке был баг, на который никто не натыкался месяцами. Третье: ты потерял сигнал, отличающий «деградировано» от «здорово», а это сигнал, на котором держатся решения дежурного. Дисциплина — относиться к фолбэкам как к исключению и инструментировать их частоту: фолбэк, срабатывающий 0,1% времени во время сбоя, — это система, работающая как задумано, тогда как тот же фолбэк, срабатывающий 40% времени, — инцидент в маске здоровья. Поэтому зрелые команды алертят на частоту деградации, не только на жёсткие ошибки — отсутствие ошибок не то же, что здоровье, когда фолбэк молча поглощает отказы.

СлойТриггерЧто возвращаетКогда применять
ТаймаутВызов сверх бюджета (~1 с)Отказ, скормленный брейкеруВсегда, на каждом сетевом вызове
Деградированный ответБрейкер openСтраница минус сломанная фичаНекритичная фича
Устаревший кэшБрейкер openПоследнее-известное-хорошее значениеЧтение, терпимое к устареванию
Статический дефолтБрейкер openУниверсальное безопасное значениеНет более свежей опции
Быстрый 503Брейкер open, фолбэка нетЧистая ошибка + Retry-AfterКритичный вызов (например платёж)
Сброс нагрузкиВесь сервис перегружен503 для долиПоследний рубеж, защитить остальное
Викторина

Брейкер добавлен к флакающему сервису, но никогда не срабатывает во время сбоя. Зависимость не ошибается — она берёт 30 с на вызов. Чего не хватает?

Викторина

Почему опытные команды алертят, когда фолбэк грациозной деградации срабатывает слишком часто, вместо того чтобы считать это просто работой системы?

Вспомните перед уходом
  1. 01
    Почему таймаут существенен для работы circuit breaker, и какие опции фолбэка есть у open-брейкера?
  2. 02
    Почему грациозная деградация должна быть редкой, и что такое сброс нагрузки?
Итог

Брейкер хорош ровно настолько, насколько хороши два куска вокруг него. Таймаут — триггер: поскольку брейкер считает отказы, а опасная медленная зависимость их не производит, зависание без таймаута невидимо, и брейкер сидит closed сквозь сбой — дефолтный таймаут Hystrix в 1 секунду превращает зависание в считаемый отказ, а брейкер поверх зависимости без таймаута молча ничего не делает. Фолбэк — ответ: open-брейкер отклоняет мгновенно, и ценность — это то, что вызывающий возвращает взамен: деградированная-но-корректная страница, последний-известный-хороший кэш, статический дефолт, поставленная в очередь запись через outbox или чистый 503 с Retry-After — выбираемые на каждом месте вызова, ведь отсутствующая карусель не должна ронять страницу, а отсутствующая авторизация платежа обязана провалить заказ. Грациозная деградация должна быть редкой, потому что постоянно срабатывающий фолбэк скрывает истинную долю отказов и оставляет нетестируемый путь несущим, поэтому алерти на его частоту. А когда весь сервис перегружен, сброс нагрузки с очередями, осведомлёнными о дедлайне, — это пол. Всё до сих пор предполагало один процесс — финальный урок масштабирует на много инстансов, где брейкеры флапают, стадятся на half-open и сталкиваются с ретраями.

Связанные уроки
Продолжить восхождение ↑На масштабе: состояние на инстанс, штормы ретраев и координированный сброс
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.