Как ломаются распределённые системы: каскады и насыщение
02:47. Медленный запрос в основной базе — забыли индекс в миграции, выкатили вечером. К 02:49 задержка API уползла с 40 мс до двух секунд. В 02:54 дежурный откатил миграцию, индекс вернулся, база отвечает за 3 мс. Графики базы — идеальные.
Сервис лежит.
03:10 — всё ещё лежит. 03:25 — добавили в парк ещё десять машин, стало хуже. 03:31 — рестартанули всё разом, стало ещё хуже. В 03:38 кто-то предложил включить лимит на входе и принимать 30 % запросов. В 03:47 сервис поднялся. Причина существовала семь минут, инцидент длился сорок четыре.
Это не редкая экзотика и не следствие чьей-то ошибки в ту ночь. Это отдельный класс отказа, у которого есть математика, воспроизводимые признаки и вполне конкретный набор рычагов. Главная его особенность ломает главную инженерную интуицию: устранение причины не восстанавливает работу. Ищите причину сколько угодно — вы её уже устранили.
Эта глава — каталог таких механик. Не «какие бывают поломки железа» (это модели отказов), а какие бывают режимы поведения системы, в которых поломка одного элемента превращается в отказ целого. Противоядия — таймауты, размыкатели, ограничение параллелизма — разбираются в главе «Деградация вместо отказа»; здесь мы разбираем болезнь, чтобы лекарства не выглядели набором ритуалов.
Три вида отказа, которые нельзя путать
| Вид | Что происходит | Помогает ли избыточность | Восстановление |
|---|---|---|---|
| Независимый | одна машина умерла, остальные не заметили | да, напрямую | само, за время замены |
| Коррелированный | общая причина ударила по всем сразу: конфиг, релиз, сертификат, DNS | нет | после устранения причины |
| Метастабильный | система застряла в плохом устойчивом состоянии, которое держит само себя | нет, часто вредит | только внешним вмешательством |
Первый вид — то, ради чего придумали резервирование, и единственный, для которого работает арифметика «два независимых узла по 99,9 % дают 99,9999 %». Второй — почему эта арифметика почти никогда не сбывается на практике; корреляция и общие причины подробно посчитаны в главе про SLI и SLO и про бюджет ошибок.
Третий вид — предмет этой главы. Его невозможно вывести из свойств компонентов: каждый компонент исправен, конфигурация верна, код тот же, что вчера. Отказ живёт в контуре обратной связи между компонентами. Ровно то же самое системное мышление называет усиливающей петлёй («Петли обратной связи»): выход контура возвращается на его вход с тем же знаком. Каскадный отказ и усиливающая петля — не аналогия и не сходство, а одно явление, описанное на двух языках. Отсюда практическая польза: теория петель заранее говорит, что лечится такое размыканием связи, ограничением коэффициента усиления или добавлением отрицательной обратной связи — и ничем больше.
Насыщение: как отказ рождается из арифметики
Начнём с самого механического. Насыщение — это когда ресурса с фиксированным числом слотов перестаёт хватать, и все дальнейшие эффекты выводятся из закона Литтла.
$$L = \lambda \cdot W$$
Здесь $L$ — среднее число одновременно занятых слотов, $\lambda$ — частота поступления запросов, $W$ — время, которое запрос проводит внутри. Закон не зависит от распределений: он верен для любой стационарной системы. И именно поэтому он опасен — из него нельзя вывернуться настройкой.
Сервис: пул из 120 обработчиков, 300 запросов в секунду, нормальная задержка 30 мс.
L = 300 × 0,030 = 9 занятых слотов из 120 — запас семикратный
Зависимость замедлилась до 800 мс:
L = 300 × 0,800 = 240 слотов — вдвое больше, чем есть
Пул кончится примерно за 120 / 300 ≈ 0,4 секунды. Обратите внимание, что произошло: нагрузка не менялась. Не было ни всплеска трафика, ни отказа железа. Изменилось только $W$ — и пул исчез за долю секунды. Это первое, что стоит запомнить: у пула нет запаса «в разах по трафику», у него есть запас по произведению.
Обратная форма закона задаёт максимально допустимый таймаут: если пул рассчитан на $L_{max}$ слотов при нагрузке $\lambda$, то любое ожидание дольше $L_{max} / \lambda$ — арифметическое обещание исчерпать пул. Для нашего примера — 120 / 300 = 0,4 с. Таймаут в 3 секунды на этом сервисе означает: «при первом же замедлении зависимости я зависну целиком». Как из этого выводят таймауты и бюджеты дедлайнов — в главе про деградацию.
Почему у порога всё взрывается нелинейно
Второй кирпич — теория массового обслуживания. Для простейшей модели с одним обслуживающим прибором время в системе растёт как
$$W = \frac{W_s}{1 - \rho}, \qquad \rho = \frac{\lambda}{\mu}$$
где $W_s$ — чистое время обслуживания, $\rho$ — утилизация, $\mu$ — ёмкость. Возьмём сервис с $W_s = 1$ мс, то есть $\mu = 1000$ запросов в секунду:
| Нагрузка λ | Утилизация ρ | Время в системе W | Во сколько раз хуже | Средняя очередь |
|---|---|---|---|---|
| 500 | 0,50 | 2 мс | ×2 | 0,5 |
| 800 | 0,80 | 5 мс | ×5 | 3,2 |
| 900 | 0,90 | 10 мс | ×10 | 8,1 |
| 950 | 0,95 | 20 мс | ×20 | 18,1 |
| 990 | 0,99 | 100 мс | ×100 | 98,0 |
| 999 | 0,999 | 1000 мс | ×1000 | 998,0 |
Между 50 % и 80 % утилизации задержка выросла в 2,5 раза — терпимо. Между 95 % и 99 % — в пять раз, и это последние 4 % нагрузки. Практический вывод, за который платят деньгами: работать выше 70–80 % утилизации по любому узкому ресурсу — значит держать систему на участке, где малое изменение входа даёт большое изменение выхода. Стоимость этого запаса — прямая: 30 % железа, за которое вы платите и которое почти всегда простаивает. Это не расточительство, это цена предсказуемости; разговор с финансами так и надо вести (ёмкость и запас, стоимость облака).
Честная оговорка про модель: формула выше предполагает пуассоновский поток и экспоненциальное обслуживание. Реальный трафик более пачечный, а времена обслуживания имеют тяжёлый хвост, поэтому настоящая задержка при той же утилизации хуже табличной, а не лучше. Модель полезна как форма кривой и как порядок величины, а не как прогноз в миллисекундах. Про то, как измерять хвосты честно, — «Измерения».
Очередь без предела превращает отказ в зависание
Неограниченная очередь выглядит как забота о клиенте: не отказываем, копим. На деле она конвертирует быстрый честный отказ в медленный бесполезный.
Пусть потребитель разбирает 5000 сообщений в секунду, приход — 3000/с, за время инцидента накопилось 4 млн сообщений. Время слива:
T = B / (μ − λ) = 4 000 000 / (5000 − 3000) = 2000 с ≈ 33 минуты
Тридцать три минуты после того, как всё уже починено. И если $\mu \le \lambda$, очередь не рассосётся никогда — тут нет вопроса терпения, есть вопрос арифметики.
Есть эффект злее. Клиенты ждут ответ 10 секунд, в очереди — 30 секунд работы. Значит, любое сообщение, которое потребитель достаёт из головы очереди, принадлежит клиенту, который ушёл двадцать секунд назад. Система работает на полную мощность и производит ноль полезного результата.
| Дисциплина | Что достаём | Полезная доля при 30 с отставания и таймауте 10 с |
|---|---|---|
| FIFO | самое старое | ≈ 0 % — все дедлайны истекли |
| LIFO | самое свежее | ≈ 100 % от того, что успеваем |
| FIFO + отброс по дедлайну | свежее после чистки | ≈ 100 %, плюс очередь худеет сама |
Переключение очереди в LIFO при перегрузке — контринтуитивный, но проверенный приём: он несправедлив к старым запросам, зато сохраняет ненулевую полезную пропускную способность. Facebook описывает именно этот приём в статье «Fail at Scale» (ACM Queue, 2015). Практичная связка: каждый запрос несёт дедлайн, потребитель выбрасывает всё просроченное до начала работы, а очередь имеет жёсткий предел, при превышении которого продюсер получает отказ немедленно. Про передачу дедлайнов по цепочке — «Деградация вместо отказа», про очереди и доставку — «Обмен сообщениями».
Усиление работы: главный двигатель каскада
Введём одну величину, вокруг которой держится вся глава. Усиление работы $a$ — сколько единиц работы система тратит на один полезный пользовательский запрос. В норме $a \approx 1$. Условие устойчивости тривиально:
$$\lambda \cdot a < \mu$$
Каскад — это ситуация, когда $a$ растёт как функция перегрузки. Тогда неравенство ломается само по себе и продолжает ломаться после исчезновения причины.
Что поднимает $a$:
- Повторы. Клиент не дождался — послал ещё раз. Первый запрос при этом продолжает выполняться и занимать слот.
- Промахи кэша. Рестарт обнулил кэш: попадание было 99 %, стало 0 %, нагрузка на базу выросла в сто раз.
- Фолбэки. Основной путь упал, включился запасной, который делает ту же работу другим способом — суммарно вдвое больше.
- Сборка мусора. Больше живых объектов в памяти из-за роста очередей → длиннее паузы → больше очереди.
- Логи. Каждая ошибка пишет стектрейс в 30 раз длиннее обычной строки («Мониторинг»).
- Health-проверки и переизбрания. Медленный узел выпадает из кворума, начинается переизбрание, которое само стоит работы («Консенсус»).
Арифметика повторов: почему три попытки — это не в три раза больше
Повторы стоят особняком, потому что их усиление перемножается по глубине цепочки. Возьмём типичный путь запроса: клиент → шлюз → сервис заказов → сервис платежей → база. На каждом уровне разумный инженер поставил «три попытки» (одна плюс две повторные).
3 попытки participant O as Заказы
3 попытки participant P as Платежи
3 попытки participant D as База C->>G: 1 запрос G->>O: попытка 1 O->>P: попытка 1 P->>D: попытка 1 (таймаут) P->>D: попытка 2 (таймаут) P->>D: попытка 3 (таймаут) P--xO: ошибка после 3 обращений Note over O,D: заказы повторяют — ещё 3 обращения к базе O->>P: попытка 2 O->>P: попытка 3 Note over G,D: шлюз повторяет весь блок ещё дважды G->>O: попытка 2 G->>O: попытка 3 Note over C,D: один пользовательский запрос → 27 обращений к базе
| Уровень | Множитель | Обращений к базе на 1 запрос пользователя |
|---|---|---|
| без повторов | 1 | 1 |
| платежи | ×3 | 3 |
| + заказы | ×3 | 9 |
| + шлюз | ×3 | 27 |
| + повтор в мобильном клиенте | ×3 | 81 |
При базовой нагрузке 200 запросов в секунду к базе прилетает 200 × 27 = 5400, а с клиентским повтором — 16 200 запросов в секунду. Ёмкость базы — 2000. Она не «немного перегружена», она перегружена в восемь раз, и никакой откат релиза этого не изменит: усиление живёт в клиентах, а не в базе.
Отсюда два правила, которые надо принять как инженерные ограничения, а не как советы:
- Повторяет только один уровень. Обычно — самый близкий к пользователю или самый близкий к сбою, но не оба и не все. Остальные пробрасывают ошибку наверх без попыток.
- Повторы живут по бюджету, а не по счётчику. Не «три попытки на запрос», а «повторы не более 10 % от общего числа запросов за скользящее окно». Тогда при массовом сбое множитель ограничен
1,1вместо3, потому что бюджет мгновенно исчерпывается. Такой адаптивный лимит реализован, например, в gRPC (retry throttling) и описан в AWS Builders’ Library.
Главный тезис, который стоит повторить вслух: повтор помогает только тогда, когда отказ независим от нагрузки. Отвалился один пакет, дёрнулась одна машина — да, повтор ровно для этого. Но если сервис отвечает ошибкой потому что перегружен, повтор добавляет ему нагрузки. В этом режиме повтор — не средство надёжности, а средство самоубийства, и различить два режима код обязан по коду ответа: на 503 с заголовком Retry-After или на явный сигнал перегрузки повторять нельзя.
Метастабильность: почему система не встаёт
Теперь соберём всё вместе. Формализация принадлежит Bronson и соавторам, «Metastable Failures in Distributed Systems» (HotOS 2021), и продолжена в «Metastable Failures in the Wild» (OSDI 2022), где разобраны публичные аварии крупных провайдеров.
Модель такая. У системы при одной и той же нагрузке есть два устойчивых состояния:
- устойчивое хорошее — $a = 1$, очередей нет, всё быстро;
- метастабильное плохое — $a > 1$, очереди полны, задержка на потолке таймаута, полезная пропускная способность в разы ниже ёмкости.
Переход из первого во второе делает триггер — кратковременное событие: всплеск, GC-пауза, замедление зависимости, потеря узла. Удержание во втором делает поддерживающий эффект (sustaining effect) — та самая петля усиления. Ключевое: триггер и поддерживающий эффект — разные вещи. Постмортем, который нашёл только триггер, не объяснил инцидент.
или ёмкость упала Уязвимое --> Устойчивое: нагрузка спала Уязвимое --> Мета: ТРИГГЕР
GC-пауза, медленная БД,
потеря узла Мета --> Мета: поддерживающий эффект:
таймауты → повторы → очередь
причина уже устранена Мета --> Уязвимое: сброс нагрузки
ниже μ/a = 333 Мета --> Мета: добавили машин
или рестартовали всё
Разберём числа, потому что в них весь смысл главы.
- Ёмкость $\mu = 1000$ единиц работы в секунду. Нормальная нагрузка $\lambda = 700$ запросов/с, $a = 1$. Утилизация 70 % — вполне здоровый сервис.
- Триггер: база тормозит 30 секунд. Задержка превышает клиентский таймаут в 2 секунды. Клиенты начинают повторять, $a \to 3$.
- Предъявленная работа:
700 × 3 = 2100 > 1000. Система в перманентной перегрузке. - Полезная отдача: сервер честно делает 1000 единиц работы в секунду, но на каждый успешный ответ уходит три — значит, пользователи получают около
1000 / 3 ≈ 333ответов в секунду из 700. 52 % ошибок при полностью исправной инфраструктуре. - База починилась. $\lambda$ вернулась к 700. Но $a$ осталась 3, потому что задержка всё ещё выше таймаута, потому что очередь всё ещё полна, потому что нагрузка всё ещё 2100. Петля замкнулась сама на себя.
Условие возврата: нужно, чтобы $\lambda \cdot a < \mu$ при текущем $a = 3$, то есть
λ_допустимая < μ / a = 1000 / 3 ≈ 333 запроса в секунду
Не 700, при которых всё сломалось, а 333. Это и есть гистерезис: порог входа в отказ и порог выхода из него — разные, и второй сильно ниже первого. Пропустить надо меньше половины нормального трафика, чтобы система вылезла.
Теперь посчитаем альтернативу, которую хочется применить в три ночи: добавить мощности. Нужно $\mu > 2100$, то есть парк втрое больше текущего. Автомасштабирование добавляет узлы за 3–5 минут, новый узел приходит с пустым кэшем и первые полторы минуты сам работает с $a > 1$, догружая общие зависимости. В типичном случае вы не успеваете и делаете хуже. Тот же счёт объясняет и рестарт: одновременный перезапуск обнуляет все кэши, поднимает $a$ ещё выше и превращает метастабильное состояние в полный отказ.
Отсюда единственный порядок действий, который работает: сначала снять нагрузку, потом всё остальное. Не потому, что «так в книжке», а потому что это единственный рычаг, который двигает левую часть неравенства быстрее, чем растёт правая.
Каталог: девять механик, которые стоит узнавать в лицо
1. Стадо у истёкшего TTL
Кэш с попаданием 99 % прикрывает базу: из 10 000 запросов/с до неё доходит 100. Деплой прогрел кэш разом, все ключи получили TTL 300 секунд одновременно — через пять минут они истекают тоже одновременно, и база получает 10 000 запросов/с при ёмкости 2000. Лечение арифметическое: джиттер TTL ±20 % размазывает истечение по 120-секундному окну, схлопывание одинаковых запросов (single-flight) превращает 5000 одновременных промахов по одному ключу в один запрос, а отдача устаревшего значения на время обновления убирает провал вовсе.
2. Холодный старт, из которого нет выхода
Тот же кэш, но после аварии: сервис перезапущен, кэш пуст, вся нагрузка идёт в базу, база не выдерживает, сервис не может ответить, кэш не наполняется. Классическая метастабильность: система не поднимается потому что она поднимается. Единственный выход — впустить 5 % трафика, дать кэшу наполниться, увеличивать долю ступенями. Отсюда требование к архитектуре: возможность управляемого частичного впуска должна существовать до аварии.
3. Чёрная дыра балансировщика
Одна из десяти машин сломалась так, что отвечает 500 за 1 мс вместо честных 50 мс работы. Балансировщик по числу активных соединений видит машину с нулевой занятостью и отдаёт ей всё больше трафика: она «самая свободная». В пределе одна сломанная машина из десяти собирает не 10 % запросов, а 80–90 %.
Здоровая машина: 50 мс на запрос → при 100 rps держит 5 соединений
Сломанная: 1 мс на запрос → при 100 rps держит 0,1 соединения
Балансировка по наименьшему числу соединений выравнивает занятость,
а не пользу: сломанной достаётся в ~50 раз больше запросов.
Отказ одной машины из десяти даёт не 10 % ошибок, а почти полный отказ сервиса. Лечение: балансировка должна учитывать долю успешных ответов, а не только занятость; быстрые 5xx должны выводить бэкенд из ротации (passive health check, outlier detection). Подробности механики — «Прокси и балансировка».
4. Серый отказ
Термин из работы Huang и соавторов «Gray Failure: The Achilles’ Heel of Cloud-Scale Systems» (HotOS 2017). Суть — различающаяся наблюдаемость: система считает себя здоровой, клиент видит отказ.
| Что видит система | Что видит клиент |
|---|---|
/health отвечает 200 за 1 мс |
запросы к API падают по таймауту |
диск смонтирован, df в порядке |
запись занимает 8 секунд из-за деградации тома |
| TCP-соединение устанавливается | 30 % пакетов теряются, TLS-хендшейк не завершается |
| узел в кворуме | реплика отстала на 40 минут |
Серые отказы страшны тем, что автоматика их не ловит: она смотрит на те же индикаторы, что и «здоровая» половина. Единственное лекарство — измерять с точки зрения клиента: SLI на реальном пути запроса, синтетические пробы из нескольких точек, метрики со стороны вызывающего сервиса, а не вызываемого. Стоит это дорого — растёт кардинальность метрик и объём наблюдаемости («Мониторинг», «Наблюдаемость в распределённых системах»). Это тот случай, где инженерное решение упирается в бюджет: «мерить всё отовсюду» никто не потянет, и придётся выбирать два-три пути, которые действительно важны.
5. Медленный узел хуже мёртвого
Смежная механика. Мёртвый узел детектируется за секунды и выводится из ротации. Узел, отвечающий в двадцать раз медленнее, продолжает получать свою долю запросов и удерживает слоты у всех вызывающих. По закону Литтла один такой узел из десяти способен занять больше слотов на клиентах, чем девять здоровых вместе. Отсюда практика: клиент должен уметь считать медленный ответ отказом (deadline вместо бесконечного ожидания), а балансировщик — снижать вес узлам с плохой задержкой, а не только мёртвым.
6. Проба живости, которая добивает
Kubernetes: livenessProbe с таймаутом 1 секунда. Сервис перегружен, обработчики заняты, проба не успевает — kubelet убивает под. Оставшиеся поды получают его нагрузку, их пробы тоже не успевают, они тоже перезапускаются. Автоматика уничтожает мощность ровно тогда, когда мощности не хватает.
Правила, которые снимают этот класс целиком:
livenessProbeпроверяет только сам процесс и никогда — внешние зависимости. Если база недоступна, перезапуск приложения не поможет ничему.readinessProbeможет проверять зависимости, но её срабатывание не убивает под, а лишь снимает трафик.- Таймауты проб — с запасом относительно худшей нормальной задержки, а не относительно средней.
- Пробы обслуживаются отдельным пулом, а не тем же, что и рабочие запросы.
7. Синхронизация клиентов
Тысяча клиентов повторяют ровно через 1 с, 2 с, 4 с. Они начали одновременно — значит, и повторяют одновременно. Вместо равномерной нагрузки сервис получает пики в тысячу запросов в одну миллисекунду. То же — с кроном в 0 * * * *, когда сто задач стартуют в круглый час.
Лечение стоит одну строчку кода: случайный джиттер. Каноническое сравнение стратегий — «Exponential Backoff And Jitter» в блоге AWS: полный джиттер (sleep = random(0, min(cap, base·2^n))) даёт заметно меньше конкуренции и общего времени завершения, чем «правильный» экспоненциальный откат без случайности.
8. Перенос нагрузки, который добивает соседей
Три машины по 70 % утилизации. Одна умерла. Её трафик разошёлся на двух оставшихся:
Было: каждая берёт 1/3 нагрузки при 70 % утилизации
Стало: каждая берёт 1/2 нагрузки → 70 % × (3/2) = 105 %
Обе выходят за 100 % и падают следом. Отказ одной машины из трёх превратился в отказ всех трёх. Условие выживания при потере одного узла из $N$:
$$\rho \le \frac{N-1}{N}$$
| N узлов | Предельная утилизация | Переплата за запас |
|---|---|---|
| 2 | 50 % | ×2,0 |
| 3 | 67 % | ×1,5 |
| 5 | 80 % | ×1,25 |
| 10 | 90 % | ×1,11 |
| 20 | 95 % | ×1,05 |
Вот тут надо честно сказать про деньги. Схема «N+1» на трёх узлах означает, что вы платите за 50 % лишней мощности постоянно, чтобы пережить событие, которое случается несколько раз в год. На двадцати узлах та же защита стоит 5 %. Мелкие установки платят за отказоустойчивость непропорционально больше — и это, а не техническая сложность, обычно и есть настоящая причина, почему в маленькой команде нет резервирования. Ответ «купите ещё узлов» здесь бесполезен; полезнее либо взять более мелкие инстансы (больше N при той же сумме), либо честно принять SLO пониже. Как это считать вместе с продуктом — «Ёмкость и нагрузка» и «Бюджет ошибок».
9. Отравленное сообщение
Одно сообщение в очереди роняет потребителя. Потребитель перезапускается, читает то же сообщение, снова падает. Очередь стоит, лаг растёт, а метрика «потребитель жив» показывает бодрое мигание рестартов. Лечение стандартное: счётчик попыток на сообщение, отправка в очередь недоставленных (DLQ) после N неудач, алерт на непустую DLQ («Обмен сообщениями»).
Как это выглядит на графиках
Каскад узнаётся по сигнатуре, и узнавать её надо за минуты, а не за полчаса.
| Признак | Что видно | Что означает |
|---|---|---|
| Пропускная способность упёрлась в потолок, а входящий поток растёт | две линии разошлись | вы за точкой насыщения |
| Полезная отдача падает при росте нагрузки | кривая пошла вниз | усиление работы $a > 1$ |
| Задержка стоит ровно на значении таймаута | плоская линия на 2000 мс | клиенты уходят по таймауту, ответы никому не нужны |
| Доля повторов в трафике выросла с 2 % до 60 % | отдельная метрика | вот он, поддерживающий эффект |
| Глубина очереди растёт линейно | прямая вверх | $\mu < \lambda$, само не рассосётся |
| Утилизация 100 %, а CPU не занят | расхождение | узкое место — не CPU, а пул, блокировка или дескрипторы |
Единственная метрика, которую в большинстве команд не собирают и о которой стоит позаботиться заранее, — доля повторов. Технически это метка attempt на исходящих запросах: attempt=1 против attempt>1. Без неё каскад выглядит как «внезапно вырос трафик», и команда идёт разбираться с трафиком вместо усиления. С ней диагноз ставится за минуту.
Второе полезное различение — пропускная способность против полезной отдачи (throughput vs goodput). Первая считает всё обслуженное, вторая — только то, что дошло до живого клиента вовремя. В нормальном режиме они совпадают, в каскаде расходятся в разы, и именно расхождение — самый ранний надёжный сигнал. Как строить такие SLI — «SLI и SLO».
Порядок действий, когда это уже происходит
причина неочевидна"] --> B{"Полезная отдача падает
при росте нагрузки?"} B -->|нет| C["Это не каскад.
Ищите поломку компонента"] B -->|да| D["Каскад. Причину ищем ПОТОМ"] D --> E["1. Снять нагрузку:
лимит на входе,
принимаем 20-30 %"] E --> F["2. Отключить повторы:
флаг конфигурации,
увеличить backoff"] F --> G["3. Отбросить просроченное:
чистка очереди по дедлайну,
переключение в LIFO"] G --> H["4. Выключить второстепенное:
рекомендации, аналитика,
дорогие отчёты"] H --> I{"Задержка ушла
ниже таймаута?"} I -->|нет| J["Резать глубже:
10 %, 5 %, 2 %"] J --> I I -->|да| K["5. Возвращать трафик
ступенями: 30 → 50 → 70 → 100,
пауза 2-3 мин на шаг"] K --> L{"Задержка держится
на каждой ступени?"} L -->|нет| M["Откатиться на ступень назад,
ждать дольше"] M --> K L -->|да| N["Восстановлено.
Теперь ищем триггер"] D -.->|"НЕ делать"| X1["Добавлять машины:
нужно ×3, приедет поздно,
кэши холодные"] D -.->|"НЕ делать"| X2["Рестартовать всё разом:
обнулит кэши,
поднимет усиление"]
Три замечания к этой схеме, без которых она превращается в бесполезный плакат.
Первое. Каждый шаг требует рычага, который существует заранее. «Принять 20 % трафика» — это конфигурация на входном балансировщике или в шлюзе, применяемая за секунды и не требующая деплоя. «Отключить повторы» — флаг, читаемый на лету. Если ничего этого нет, совет «сбросьте нагрузку» неисполним, и вы будете писать нужный код в три часа ночи под давлением. Флаги и переключатели — тема главы «Безопасные релизы», проверка того, что рычаги работают, — «Проверка отказом».
Второе. Сброс нагрузки — это решение о том, кому отказать. Технически безразлично, кого дропать; для бизнеса — нет. Оплаченный заказ, регистрация нового пользователя, фоновая синхронизация и выгрузка отчёта имеют разную цену, и классифицировать их надо заранее, на трезвую голову, вместе с продуктом. Договорённость вида «при перегрузке первыми режем фоновые задачи и экспорт, последними — оплату» стоит одного часа обсуждения в спокойный день и экономит двадцать минут отказа в аварийный. Это организационная работа, а не техническая, и её нельзя ни автоматизировать, ни отложить на момент инцидента.
Третье. Возврат трафика ступенями — не перестраховка. Из-за гистерезиса система, вернувшаяся к 90 % нагрузки, всё ещё может свалиться обратно, если очередь не слита до конца. Ступени с паузами дают очереди дренироваться, а кэшам прогреться. Кто принимает решение о каждой ступени и как это устроено по ролям — «Реакция на инцидент».
Что показывают публичные разборы
Полезно посмотреть на две подробно описанные аварии, где механика видна целиком.
AWS EBS, апрель 2011 (разбор). Сетевое изменение на короткое время увело трафик на канал меньшей ёмкости. Тома EBS потеряли связь с репликами и начали искать место для повторного зеркалирования. Свободное место в кластере кончилось — узлы продолжали искать, генерируя запросы, которые нельзя удовлетворить. Сеть починили за минуты; «шторм зеркалирования» держался часами и потребовал добавления физической ёмкости и отключения части API. Каноническая метастабильность: триггер длился минуты, поддерживающий эффект — почти сутки.
Amazon DynamoDB, сентябрь 2015 (разбор). Всплеск нагрузки на внутренний сервис метаданных увеличил время его ответа. Узлы хранения не успели получить назначения в отведённое время, сочли себя выпавшими и начали запрашивать метаданные заново — усиливая нагрузку на уже перегруженный сервис. Восстановление потребовало явно ограничить обращения к метаданным и добавить ему ёмкости, то есть ровно двух шагов из схемы выше.
В обоих случаях бросается в глаза одно: компании с почти неограниченными ресурсами не смогли выйти из состояния наращиванием мощности. Им пришлось сначала снять нагрузку. Если у вас в подобной ситуации первая мысль «добавим инстансов» — это не вопрос масштаба, это вопрос понимания механики.
Общий взгляд на то, почему сложные системы ломаются именно так, стоит прочитать у Richard Cook, «How Complex Systems Fail» — четыре страницы, которые экономят годы. Каталог антипаттернов устойчивости в коде — Michael Nygard, «Release It!» (2nd ed., Pragmatic Bookshelf, 2018), главы про Stability Antipatterns.
Что берём у Google SRE, а что нет
Двадцать вторая глава книги SRE, «Addressing Cascading Failures», — лучший существующий текст на эту тему, и читать её нужно. Но большая часть рецептов там опирается на инфраструктуру, которой у вас нет: единый RPC-стек с приоритетами и критичностью запросов во всех сервисах компании, глобальный балансировщик с подмножествами бэкендов, общий планировщик кластера, возможность одолжить ёмкость у соседнего сервиса за минуты.
| Практика | Переносится? | Дешёвая замена |
|---|---|---|
| Закон Литтла, пределы очередей, ограничение параллелизма | полностью | ничего не нужно, это арифметика |
| Бюджет повторов вместо счётчика попыток | полностью | клиентская библиотека или сайдкар |
| Джиттер везде, где есть периодичность | полностью | одна строка кода |
| Сброс нагрузки на входе | почти полностью | лимит в ingress/шлюзе, флаг |
| Приоритеты и критичность запросов на уровне RPC | плохо | два класса трафика по пути или заголовку в ingress |
| Автоматическая переброска ёмкости между сервисами | нет | заранее согласованный список того, что выключаем |
| Тонкая настройка подмножеств бэкендов | нет | outlier detection в прокси |
| Учения на боевом трафике в масштабе DiRT | частично | game day на одном сервисе (глава 12) |
Правило перевода простое: берите то, что является арифметикой или одной строкой конфигурации, и не пытайтесь воспроизвести то, что у Google является платформой. Два класса трафика в шлюзе дают процентов восемьдесят пользы от полноценной системы приоритетов и стоят один спринт вместо года. Подробнее о переносимости практик в маленькую команду — «SRE в небольшой команде».
Цена: где инженерия упирается в организацию
Всё в этой главе имеет ценник, и его стоит проговорить прямо.
Запас мощности стоит денег постоянно. 30 % незанятой ёмкости — это 30 % счёта, которые нельзя объяснить графиком загрузки: график покажет, что железо простаивает. Аргумент строится не «нам нужен запас», а «при 95 % утилизации задержка вырастет в пять раз от малейшего толчка, и вот арифметика».
Изоляция стоит сложности. Разделение на ячейки снижает радиус поражения со 100 % до 1/N, но каждый деплой, каждая миграция и каждый дашборд умножаются на N. Команда из шести человек с восемью ячейками будет заниматься ячейками, а не продуктом.
Наблюдаемость для серых отказов стоит дорого. Клиентские метрики, синтетика из нескольких регионов, per-node SLI — это рост кардинальности, а значит счёта за хранение метрик и времени на поддержку.
Каскады выжигают дежурных сильнее всего остального. Обычный инцидент — двадцать минут работы по рунбуку. Каскад — час-полтора ночью, в состоянии, когда очевидные действия делают хуже, а правильные требуют разрешения снять половину трафика. После такой ночи человек не работает следующий день, и это надо планировать, а не героизировать («Дежурство», и со стороны руководителя — «Дежурства и инциденты»).
Право отказать пользователю — не техническое решение. Инженер может реализовать сброс нагрузки, но не может в одиночку решить, что при перегрузке новые регистрации важнее фоновой синхронизации. Если такой договорённости нет, дежурный в три ночи либо не воспользуется рычагом (и продлит отказ), либо воспользуется и потом будет объясняться. Оба исхода — организационный дефект, а не человеческий.
И главное про девятки. Один каскад продолжительностью 44 минуты — это весь месячный бюджет SLO 99,9 % (43 200 × 0,001 = 43,2 минуты) плюс небольшой перерасход. Посчитаем, что означают более высокие цели:
| SLO | Бюджет в месяц | Что это значит для каскада |
|---|---|---|
| 99,9 % | 43,2 мин | один каскад в месяц — впритык, два — нарушение |
| 99,95 % | 21,6 мин | человек должен успеть проснуться, понять и снять нагрузку за 20 мин |
| 99,99 % | 4,32 мин | человек физически не помещается: только автоматика |
| 99,999 % | 25,9 с | каскад должен быть невозможен по конструкции |
Строка про 99,99 % — самая важная. Медианное время «звонок → человек за ноутбуком → человек понял, что происходит» — 5–10 минут даже у хорошей команды. Любое SLO строже 99,99 % — это не обещание про надёжность, это обещание про автоматику: всё восстановление должно происходить без участия людей. А «пять девяток» на уровне сервиса, куда ходят по сети, означают, что за год у вас есть пять минут суммарно на все каскады, релизы и обновления зависимостей. Стоит это, по опыту отрасли, в разы дороже четырёх девяток, а нужно почти никому: пользователь, у которого мобильная сеть даёт 99,5 %, разницы между 99,99 % и 99,999 % не заметит вовсе. Как выбрать цель, глядя на ущерб, а не на престиж, — «SLI и SLO».
Типичные ошибки
- Добавлять мощность во время каскада. Нужно втрое, приедет поздно, придёт с холодными кэшами.
- Рестартовать всё разом. Обнуляет кэши, синхронизирует клиентов, поднимает усиление.
- Повторы без бюджета и без джиттера. Множитель перемножается по глубине цепочки.
- Повторять на сигнал перегрузки.
503и429— это «мне уже плохо», а не «попробуй ещё». - Таймаут больше, чем позволяет пул. Считается арифметически: $W \le L_{max} / \lambda$.
- Проба живости, зависящая от внешних сервисов. Убивает мощность в момент её дефицита.
- Очередь без предела. Превращает быстрый отказ в получасовое зависание с нулевой пользой.
- Один общий пул на все зависимости. Одна медленная зависимость забирает слоты у всех остальных.
- Отсутствие метрики доли повторов. Каскад выглядит как рост трафика, и команда лечит не то.
- Постмортем, объяснивший только триггер. «Забыли индекс» не объясняет 44 минуты — нужен разбор поддерживающего эффекта («Постмортемы»).
Мини-итог
- Отказы делятся на независимые, коррелированные и метастабильные. Резервирование лечит только первый вид.
- Насыщение выводится из закона Литтла: $L = \lambda \cdot W$. Пул кончается не от роста трафика, а от роста времени ответа.
- У порога утилизации задержка растёт как $1/(1-\rho)$: последние проценты нагрузки стоят кратного роста ожидания.
- Каскад — усиливающая петля. Ключевая величина — усиление работы $a$; отказ самоподдерживается, когда $\lambda \cdot a > \mu$.
- Гистерезис: система свалилась при одной нагрузке, а вернётся только ниже $\mu / a$ — обычно втрое ниже.
- Порядок действий: снять нагрузку → отключить повторы → выбросить просроченное → выключить второстепенное → возвращать ступенями. Мощность и рестарт — в конце, если вообще.
- Все рычаги должны существовать до аварии, а приоритеты трафика — быть согласованы с продуктом заранее.
- Запас, изоляция и наблюдаемость стоят реальных денег; SLO строже 99,99 % — это требование полной автоматизации восстановления, а не пожелание.
Источники
- Bronson N., Wu A., Kesavan A., Fuller D. «Metastable Failures in Distributed Systems», HotOS 2021.
- Huang L. и др. «Metastable Failures in the Wild», OSDI 2022.
- Huang P. и др. «Gray Failure: The Achilles’ Heel of Cloud-Scale Systems», HotOS 2017.
- Maurer B. «Fail at Scale», ACM Queue, 2015.
- Google SRE Book, глава 22 «Addressing Cascading Failures».
- AWS Builders’ Library: «Timeouts, retries, and backoff with jitter» и «Using load shedding to avoid overload».
- Cook R. «How Complex Systems Fail».
- Nygard M. «Release It! Design and Deploy Production-Ready Software», 2nd ed., Pragmatic Bookshelf, 2018.
- Разборы аварий: AWS EBS, 2011, DynamoDB, 2015.
Что дальше
Мы разобрали, как ломаются распределённые системы и почему знание механики меняет порядок действий в аварии. Но всё это знание бесполезно, пока рычаги не проверены: конфигурация сброса нагрузки, которую никто ни разу не применял, с большой вероятностью не сработает в нужный момент — а узнать об этом в три часа ночи дорого.
Дальше — Проверка отказом: учения, chaos-эксперименты, game day: как ломать систему намеренно и контролируемо, чтобы проверить, что описанные здесь механики вы действительно умеете распознавать и разрывать.