Три концепции отказов, которые стоит знать
Добро пожаловать
Распределённые системы отказывают по определённым паттернам. Как только вы изучите эти паттерны, каждый постмортем станет упражнением на распознавание, а не загадкой.
Три концепции покрывают большую часть того, что важно в анализе сбоев в продакшене:
Единая точка отказа (SPOF): компонент, отказ которого приводит к падению более крупной системы. Часто скрыта: сервер DNS, от которого зависит все; сертификат, по которому обновляется всё; единственный мастер-база данных.
Каскадный сбой: отказ одного компонента вызывает отказ другого, который, в свою очередь, вызывает отказ следующего. Медленная база данных вызывает таймауты в слое API, что приводит к повторным попыткам, которые создают дополнительную нагрузку на базу данных, что вызывает ещё больше таймаутов. Всплеск распространяется.
Радиус поражения: насколько большая часть системы выходит из строя при отказе одного элемента. Архитектурные решения либо ограничивают, либо не ограничивают этот радиус. У ОЕЗ (одной точки отказа) радиус поражения неограничен. У сервиса с изоляцией (bulkhead) радиус ограничен.
К концу этого урока вы сможете:
- Определять ОЕЗ в архитектуре путём визуального анализа
- Распознавать паттерны каскадных сбоев: «грохот стада» (thundering herd), «шторм повторных попыток» (retry storm), «очередь смерти» (queue of death)
- Читать реальную хронологию инцидента и отделять триггер от латентного дефекта, который этот триггер выявил
- Формулировать пункты плана действий без поиска виноватых, направленные на системы, а не на людей, охватывающие предотвращение / обнаружение / восстановление
- Рассуждать об изоляторах (bulkheads) и предохранителях (circuit breakers) как об инструментах ограничения радиуса поражения
Найдите единственную точку отказа
Инспекция многоуровневой архитектуры
Рассмотрим небольшую веб-архитектуру:
- DNS: api.example.com -> один IP-адрес именованного сервера 203.0.113.10, обслуживаемый одним провайдером DNS
- CDN: один вендор CDN перед api.example.com
- Ingress: два обратных прокси-сервера за балансировщиком нагрузки
- Backend: шесть реплик API в двух зонах доступности (по три в каждой)
- База данных: один основной сервер + одна реплика для чтения, в одной и той же зоне доступности
- Кэш: кластер Redis, три узла, распределённые по тем же двум зонам доступности
Вопрос: какие компоненты являются SPOF? Подсказка: SPOF — это не всегда очевидный тип «один сервер». Кластер из трёх машин, все в одной зоне доступности, является SPOF для сбоя этой зоны.
Три классических паттерна каскада
Отказы распространяются через зависимости
Паттерн 1: Громкое стадо (Thundering herd). Общий ресурс (кэш, блокировка, база данных) выходит из строя или перезапускается. Все клиенты, которые от него зависели, одновременно повторяют запросы. Поток запросов перегружает восстанавливающуюся систему; повторные попытки накапливаются быстрее, чем система успевает их обработать; восстановление никогда не завершается.
Паттерн 2: Шторм повторных попыток (Retry storm). Входящий сервис замедляется. Вызывающие его сервисы, вместо того чтобы завершаться с ошибкой, повторяют запросы. Повторные попытки умножают исходную нагрузку. Медленный сервис замедляется ещё сильнее, что вызывает ещё больше повторных попыток. В конечном итоге нагрузка превышает возможности даже полностью здорового экземпляра сервиса.
Паттерн 3: Очередь смерти. Очередь обработки без механизма обратного давления (backpressure) получает задачи быстрее, чем успевает их обрабатывать. Очередь растёт без ограничений. Память исчерпывается; потребитель (consumer) падает; перезапускается; обнаруживает ещё более большую очередь; падает снова.
Общая черта: малое начальное возмущение запускает петлю положительной обратной связи. Собственная реакция системы усиливает отказ, а не гасит его.
Механизмы затухания
Экспоненциальная задержка с джиттером. Клиенты, выполняющие повторные попытки, ждут всё дольше с каждым разом, с добавлением случайного смещения. Это предотвращает синхронизированные волны повторных запросов.
Предохранитель (Circuit breaker). Вызывающая сторона отслеживает частоту сбоев в нижележащих сервисах. При превышении порога вызывающая сторона прекращает отправку запросов на период охлаждения и сразу же возвращает ошибки для собственных запросов. Это предотвращает бесполезные вычисления и позволяет нижележащему сервису восстановиться.
Компартментация (Bulkhead). Изоляция ресурсов для каждого зависимого компонента. Пул соединений A для базы данных, отдельный пул соединений B для кэша. Медленная база данных не сможет занять все соединения; запросы к кэшу продолжат выполняться.
Сброс нагрузки (Load shedding). При перегрузке запросы отбрасываются на границе системы, вместо того чтобы приниматься и медленно завершаться с ошибкой. Ответ 429 за 1 мс лучше, чем ответ 500 за 30 секунд.
Обратное давление (Backpressure). Замедление источников данных, когда потребители не успевают обрабатывать поток. Очереди становятся ограниченными по размеру; отправители блокируются; исходный источник нагрузки ощущает это сопротивление.
Диагностика каскадного сбоя
API-слой команды выходит из строя во время планового переключения базы данных. Хронология событий:
- 14:00:00 — оператор повышает резервную базу данных. Ожидаемое время недоступности: ~10 секунд.
- 14:00:08 — основная база данных недоступна. Запросы API-слоя начинают завершаться с ошибками подключения к базе данных.
- 14:00:08 — API-слой выполняет повторные попытки (настройки по умолчанию: 5 повторений, без экспоненциального отката, интервал 100 мс).
- 14:00:11 — резервная база данных повышена, начинает принимать новые подключения.
- 14:00:11 — API-слой одновременно открывает тысячи новых подключений к базе данных (каждый репликат × каждый параллельный запрос × каждое повторение).
- 14:00:13 — пул подключений новой основной базы данных исчерпан; новые подключения отклоняются.
- 14:00:13-14:05:00 — репликаты API-слоя исчерпывают пулы подключений, выбрасывают исключения, аварийно завершают работу, перезапускаются, и цикл повторяется.
- 14:05:00 — оператор вручную останавливает трафик API-слоя; база данных стабилизируется.
- 14:10:00 — завершено постепенное восстановление трафика. Общее время простоя: ~10 минут (вместо ожидаемых ~10 секунд).
DNS SERVFAIL: Два усугубляющих дефекта
Постмортем реального инцидента
Ниже приведена анонимизированная версия реального инцидента. Названия вендоров изменены, IP-адреса скрыты; однако структура, хронология и уроки — подлинные.
Резюме
Сайт example.com возвращал SERVFAIL со всех публичных DNS-резолверов в течение примерно 3–4 часов. Все остальные 46 зоны на том же DNS-мастере не пострадали. Корневая причина: два усугубляющих дефекта.
1. Вендор A (вторичный DNS-провайдер) добавил новый внутренний IP для синхронизации, который отсутствовал в списке разрешений allow-axfr-ips основного сервера.
2. В зоне example.com присутствовало устаревшее нарушение RFC: конфликт CNAME (у demo.example.com на одном и том же лейбле одновременно находились записи CNAME и MX/TXT), из-за чего Вендор A отклонял зону при выполнении нового AXFR.
Хронология (UTC)
- ~15:00 — Поставщик A добавляет новый IP-адрес синхронизации 198.51.100.42 в свою инфраструктуру
- 15:02 — в логах основного DNS-сервера впервые появляется запись AXFR-out denied для 198.51.100.42 (оповещения по этому сигналу не настроены)
- ~18:00 — истёк срок действия записи SOA (expire); Поставщик A удаляет зону example.com из кэша
- ~18:30 — извне обнаружен статус SERVFAIL
- ~19:45 — установлена корневая причина
- 20:00 — 198.51.100.42 добавлен в allow-axfr-ips; основной сервер перезапущен
- 20:05 — отправлено уведомление NOTIFY; инициирован AXFR; зона по-прежнему возвращает SERVFAIL (конфликт CNAME)
- 20:07 — команда check-zone выявляет 1 ошибку: конфликт CNAME для demo.example.com
- 20:09 — CNAME заменён на A-запись; проверка зоны чистая (0 ошибок)
- 20:10 — Отправлен NOTIFY; AXFR завершён; Поставщик A начинает обслуживать зону
- 20:11 — dig @8.8.8.8 example.com A возвращает правильный IP — РЕШЕНО
Почему только example.com?
Все 47 зоны используют один и тот же основной DNS-сервер. Блокировка IP при AXFR затронула все зоны. Однако конфликт CNAME был только у example.com, & только example.com требовал свежего AXFR в момент, когда был применён запрет. Другие зоны уже обновились до запрета или ещё не нуждались в обновлении.
Латентный дефект
Конфликт CNAME на demo.example.com существовал годами. Он работал, потому что основной сервер обслуживал зону из своей базы данных (был терпим к нарушениям RFC), & Поставщик A обслуживал запросы из устаревших кэшированных данных, полученных до появления нарушения. Когда Поставщик A сбросил кэш & потребовал свежие данные, нарушение проявилось.
Триггер
Поставщик A молча добавил новый IP-адрес для синхронизации. В списке разрешений основного сервера его не было. AXFR был отклонен. Три часа спустя (после истечения срока действия SOA) Поставщик A удалил зону. Латентный дефект проявился, когда система попыталась восстановиться.
Составление беспричинных пунктов действий
Беспричинность: фокус на системах, а не на людях
Беспричинный пункт действий указывает на то, что система должна делать иначе, а не на то, что человек должен делать иначе. «Обучить оператора» — это обвинительный подход. «Добавить автоматическую проверку, которая обнаружит это до развёртывания» — это беспричинный подход.
Хорошие беспричинные пункты действий группируются в три измерения:
- Предотвращение: сделать нежелательное событие более трудным или невозможным
- Обнаружение: заметить его раньше, если оно произойдёт
- Восстановление: ограничить ущерб, когда оно произойдёт
Каждый пункт должен указывать: (1) конкретное изменение в системе, (2) команду-владельца и (3) измерение, которому он соответствует.
Отсеки, которые тонут без всего корабля
Заимствовано из кораблестроения
Корабли имеют герметичные переборки: вертикальные стены, разделяющие корпус на отсеки. Один отсек может затопиться, не потопив корабль; другой может выйти из строя, не повлияв на остальные.
Распределённые системы заимствуют то же самое слово и ту же самую идею.
Паттерн «переборки»: изоляция ресурсов для каждой зависимости. Сервис, вызывающий три нижестоящих API, использует три отдельных пула соединений, три отдельных пула потоков и три отдельных бюджета повторных попыток. Медленная или неисправная нижестоящая зависимость не может потребовать ресурсы, выделенные для двух других.
Без переборок: одна медленная зависимость исчерпывает общий пул потоков; вызовы других зависимостей блокируются, ожидая потоки; весь сервис становится неответственным.
С переборками: одна медленная зависимость исчерпывает собственный пул; вызовы к ней быстро завершаются с ошибкой; вызовы к другим зависимостям продолжаются в обычном режиме; радиус поражения остаётся ограниченным неисправной зависимостью.
Автоматические выключатели
Паттерн «предохранитель» (Circuit breaker): обёртка с состоянием вокруг зависимого сервиса, отслеживающая частоту сбоев. Три состояния:
- Закрыто (норма): запросы проходят. Сбои учитываются.
- Открыто (срабатывание): при превышении порога сбоев (например, 50% сбоев за последние 30 секунд) предохранитель открывается. Запросы завершаются с ошибкой немедленно, без попытки обращения к зависимому сервису. Это экономит ресурсы вызывающей стороны и защищает зависимый сервис от нагрузки, пока он не здоров.
- Полуоткрыто (тестирование): после периода ожидания предохранитель пропускает небольшую долю запросов. Если они успешны, он возвращается в нормальное состояние (закрывается). Если они не успешны, он снова открывается на новый период ожидания.
Ключевой момент: предохранитель предотвращает бесполезные попытки во время известных периодов нестабильности и даёт зависимому сервису шанс восстановиться без постоянной нагрузки.
Балласты ограничивают радиус поражения. Предохранители предотвращают самоподдерживающийся характер «взрыва».
Ограничение радиуса поражения
Ваш API-сервис вызывает четыре зависимых сервиса: Сервис пользователей, Сервис рекомендаций, Сервис уведомлений и сторонний API платежей. Команда слышала, что «Сервис рекомендаций немного нестабилен», и хочет убедиться, что при его сбое остальная система останется в рабочем состоянии.
Сегодня сервис использует один общий пул из 200 потоков и один общий пул HTTP-соединений. Все четыре нижестоящих сервиса конкурируют за эти ресурсы. Предельных выключателей (circuit breakers) нет.
Проведите анализ режимов отказа
Синтез
Вы научились выявлять ОЕЗ (одиночные точки отказа) при визуальном осмотре, распознавать паттерны каскадных отказов, отделять триггер от скрытого дефекта при чтении ретроспективы, формулировать беспристрастные пункты плана действий в категориях «предотвращение / обнаружение / восстановление» и ограничивать радиус поражения с помощью bulkheads (разделителей), circuit breakers (предохранителей) и graceful degradation (грациозного деградирования).
Примените все пять навыков.
Ваша команда запускает новый сервис search.example.com, который зависит от трех нижестоящих сервисов: основного поискового индекса (index.example.com), аналитического сервиса (analytics.example.com) и сервиса рекомендаций (recs.example.com). Команда просит вас возглавить «анализ режимов отказа» перед запуском.
Куда ведёт этот курс дальше
Куда ведёт этот курс дальше
Теперь вы можете выявлять SPOF, распознавать каскадные сбои, продуктивно читать постмортемы, формулировать безвинные пункты плана действий и ограничивать радиус поражения (blast radius) на этапе проектирования.
Последний урок этого курса (cs_distsys_observability_and_capacity) объясняет, что нужно измерять, чтобы обнаруживать проблемы до того, как их заметят пользователи. Проверки состояния (health checks), эндпоинты версий, четыре золотых сигнала на уровне прокси и то, как решения по резервной ёмкости (surge capacity) связаны с наблюдаемыми данными.
Сопутствующий урок: geometry_of_failure_modes_and_blast_radius выводит центральность междуности (betweenness centrality, который узел графа является узким местом) и минимальный разрез (min-cut, верхняя граница радиуса поражения).
Отличная работа. Вперёд.