Основные выводы
Событийно-ориентированные архитектуры часто ломаются под нагрузкой из-за повторных попыток, обратного давления и задержек при старте, особенно во время пиковых нагрузок.
Задержка не всегда является основной проблемой; отказоустойчивость зависит от слаженной работы всех компонентов системы, включая очереди, потребителей и механизмы наблюдаемости.
Паттерны, такие как перемешивающее шардирование (или шаффл-шардинг, от англ. shuffle sharding), предварительное выделение ресурсов и быстрые сбои, значительно повышают долговечность и экономичность.
Обычно ошибки включают проектирование под средние нагрузки, неправильно настроенные повторные попытки и одинаковое обращение с различными событиями.
Проектирование с учётом отказоустойчивости означает прогнозирование крайних операционных ситуаций, а не просто оптимизацию для идеальных сценариев.
Событийно-ориентированные архитектуры (EDA) выглядят перспективно на бумаге, так как они обеспечивают разделение продюсеров и консюмеров, масштабируемость и чистые асинхронные потоки. Но реальные системы гораздо сложнее.
Представьте себе такую распространённую ситуацию: во время распродажи на «Чёрную пятницу» ваша система обработки платежей получает в пять раз больше трафика, чем обычно. В этот момент serverless-архитектура сталкивается с крайними случаями. Например, функции Lambda начинают запускаться холодно, ваш сервис очередей (SQS) заполняется, а при этом в DynamoDB происходит троттлинг. В этом хаосе заказы клиентов начинают сбоить. Это не гипотетическая проблема, а реальная задача для многих команд.
И это касается не только eCommerce. В SaaS платформах запуск новых фичей приводит к пикам в конфигурации серверов. В FinTech бизнесе, где наблюдается огромный приток событий при мошеннической активности, даже несколько миллисекунд могут сыграть большую роль. Подобные примеры можно найти и в повседневной жизни (популярные телепередачи, прямые трансляции таких событий, как Суперкубок).
Если взглянуть на систему в высокоуровневом представлении, она состоит из трёх частей: продюсера, промежуточного буфера и консюмера.

Когда мы говорим об устойчивости таких систем, речь идёт не только о непрерывной работе, но и о сохранении предсказуемости под давлением. Пиковые нагрузки могут быть вызваны интеграциями с внешними системами или узкими местами на стороне потребителей или компонентами, выполняющими бесконечные повторные попытки — всё это проверяет, насколько ваша архитектура способна справляться с нагрузкой. Но реальные системы часто ведут себя по-своему.
В этой статье мы расскажем, как проектировать отказоустойчивые и масштабируемые системы обработки событий. Мы рассмотрим различные операционные события, которые нарушают надёжность и масштабируемость, и используем эти знания для проектирования лучших систем.
Задержка — не единственная проблема
Часто, когда люди говорят о производительности в событийно-ориентированных системах, они имеют в виду задержку. Однако они забывают, что задержка — это лишь часть проблемы. Для отказоустойчивых систем важны не только задержка, но и пропускная способность, эффективное использование ресурсов и передача данных между компонентами.
Рассмотрим следующий пример. У вас есть сервис, чья инфраструктура зависит от SQS-очереди. Внезапно происходит всплеск трафика, который перегружает downstream-системы, что приводит к частичному или полному отказу. Этот отказ вызывает увеличение повторных попыток, а затем искажает метрики мониторинга. Кроме того, если у вашего консюмера длительное время запуска, будь то из-за холодного старта или времени загрузки контейнера, возникает конкуренция между сообщениями, которые требуют быстрого обработки, и инфраструктурой, которая всё ещё готовится. Если подумать, то режим отказа не является таймаутом. Проблема заключается в некорректной настройке, которая вызывает задержки, повторные попытки и увеличение затрат для клиентов.
Теперь давайте добавим в систему очереди мёртвых писем (DLQ), экспоненциальное затухание, политику троттлинга или разделение потоков, и проблема становится ещё более сложной. Вместо того чтобы отлаживать функцию, вам нужно понять, что за контракты существуют и что может происходить.
Для проектирования с учётом отказоустойчивости мы должны воспринимать задержку как сигнал о накоплении давления в системе. Задержка — это не только то, что нужно минимизировать. Необходима смена мышления.
Учитывая всё это, давайте рассмотрим несколько практических подходов, которые могут помочь решить выявленные проблемы.
Паттерны, которые помогают системам масштабироваться под давлением
Говоря об откзоустойчивости, мне бы хотелось, чтобы вы думали не только о таких вещах, как уменьшение задержки, настройка повторных попыток или снижение числа сбоев. Рассматривайте проектирование системы, которая устойчиво деградирует при столкновении с новым сценарием и восстанавливается автоматически. Давайте обсудим некоторые из этих паттернов на различных уровнях архитектуры.
Паттерны проектирования
Шардирование и перемешивающее шардирование
Одним из основных принципов построения отказоустойчивых систем является их элегантное деградирование с минимизацией разрушительных последствий. Один из способов достичь этого — сегментировать клиентов и убедиться, что проблемный клиент не выведет из строя всю систему. Вы можете развить свою архитектуру, добавив перемешивающее шардирование. Это означает назначение клиентов на случайный подмножество шардов, что снижает вероятность того, что хорошо себя ведущие клиенты столкнутся с шумными клиентами. Асинхронные системы, поддерживаемые очередями, например, часто хэшируют всех своих клиентов на несколько очередей. Когда шумный клиент появляется, он перегружает очередь, что, в свою очередь, влияет на других клиентов, хэшированных в ту же очередь. Применяя перемешивающее шардирование, шанс, что шумный клиент попадёт в тот же шард, что и другой, значительно снижается, а изолированный сбой минимизирует воздействие на остальных.
Предварительное выделение ресурсов для нагрузок с высокой чувствительностью к задержке
Предварительное выделение ресурсов означает заранее бронирование необходимых мощностей. Это похоже на предварительное бронирование EC2; однако это связано с дополнительными затратами, поэтому следует быть осторожным. Не все рабочие нагрузки требуют выделенной параллельности (provisioned concurrency), но некоторые могут. Например, в индустрии FinTech системы обнаружения мошенничества часто зависят от сигналов в реальном времени, поэтому, если мошенническая транзакция не будет обработана за несколько секунд, это может оказать серьёзное влияние на всю систему. В таком случае необходимо выявить те пути, где каждая секунда критична, и инвестировать в их оптимизацию. Можно сделать это более экономичным, используя авто-масштабирование с выделенной параллельностью, чтобы сделать это более экономичным, если нагрузка изменяется резко и если время имеет значение.
Паттерны инфраструктуры
Разделение с использованием очередей и буферов
Отказоустойчивые системы поглощают нагрузку, а не отказываются от неё. Очереди, такие как SQS, Kafka и Kinesis, а также буферы, такие как EventBridge, действуют как поглотители нагрузки между продюсерами и консюмерами. Они защищают потребителей от резких всплесков и предлагают естественную семантику повторных попыток и повторной отправки сообщений.
С Amazon SQS доступны мощные настройки, такие как тайм-аут видимости для контроля поведения повторных попыток, перегрузка сообщений для повторной обработки, DLQ для изоляции сообщений с ошибками и пакетирование/долгий опрос для повышения эффективности и снижения затрат. Если вам нужно гарантированное упорядочивание и обработка сообщений один раз, то лучше подходят FIFO очереди. Аналогично, Kafka и Kinesis обеспечивают высокую пропускную способность через разделение на партиции, сохраняя порядок записей в каждой партиции.
Например, система реального времени для торгов в рекламной платформе разделяет данные о кликах с высокой нагрузкой через Kinesis, используя идентификатор региона для шардирования. С другой стороны, события выставления счетов направляются через FIFO очереди для гарантии порядка и предотвращения дублирования платежей (особенно при повторных попытках). Этот паттерн гарантирует, что каждый тип нагрузки может масштабироваться или выходить из строя без взаимного воздействия на другие.
Операционные паттерны
Быстрый сбой и выход из строя
Это не только принцип инженерии Meta/Facebook, но и подход, основанный на мышлении с фокусом на отказоустойчивость. В этом контексте, если ваш потребитель понимает, что он в затруднении (например, не может подключиться к базе данных или получить конфигурацию), следует быстро завершить операцию с ошибкой. Это помогает избежать тайм-аутов видимости, повторных попыток из-за записи с защитным механизмом и сигнализирует платформе о необходимости приостановить операции быстрее, а не позже. Я однажды отлаживал проблему, когда контейнеризированный консюмер зависал на вызове аутентификации в базе данных на 30 секунд. После того как мы добавили время ожидания в 5 секунд и явное сигнализирование об ошибке, ошибки с тайм-аутом видимости уменьшились, а повторные попытки больше не добавлялись к сбою. Такие случаи встречаются довольно часто. Другой распространённый пример — когда обработка сообщений в начале очереди происходит без строгого тайм-аута, что ведёт к накоплению отставания в очереди. Этот паттерн не направлен на агрессивную работу системы, а на её предсказуемость и восстановимость.
Другие инструменты проектирования, такие как использование пакетирования и интервалов опроса для уменьшения нагрузки или ленивой инициализации, чтобы избежать загрузки больших зависимостей, когда они не нужны, могут быть полезными для улучшения общей отказоустойчивости.
Распространённые ошибки (и как с ними бороться)
Отказоустойчивые системы часто отказывают не из-за одного крупного сбоя, а из-за медленного накопления архитектурного долга. Эта идея прекрасно описана в статье, которую я читал пару лет назад, где отлично объяснялось, что такое метастабильные системы и когда они ломаются, вызывая катастрофические последствия. В статье конкретно обсуждалось, как система переходит из стабильного состояния в уязвимое под нагрузкой, а затем в метастабильное состояние, где наблюдаются долговременные последствия, прежде чем обычно потребуется вмешательство вручную. Я не буду углубляться в подробности, но хочу отметить, что это связано с изменением подхода, которое помогает избежать серьёзных сбоев в сервисах.
Давайте рассмотрим некоторые характеристики, которые ведут к этому.
Переоценка средней нагрузки вместо учёта резких изменений
Трафик в реальном мире редко бывает плавным; чаще всего он непредсказуем. Если вы настраиваете размеры батчей, память или параллельность под 50-й процентиль, ваша система выйдет из строя при нагрузке на 90-й процентиль и выше. Даже хорошо спроектированная система может сломаться под давлением, если она не подготовлена к непредсказуемым нагрузкам и не может их поглотить. Вопрос не в том, случится ли сбой, а когда именно; ключевое здесь — быть готовым к сбоям. В большинстве случаев к таким ситуациям можно подготовиться. Рассмотрим случай с рабочими нагрузками, чувствительными к задержке, которые обрабатываются через функции AWS Lambda. Вы можете настроить политику авто-масштабирования для корректировки конфигурации выделенной конкурентности, анализируя различные метрики в CloudWatch, такие как ошибки вызова, задержка или глубина очереди. Также можно провести нагрузочное тестирование в вашей тестовой среде, чтобы протестировать сценарии для более высоких перцентилей (p95, p99).
Отношение к повторным попыткам как к панацее
Повторные попытки дешёвы, пока они не становятся дорогими. Если повторные попытки — это ваша единственная защита, этого может быть недостаточно. Они также могут умножить ошибку. Повторные попытки могут перегрузить downstream-системы; создать невидимые петли трафика очень просто, если логика повторных попыток не является умной. Такая логика повторных попыток часто встречается в системах, где каждая ошибка, временная или нет, повторяется без ограничений, задержек и контекстной осведомлённости. Этот подход приводит к проблемам, таким как троттлинг баз данных, увеличению задержки и даже полному сбою системы, что, к сожалению, часто бывает.
Вместо этого, вам нужны ограниченные повторные попытки, чтобы избежать бесконечных петель ошибок, или если вы повторяете попытки, используйте экспоненциальное затухание с джиттером, чтобы избежать конкуренции. Вместе с этим подходом важно всегда учитывать контекст. Разделяйте ошибки на те, которые можно повторить, и на те, которые нельзя, и повторяйте попытки умно. Когда upstream-система выходит из строя, не стоит продолжать грузить сеть с той же скоростью. Это не поможет ускорить восстановление сервиса и может наоборот привести к его замедлению из-за дополнительного давления, вызванного повторными попытками.
Невозможность учитывать наблюдаемость
Предположение, что ваша система отказоустойчива, — это одно, а уверенность в её отказоустойчивости — совсем другое. Я часто напоминаю командам, что
«Наблюдаемость помогает отделить намерения от реальности».
Вы можете намереваться сделать свою систему отказоустойчивой, но только наблюдаемость подтверждает, действительно ли это так. Недостаточно просто мониторить метрики времени отклика или ошибки. Отказоустойчивые системы должны иметь чёткие индикаторы устойчивости, которые выходят за рамки поверхностного мониторинга. Эти индикаторы должны задавать более глубокие вопросы. Как быстро вы обнаруживаете сбой и восстанавливаетесь после него? Элегантно ли система завершает сбой? Влияние сбоя ограничено одним пользователем, зоной доступности или регионом? Помогают ли повторные попытки или они просто скрывают настоящую проблему? Как система справляется с обратным давлением или сбоями upstream-систем? Это высокоуровневые сигналы, которые проверяют вашу архитектуру под нагрузкой; они имеют смысл только в совокупности, а не по отдельности.
Эти идеи можно реализовать с помощью метрик CloudWatch, таких как глубина очереди, Log Insights для анализа паттернов повторных попыток и X-ray для отслеживания потока запросов между сервисами. Например, в одном случае система клиента работала нормально, пока ошибка Lambda не начала незаметно отправлять сообщения в DLQ. Всё казалось нормальным, пока пользователи не сообщили о пропавших данных. Проблема была обнаружена только через несколько часов, потому что никто не настроил оповещение по размеру DLQ. После этого команда добавила оповещения по DLQ и интегрировала их в панель мониторинга внутренних SLO.
Наблюдаемость — это единственный инструмент, который позволяет задать вопрос: «Выполняет ли система то, что я ожидаю, даже под нагрузкой?» Если ответ «Я не знаю», значит, пришло время улучшить подход!
Одинаковое обращение со всеми событиями
Не все события одинаковы. Событие подтверждения оплаты — это не то же самое, что событие логирования. Если ваша архитектура обрабатывает их одинаково, вы либо тратите ресурсы впустую, либо рискуете столкнуться с проблемами. Рассмотрим пример, когда событие подтверждения оплаты оказывается за сотнями событий логирования низкого приоритета в очереди, влияя на результаты бизнеса. Ещё хуже, если эти низкоприоритетные события по какой-то причине будут повторно обработаны, из-за чего пострадают критические события. Вам нужен способ различать критические и низкоприоритетные события.
Установите разные очереди для событий высокого и низкого приоритета, либо правила маршрутизации событий, которые направляют эти события в два разных Lambdas. Эта фильтрация также поможет использовать режим provisioned только для очереди с высоким приоритетом, что повысит экономичность. Команды часто обнаруживают такие проблемы слишком поздно, когда происходят скачки затрат, повторные попытки выходят из-под контроля или нарушаются SLA. Но с правильными сигналами и архитектурным подходом большинство проблем можно избежать или хотя бы восстановиться предсказуемо.
Заключительные мысли
Когда мы проектируем событийно-ориентированные системы в больших масштабах, отказоустойчивость — это не об избегании сбоев, а об их принятии. Мы не стремимся создать мифическую «совершенную» систему. Вместо этого мы строим системы, которые могут выдержать удар и продолжать работать.
Подумайте об этом: надёжные механизмы повторных попыток, которые не приводят к сбоям по всей системе, эластичность, которая поглощает всплески трафика без сбоев, и режимы отказа, которые предсказуемы и управляемы. Это и есть цель. Но если вы только начинаете, построить отказоустойчивую систему может показаться сложной задачей. С чего начать?
Начните с малого! Попробуйте создать простое событийно-ориентированное приложение с использованием Amazon SQS и AWS Lambda. Не пытайтесь сделать что-то сложное сразу. Просто простая очередь и функция Lambda. Когда это будет работать, исследуйте другие возможности, такие как DLQ, обработка сбоев и т. д. Вы можете использовать EventBridge Event Bus и узнать, как маршрутизировать события к разным целям с помощью правил. Когда освоитесь, добавьте такие техники, как перемешивающее шардирование и авто-масштабирование выделенной параллельности с использованием метрик.
Создание отказоустойчивости — это не просто шаг, а целый подход. Начните с малого, учитесь на том, как ведёт себя ваша система, и постепенно добавляйте сложность по мере роста уверенности.
Проектирование отказоустойчивых и масштабируемых систем — не просто задача, а постоянный процесс, требующий точных инструментов и подходов. Если вы сталкиваетесь с проблемами, когда нагрузка или асинхронность становятся узким местом, рекомендуем посетить открытые уроки — на них резберем лучшие практики для повышения стабильности и производительности:
14 июля в 20:00 — Мониторинг распределённых систем
Как правильно мониторить распределенные системы, чтобы предотвратить сбои под давлением.29 июля в 20:00 — Асинхронная обработка данных в высоконагруженных системах
Асинхронность как решение для масштабируемых и отзывчивых сервисов.30 июля в 19:00 — Apache Kafka в микросервисной архитектуре — лучшие практики асинхронного обмена
Как Kafka может улучшить асинхронный обмен сообщениями и повысить отказоустойчивость.
Тем, кому интересно системное повышение скиллов, рекомендуем пройти вступительный тест по курсу DevOps практики и инструменты — узнаете, достаточно ли вашего текущего уровня знаний для поступления на курс.