Звуки грустного тромбона теперь преследуют нас даже во сне. Каскадный сбой проявляется по-разному: процессы падают, пользователи переподключаются. А дежурный инженер часто узнаёт о происходящем одним из первых – когда на телефон начинают сыпаться уведомления мониторинга.
25 марта с 12:13 до 15:30 по тихоокеанскому летнему времени работа голосовой и видеосвязи в Discord была серьёзно нарушена. Большинство пользователей не могли начать звонок или подключиться к нему, а в статусе вызова отображалось сообщение «Ожидание конечной точки».
Во время планового изменения инфраструктуры из-за ошибки в конфигурации одновременно отключилась значительная часть серверов управления сессиями Discord. Сессии – это пульс инфраструктуры Discord реального времени: каждое подключённое устройство поддерживает собственную сессию, а сами сессии координируют почти всё, что пользователь видит и слышит в приложении. Одновременная потеря 17% сессий запустила цепную реакцию в нескольких нижестоящих системах. В итоге оказался перегружен сервис, который направляет голосовые и видеозвонки на нужные серверы по всему миру.
После инцидента мы подробно разобрали работу наших систем: выяснили, почему они начали деградировать под каскадной нагрузкой, вызванной сбоем сессий, и определили, как использовать полученный опыт для дальнейшего развития инфраструктуры. В распределённой системе резкий скачок нагрузки всегда опасен. Он пробивает старые узкие места и быстро находит новые.
В этой статье мы заглянем за кулисы и разберём, как одно на первый взгляд безобидное изменение перегрузило систему, находившуюся в нескольких переходах от исходной точки, и как этот совсем не весёлый день помог нам сделать Discord надёжнее.
Как всё началось
Сейчас наша команда инфраструктуры реального времени переносит Elixir-сервисы в Kubernetes. Это стандартный способ развёртывания сервисов в Discord, поэтому мы постепенно адаптируем существующие системы под эту модель, чтобы использовать все преимущества и инструменты, которые уже накопились вокруг платформы.
Системы Discord на Elixir с состоянием обеспечивают работу значительной части бэкенда. На каждом хосте запущены тысячи процессов с состоянием, хранящимся в памяти. Они отвечают за критически важные функции: серверы Discord – далее будем использовать их внутреннее название guilds, – статусы присутствия и звонки.
Когда нам нужно вывести хост из эксплуатации, необходимо убедиться, что все работающие на нём процессы передали свои данные другому узлу и обслуживание не прервётся. Для проверки при развёртывании отслеживается количество сущностей на каждом сервере. Pod (под) завершается, а сервер останавливается только после того, как этот счётчик опустится до нуля.

Незадолго до инцидента мы завершали перенос сервиса управления сессиями. Сервис сессий управляет пользовательскими сессиями – название мы выбрали удачное. Каждому устройству, с которого пользователь подключён к Discord, соответствует отдельный процесс сессии в нашем кластере. Если Discord открыт и в браузере, и в десктопном приложении, у вас будет две сессии. Если каким-то чудом вы запустили Discord на умном холодильнике, для него тоже появится своя сессия. Все сообщения чата, обновления статуса присутствия и вообще всё, что мы отправляем клиенту через WebSocket, проходит через вашу сессию. Так что это очень важный сервис.
Мы заметили, что по выходным загрузка CPU поднимается выше желаемого уровня, и решили настроить кластер так, чтобы снизить этот показатель. Для этого мы собирались вертикально масштабировать поды: увеличить выделенные им CPU и память, пропорционально сократив общее число подов. Так мы могли проверить, связана ли повышенная загрузка планировщика с постоянными накладными расходами на каждый под или она растёт вместе с количеством сессий.
Мы подготовили пулл-реквест с изменениями ресурсов и числа подов и начали развёртывание.
Потеря сессий
В первой зоне изменения развернули в 12:13 по тихоокеанскому летнему времени. Когда Kubernetes начал применять новую конфигурацию, из-за уменьшения числа реплик он завершил 50% подов. В качестве дополнительной защиты сервис после получения сигнала от Kubernetes пытается передать свои процессы другому узлу. Однако проверка безопасности, которая должна была дождаться завершения других уже выполнявшихся событий, задержала этот процесс. В результате время, отведённое Kubernetes на корректное завершение, истекло ещё до начала передачи процессов. Поскольку сервис сессий работает в трёх зонах с равномерно распределённой нагрузкой, 17% всех сессий Discord были остановлены без корректного завершения.
Наши системы на Elixir построены на процессах GenServer – универсальных серверных процессах Elixir. Во всех наших сервисах в каждый момент времени работают миллионы таких процессов. Если вы знакомы с акторной моделью, их можно представить как акторов. Каждый процесс извлекает одно сообщение из своего почтового ящика – очереди входящих сообщений, – а затем выполняет необходимые действия в зависимости от его содержимого. Важно, что каждый экземпляр GenServer обрабатывает только одно сообщение за раз. Благодаря такому однопоточному поведению нам удаётся обходить некоторые особенно неприятные проблемы конкурентного выполнения.
Один из ключевых строительных блоков наших систем реального времени на Elixir – монитор процесса. Вызвав Process.monitor или наш более масштабируемый ZenMonitor, можно гарантировать, что при завершении указанного процесса Elixir по любой причине вызывающий процесс получит в свой почтовый ящик сообщение {:DOWN, …}.
Допустим, вечером вы выключили компьютер и закрыли Discord. Все процессы ваших guild’ов получат сообщение о завершении сессии. После этого они перестанут отправлять вам сообщения, обновления статуса присутствия и прочие данные, пока вы снова не войдёте в Discord и не создадите новую сессию.
Одновременная остановка 17% сессий активировала мониторы каждой из них и вызвала шквал сообщений во всех системах реального времени – по одному сообщению на каждую сессию для каждой отслеживающей её сущности. Первые последствия этого потока проявились в шлюзовом сервисе: он начал возвращать отключённых пользователей в систему.
Переподключение пользователей
Следующая точка в цепочке инцидента – шлюзовой сервис, через который проходит весь входящий и исходящий WebSocket-трафик. При запуске клиент подключается к шлюзу, а тот обращается к сервису сессий, создаёт сессию и передаёт клиенту всё, без чего Discord не был бы Discord: серверы, каналы, личные сообщения, изображения профилей и другие данные. После этого шлюз держит соединение открытым и отправляет клиенту новые сообщения.

Неожиданное завершение сессии само по себе не является чем-то необычным. Причиной может стать сбой оборудования у облачного провайдера, ошибка сети, наш собственный баг или, например, поездка в метро. Сессия может разорваться по множеству причин. Чтобы восстановить подключение, шлюзовой сервис отслеживает каждую пользовательскую сессию и при возникновении проблемы сразу сообщает клиенту, что нужно переподключиться.
Затем мы оптимистично пытаемся восстановить сессию пользователя, подключившись к экземпляру шлюзового сервиса в той же зоне, где находилась его сессия. Если сессия всё ещё существует – отлично: мы заново связываем все компоненты, и пользователь может продолжить обсуждать, как украсть очередной брейнрот. Если сессии уже нет, мы создаём новую.
В 12:13 по тихоокеанскому летнему времени такое восстановление потребовалось не одной сессии, а сразу 17% всех сессий Discord. Все затронутые сессии работали в зоне us-east1-b, поэтому и большинство соответствующих шлюзовых соединений находилось там же. Каждый шлюзовой процесс, обслуживавший затронутого пользователя, получил сообщение :DOWN и уведомил клиент, что сессия больше недействительна и требуется заново подключиться к нашим системам.
Когда эти запросы начали поступать, сработало ограничение частоты создания сессий, которое обеспечивает обратное давление при лавине запросов. Но после миграции его ещё не успели перенастроить. Поскольку теперь подов было больше, чем виртуальных машин в прежней архитектуре, ограничение на один хост пропускало суммарно больше запросов. Из-за большего числа одновременно создаваемых сессий усилилась последовательная обработка операций. Потребление памяти резко выросло на всех шлюзовых узлах в us-east1-b, и память начала заканчиваться.

Запас памяти шлюзов: выше видно, как потребление памяти на узлах в us-east1-b резко выросло, после чего они перезапустились.

Идентификации шлюзов во времени: здесь виден первоначальный всплеск, за которым последовал длинный хвост переподключений.
Из-за этого разорвались шлюзовые соединения и у остальных пользователей в us-east1-b, но, как и было задумано, их сессии продолжали работать. Новые затронутые пользователи и пользователи, чьи сессии уже завершились, запустили ещё одну волну переподключений, переключившись на резервные серверы в зонах us-east1-c и us-east1-d. Те, чьи сессии сохранились, сразу восстановили связь и вернулись в систему. Остальные смогли начать создание новых сессий.
Но на этом последствия не закончились. Одновременно с исчерпанием памяти шлюзов в одной из зон пользователи столкнулись с проблемами при подключении к голосовым чатам. Массовое восстановление сессий в итоге докатилось до нашей аудио- и видеоинфраструктуры и выявило новый сценарий отказа в сервисе синхронизации голосовых состояний.
Перегрузка сервиса синхронизации голосовых состояний
У каждой подключённой к звонку сессии Discord есть «голосовое состояние» (voice state). Оно сообщает сервису, управляющему звонком, например discord_guilds или discord_calls, что сессия хочет подключиться к голосовому каналу определённого guild’а либо к личному или групповому звонку.
Сервисы, управляющие звонками, пересылают обновления голосовых состояний в сервис синхронизации голосовых состояний. Он анализирует активные голосовые состояния, определяет, где разместить каждый текущий звонок и как им управлять, а также постоянно отправляет RPC-команды нашему парку из более чем 25 000 внешних экземпляров SFU – серверов избирательной пересылки медиапотоков. SFU передаёт между участниками звонка медиатрафик со сквозным шифрованием. Если вы разговариваете с другом из Калифорнии, размещать звонок в Европе было бы неэффективно.
Когда сервис, управляющий звонком, узнаёт о завершении вашей сессии – снова спасибо мониторам процессов, – он создаёт обновление голосового состояния об отключении и отправляет его сервису синхронизации. Процесс-синхронизатор, в свою очередь, отправляет RPC-команду SFU, сообщая, что сессия отключается. Когда активный звонок покидает последняя сессия, синхронизатор отправляет SFU блокирующий RPC-вызов остановки и только после этого очищает собственное состояние.
Если множество сессий отключается одновременно, множество пользователей одновременно покидает голосовые звонки. В результате большое число звонков остаётся без участников и завершается. Когда сессии восстанавливаются и пытаются снова подключиться к голосовым каналам, синхронизаторы отправляют SFU новые RPC-команды: заново создают звонки и подключают к ним сессии. Такое массовое переподключение закономерно порождает внушительный поток исходящих HTTPS-соединений от сервиса синхронизации голосовых состояний к нашему глобальному парку SFU.

При каждом исходящем HTTPS-соединении voice syncers обращается к внутренней библиотеке Discord под названием Holster. Она использует пул соединений из библиотеки gun, HTTP-клиента для Erlang. Как выяснилось во время инцидента, из-за такой схемы создание нового соединения проходит сразу через два узких места – два процесса-супервизора Erlang: один обслуживает всё, что использует Holster.Pool, а второй – все соединения gun.
Каждый из этих супервизоров представляет собой отдельный процесс GenServer. При запуске дочернего процесса супервизор выполняет выборочное получение сообщений: просматривает свой почтовый ящик в поиске конкретного сообщения и ждёт подтверждения ACK от дочернего процесса. Во время тестов после инцидента мы подтвердили, что при очереди примерно из 100 тысяч сообщений такое выборочное получение добавляет около 1 мс ко времени запуска процесса. Если почтовый ящик достаточно велик, а новые соединения создаются достаточно быстро, наверстать отставание уже невозможно. Например, это происходит при очереди из миллиона сообщений, 100 запросах на запуск в секунду и задержке запуска в 1 мс.
Когда в почтовом ящике супервизора соединений gun накапливаются сообщения, открытие новых соединений блокируется: они не успевают установиться до истечения тайм-аута. Когда перестаёт справляться DynamicSupervisor пула Holster.Pool, блокируются уже и новые, и существующие соединения, поскольку получить соединение из пула не удаётся до истечения тайм-аута исходного запроса.
К несчастью для voice syncers, соединение с etcd, на котором работает механизм обнаружения наших Elixir-сервисов, тоже проходит через эти перегруженные супервизоры. Мы распределяем процессы по узлам в зависимости от их положения в кольце консистентного хеширования, которое опирается на etcd – распределённое хранилище «ключ – значение». Каждый экземпляр периодически сообщает etcd о своём состоянии. Из-за этой связанности обновление регистрации сервиса тоже могло блокироваться по мере роста почтового ящика супервизора. В результате экземпляры voice syncers исчезали из etcd после истечения 60-секундного времени жизни TTL.
Проблемы в аудио- и видеосервисах начались в 12:13, когда суммарная длина почтовых ящиков процессов на всех 15 экземплярах voice syncers стала расти, а число RPC-вызовов к SFU резко упало.


Поскольку все хосты «выпали» из кольца консистентного хеширования, перестали принимать новые процессы-синхронизаторы и больше не могли выполнять RPC-запросы, можно с достаточной уверенностью заключить, что DynamicSupervisor пула Holster.Pool был перегружен на всех наших экземплярах.
Чтобы подтвердить эту гипотезу, мы воспользовались Recon – отличным инструментом для диагностики проблем в production-системах на Elixir и Erlang. Связанная с ним книга Erlang in Anger практически обязательна к прочтению: рано или поздно каждый дежурный инженер начинает лихорадочно листать её страницы. Запустив Recon, мы сделали снимок работающих процессов Erlang и отсортировали их по длине очереди в почтовом ящике. Угадайте, кто оказался на первом месте? Наш старый знакомый – DynamicSupervisor пула Holster.
Одному экземпляру, 2-8, повезло наверстать отставание, очистить почтовый ящик и повторно зарегистрироваться в etcd. Из-за конфигурации нашего хеш-кольца один здоровый экземпляр может обрабатывать 3/15 всего глобального трафика синхронизаторов: каждой сущности в наших системах назначаются основной, вторичный и третичный экземпляры. Именно этот единственный выживший экземпляр вызвал всплеск успешного выбора конечных точек и создания RPC-запросов. Но для полного восстановления обслуживания глобального трафика потребовалось дополнительное вмешательство.
Поймать синхронизаторы на крючок: возвращаем голосовую связь в строй
Запросив данные из etcd, мы увидели, что большинство экземпляров voice syncers больше не регистрировались и выпали из хеш-кольца. Новые процессы-синхронизаторы они уже не принимали, но при этом продолжали удерживать большую часть процессов, созданных до начала инцидента. Мы не хотели затронуть ещё работающие звонки, поэтому сначала попробовали точечно восстановить отдельные экземпляры.
В 12:43 мы полностью перезапустили приложение voice syncers на экземпляре 2-13. В 12:47 завершили и заново запустили DynamicSupervisor пула Holster.Pool на 2-1 после того, как модуль recon для Erlang подтвердил, что в его почтовом ящике накапливаются сообщения.
В обоих случаях экземпляры ненадолго восстановились: снова зарегистрировались в etcd, быстро приняли новые процессы voice syncers и успели отправить несколько RPC-сообщений на SFU.
![Линейный график для «Синхронизаторов голоса по экземплярам». Line chart for “Voice Syncers by Instance.” [CONT]](https://habrastorage.org/r/w1560/getpro/habr/upload_files/f92/442/9f7/f924429f7d281c7ab5b058baae80b4c9.png)
Поскольку большинство экземпляров отсутствовало в кольце, здоровые экземпляры принимали дополнительный трафик, выступая вторичными и третичными резервными узлами для неработоспособных. Из-за огромного притока новых синхронизаторов, а в случае 2-1 ещё и ожидавших повторного выполнения RPC-запросов к уже существующим voice syncers, почтовые ящики почти сразу снова начали расти, и система вернулась в похожее состояние отказа.

Поняв, что точечное восстановление не помогает, мы решили, что наибольшие шансы даст одновременный запуск максимального числа здоровых экземпляров. В 13:05 мы попытались перезапустить весь кластер voice syncers. С точки зрения аудио- и видеоинфраструктуры это фактически «перезапускает Discord» и быстро воссоздаёт миллионы активных звонков. Экземпляры перезапустились, часть первых RPC-соединений успешно установилась, и некоторые звонки вышли из состояния «Ожидание конечной точки».

Но и на этот раз восстановление продлилось недолго. Лавина запросов при холодном запуске оказалась слишком мощной, и к 13:09 почтовые ящики всех перезапущенных узлов снова непрерывно росли. На разных экземплярах сбой проявлялся по-разному, в зависимости от того, успел ли перегрузиться супервизор Holster.Pool, но в итоге все они потеряли возможность создавать новые соединения и требовали дальнейшего вмешательства.
Стало ясно, что нужно как-то замедлить поток исходящих HTTP-запросов от перезапущенных экземпляров voice syncers. Мы пошли сразу по двум направлениям: начали настраивать существующее ограничение частоты создания синхронизаторов и одновременно расширять кластер voice syncers.
У нас уже действовало ограничение частоты создания voice syncers в сервисах, управляющих звонками, но во время устранения инцидента выяснилось, что в разных сервисах оно либо устарело, либо вообще не было задано. Особенно бесполезным это ограничение оказалось в guild’ах: оно контролировало только запуск «координатора» voice syncer для каждого guild’а, но никак не ограничивало создание дочерних синхронизаторов для отдельных голосовых каналов. А каждый такой синхронизатор должен выбрать как минимум одну конечную точку SFU, к которой ему предстоит подключиться.
Пока аудио- и видеоинфраструктура не перейдёт на Kubernetes по пути, который уже проложила команда инфраструктуры реального времени, расширение кластера voice syncers остаётся довольно ручным и, к сожалению, медленным процессом. Экземпляры работают на виртуальных машинах GCP, описанных в Terraform, настраиваются через Salt, а затем вручную добавляются в etcd как кандидаты на включение в кольцо.
Сначала были готовы изменения ограничений частоты. К 13:43 в сервисах calls, streams и guilds уже действовали низкие лимиты на создание синхронизаторов, после чего мы снова попытались перезапустить кластер voice syncers. Сценарий в основном повторил перезапуск в 13:09: сначала система начала восстанавливаться, а затем почтовые ящики снова стали непрерывно расти. Но на этот раз было одно важное отличие: на всех экземплярах стабильно перегружался супервизор gun, а не Holster.Pool. Благодаря этому экземпляры могли получать из пула соединения, созданные до того, как супервизор gun окончательно отстал. Все перезапущенные экземпляры оставались зарегистрированными в etcd, сохраняли назначенные им voice syncers и успешно отправляли RPC-вызовы к SFU хотя бы для части синхронизаторов.

Пока мы ждали, когда Salt завершит подготовку 15 новых экземпляров voice syncers, частичное улучшение после перезапуска в 13:43 придало нам уверенности, и мы снова перешли к точечному восстановлению отдельных экземпляров.
На этот раз некоторые точечные перезапуски наконец сработали и позволили полностью восстановить отдельные экземпляры. В 14:03 мы перезапустили приложение voice syncers на экземпляре 2-3. Он без проблем принял назначенные ему синхронизаторы, суммарная длина почтовых ящиков оставалась небольшой, а новые и существующие соединения стабильно устанавливались. Мы связываем этот успех сразу с несколькими факторами: сниженные лимиты замедлили создание синхронизаторов, общее число глобальных синхронизаторов уменьшилось из-за спада нагрузки и продолжающегося инцидента, а более здоровому кластеру реже приходилось назначать вторичные и третичные экземпляры.


К 14:15 мы перезапустили пять экземпляров, и четыре из них полностью восстановились. Одновременно с этим в строй вошли ещё 15 экземпляров, удвоившие ёмкость кластера. Они успешно приняли синхронизаторы без накопления сообщений в почтовых ящиках. Благодаря этому число синхронизаторов на один экземпляр сократилось вдвое, а вероятность успешного восстановления оставшихся узлов выросла. К 14:26 последний перегруженный экземпляр был перезапущен, и удвоенный кластер полностью вернулся в здоровое состояние.


Что мы улучшили
Каждый инцидент позволяет узнать о системе что-то важное, а наша задача как инженеров – применить эти знания на практике. Сразу после завершения инцидента уровня SEV мы начали расследование, провели постмортем и внесли в системы несколько изменений.
Начнём с первопричины сбоя. Для наших Elixir-нагрузок мы добавили в Kubernetes-кластер проверяющий admission webhook. Корректная разгрузка сущностей критически важна для доступности Discord, поэтому теперь перед уменьшением масштаба нагрузки мы проверяем, что каждый под действительно разгружен. Вместо немедленного завершения подов обновление отклоняется до тех пор, пока под не передаст свои процессы другому узлу. Пока миграция на Kubernetes идёт очень успешно: мы заметно повысили надёжность развёртываний и сократили объём ручной работы. Мы редко уменьшаем масштаб, но такая проверка позволяет работать безопаснее, а значит, и быстрее.
Однако даже с более безопасным уменьшением масштаба последствия для голосовых систем оказались слишком серьёзными. Чтобы в будущем ограничить ущерб, мы атаковали узкое место в voice syncers сразу с двух сторон. После нагрузочного тестирования мы повысили пропускную способность, заменив супервизор Holster.Pool на PartitionSupervisor. Теперь запросы к пулам соединений распределяются между несколькими независимыми супервизорами, а исходящие соединения можно создавать и получать из пула параллельно.
Это помогает избежать конкуренции за ресурс, которую мы наблюдали во время инцидента, когда поток входящего трафика от вышестоящих сервисов блокировал обновление данных в etcd. Кроме того, управление жизненным циклом соединений gun мы перенесли в создающий их Holster.Pool, полностью избавившись от необходимости использовать отдельный супервизор соединений gun.
Мы также перенастроили ограничения частоты в вызывающих сервисах, обращающихся к voice syncers. Теперь они срабатывают точнее, а при необходимости их можно принудительно задействовать с помощью механизма сброса нагрузки, который запускает повторное создание voice syncers. Кроме того, мы добавили и настроили новые ограничения частоты выбора конечных точек внутри voice syncers. Это ещё один способ сдерживать создание исходящих соединений. Благодаря большему параллелизму и меньшей входящей нагрузке на голосовые системы во время подобных инцидентов мы теперь лучше готовы к резким скачкам трафика.
Во внутреннем постмортеме мы спрашивали себя не только о том, как можно было избежать проблемы, но и о том, как после её возникновения устранить её быстрее. Мы серьёзно улучшили наблюдаемость. Предотвращение инцидента это, безусловно, хорошо, но сокращение времени восстановления поможет быстрее справляться и с будущими сбоями, даже если они будут совсем другого рода.
В рамках описанных выше изменений мы добавили мониторинг почтовых ящиков, связанных с пулами HTTP-соединений. Теперь проще определить, когда они действительно становятся проблемой. Мы также тщательно пересмотрели и расширили инструментирование и логирование регистрации сервисов в механизме обнаружения, а также RPC-трафика между voice syncers и SFU. Это даёт гораздо более полное представление обо всех обязанностях voice syncers.
В перспективе мы не хотим решать одну и ту же проблему отдельно в каждом сервисе. Следуя идее GenServer как универсального серверного процесса, мы хотим столь же универсально подойти к мониторингу Elixir. Вместо общих косвенных метрик вроде совокупного состояния почтовых ящиков мы продолжаем развивать наблюдаемость и расширяем мониторинг наиболее критичных процессов во всех сервисах. Параллельно всё больше нагрузок будет переезжать в Kubernetes. Масштабировать поды одной командой гораздо эффективнее, чем вручную подготавливать виртуальные машины.
Команда аудио- и видеоинфраструктуры уже активно работает над серьёзной переработкой архитектуры, включая долгосрочные решения для части проблем, выявленных во время этого инцидента. Горизонтальное масштабирование сервисов управляющего контура между несколькими регионами повысит надёжность и региональную отказоустойчивость, а всплески исходящих соединений будут равномернее распределяться по миру.
Ограничения подсказывают решение
Наша боль может принести пользу не только нам. Наблюдать за инцидентом в чужой продакшен-системе – беспроигрышный вариант: самому переживать сбой не приходится, но его уроками всё равно можно воспользоваться.
Достаточно большой скачок трафика обязательно найдёт узкое место в вашей системе. Возможно, вы заранее знаете, где оно находится. Возможно, только подозреваете. А может быть, столкнётесь с совершенно новым сценарием деградации. Во время прошлых инцидентов с высокой нагрузкой восстановление у нас обычно упиралось в различные базы данных ScyllaDB. В прошлом году мы реализовали механизм, который при перегрузке автоматически и точечно снижает нагрузку на API с помощью circuit breaker. Во время этого инцидента базы данных отработали отлично, а узкое место переместилось в голосовые сервисы.
Столкнувшись с ограничением ёмкости, вы фактически решаете экономическую задачу: либо увеличиваете предложение, добавляя новые экземпляры сервисов, либо сокращаете спрос, ограничивая входящий трафик. Обнаружив такое ограничение, мы стараемся прямо спросить себя: как увеличить предложение или снизить спрос? Этот вопрос направляет и устранение инцидента, и выбор задач по итогам постмортема.
Во время инцидента также важно хорошо знать свои инструменты. Elixir и виртуальная машина BEAM дают инженерам множество возможностей для анализа работающей системы: можно получить состояние любого процесса или даже установить хотпатч, добавив дополнительную наблюдаемость без остановки. Во время этого инцидента мы даже точечно перезапустили только супервизор пула Holster на одном конкретном узле. Это не помогло. Но возможность видеть содержимое почтовых ящиков каждого процесса позволила нам построить более детальный мониторинг на будущее.
Наконец, при работе с распределёнными системами необходимо заранее продумывать сценарии отказа. Из-за однопоточной модели Elixir с передачей сообщений мы всегда помним, что деградация критического цикла обработки одного процесса может повлиять не только на сервис, но и непосредственно на пользовательский опыт. Это понимание направляет проектирование наших систем, а инциденты, в которых мы отступаем от этого принципа, сразу показывают, что именно нужно улучшить.
Ограничение частоты, отбрасывание входящих сообщений, жёсткие тайм-ауты или даже запрет синхронных вызовов в самых загруженных процессах – такой сценарий отказа всегда остаётся в центре внимания. Наши инструменты, например семафоры на стороне вызывающих сервисов, помогают восстановлению, даже если не могут напрямую устранить саму причину.

Каскадные сбои редко укладываются в одну причину: ошибка при развёртывании быстро упирается в ограничения архитектуры, перегруженные сервисы и неподготовленные сценарии восстановления. Разобраться в этих связях подробнее можно на бесплатных демо-уроках:
10 августа, 20:00. «Kubernetes + CI/CD + GitOps — делаем стабильный деплой без выхода из кластера». Записаться
12 августа, 20:00. «Паттерны отказоустойчивости и масштабируемости микросервисной архитектуры». Записаться
Полный список бесплатных уроков августа смотрите в дайджесте.

