Отказоустойчивость и High Availability
Назначение: дисциплина проектирования систем, сохраняющих работоспособность при отказах своих частей: паттерны устойчивости (Circuit Breaker, Bulkhead, Retry, Timeout, Rate Limiter, fallback), избыточность, failover и метрики доступности — «девятки», RTO/RPO, SLA/SLO/SLI.
Аудитория: архитекторы, техлиды, backend- и platform-разработчики, SRE-инженеры.
Статус: устоявшаяся дисциплина; каталог паттернов устойчивости — Майкл Нйгард, Release It! (2007, 2-е издание — 2018); идейный фон — «восемь заблуждений о распределённых вычислениях» (Sun Microsystems, 1990-е) и учебник Беркли Engineering Long-Lasting Software (Fox & Patterson, 2012); сегодня — стандартная часть cloud-native-стека: resilience-библиотеки (Polly, resilience4j), service mesh, Kubernetes, практики chaos engineering.
Не путать с: наблюдаемостью (обнаружение и диагностика отказов — необходимый партнёр, но не защита), масштабируемостью (реакция на рост нагрузки, а не на отказы), disaster recovery (восстановление после полной остановки; HA — про продолжение работы без остановки).
Общее
Отказоустойчивость (resilience, fault tolerance) — свойство системы сохранять работоспособность при отказе отдельных компонентов: полностью или с контролируемо уменьшенной функциональностью. Высокая доступность (high availability, HA) — сопряжённое, но другое понятие: свойство системы оставаться доступной для пользователей в течение согласованной доли времени, измеряемое процентом доступности. Различие удобно фиксировать формулой: resilience — про поведение системы во время отказа, HA — про измеримую цель по доле времени без отказов. При этом fault tolerance в узкой трактовке — переживание отказа без потери функции (репликация, кластер, голосование большинства), а resilience в современной трактовке шире: система обязана корректно вести себя и тогда, когда полноценная работа невозможна, — деградировать управляемо, а не падать целиком.
Отказоустойчивость и наблюдаемость — две половины одной задачи. Наблюдаемость отвечает на вопросы «что сломалось, где и почему» — до, во время и после инцидента; устойчивость определяет, как система ведёт себя, пока причина ещё не найдена и не устранена. Система, неспособная пережить отказ без остановки, делает наблюдаемость бессмысленной (наблюдать нечего — система мертва); система, устойчивая, но ненаблюдаемая, тихо работает в деградированном режиме, о котором никто не знает. Поэтому SLI/SLO и бюджеты ошибок, разобранные в статье о наблюдаемости, — общий инструмент обеих дисциплин: они задают измеримую цель доступности и превращают надёжность из лозунга в арифметику.
Метрики доступности. Доступность традиционно измеряют «девятками» — долей времени, когда система выполняет свои функции:
| Доступность | Простой в день | Простой в месяц | Простой в год |
|---|---|---|---|
| 99% («две девятки») | 14 минут 24 секунды | ~7 часов 18 минут | ~3 дня 15 часов |
| 99,9% («три девятки») | 1 минута 26 секунд | ~43 минуты 50 секунд | ~8 часов 46 минут |
| 99,99% («четыре девятки») | 8,6 секунды | ~4 минуты 22 секунды | ~52 минуты 36 секунд |
| 99,999% («пять девяток») | 0,86 секунды | ~26 секунд | ~5 минут 16 секунд |
Восстановление после отказа описывают две метрики. RTO (recovery time objective) — целевое время восстановления: сколько времени может пройти от отказа до возобновления службы. RPO (recovery point objective) — целевая точка восстановления: какой объём данных допустимо потерять (измеряется «окном» между последней сохранённой копией и моментом отказа). RTO отвечает за доступность, RPO — за данные: репликация в реальном времени даёт RPO, близкий к нулю, но ценой сложности; еженощный бэкап — RPO до 24 часов при минимальной цене. Эти метрики — язык разговора архитектора с бизнесом: «можем ли мы позволить себе терять час заказов?» — вопрос не технический, и ответ определяет архитектуру.
С арифметикой «девяток» связано и правило композиции: доступность цепочки перемножается. Если запрос проходит последовательно через три зависимости с доступностью 99,9% каждая, то доля запросов, не задевших ни одного отказа, — 0,999³ ≈ 99,7%: три «девятки» в цепочке деградировали до двух с половиной. Отсюда два следствия. Первое — чем длиннее синхронная цепочка вызовов, тем ниже доступность целого и тем дороже каждая её «девятка»; асинхронная развязка через брокер (см. событийную архитектуру) вынимает звенья из произведения, заменяя строгую зависимость буферизацией. Второе — параллельные избыточные копии, напротив, доступность повышают: два независимых экземпляра с 99,9% дают 1 − 0,001² ≈ 99,9999% — арифметическое обоснование избыточности из раздела о failover.
Контракты уровня сервиса. Внешнее обязательство перед пользователем фиксируется иерархией терминов: SLA (service level agreement) — юридический договор с санкциями за недостижение уровня; SLO (service level objective) — внутренняя цель уровня сервиса («99,9% запросов успешно за 30 дней»); SLI (service level indicator) — измеримый индикатор, на котором строится SLO (доля успешных ответов, квантиль задержки). Разница между 100% и SLO образует бюджет ошибок — допустимый объём нарушений, который можно тратить на рискованные релизы. Культура SRE (книга Site Reliability Engineering, Google, 2016) сделала бюджеты ошибок основным механизмом примирения скорости поставки и надёжности.
Источники дисциплины. Канонический каталог паттернов устойчивости дал Майкл Нйгард (Michael T. Nygard) в книге Release It! Design and Deploy Production-Ready Software (2007; второе издание — 2018): circuit breaker, bulkhead, timeout, «стабильные паттерны» против каскадных сбоев — всё это впервые систематизировано там, на материале промышленных инцидентов. Идейный фон — «восемь заблуждений о распределённых вычислениях» (fallacies of distributed computing): список наивных допущений о сети («сеть надёжна», «задержка нулевая», «пропускная способность бесконечна» и далее), сформулированный в Sun Microsystems в начале 1990-х; публикация первых семи приписывается Л. Питеру Дойчу (L. Peter Deutsch, 1994), восьмое добавил Джеймс Гослинг. Историю и разбор заблуждений собрал Арнон Ротем-Галь-Оз (Arnon Rotem-Gal-Oz) в заметке Fallacies of Distributed Computing Explained (2006). Каждое заблуждение — прямое требование к устойчивости: раз сеть ненадёжна и задержка ненулевая, каждый сетевой вызов обязан иметь тайм-аут, повтор и размыкатель. Третий первоисточник — Армандо Фокс и Дэвид Паттерсон (Berkeley), учебник Engineering Long-Lasting Software (2012), сопровождающий их курс о SaaS: там сформулированы принципы долговечного облачного ПО — проектирование с расчётом на отказ дешёвого commodity-железа, горизонтальное масштабирование и статистический контроль качества на основе телеметрии. Наконец, индустриальную школу сформировала практика: Netflix (Chaos Monkey, 2011 — намеренное «убийство» экземпляров в продакшене; библиотека Hystrix, 2012) и Google (SRE, бюджеты ошибок).
Design for failure. Все источники сходятся в одном принципе: отказ — не исключительная ситуация, а нормальный режим работы распределённой системы. Компоненты откажут — вопрос не «если», а «когда»; архитектор проектирует не предотвращение отказов, а поведение системы при них. В микросервисной архитектуре этот принцип становится обязательным: количество сетевых вызовов растёт, и без явной обработки отказов вероятность каскадного сбоя приближается к единице. В монолите картина иная: отказы другого рода — весь процесс жив или мёртв целиком, частичных отказов между внутренними вызовами нет; зато один дефект памяти валит всё. Поэтому отказоустойчивость распределённой системы — это в первую очередь работа с частичными отказами (partial failures): одни узлы живы, другие нет, и система в целом находится в состоянии, которое не бывает у однопроцессного приложения.
Паттерны устойчивости
Паттерны устойчивости — типовые приёмы обработки отказов зависимостей. Ниже — сводная таблица, затем разбор каждого паттерна: суть, когда применять, риски.
| Паттерн | Механизм | Против чего |
|---|---|---|
| Timeout | Верхняя граница ожидания ответа | «Висящие» потоки при медленной зависимости |
| Retry | Повторный вызов с нарастающей задержкой | Переходные (временные) сбои |
| Circuit Breaker | Прекращение вызовов к отказавшей зависимости | Систематические сбои, каскадные отказы |
| Bulkhead | Изоляция пулов ресурсов per-зависимость | Истощение общих ресурсов одной зависимостью |
| Rate Limiter | Ограничение интенсивности вызовов | Перегрузка, штормы запросов |
| Fallback | Заранее подготовленный запасной вариант | Недоступность зависимости |
| Graceful shutdown | Корректное завершение работы | Потери при деплоях и рестартах |
Паттерны реализуются на трёх уровнях: библиотеки в приложении (Polly для .NET, resilience4j для Java; исторически первая — Netflix Hystrix, ныне в режиме сопровождения), инфраструктура service mesh (Envoy, Istio, Linkerd выполняют тайм-ауты, повторы и размыкание в sidecar-прокси, не трогая код приложения — см. интеграционные паттерны) и платформа оркестрации (Kubernetes перезапускает отказавшие контейнеры и выводит неготовые из балансировки). Выбор уровня — компромисс: библиотеки дают доменную точность (разные политики для разных операций), mesh — единообразие и нулевой код.
Circuit Breaker
Суть. Размыкатель цепи — электрическая метафора: автомат в щитке размыкает цепь при перегрузке, предотвращая пожар. Программный размыкатель следит за отказами вызовов к зависимости и при превышении порога перестаёт вызывать её вовсе — вызовы завершаются ошибкой немедленно, без затрат на тайм-ауты. У размыкателя три состояния:
failures ≥ порог пауза истекла
┌─────────┐ ─────────────────────► ┌─────────┐ ───────────► ┌────────────┐
│ closed │ │ open │ │ half-open │
│ цепь за-│ ◄───────────────────── │ вызовы │ ◄─────────── │ пробный │
│ мкнута │ проба успешна │ откло- │ проба │ вызов │
└─────────┘ │ няются │ неуспешна └────────────┘
│мгновен- │
│ но │
└─────────┘
- closed (замкнуто) — рабочее состояние: вызовы идут, размыкатель считает ошибки (долю ошибочных вызовов в скользящем окне или число подряд);
- open (разомкнуто) — после превышения порога: вызовы отклоняются мгновенно, зависимость получает паузу на восстановление;
- half-open (полузамкнуто) — по истечении паузы допускается пробный вызов: успех замыкает цепь, отказ — размыкает снова.
Ключевой эффект — быстрое отклонение вместо медленного ожидания: пока размыкатель открыт, вызывающий сервис не тратит потоки и соединения на гарантированно безуспешные попытки. Это и защищает от каскадных сбоев: отказавшая зависимость перестаёт высасывать ресурсы из всех, кто её вызывает (раздел о каскадах ниже).
Когда применять. К каждой значимой удалённой зависимости с регулярным трафиком: базам данных, внешним API, межсервисным вызовам. Статистика требует потока: для зависимости, вызываемой раз в час, порог «5 ошибок подряд» не сработает никогда — там достаточны тайм-аут и fallback.
Риски. Неточные пороги: слишком чувствительный размыкатель размыкается от случайной серии тайм-аутов (ложное срабатывание), слишком грубый — не спасает. Пороги настраивают по измеренной доле ошибок, а не «на глаз». Размыкатели по цепочке вызовов должны быть согласованы: если разомкнуты все уровни, восстановление тоже пройдёт каскадом. Наконец, открытая цепь не должна быть тихой: каждое срабатывание обязано быть видимым в метриках (иначе размыкатель маскирует хронический сбой).
Bulkhead
Суть. Название — от корабельных переборок (bulkhead): корпус разделён на водонепроницаемые отсеки, и одна пробоина топит отсек, а не корабль. В программных системах эта идея применяется к ресурсам: на каждую зависимость выделяется собственный ограниченный пул — потоков, соединений, слотов семафора, — и отказавшая (или медленная) зависимость исчерпывает только свой отсек, оставляя остальные работоспособными.
общий пул (без bulkhead) изолированные пулы (bulkhead)
┌───────────────────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ потоки/соединения │ │ payments│ │ cache │ │ search │
│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ │ ▓▓▓▓▓ │ │ ░░░░░ │ │ ░░░░░ │
│ всё занято │ │ переполн│ │ свобод-│ │ свобод-│
│ вызовами │ │ ен │ │ но │ │ но │
│ «payments» │ └─────────┘ └─────────┘ └─────────┘
└───────────────────────┘ поиск и кэш продолжают работать
cache и search ждут в очереди
Когда применять. Всегда, когда несколько зависимостей делят общий ограниченный ресурс: пул потоков приложения, пул соединений с СУБД, клиентский пул HTTP-соединений. Особенно важно при разной скорости зависимостей: мгновенный локальный кэш и медленный внешний API не должны делить одну очередь.
Риски. Фрагментация: сумма изолированных пулов ограничивает общий ресурс, и простаивающие слоты одного отсека недоступны другому — изоляция платится недоиспользованием. Размеры пулов подбираются по измеренной пропускной способности (сколько вызовов к зависимости реально нужно в пике), иначе слишком тесный отсек сам становится узким местом в нормальном режиме. В JVM-мире исторический эталон реализации — пулы потоков Hystrix; современный инструмент — bulkhead-модуль resilience4j (семафорный и thread-pool варианты).
Retry
Суть. Повторный вызов при переходном сбое: сетевой глюк, тайм-аут, отказ экземпляра за балансировщиком, ответ 503. Наивный немедленный повтор бесполезен и вреден, поэтому задержка между попытками растёт экспоненциально — экспоненциальный backoff:
delay(n) = min(cap, base × 2ⁿ) n — номер попытки
К экспоненте добавляется джиттер (jitter) — случайный разброс задержки. Без него все клиенты, получив отказ, повторяют синхронно, через равные интервалы — волны попыток бьют по восстанавливающейся системе в такт. Канонический рецепт — full jitter из заметки AWS Architecture Blog Exponential Backoff and Jitter (2015): фактическая задержка выбирается равномерно из диапазона от нуля до вычисленной экспоненты.
async function callWithRetry<T>(fn: () => Promise<T>, maxAttempts = 4): Promise<T> {
const baseMs = 100;
const capMs = 5_000;
for (let attempt = 1; ; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt >= maxAttempts || !isTransient(err)) throw err;
const backoff = Math.min(capMs, baseMs * 2 ** attempt);
const jittered = Math.random() * backoff; // full jitter
await sleep(jittered);
}
}
}
Требование идемпотентности. Повтор безопасен только для идемпотентных операций — тех, где повторная доставка того же запроса не меняет результат. Повтор неидемпотентной операции создаёт дубли: два списания с карты, два заказа, два письма. HTTP-методы GET/PUT/DELETE идемпотентны по семантике, POST — нет; для него идемпотентность обеспечивают явно — ключом идемпотентности (Idempotency-Key), который сервер запоминает и по которому распознаёт повтор. Правило: retry без идемпотентности — это не устойчивость, а генератор дубликатов.
Когда применять. Только к переходным ошибкам: сетевым сбоям, тайм-аутам, 502/503/504, сообщению 429 с заголовком Retry-After. Ошибки 4xx (кроме 429) повторять бессмысленно — запрос неверен и останется неверным. Число попыток ограничивают (обычно 2–3), суммарное время повторов укладывают в бюджет тайм-аута вызывающего.
Риски. Шторм повторов (retry storm): при деградации зависимости каждый отказ умножается на число попыток у каждого клиента — нагрузка растёт именно тогда, когда система менее всего способна её выдержать, и восстановление, отягощённое повторами, затягивается даже после устранения причины. Лекарства — джиттер (рассинхронизация клиентов), размыкатель (прекращение попыток вовсе) и бюджет повторов: повторять не более N% от недавнего объёма успешных вызовов. Второй риск — семантический: повтор по тайм-ауту небезопасен даже для формально идемпотентной операции, если сервер успел частично выполнить её с побочными эффектами.
Timeout
Суть. Тайм-аут — верхняя граница ожидания ответа на вызов. Правило звучит банально, но именно его нарушение — самая частая причина каскадных сбоев: любой сетевой вызов без тайм-аута — потенциально «висящий» навсегда поток с занятым соединением и памятью. Многие клиентские библиотеки по умолчанию тайм-аута не имеют вовсе или имеют неприлично щедрый (десятки секунд и более) — это значение наследуется приложением, которое «не настраивало».
Тайм-ауты бывают гранулярными: на установку соединения (connect), на чтение ответа (read/socket), на вызов в целом (request/end-to-end). Практика — явный общий тайм-аут на каждый вызов, покрывающий весь цикл.
Бюджет тайм-аутов. В цепочке вызовов тайм-ауты складываются и должны укладываться в бюджет вышестоящего вызова: если шлюз ждёт checkout 3 секунды, а checkout ждёт payments 5 секунд, то отказ payments будет «обрезан» шлюзом раньше, чем checkout успеет его обработать, — а поток checkout продолжит ждать впустую. Правило: тайм-аут вызывающего ≥ суммы тайм-аутов вызываемых вниз по цепи, иначе нижние уровни жгут ресурсы после того, как верхний уже сдался.
Когда применять. Всегда и везде, где есть ожидание чужого ответа: HTTP-клиенты, драйверы БД, брокеры сообщений, внешние API. Значение выбирается по измеренному распределению задержки (например, p99 + запас), а не из воздуха: тайм-аут меньше p99 честной задержки рубит валидные медленные операции; сильно больше — держит ресурсы дольше необходимого.
Риски. Слишком агрессивный тайм-аут на медленной, но рабочей зависимости превращается в генератор ошибок и может спровоцировать размыкатель и шторм повторов. Кроме того, истёкший тайм-аут не отменяет работу на стороне сервера — она продолжает выполняться впустую (поэтому тайм-ауты сопровождают отменой запроса — cancellation token в .NET, AbortController в браузерах).
Rate Limiter / Throttling
Суть. Ограничитель интенсивности (rate limiter) разрешает не более N операций в единицу времени, отклоняя или выстраивая в очередь излишек. Базовые алгоритмы:
- token bucket — в корзину с ограниченной ёмкостью равномерно поступают токены; запрос требует токен; ёмкость допускает короткие всплески. Промышленный стандарт;
- leaky bucket — запросы поступают в «ведро» и вытекают с постоянной скоростью, сглаживая поток;
- fixed/sliding window — счётчик запросов за окно времени; скользящее окно устраняет удвоение нагрузки на стыке окон.
Защита сервера. На стороне сервера лимитер — защита от перегрузки: при исчерпании пропускной способности лучше отклонить часть запросов честным 429 с заголовком Retry-After, чем принимать все и отвечать всем тайм-аутами. Это называется load shedding (сброс нагрузки): деградация части запросов вместо деградации всех. Отклонённые простым отказом клиенты и очереди способны повторить позже; система, вошедшая в режим перегрузки (thrashing), не способна ничего.
Клиентские квоты. Защите подлежит не только сервер: клиентская сторона лимитера — это квоты. Вызывающий обязан учитывать лимиты своих зависимостей (квоты внешних API, лицензионные ограничения, договорные обязательства) и самостоятельно ограничивать собственную интенсивность, вместо того чтобы узнавать о лимите по 429 в момент пика. Симметрично, сервис-провайдер публикует квоты по клиентам, чтобы один жадный потребитель не занимал всю пропускную способность.
Когда применять. На границах системы (публичные API, шлюзы) — всегда; между внутренними сервисами — при заметной разнице пропускной способности; на клиентах внешних API — в соответствии с их квотами.
Риски. Распределённый лимитер (единый лимит на кластер) требует общей точки подсчёта (например, Redis) — и сам становится зависимостью со своими требованиями устойчивости; локальные лимитеры (по экземпляру) проще, но допускают суммарное превышение. Снятие ограничения после долгой блокировки порождает всплеск от накопившихся клиентов (thundering herd) — сглаживается джиттером и «прогревом». Наконец, лимитер — часть контракта: изменившийся без предупреждения лимит ломает клиентов так же, как ломающийся API.
Fallback и graceful degradation
Суть. Fallback — заранее подготовленный запасной вариант на случай отказа основной операции: не импровизация в catch-блоке, а спроектированная ветка поведения. Типовые варианты по убыванию качества ответа:
- кэш последнего хорошего значения — «остатки» данных: каталог вчерашней свежести вместо живого прайса;
- значение по умолчанию — нейтральный, но корректный ответ: «рейтинг недоступен» вместо рейтинга, главная страница вместо персональной подборки;
- деградация функциональности (graceful degradation) — система осознанно отключает часть функций, сохраняя главные: магазин без блока рекомендаций, но с работающим оформлением заказа;
- честный отказ — когда деградация невозможна, явная и понятная ошибка лучше висящего спиннера.
Когда применять. К каждой зависимости, чья недоступность не должна останавливать пользовательский сценарий. Критерий — бизнес-классификация зависимостей: «критичные» (оплата, остатки на складе) не прощают заглушек, «обогащающие» (рекомендации, отзывы, картинки-превью) — прощают.
Риски. Устаревший кэш, выдаваемый без пометки за свежие данные, вводит в заблуждение (пользователь видит отменённый товар) — ответ деградации обязан быть помечен. Главная опасность — маскировка сбоев: система, «тихо» раздающая заглушки, выглядит работающей и для пользователя, и для оператора; поэтому каждый fallback обязан считаться метрикой и алертом не хуже, чем ошибка. Наконец, fallback-цепочки (заглушка вызывает зависимость, у которой своя заглушка) требуют сквозного тестирования.
Graceful shutdown
Суть. Плавное завершение — корректное закрытие процесса по сигналу остановки (SIGTERM), в отличие от мгновенного SIGKILL. При получении сигнала процесс: перестаёт принимать новые запросы (закрывает слушающий сокет или регистрируется как неготовый), дорабатывает начатые запросы, завершает взятые из очереди задачи, сбрасывает буферы и метрики, освобождает блокировки и lease — и только затем выходит.
const server = app.listen(8080);
process.on('SIGTERM', async () => {
server.close(); // перестать принимать новые соединения
await jobQueue.drain(); // доработать взятые из очереди задачи
await db.close(); // освободить соединения и блокировки
process.exit(0);
});
Звучит рутинно, но именно graceful shutdown делает возможными беспростойные деплои: rolling update в Kubernetes — это серия «убей старый под, подними новый», где между выведением из балансировки и фактическим завершением процесс должен спокойно дотянуть текущие запросы. Оркестратор даёт на это terminationGracePeriodSeconds (по умолчанию 30 секунд), после чего посылает SIGKILL — и процесс, не уложившийся, теряет всё то, что должен был доработать.
Когда применять. Всегда, где процесс обслуживает запросы или держит фоновые задачи: веб-серверы, консьюмеры очередей, cron-воркеры, фоновые процессы контейнеров.
Риски. Расхождение моментов: балансировщик ещё направляет трафик на уже «завершающийся» экземпляр — лечится задержкой (preStop) и активным выведением из ротации по readiness-пробе. Внутренние таймауты доработки должны укладываться в grace period оркестратора, иначе «плавное» завершение оборачивается жёстким. Наконец, фоновые задачи, переживающие рестарт, требуют персистентности состояния (взятое из очереди сообщение подтверждается после обработки — см. событийную архитектуру о семантике доставок).
Каскадные сбои и как их останавливать
Каскадный сбой (cascading failure) — распространение отказа вверх по цепочке вызовов: отказавшая или затормозившая зависимость выводит из строя вызывающего, тот — своего вызывающего, пока не откажет функция, за которой стоит пользователь. Анатомия каскада:
payments отвечает медленно (деградация БД)
│
▼
checkout: потоки ждут ответа «payments»
тайм-аутов нет → потоки не освобождаются
│ пул потоков / соединений checkout исчерпан
▼
checkout недоступен ДЛЯ ВСЕГО трафика —
включая запросы, не связанные с оплатой
│
▼
шлюз и фронтенд: таймауты на весь раздел «заказы»
│
▼
пользователь видит неработающий магазин
Обратите внимание на звено 2: медленная зависимость опаснее упавшей. Упавшая отвечает ошибкой мгновенно (fail fast) — вызывающий свободен обработать её и идти дальше. Медленная отвечает… когда-нибудь: каждый вызов удерживает поток, соединение и память в ожидании, и при достаточном трафике пул исчерпывается за секунды. Отказ одного компонента превращается в отказ другого не «через ошибку», а через истощение ресурсов — тем же механизмом, каким работает DoS-атака.
Каскад останавливается комбинацией паттернов, каждый из которых режет своё звено цепи:
- тайм-аут освобождает ожидающий поток через bounded время — ожидание не бесконечно;
- bulkhead ограждает общий пул — исчерпывается только отсек данной зависимости, соседние вызовы не затронуты;
- circuit breaker прекращает попытки вовсе — потоки вообще не тратятся на проигрышные вызовы;
- fallback возвращает управляемый ответ — пользователь не видит ошибки, а система не распространяет отказ;
- rate limiter на входе системы срезает волны повторов и паники — усилители каскада отключены.
Пример из e-commerce: отказал платёжный шлюз. Без устойчивости через минуту недоступен весь магазин — корзина и каталог падают вместе с оплатой, потому что пулы истощены ждущими потоками. С устойчивостью: размыкатель размыкает вызовы шлюза, checkout мгновенно получает ошибку, оформляет заказ со статусом «оплата отложена» (сага с отложенным списанием — см. микросервисы), пользователь видит «заказ принят, оплата будет списана в течение часа» — оплата недоступна, но корзина жива и заказы принимаются. Функция потеряла одно звено, продукт — ничего.
Отдельный усилитель каскада — шторм повторов (раздел о Retry): после сбоя зависимость восстанавливается, но на неё обрушивается отложенный поток всех повторов сразу, и восстановление откладывается. Именно поэтому повторы без джиттера и бюджета повторов считаются анти-паттерном устойчивости: попытка лечить симптом ухудшает болезнь.
Избыточность и failover
Если паттерны устойчивости работают с отказами зависимостей в рантайме, то избыточность устраняет сами единые точки отказа (single point of failure, SPOF) — компоненты, чей отказ останавливает систему целиком.
Избыточность (redundancy) — дублирование компонентов: несколько экземпляров сервиса, реплики базы данных, пара балансировщиков, дублированные сетевые пути. Классификация по режиму работы резерва:
- активная (active/active) — все копии обслуживают трафик; отказ одной лишь уменьшает пропускную способность;
- пассивная (active/passive, standby) — резерв ждёт и включается при отказе; варианты: hot standby (включён и синхронизирован, переключение за секунды), warm (включён, данные подкачиваются), cold (выключен, восстановление с носителей);
- N+1 — промышленная норма запаса: столько узлов, сколько нужно для пиковой нагрузки, плюс один на отказ; для отказоустойчивости при обслуживании одновременно с отказом — N+2.
Failover — автоматическое переключение на резерв при отказе основного. Механизм включает три фазы: обнаружение (health check не проходит), решение (кворум или монитор решает переключить), переключение (трафик направляется резерву, реплика повышается до ведущей). RTO системы прямо определяется скоростью этих фаз: детекция по секундам, переключение — от секунд (балансировщик перестал слать на мёртвый инстанс) до минут (promote реплики БД) и часов (восстановление из бэкапа).
Кластеризация. Отказавший узел обнаруживают сами узлы: кластер голосованием большинства (кворум) отличает отказ узла от разделения сети и предотвращает split-brain — два «ведущих», разрушающих данные. Отсюда нечётное число узлов и правило кворума: изменения подтверждаются большинством.
Географическое резервирование. Уровни удаления резерва от отказа:
- multi-AZ — экземпляры в разных зонах доступности одного региона; переживает пожар в дата-центре; стандартная первая ступень — заметное удорожание невелико;
- multi-region — копии в разных регионах; переживает недоступность региона целиком; цена — межрегиональная репликация (задержки и трафик) и сложность маршрутизации; уровень для «четырёх девяток» и выше;
- стратегии DR — лестница компромиссов между ценой и RTO/RPO:
| Стратегия | Суть | RTO | RPO | Цена |
|---|---|---|---|---|
| Backup & restore | Восстановление из копий по факту аварии | часы | до окна бэкапа | минимальная |
| Pilot light | Данные реплицируются, минимальный стек выключен | десятки минут | минуты | низкая |
| Warm standby | Уменьшенная копия системы включена | минуты | секунды | средняя |
| Multi-site active/active | Полная система в нескольких регионах | секунды | ≈ 0 | высокая |
Балансировщики и health checks. Балансировщик — точка, в которой failover происходит незаметно для клиента: он распределяет трафик между инстансами и отводит трафик от умерших, судя по проверкам здоровья. Kubernetes различает два рода проб: liveness («жив ли процесс?» — нет: перезапустить контейнер) и readiness («готов ли принимать трафик?» — нет: вывести из балансировки, не перезапуская). Различие существенно: неготовый из-за прогрева экземпляр не надо «лечить» рестартом, а зависший — именно надо.
Sticky sessions. Привязка сессии пользователя к конкретному инстансу (session affinity) — компромисс, конфликтующий с failover: привязанный к умершему инстансу пользователь теряет сессию. Решения — репликация состояния сессии между инстансами либо вынос состояния из процесса (stateless-инстансы + хранилище сессий или токены), что одновременно упрощает горизонтальное масштабирование.
Связь с масштабируемостью двусторонняя: репликация одновременно служит и росту нагрузки, и отказоустойчивости — но это разные цели и разные требования. Один мощный сервер масштабируется вертикально и является SPOF; десять мелких экземпляров переживают отказ одного, но требуют балансировки и работы с состоянием. Масштабирование не равно доступности, доступность не равна масштабируемости — зрелая архитектура проектирует их совместно, но не путает.
Связь с CAP и eventual consistency
Высокая доступность — не только инженерная практика, но и ось фундаментального компромисса распределённых систем. CAP-теорема связывает согласованность (Consistency), доступность (Availability) и устойчивость к разделению сети (Partition tolerance); поскольку разделения в распределённой сети неизбежны, реальный выбор происходит при партиции: CP — отказываться отвечать, сохраняя согласованность, или AP — продолжать отвечать, рискуя устаревшими данными.
HA-система по определению стремится к AP-краю: доступность в ущерб строгой согласованности. Цена этого выбора — eventual consistency (итоговая согласованность): реплики, недоступные для синхронизации в момент записи, сходятся к общему состоянию позже, а окно расхождения переживается пользователем как «комментарий появился у друга не сразу». Подход BASE фиксирует этот компромисс как осознанную альтернативу строгим ACID-гарантиям. Расширение PACELC добавляет очевидный из практики факт: даже без партиций выбор стоит между задержкой (Latency) и согласованностью — синхронная репликация на все копии даёт согласованность ценой ожидания самой медленной.
Практический вывод: компромисс CAP выбирают не для системы целиком, а для каждой доменной операции отдельно. Списание средств — CP: лучше отказ, чем двойное списание или списание мимо инвентаря. Корзина и каталог — AP: устаревшая цена переживётся, потерянная корзина — нет. Именно поэтому статья о наблюдаемости и бюджеты ошибок опираются на SLI по операциям, а не «доступность системы» одним числом.
Плюсы и минусы
Плюсы
Инциденты компонентов не становятся инцидентами продукта. Отказ узла, зоны доступности или стороннего API переживается внутри системы: балансировщик отводит трафик, размыкатель гасит каскад, fallback прячет ошибку от пользователя. Пользователь либо не замечает отказа, либо видит деградацию, а не простой.
Измеримое соответствие SLO. Доступность становится управляемой величиной: RTO/RPO и «девятки» переводят разговор с бизнесом на язык цифр, бюджеты ошибок позволяют осознанно тратить надёжность на скорость поставки. Отказ перестаёт быть аварией «по воле случая» и становится запланированным событием с известной ценой.
Меньше ночных звонков. Устойчивость — это деградация вместо простоя и автофейловер вместо ручного подъёма: часть инцидентов проходит без дежурного вообще, часть — утром за кофе, а не в три часа ночи с миганием телефона. Для команд это аргумент не комфорта, а удержания кадров и скорости реакции.
Беспростойные поставки. Graceful shutdown, health checks и избыточные экземпляры делают деплой рутиной без остановки сервиса: rolling update заменяет экземпляры по одному, канареечные релизы ограничивают риск долей трафика.
Минусы
Операционная сложность. Каждый паттерн — это конфигурация: пороги размыкателя, размеры отсеков, бюджеты повторов, лимиты и grace-периоды. Неверные значения хуже отсутствия паттерна: слишком чувствительный размыкатель устраивает простои там, где их не было; слишком тесный bulkhead — узкое место в нормальном режиме. Конфигурация требует измерений, сопровождения и дисциплины владения: пороги, верные для одного трафика, неверны для другого, и их приходится пересматривать по мере роста системы.
Рост задержек. Устойчивость платит латентностью: повторы добавляют секунды к худшим случаям, асинхронная репликация на расстояние — миллисекунды к каждому чтению с отстающей реплики, георезервирование — межрегиональные задержки к записи. Система «три девятки» почти неизбежно медленнее той же системы без резервирования.
«Девятки» стоят экспоненциально дорого. От «двух девяток» к «трём» — это избыточные инстансы и балансировка; к «четырём» — multi-AZ, автоматический failover и команда, способная его обслуживать; к «пяти» — multi-region актив-актив, строгие процессы и тестирование отказов. Каждая следующая девятка — примерно на порядок дороже предыдущей, поэтому выбор уровня доступности — экономическое решение: для внутреннего инструмента 99% честнее и дешевле, чем 99,99%, купленные «на вырост».
Тестирование отказов — само по себе инженерия. Устойчивость без проверки — гипотеза. Отказы надо впрыскивать намеренно: chaos engineering — практика контролируемых экспериментов над живой системой (Netflix Chaos Monkey, принципы — Principles of Chaos): убить инстанс, разорвать связь между зонами, добавить задержку. Эксперименты требуют наблюдаемости, гипотез и ограничителей взрыва (blast radius) — иначе проверка отказоустойчивости сама станет инцидентом.
Скрытые сбои. Fallback маскирует деградацию в обе стороны: пользователь не видит проблемы, оператор — тем более, если fallback не метится телеметрией. Система может месяцами работать «в режиме заглушек» — формально доступная, фактически без половины функций. Контрмера — метрики и алерты на каждый fallback, разомкнутую цепь и сработавший лимитер.
Связанные материалы
- Микросервисы — design for failure как принцип распределённой архитектуры; базовый набор паттернов устойчивости и их место среди сквозных задач.
- Наблюдаемость (Observability) — SLI/SLO, бюджеты ошибок и обнаружение отказов: устойчивость без телеметрии слепа.
- Масштабируемость (Scalability) — соседняя дисциплина: репликация и горизонтальное масштабирование, общие механизмы, разные цели.
- CAP-теорема — доступность как ось фундаментального компромисса; PACELC.
- BASE — eventual consistency как цена высокой доступности.
- Интеграционные паттерны (API Gateway, BFF, Service Mesh, Sidecar) — где реализуются тайм-ауты, повторы и размыкатели на уровне инфраструктуры.
- Событийно-ориентированная архитектура (EDA) — буферизация и сглаживание пиков брокерами; семантика доставок и идемпотентные обработчики.
- Эволюционная архитектура — надёжность как фитнес-функция: требования доступности, защищаемые автоматически при каждом изменении.
- Монолит — контраст: «всё или ничего» одного процесса против частичных отказов распределённой системы.