Перейти к содержимому

Устойчивость — не магия, а набор явных правил: circuit breaker, повторы с экспоненциальной задержкой и разбросом, каскад таймаутов, изоляция по отсекам, плавная деградация, идемпотентность. Каждое правило описывается в одну фразу, но я регулярно вижу команды, которые знают слова и не применяют их дисциплинированно: повторы без разброса (и все клиенты бьются в дверь синхронно при первом же сбое), circuit breaker без стратегии восстановления («открылся навсегда»), идемпотентность в режиме «допилим потом», хотя выкатка завтра. Лист — про дисциплину применения. Соседний к Capacity Planning и SLO Engineering под L1 Reliability Engineering. Граница: там готовятся к нагрузке, здесь выживают, когда подготовка не сработала.

Главный навык на уровне L4 — правильно реализовать экспоненциальную задержку с разбросом (exponential backoff with jitter). Я регулярно вижу команды, которые написали повторы «с задержкой», но забыли про разброс, — и при первом же сбое получили шторм повторов, добивший сервис, который только начал вставать. Лучший разбор написал Marc Brooker в блоге AWS в 2015 году: три варианта разброса с симуляциями и графиками. Час чтения и год пользы.

L3

  • Знает базовый набор паттернов (circuit breaker, повторы, таймауты, запасной ответ); применяет их через библиотеки своего стека (Polly, resilience4j, Tenacity, retry-axios), а не пишет заново.
  • Понимает разницу между пробами liveness и readiness; пишет адекватные проверки здоровья: поверхностную «процесс жив» и глубокую «зависимости доступны».

L4

  • Реализует повторы с экспоненциальной задержкой и разбросом; понимает, почему без разброса получается синхронная волна и почему бесконечные повторы без circuit breaker раскручивают каскад.
  • Выстраивает таймауты иерархически: внутренний меньше внешнего с запасом на повторы, никаких сетевых умолчаний по умолчанию, проброс оставшегося времени между сервисами.
  • Ограничивает повторы бюджетом и делает их в одном слое стека; считает, во что превращается один запрос, когда повторяет каждый слой.

L5

  • Проектирует изоляцию по отсекам: пулы соединений, пулы потоков, разделённые очереди — чтобы перегрузка одной зависимости не съедала ресурсы остальных.
  • Реализует плавную деградацию с явными уровнями критичности: feature flags для отключения второстепенного, запасные ответы, отдача из кеша. Режим деградации описан словами и проверяется на game day.
  • Различает жёсткие и мягкие зависимости; отключением проверяет, что второстепенная зависимость не роняет главный сценарий.
  • Делает идемпотентность обязательной для всего, что можно повторять: ключи идемпотентности, ETag, условные записи, исходящий журнал транзакций.

L6+

  • Проектирует сброс нагрузки и обратное давление: приоритеты по критичности, отказ второстепенному трафику при перегрузке, контроль допуска по глубине очереди, бюджет повторов на уровне сервиса.
  • Внедряет chaos engineering как способ проверить, что все эти механизмы действительно работают.
  • Ищет в системе петли, которые удерживают отказ после исчезновения триггера: повторы, прогрев кеша, переключение на резерв. В разборе инцидента называет причиной петлю, а не триггер.
  • Michael Nygard — Release It! Design and Deploy Production-Ready Software (Pragmatic Bookshelf, 2-е изд., 2018). Каноническая книга по теме: главы про паттерны и антипаттерны стабильности — circuit breaker, отсеки, устойчивое состояние, быстрый отказ. Спустя двадцать лет всё ещё лучший из единых источников.
  • Cindy Sridharan — Distributed Systems Observability (O’Reilly, 2018). Не читал. Строка здесь как указатель: устойчивость без наблюдаемости не проверить, а книга о том, что в распределённой системе стоит видеть.
  • Наталья Савенкова — SRE. Рецепты выживания в продакшне для инженера по надежности (2024). Читал и рекомендую: шестьдесят страниц коротких рецептов без теории. К этому листу относятся рецепты про деградацию: согласованный с продуктом ответ по умолчанию, несколько уровней отключения и повторы с ограничением.
  • Addressing Cascading Failures — SRE Book, глава 22. Канонический разбор: откуда берётся каскад, как повторы усиливают нагрузку, что такое перегруженный сервер и запрос-убийца. Советую читать в паре со следующей: здесь механика каскада, там средства против него.
  • Handling Overload — SRE Book, глава 21. Дополняет предыдущую: придушивание на стороне клиента, уровни критичности, бюджеты повторов, дедлайны и их проброс.
  • Marc Brooker — Exponential Backoff and Jitter (AWS Architecture Blog, 2015). Главный публичный кейс, разбор ниже. Советую прочитать первой из всего списка.
  • Netflix Tech Blog — Making the Netflix API more resilient. История появления Hystrix и обоснование изоляции по отсекам на живой системе.
  • Nathan Bronson, Abutalib Aghayev, Aleksey Charapko, Timothy Zhu — Metastable Failures in Distributed Systems (HotOS ’21). Советую прочитать целиком: семь страниц, после которых шторм повторов перестаёт выглядеть невезением: у отказа, который не проходит сам, есть определение, три состояния и петля в роли причины.
  • Мой разбор — Digital Immune System: инженерия устойчивости как продукт (jtprog.ru, август 2026). Раздел про право на отказ и организованную деградацию — про то, чего в главах SRE Book выше нет: деградация срабатывает только тогда, когда заранее решено, какие функции гаснут первыми и кто это решение принимает. Входная точка в саму концепцию — мой более ранний обзор Digital Immune System (февраль 2026).
  • Мой разбор — Ночь с пятницы на понедельник: как не утонуть в крупном инциденте (Telegram, март 2026). Конспект статьи Яндекса о региональном сбое в облаке. К этому листу относятся ставки, которые, по моему мнению, стоит примерять каждому: плоскость данных, переживающая падение плоскости управления, рубильник для вывода зоны и жёсткое ограничение очередей.
  • resilience4j (Java) — современная замена Hystrix. Circuit breaker, повторы, ограничитель частоты, отсеки и таймауты собираются как отдельные сочетаемые модули, а не монолитом. По моим наблюдениям, для нового кода на Java чаще выбирают именно её.
  • Polly (.NET) — каноническая библиотека для той же задачи. Текучий интерфейс, те же паттерны.
  • Tenacity (Python) — простая библиотека повторов с богатой настройкой задержки, разброса и условий остановки. Если кроме повторов ничего не нужно, её достаточно.
  • Envoy / Istio circuit breaking — уровень service mesh: размыкание, отсев неисправных адресатов, повторы прямо в прокси, без правок кода. Подходит, когда сервис менять нельзя или в языке нет приличной библиотеки.
  • Chaos Mesh / Litmus / AWS Fault Injection Service — чем проверять, что все эти механизмы срабатывают.

Главный публичный кейс на тему задержки между повторами — Marc Brooker, «Exponential Backoff and Jitter» (AWS Architecture Blog, 2015). Симуляции в статье показывают: с одной только экспоненциальной задержкой клиенты продолжают приходить сгустками, и это худший вариант из всех сравниваемых. Полный разброс — случайная пауза от нуля до текущего потолка задержки — размазывает вызовы по времени и на сотне конкурирующих клиентов сокращает их число больше чем вдвое. Вариант «половина задержки плюс случайная половина» в том же сравнении проиграл: работы чуть больше, времени заметно больше. Между полным и декоррелированным разбросом выбор неочевиден; формулы и графики там же. Если кто-то в команде написал повторы без разброса, отправляйте на эту статью первым делом. Час чтения окупается на первом же сбое.

Три правила, вокруг которых крутится всё остальное. Повторы живут только на идемпотентных операциях, и идемпотентность — предусловие, а не «допилим потом»: каждый повтор без ключа рано или поздно превращается в двойную запись или двойное списание. Либо идемпотентность зашита в контракт API, либо повторы выключены, третьего нет. Экспоненциальная задержка с разбросом обязательна, потому что «пять раз с интервалом в секунду» при сбое соседа означает, что все клиенты бьются синхронно. И проверка здоровья — не проверка бизнес-логики: liveness, который лезет в базу, при её недоступности заставит Kubernetes перезапустить все поды разом и превратит частный отказ в каскад. liveness отвечает на вопрос «процесс жив», readiness — «готов принимать трафик»; отказ базы гасит второе и не трогает первое.

Задержка с разбросом решает половину задачи. Она разносит повторы во времени, а объём их не ограничивает: при массовом сбое повторяют все клиенты сразу, и разброс только меняет форму волны. Объём ограничивает бюджет повторов. В двадцать первой главе SRE Book он двухуровневый: не больше трёх попыток на запрос и не больше 10% повторов в потоке клиента. С одним только первым ограничением нагрузка на перегруженный датацентр вырастает почти втрое, со вторым — в 1.1 раза. Вторая половина той же арифметики — слои. В двадцать второй главе посчитан стек из трёх слоёв, где каждый делает по три повтора: одно действие пользователя превращается в 64 попытки на базе. Поэтому повторяют в одном месте стека, а остальные слои передают отказ наверх как есть. В разборе книги Савенковой я записал то же короче: задержка нужна затем, чтобы повторы не продлевали агонию инцидента.

Circuit breaker без явной стратегии восстановления открывается навсегда. Схему «разомкнут — пробуем — замкнут» пишут все, а вот условие возврата продумывают редко. Работает так: после паузы пропускается один пробный запрос, успех замыкает цепь, неудача возвращает в разомкнутое состояние с увеличенным интервалом. Метрики состояния — обязательная часть дашборда сервиса, иначе в инциденте никто не понимает, сработал он или нет. Отдельный вопрос — на что его вешать. Размыкатель на весь сервис сразу почти всегда компромисс: у методов разная нагрузка и разный профиль ошибок, и общий порог выходит либо слишком нервным, либо слишком терпеливым. У паттерна есть и критики. В Amazon Builders’ Library про него сказано, что он вносит в систему модальное поведение, которое трудно тестировать, и способен заметно удлинить восстановление. Сами авторы ограничивают повторы локально корзиной токенов, и с 2016 года это поведение встроено в AWS SDK.

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

Таймауты выстраиваются каскадом: внутренний меньше внешнего с запасом на повторы. Классическая ошибка — тридцать секунд у внешнего клиента и те же тридцать у внутреннего вызова: внутренний всегда «успевает» по своему таймеру, но отвечать уже некому, клиент отвалился. Каждый уровень отсекается раньше родителя на величину бюджета повторов плюс запас. Следующий уровень — проброс дедлайна: срок назначается один раз на входе, и каждый сервис передаёт дальше остаток. Пример из двадцать второй главы SRE Book: сервис A дал сервису B десять секунд, B восемь секунд не начинал обработку, а потом вызвал C с зашитым в код дедлайном в двадцать. C честно отрабатывает запрос, ответ на который уже никому не нужен. С пробросом C получил бы две секунды, увидел бы, что они истекли, и отказал сразу. Сам по себе gRPC этого не гарантирует. В его документации сказано, что дедлайна по умолчанию нет и клиент может ждать ответа бесконечно, а передавать срок из входящего вызова в исходящий умеют только некоторые реализации.

Плавная деградация требует явных уровней критичности. «При перегрузке что-нибудь отключим» — не план: пока запросы и сервисы не разложены на критичные, важные и те, которыми можно пожертвовать, решать в моменте нечем. В двадцать первой главе SRE Book уровней четыре: CRITICAL_PLUS, CRITICAL, SHEDDABLE_PLUS и SHEDDABLE. Признак зрелого сервиса простой: режим деградации описан словами и регулярно проверяется на game day. По моим наблюдениям, без разметки критичности деградация существует только на бумаге и в перегрузке не работает. У AWS та же мысль записана со стороны зависимостей: плавная деградация превращает жёсткую зависимость в мягкую, и компонент продолжает делать главное на слегка устаревших данных, на запасных или вовсе без них. Сама разметка остаётся гипотезой. Второстепенный сервис, который вызывают синхронно и без запасного ответа, роняет главный сценарий не хуже критичного, и выясняется это только отключением.

Паттерны без наблюдаемости — слепое пятно. Circuit breaker и повторы добавили, а метрик «сколько повторов», «в каком состоянии размыкатель» и «сколько трафика сбросили» нет; в инциденте не видно, работают ли они вообще, а на ревью не понять, помогают они или маскируют настоящую проблему. Каждый паттерн заслуживает отдельного SLI и панели на дашборде. Дисциплина базовая. Откладывают её постоянно.

Есть класс отказов, ради которого всё перечисленное стоит собирать вместе, а не по одному: метастабильные. Nathan Bronson с соавторами описали его в 2021 году так: система попадает в плохое состояние от триггера и остаётся в нём, когда триггер уже убран, потому что её держит петля обратной связи. Пример из статьи умещается в несколько чисел. База отвечает быстро до 300 запросов в секунду, приложение делает один повтор по секундному таймауту и работает на 280. Сеть пропадает на десять секунд. После восстановления повторы держат поток на 560, каждый запрос уходит в таймаут, и полезной работы не остаётся совсем. Триггера давно нет, а система лежит, пока нагрузку резко не снизят или не поменяют политику повторов. Два вывода оттуда я считаю главными. Причина такого отказа — петля, а не триггер: триггеров много, и разбор, остановившийся на «моргнула сеть», оставляет место для повтора с другим поводом. И вырастают петли из того, что в обычный день повышает эффективность и надёжность, из тех же повторов и кеша. По подсчёту авторов следующей работы, так устроены минимум четыре из пятнадцати крупных сбоев AWS за десятилетие. Бюджет повторов, проброс дедлайна и сброс нагрузки в этой рамке нужны, чтобы рвать петлю.

Про границу. Всё это окупается там, где у сервиса есть сетевые зависимости и заметный трафик. Для внутренней утилиты, которая раз в час ходит в одну базу, полный набор паттернов — лишний код, который сам становится источником багов: неверно настроенный circuit breaker роняет вызовы, которые прошли бы. И обратная сторона общая для всех этих механизмов: они прячут симптом. Сервис с идеальными повторами и деградацией месяцами живёт поверх зависимости, которая отвечает через раз, — пока об этом не узнают из счёта за трафик или из жалобы на «иногда медленно».

  • Capacity Planning — эти паттерны закрывают ситуацию, когда мощности кончились или прогноз ошибся. Деградация и сброс нагрузки — то, что происходит, пока масштабирование догоняет. Если догонит.
  • SLO Engineering — паттерны держат цель под нагрузкой и при отказах. Circuit breaker не даёт бюджету ошибок сгореть на каскаде.
  • Networking — таймауты, повторы и размыкание латают ненадёжность сети. Service mesh реализует часть этого на инфраструктурном слое.
  • SLI-based Alerting — алерты ловят момент срабатывания: цепь разомкнулась, повторы выросли, трафик начали сбрасывать.
  • Chaos Engineering — эксперимент проверяет, что механизмы живые: circuit breaker правда открывается? повторы не усиливают шторм? отсеки изолируют?
  • Game Day / Chaos Drills — режим деградации и разметку критичности проверяют там: отключают второстепенную зависимость и смотрят, устоял ли главный сценарий.
  • Progressive Delivery — канареечная выкатка с проверкой здоровья опирается на пробы readiness и метрики размыкателя.
  • Operating Systems — состояние на уровне системы (открытые файловые дескрипторы, таблица conntrack, давление на страничный кеш) часто и есть то, на что реагирует деградация.
  • Service Mesh — прокси рядом с сервисом умеет размыкание, отсев неисправных адресатов и политики повторов без правок кода; это одна из инфраструктурных реализаций паттернов отсюда.
  • Containerization & Orchestration — PodDisruptionBudget, автомасштабирование, распределение по зонам, пробы liveness и readiness — родные для k8s реализации тех же идей.
  • Idempotency Patterns (TBD) — отдельный лист: ключи идемпотентности, ETag, условные записи, исходящий журнал транзакций, иллюзия доставки ровно один раз.
  • Backpressure & Load Shedding (TBD) — деградация в сторону источника нагрузки: классификация по критичности, контроль допуска, управление очередями, бюджеты повторов.
  • SLI для самих паттернов — что брать за показатель: долю времени в разомкнутом состоянии, успешность повторов, долю сброшенного трафика?
  • Как заранее увидеть уязвимое состояние — по Bronson система может годами работать в уязвимом состоянии: отвечает штатно, но после триггера сама не восстановится. Границу, ниже которой она восстанавливается сама, авторы называют скрытой ёмкостью, и она заметно ниже заявленной: в их примере с кешем это 300 запросов в секунду против 3 000. Систематического способа строить системы, устойчивые к ещё не известным метастабильным отказам, по их же словам, пока нет.