…Был обычный ноябрьский вечер, 2024 год шёл к своему завершению: на носу была «чёрная пятница». Я вернулся домой в Новосибирск из почти двухнедельной командировки, пробыв в пути 12 часов и поспав часа четыре. В 19:07 алерт сообщил мне о падении одного из контроллеров. В целом, проблема не критичная, так как сервисы зарезервированы. Но всё же одним глазом я заглянул в чат с разбором.
Через час ситуация стремительно ухудшилась: каскадом начали отказывать узлы, отвечающие за внешнюю связность. А затем развитие событий приняло фатальный оборот — в какой‑то момент одновременно отказали сервисы внешней связности сразу в двух зонах доступности…
Это был один из самых крупных региональных инцидентов в облаке, после которого мы многое изменили в сети, чтобы сделать её устойчивее. С того момента прошло больше года, так что пришла пора рассказать эту историю от начала и до конца.
В прошлой статье я уже показал наши основные подходы к повышению отказоустойчивости в этой ситуации. Однако за кадром остался сам процесс разработки новых решений и то, как мы мыслили, чтобы найти наилучший выход. Сегодня расскажу об этом подробнее. Статья основана на моём недавнем выступлении на Highload++ и дополнена по следам дальнейших расследований инцидентов.
Анатомия сложных систем vs модель швейцарского сыра
В тот раз мы уже рассмотрели облачную платформу как сложную систему с большим количеством движущихся частей. Такие системы опасны по своей сути: в них сочетается множество скрытых дефектов — элементы подвергаются незапланированной нагрузке, изнашиваются раньше времени и так далее. Но одновременно с этим сложные системы защищены от отказов множественными слоями защиты: техническими, организационными, регуляторными. Следовательно, катастрофический инцидент никогда не ограничивается одной точкой отказа, как правило, при крупной аварии возникает сразу множество сбоев.
Разумеется, нам хотелось бы, чтобы сложная система была «надёжной как швейцарские часы». Но когда мы расследуем крупные инциденты, важнее рассмотреть множество сбоев с точки зрения модели швейцарского сыра (aka defense in depth).

Слоёный пирог нашего инцидента
Вот какие слои выделили мы в процессе разбора.
Слой первый. Падение сетевого контроллера в зоне доступности. В целом это штатная ситуация, единичные отказы контроллеров могут происходить то тут, то там. Но мы всегда надеемся, что этого не произойдёт. Если в системе есть сложный, критически важный компонент, то возникает естественное желание относиться к нему с повышенной осторожностью. Краткосрочно это решение может быть верным, но на длинном интервале может приводить к незаметной деградации:

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

Но при падении нашего контроллера эта миграция виртуальных машин сработала как амплификатор — фактор, который приводит к тому, что число запросов растёт без роста естественной нагрузки. Сейчас поясню почему.
Слой третий. Внесённый баг. Затем свою роль сыграла ошибка, из‑за которой была нарушена правильная приоритизация маршрутов, что важно в случае наложения отказов и миграции ВМ.
В норме SDN работает так: виртуальная машина подключается к своему агенту виртуального роутера и анонсирует свой маршрут в контроллер, который разносит информацию дальше по всей системе.

При падении одного из контроллеров его оставшийся в живых собрат кеширует маршруты на какое‑то время.
Если же в этот момент виртуальная машина переезжает, то фактически она меняет свой маршрут и анонсирует его через другой агент виртуального роутера. У выжившего контроллера оказывается сразу два конкурирующих маршрута для этой ВМ.
Из‑за внесённого бага контроллер не мог понять, какой же из маршрутов важнее, что привело к петле реанонсов маршрута в системе:

Слой четвёртый. Нет Back Pressure. Как можно догадаться, дальше эта паразитная нагрузка расходилась по всей системе и в том числе попадала на пограничные виртуальные маршрутизаторы. При большом потоке информации система успевала его обрабатывать и не давала обратной связи, чтобы снизить поток. Всё это выливалось в рост очередей и потребления памяти на уровне пограничных маршрутизаторов.

Слой пятый. Нет Fail-Static. Следом мы увидели последствия того, что у нас не везде и не во всех сценариях был режим Headless или Fail Safe, о котором я уже рассказывал в прошлый раз. Чуть углубимся в устройство сервисов, чтобы это проиллюстрировать.
Если посмотреть на устройство сети верхнеуровнево, то может показаться, что маршрутизаторы Cloud Gateways относятся к слою DataPlane.

Но на самом деле они не являются монолитной сущностью — это виртуальные машины, внутри которых есть набор микросервисов. Часть из них относятся к слою Control Plane, а часть — Data Plane.

Маршрутная информация попадала на GoBGP, который выступал как Control‑Plane‑слой в контроллере. В какой‑то момент он падал из‑за космического роста по памяти. А если GoBGP падает, то это приводит к разрыву пирингов. При этом для внешнего наблюдателя это выглядит как полный отказ узла, включая Data Plane. И в случае реального отказа Data Plane нужно как можно быстрее отзывать анонсы, так как иначе трафик может частично теряться:

Однако конкретно в случае нашего инцидента Data Plane оставался рабочим, но так как все маршруты сняты, он фактически был выведен из маршрутизации.
Слой шестой. Static Stability слабо распространён. По сути это был ещё один амплификатор. Что имеется в виду: часть пользователей облака, которые держат нагрузку только в одной зоне, по сути рассчитывают на динамическую стабильность как возможность переехать в другую зону в случае инцидента.
Однако при реальных нештатных ситуациях среди таких желающих оказываются как реальные клиенты, так и автоматические системы (например, Instance Groups). В результате очереди улетают в космос, а переехать не получается.

Теперь должно стать понятно, что именно пошло не так и что к этому привело. Суммарно это всё выглядело как системная проблема, с которой что‑то надо делать. Было недостаточно починить конкретные корневые причины инцидента в существующих слоях. Было важно как‑то иначе подойти к работе с надёжностью, чтобы быть более уверенным в устойчивости сервисов.
Чтобы приступить к решению, для начала попробуем формализовать задачу
Формулировка задачи
Для чего мы выделяем эти слои — это позволяет на следующем этапе послойно продумать улучшения для защиты от подобных ситуаций, например, вот так:

Сложность постановки целей для изменения системы состоит в том, что часто возникает некоторый перекос в восприятии, или фрейминг: у инженеров есть гиперфокус на определённых слоях, из‑за чего «низковисящие фрукты» в других слоях упускаются из виду. Опыт подсказывает, что обычно разработчики больше всего фокусируются на больших архитектурных изменениях. Ход мысли примерно такой: «Как бы нам сделать такую систему, чтобы она никогда не падала». Но давайте посмотрим, к чему это приводит.
У крупных архитектурных изменений помимо множества плюсов есть важная особенность — для разработки и внедрения требуется существенное время. Это неизбежно создает определённую цикличность. Представим устойчивость системы как некоторую функцию от времени и изобразим её на графике. Пока мы разрабатываем, нагр��зка продолжает расти, отдельные части системы могут деградировать, но с внедрением проблемы решаются, а мы идём на следующий виток.

Но чем больше становится система, тем сложнее все эти изменения, тем дольше их внедрять и помимо прочего «болевой порог» сдвигается. Потому что чем дольше сервис на рынке, чем больше у него пользователей, тем выше ожидания. Таким образом время нахождения в «красном уровне» становится всё дольше.

Добавим к этому специфику нашего инцидента, где первопричиной стали проблемы в сетевой связности. Наивный взгляд на проблему выглядит так: если источником проблемы является сетевой компонент, то и основной фокус стоит сделать на сети. Думаю многим коллегам по цеху может быть близко такое отношение:

В реальности в других местах системы также требуются улучшения для снижения влияния инцидента (импакта), просто эти места сложнее локализовать.
В нашем случае всё это приводило к такому конфликту:
Много фокуса на больших архитектурных изменениях.
Хтоническая сложность системы распределена неравномерно, и это создаёт перекосы в нагрузке и ресурсах на изменения.
При этом само резкое внесение больших изменений надёжности может привести к новым проблемам с надёжностью.
Ещё в прошлый раз мы сформулировали главную проблему улучшений сложной системы: любые изменения сами могут стать причиной отказа. Поэтому задачу можно сформулировать так:
Нужно добавить минимальное количество дополнительных слоёв защиты, каждый из которых при этом закрывает максимальное число дыр.
Следовательно, нам нужно было понять, что это за слои, и действительно ли дело в архитектуре.
Для достижения этой цели попробуем оттолкнуться от метрик, которые помогут нам увидеть желаемые изменения, но сначала зафиксируем терминологию.
Lagging Indicator — это метрика, которая измеряет результат работы сложной системы, то есть это именно то, что мы хотим оптимизировать (скажем, SLA). Но её особенность в том, что такая метрика статистически значимо меняется не сразу, часто только через 1–5 лет после изменений свойств системы. То есть существует лаг во времени, от внедрения изменения и до получения видимого результата. Бытовой пример — влияние тренировок на изменение веса и физической формы зачастую становится нам очевидно далеко не сразу.
А когда метрика недостаточно детализирована и «прокрашивается» (то есть становится видимой) месяцами, непонятно, как на неё влиять. Так что нам нужно спуститься на уровень ниже и ввести промежуточные метрики, которые будут более гранулярными (детализированными).
Leading Indicator — это прокси‑метрика, которая изменяется почти сразу же
при изменении свойств системы. Её легко измерить, но она не всегда может быть связана с конечной метрикой. Например, если мы хотим улучшить SLA, можно попробовать следить за количеством ежедневных ошибок 5хх — если они не превышают установленного порога, есть шанс получить нужный SLA.
Декомпозируем проблему
Мы собрали все инциденты в единую таблицу и попробовали разметить сырые данные, исходя из наших знаний об инциденте.

Что было в первоначальной таблице:
Зона инцидента.
Категория инцидента.
Компоненты (какие сервисы).
Количество жалоб.
Был ли внешний импакт.
Был ли импакт на регион.
Был ли импакт на Data Plane.
На основе сырых данных мы уже могли построить агрегатные метрики:

Теперь столбцы выглядели так:
Процент пользователей.
Tier боли — уровень критичности, по нашей оценке, в зависимости от того, насколько инцидент ощущался как болезненный для клиентов.
Время восстановления.
% задетой выручки.
Минуты × процент выручки.
Минуты × процент клиентов.
Минуты × процент клиентов × Tier боли.
Благодаря этому получилось дерево метрик, которые мы расположили в зависимости от гранулярности — того самого лага. Наверху — конечные lagging‑индикаторы, внизу — наиболее гранулярные и быстро меняющиеся leading‑индикаторы.

На вершине мы расположили ту самую надёжность системы для клиентов. Как показал наш анализ, примерно 80% боли для клиентов шло из критичных инцидентов: например, с длительными простоями и влиянием более чем на 10% пользователей. Дальше «боль» клиента можно декомпозировать:
Насколько сильно задело конкретного клиента.
Длительность деградации сервиса.
Как это влияет на выручку.
Всё это сегментируется дальше: например, импакт на клиента зависит от того, какой сервис пострадал, что именно было недоступно, и как зарезервированы ресурсы. Скажем, если был недоступен мониторинг — это само по себе не так страшно, как ситуация, когда не идёт трафик.
В основании дерева — понятные измеримые «ставки». Это те изменения, на которые мы ставим — они наиболее вероятно ведут к улучшению системы: например, снижение blast‑радиуса при инциденте.
Даже в таком виде это неполная декомпозиция, скорее, это пример того, как можно анализировать систему, исходя из lagging‑ и leading‑индикаторов.
Ранжируем изменения от выбранных метрик
Здесь снова хочется остановиться на прояснении терминологии.
Асимметрия — те места в системе, которые существенно амплифицируют результат относительно приложенных усилий. Это источник ключевых трейд‑офов.
Универсальные примеры:
Длительность отказов: даунтаймы по 30+ мин. влияют гораздо хуже, чем простои длиной до получаса.
Охват инцидента: когда затронуты более 10% пользователей, это гораздо хуже, чем отказы на 0–9% пользователей.
Какая среда затронута: импакт на продакшн‑окружение всегда намного хуже, чем на тестовую среду или stage.
Восстановление важнее предотвращения. Чуть менее очевидный пункт, который затрагивает философию работы. Мы предотвращаем конкретные инциденты, а восстанавливаем — от широкого класса проблем.
Система может сломаться десятью разными способами. Какие‑то инциденты частично ломают систему, какие‑то полностью и тд. Чинить каждый из них независимо, во‑первых, трудоёмко, во‑вторых, нереально предусмотреть всё, а в‑третьих — система эволюционирует и появляются новые способы.
При этом, если фокусироваться на ускорении холодного старта системы и добиваться того, чтобы поддерживать время холодного старта на уровне 5 минут, то дальше любые поломки можно сводить к «доламываем полностью и восстанавливаемся за 5 минут».
В нашем случае есть ещё и специфичные асимметрии: например, отказ региона намного хуже отказа одной зоны, а сценарии Data Plane важнее, чем Control Plane, поскольку если трафик не ходит, это намного хуже невозможности создать виртуальную машину.
Теперь нам было необходимо найти такие асимметрии в построении слоев защиты и проранжиров��ть необходимые изменения в порядке приоритетности. У каждого изменения в списке появилась экспертная оценка: стоимость внедрения с учётом вероятности возможных рисков соотносилась с оценкой влияния на систему.

Итоговые ставки по слоям
Опираясь на формулу оценки, попробуем посмотреть на те изменения, которые мы выбрали в первую очередь.
Ставка 1. Headless (Fail Safe). Учитывая специфические асимметрии, было важно, чтобы при отказе управляющего контура Data Plane продолжал работать. А как мы помним, без режима fail‑safe при отказе контроллеров в системе происходит автоматический отзыв маршрутов, и трафик перестаёт ходить. Поэтому этот режим и стал номером один в списке.
Но важно добавить, что это требовало нетривиальных решений из-за проблемы кеша. Инвалидировать кеши в нашем случае было не так просто, так как нельзя допускать нарушения изоляции между разными виртуальными сетями. Однако несмотря на сложность реализации, мы увидели профит: уже несколько частичных или полных падений контроллеров не превратились в инциденты благодаря Fail Safe.
Ставка 2. Zonal Shift. Это инструмент для полного закрытия зоны на балансировщиках, о котором мы также рассказывали. Мы реализовали своего рода «царь-рубильник» для тушения всех балансировщиков в зоне: в случае серьёзных проблем с зоной это позволяет сразу снять импакт на всех находящихся там клиентов.
Ставка 3. Ограничение очередей. Это решение для описанной проблемы Static Stability, которая часто выступает амплификатором инцидентов.

Если очереди резко начинают расти выше определённого порога, включается ограничитель. Так мы избавляемся от ненужной холостой работы и одновременно видим улучшения при возникновении инцидентов: вместо 2–3 часов на полное восстановление системы получаем не более десятка минут.
Ставка 4. Maturity Model. Мы переформулировали модель зрелости релизных процессов, и стали рассматривать их с точки зрения критичности проблем, которые могут наступить по результатам внесённого изменения:
0 — если сервис падает
1 — деградация функциональности, которая влияет на сервис
2 — деградация Control Plane, когда нельзя развернуть новую нагрузку или остановить её
3 — нет заметных деградаций для пользователя, но есть для внутренних процессов
4 — нет заметных деградаций ни для пользователя, ни для внутренних процессов
С другой стороны мы оценивали, сколько времени с момента выкатки потребуется до наступления таких последствий. Получилась такая матрица:

Так мы смогли оценить влияние релизов на надёжность и пройти путь от полного фриза релизов в системе к управляемым релизам, когда мы стремимся оценить последствия каждого улучшения и смещать сервисы вверх и вправо по этой шкале.
Ставка 5. 10+ безопасных улучшений. Ну и следом за этим шёл набор улучшений с минимальными рисками, который суммарно давал заметный результат:
более «душный» Incident Management,
безопасные ретраи,
глобальная ручка остановки миграций,
срезание ненужных маршрутов по системе,
ускорение VPC Config Plane,
И многое другое.
Выводы, которые могут пригодиться вам на случай инцидентов
Как можно учесть этот опыт у себя, чтобы избежать критичных инцидентов в сложных системах? Суммирую по пунктам.
Сформулируйте целевой Lagging‑индикатор, который хотите улучшать на длинной дистанции.
Декомпозируйте проблему, опираясь на данные: что больше всего влияло на целевой показатель в прошлом.
Сформулируйте иерархичную модель Leading‑индикаторов, которые влияют на ваш целевой показатель. Провалидируйте её на данных.
С помощью асимметрий распределите усилия.
А чтобы проверить, насколько итоговая система оказалась рабочей, не помешает ответить на проверочные вопросы из чек‑листа:
Какой баланс краткосрочного и долгосрочного?
Какой баланс предотвращения и восстановления?
Закрывают ли решения широкие классы инцидентов?
Нет ли расчёта на то, что не проверяется регулярно?
Учтён ли человеческий фактор?
Насколько решения кастомные vs стандарт в индустрии?
Не скатываетесь ли вы в незаметную деградацию?
Если все эти вопросы нашли удовлетворительный ответ — поздравляем, вы великолепны!
Ну и в заключение хочется упомянуть о том, как поддержать себя и команду. Я начал статью с личной истории, как всё происходящее выглядело для меня, и в завершение хотелось бы напомнить: за каждым восстановлением после инцидента стоят реальные люди.
В рассказанной истории кто‑то в ночи катил релизы, кто‑то готовил коммуникацию, кто‑то экстренно перекраивал планы и прорабатывал планы по выходу из кризиса. Это тяжело, эмоционально и физически, поэтому хочу напомнить: вы можете помочь своей команде, своим сервисам, своим пользователям, только если вы в порядке сами. Так что даже в критичных ситуациях не забывайте «надевать маску сначала на себя, а потом на ребёнка».
