DarkRiDDeR16 мин

Как разорвать каскад: один бюджет раньше retry, fallback и реплик

НадёжностьАрхитектура

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

Ниже нет реального сервиса, замера или нагрузки. Имена primary-a, edge и named-summary — fixed synthetic JS literals. Units латентности — только подписи внутри модели, не миллисекунды. Проверка не открывает сеть, не читает файлов, не обращается к клиенту и не делает production-вывод. Её единственный положительный результат — узкий hand-off на synthetic resilience review: у карточки есть один владелец retry, конечный fan-out, именованный fallback и точка, где дальнейшая работа прекращается.

Граф fixed каскада: логический запрос проходит через единственного владельца retry, выбирает только одну реплику на попытку и при исчерпании бюджета останавливается в именованном деградированном результате.
Схема показывает не настоящую топологию, а порядок контроля: limit стоит перед расширением маршрута, поэтому fallback не становится вторым retry-слоем.

Сначала посчитать не запросы, а добавленную работу

Полезная единица здесь — логический запрос, а не каждое исходящее действие. Для него нужно выписать все способы создать дополнительную работу: повтор после временного исхода, параллельный либо последовательный выбор другой реплики, переход на альтернативный ответ, повтор уже внутри альтернативы. Если таблица не различает эти причины, она скрывает главный риск. Два действия с одинаковым именем retry могут иметь разный смысл: одно возвращает тот же маршрут после разрешённого исхода, другое запускает новый fan-out и фактически удваивает давление.

Карта расширения fixed маршрута
ЗащитаКогда добавляет работуКонечная границаЧто остаётся в trace
retry edgeтолько temporary-timeoutдве попытки на логический запросуменьшение retry budget
выбор репликиперед каждой разрешённой попыткойровно одно имя за попыткуprimary-a или primary-b
named-summaryтолько fixed optional result unavailableодна известная работа, без своего retryимя fallback и degraded output
limit pointдо route expansionдва дополнительных запуска всегоreturn fixed degraded result
failure injectionодин названный synthetic исходне является наблюдением о миреfixed-primary-a-temporary-timeout

Минимальная карточка до обсуждения инфраструктуры

Хороший review начинается не с выбора библиотеки и не с перечня доступных реплик. Нужна карточка с одной семейством сценария: одинаковое число logical requests, одно фиксированное injection-событие, один владелец retry и понятный отказ после лимита. В рабочем варианте два logical request. Первый встречает temporary-timeout на primary-a и получает разрешённую вторую попытку на primary-b. Второй не расширяет маршрут при optional-result-unavailable, а получает named-summary. Так видно, какой work added каждой защитой и почему сумма не растёт сама от себя.

import { createFixedResilienceScenario, assessFixedResilienceScenario } from './upgrade-2026-02.mjs';

const scenario = createFixedResilienceScenario('bounded-cascade-v1');
const report = assessFixedResilienceScenario(scenario);
console.log({ status: report.status, bound: report.bound, handoff: report.handoff });
// { status: 'synthetic-resilience-ready', bound: { retryOwner: 'edge', maxAttemptsPerLogicalRequest: 2, maxReplicaFanout: 1, maxExtraAttempts: 2, limitPoint: 'edge-before-route-expansion' }, handoff: 'synthetic-resilience-review' }

Фрагмент импортирует только публичные exports. Он сравнивает заранее зашитую карточку с правилами fixture и возвращает структуру review. Здесь нет таймера backoff, настоящего обращения к primary-b и выбора по здоровью. Именно поэтому результат нельзя читать как обещание latency или availability. Он доказывает лишь то, что синтетическая карточка не потеряла четыре ограничения во время обсуждения: retry принадлежит edge, максимум попыток назван, fan-out конечен, а лимит расположен до fallback.

Порядок разрыва каскада

  1. Назовите logical request и перечислите, какие действия считаются дополнительной работой именно для него.
  2. Оставьте одного владельца retry; другие слои получают исход, но не запускают второй цикл попыток.
  3. Запишите максимальный fan-out числом и именами, а не формулой вида «все доступные реплики».
  4. Дайте fallback собственное имя, входной исход, фиксированную стоимость и запрет на вложенный retry.
  5. Поставьте один limit point перед расширением route и определите дешёвый результат после исчерпания бюджета.
  6. Зафиксируйте injection и trace, затем откажитесь сравнивать карточку с другим объёмом logical work или другим типом сбоя.

Почему fallback нельзя считать бесплатной добротой

Fallback часто называют защитой, хотя он меняет контракт результата. Это нормально, если изменение явно названо. Плохо, когда запасной путь скрывает новую работу: например, сам делает повтор, выбирает несколько реплик или пытается восстановить необязательную часть ответа до возврата пользователю. В таком виде команда видит слово fallback и предполагает деградацию, а каскад получает ещё один производитель попыток. В fixed карточке named-summary имеет один work unit и не содержит retry plan. Это не универсальная архитектурная норма; это минимальная граница, которую reviewer может проверить без догадки.

Отдельно проверьте семантику результата. Фраза «вернули ответ» слишком широка. Для одного сценария полезно определить, что именно сохраняется: fixed degraded summary, причина ограничена, а попытка восстановить необязательный фрагмент не продолжается. Тогда сохранённый trace говорит не только о том, что остановились, но и почему остановка произошла. Если fallback не может описать выход коротким именем, он ещё не готов быть отдельным режимом и не должен автоматически включаться в каскаде.

Точка ограничения — это контракт, а не счётчик где-нибудь рядом

Счётчик полезен только тогда, когда ясно, что он ограничивает и когда читается. Для данного fixture point называется edge-before-route-expansion. Он срабатывает до retry, выбора следующей реплики и fallback. Поэтому исчерпанный budget не допускает ситуацию, где последний retry уже породил два новых маршрута, а лимит узнали постфактум. Контракту нужны четыре части: место, конечное значение, область действия и terminal action. Уберите одну — и reviewer не может доказать, что такая же проверка действительно разрывает цепочку.

Не подменяйте эту точку общим лимитом параллелизма, TTL или длиной очереди. Они могут быть важны, но отвечают на другие вопросы. Здесь требуется граница для создания дополнительной работы после failure outcome. Разделение делает обсуждение конкретным: retry budget защищает от повторов, replica fan-out ограничивает ширину, fallback cost ограничивает альтернативный путь. В зрелом дизайне эти цифры могут зависеть от домена; в этой статье они fixed, чтобы не создать видимость реальной capacity-модели.

Как убедиться, что limit поставлен достаточно рано

Проверяйте не наличие слова budget в конфигурации, а порядок переходов в trace. После fixed timeout сначала должна появиться запись о расходе разрешённого retry, и лишь затем — новый выбранный маршрут. Если trace сначала показывает выбор нескольких реплик или вход в fallback, а ограничение приходит последней строкой, point стоит после расширения. Такой счётчик может хорошо выглядеть на панели, но не ограничивать первую волну лишней работы. Для review достаточно простого вопроса: какой конкретный переход запрещён, когда budget стал нулём? В карточке ответ должен быть одним terminal action, а не перечнем надежд.

Есть и вторая проверка: граница обязана пережить смену режима. Нельзя списывать retry token на primary, а затем считать fallback независимым жестом без стоимости. Иначе budget контролирует только часть каскада. В fixed маршруте named-summary указан до запуска, имеет одну synthetic единицу и не расширяет replica selection. Это делает его видимым исключением, а не лазейкой. Если альтернативный ответ требует отдельного поиска, считайте его самостоятельной веткой с собственным ограничением и не называйте её бесплатным fallback.

Граница метода и следующий безопасный шаг

Модель не оценивает идемпотентность, корректность компенсации, распределённое состояние, реальную доступность реплик или пользовательский ущерб. Она не заменяет load test, failure exercise, incident review и решение владельца системы. Официальные документы ниже дают полезный язык: несколько уровней retry могут перемножать попытки, а политика повтора может иметь предел и throttling. Но ни RFC, ни design proposal не подтверждают, что конкретный fixture переживёт отказ. Здесь даже не существует реального отказа.

Следующий шаг не должен быть «включить все защиты». Возьмите одну учебную карточку с тем же числом logical requests и одним именованным injection. Добавьте один trace transition на каждую дополнительную попытку. Если нельзя показать, где эта попытка списывает budget, сократите маршрут. Если fallback нельзя назвать и оценить как отдельную работу, оставьте его выключенным. Только после такого narrow review имеет смысл создавать отдельный work item для настоящего теста с правами, данными, наблюдением и отдельной безопасностной проверкой.

Проверяемые источники

  • Архивная глава Google SRE о положительной обратной связи — версия: Internet Archive snapshot 2025-01-23; chapter from the Google SRE Book, 2017 edition. Источник задаёт термин каскада и показывает риск произведения попыток при retry на нескольких уровнях. Граница: Архивная страница не измеряет этот fixture и не подтверждает его capacity или recovery.
  • gRPC A6: fixed revision retry and hedging design — версия: immutable commit dc9fd4fe5b94b90b82fe2833ad1d80938e6a49c1, authored 2024-08-29. Документ позволяет точно назвать maxAttempts, backoff и throttling как параметры политики попыток. Граница: Design proposal не является требованием к произвольному стеку и не доказывает эффект synthetic проверки.