Асинхронная обработка данных в высоконагруженных системах
Асинхронная обработка — способ организовать систему так, чтобы долгая работа не блокировала ответ пользователю.
Клиент получает быстрое подтверждение ("задача принята"). Тяжёлую часть система выполняет позже:
- в фоновом процессе (воркер);
- через очередь сообщений;
- с уведомлением по webhook;
- при следующем запросе статуса (polling).
Высоконагруженная система — это программно-аппаратный комплекс, который должен обрабатывать огромное количество запросов в единицу времени с высокой производительностью, минимальной задержкой и максимальной доступностью. Такие системы характеризуются требованиями к обработке сотен тысяч или миллионов запросов в секунду, работе с петабайтами данных, обеспечением времени отклика в миллисекунды и непрерывной доступностью 24/7. Примерами высоконагруженных систем служат поисковые системы, платежные платформы, социальные сети, системы онлайн-бронирования, биржевые платформы и крупные интернет-магазины в дни распродаж. Проектирование таких систем требует особого подхода к архитектуре, включающего горизонтальное масштабирование, распределенные вычисления, кэширование, асинхронную обработку, отказоустойчивость и тщательное управление ресурсами. Каждое принятое архитектурное решение в высоконагруженной системе должно быть обосновано с точки зрения производительности и масштабируемости, а цена ошибки чрезвычайно высока.
Асинхронная обработка данных в высоконагруженных системах — это подход к построению систем, при котором операции, требующие значительного времени или ресурсов, выполняются без блокировки основного потока выполнения, позволяя системе обрабатывать множество запросов параллельно и эффективно использовать доступные ресурсы. Вместо того чтобы заставлять клиента ждать завершения длительной операции, система немедленно возвращает подтверждение о принятии запроса, а сама операция выполняется в фоновом режиме, часто с использованием очередей, брокеров сообщений и пулов воркеров. Такой подход критически важен для высоконагруженных систем, где синхронная обработка с ожиданием завершения операций привела бы к быстрому исчерпанию ресурсов, росту времени отклика и отказ в обслуживании. Асинхронная обработка позволяет системе выдерживать пиковые нагрузки, эффективно распределять нагрузку между доступными вычислительными ресурсами и обеспечивать предсказуемое время отклика даже в условиях перегрузки.
Типичные примеры фоновой работы:
- отправка email и push-уведомлений;
- генерация PDF-отчёта или выгрузки;
- обработка и сжатие видео;
- перенос событий в аналитику (ClickHouse, Elasticsearch);
- пересчёт рекомендаций и поискового индекса.
Соседние материалы:
- 12 концепций распределённой архитектуры — краткая шпаргалка;
- System Design — карта тем — порядок изучения;
- Email-рассылка как распределённая система — сквозной кейс с outbox и webhooks;
- Асинхронная коммуникация — протоколы и брокеры в продакшене;
- Брокеры сообщений — ack, retry, DLQ;
- Асинхронность в коде — event loop,
async/await.
Ключевые термины
Перед разбором архитектуры — словарь, который встретится дальше по тексту.
| Термин | Простыми словами | Подробнее |
|---|---|---|
| API | Программный интерфейс, через который клиент (браузер, мобильное приложение, другой сервис) вызывает ваш backend | Основы интеграционного взаимодействия |
| Воркер (worker) | Отдельный процесс или сервис, который забирает задачи из очереди и выполняет тяжёлую логику | § воркеры |
| Брокер сообщений | Промежуточное хранилище между отправителем и исполнителем задачи (RabbitMQ, Kafka, SQS) | Брокеры |
| Очередь (queue) | Список задач: каждую задачу обычно выполняет один воркер | §4 в 12 концепциях |
| Топик (topic) | Канал для рассылки события нескольким подписчикам | Pub/Sub |
| Publish | Отправить сообщение в брокер | Асинхронная коммуникация |
| Ack (acknowledgment) | Подтверждение воркером, что задача обработана; до ack брокер может отдать задачу снова | Брокеры § ack |
| DLQ (Dead Letter Queue) | Отдельная очередь для "битых" задач, которые не удалось выполнить после нескольких попыток | § DLQ |
| Outbox | Таблица в БД, куда в одной транзакции с бизнес-данными записывается намерение отправить задачу в очередь | § outbox |
| Идемпотентность | Повторный запуск даёт тот же результат, что и первый (важно при двойной доставке) | design/213 |
| Webhook | HTTP-запрос на URL клиента, когда задача завершена | Polling, SSE, Webhook |
| SLA | Договорённость о допустимой задержке и доступности сервиса | NFR в цифрах |
| p95 latency | В 95% случаев ответ быстрее этого порога; "хвост" медленных запросов виден именно здесь | Задержка и throughput |
| RPS | Requests per second — сколько запросов в секунду обрабатывает сервис | Масштабируемость |
| Backpressure | Очередь растёт быстрее, чем воркеры успевают разбирать задачи | § backpressure |
| Eventual consistency | Данные на разных узлах сходятся через короткое время после записи | PACELC |
Когда вводить асинхронность
Асинхронность и синхронный путь — это две противоположные парадигмы обработки операций. Синхронный путь подразумевает, что клиент отправляет запрос и ожидает его полного выполнения, блокируя свое выполнение до получения ответа, что создает прямую зависимость времени отклика от времени выполнения самой медленной операции в цепочке вызовов. Асинхронность же позволяет клиенту не дожидаться завершения операции, а получить подтверждение о принятии запроса и продолжать свою работу, при этом уведомление о результате приходит позже через колбэк, событие, вебхук или другие механизмы. В контексте высоконагруженных систем асинхронность является предпочтительным подходом, так как она позволяет развязать компоненты системы по времени, сглаживать пиковые нагрузки, эффективно использовать ресурсы и избегать каскадных отказов. Синхронный путь применяется в тех случаях, когда время выполнения операции гарантированно мало и предсказуемо, а также когда критически важно получить немедленный результат, например, при авторизации или проверке баланса счета.
Очередь добавляет отдельный компонент в инфраструктуру: кластер брокера, мониторинг, политику повторов, разбор "мёртвых" сообщений. Имеет смысл, когда синхронный путь упирается в измеримый предел.
| Симптом | Что происходит | Типичное решение |
|---|---|---|
| Растёт p95 API из-за редких долгих запросов | Пул потоков API занят генерацией отчётов по 30 с | Задача уходит в очередь; API отвечает за сотни миллисекунд |
| Пики трафика кладут все инстансы | Между всплеском запросов и обработкой нет буфера | Очередь как амортизатор + autoscaling воркеров (§12 в 141) |
| Внешний сервис медленный или нестабилен | Повтор внутри HTTP-запроса удлиняет ответ клиенту | Асинхронный вызов + callback или webhook |
| На одно событие реагируют несколько подсистем | Длинная цепочка синхронных HTTP-вызовов | Топик событий и несколько подписчиков |
| Задача должна пережить рестарт API | BackgroundTasks во фреймворке хранит работу только в памяти процесса | Персистентная очередь и подтверждение ack |
Пул потоков API — это механизм управления параллельным выполнением запросов к API, при котором заранее создается ограниченное количество потоков обработки для обслуживания входящих запросов, и каждый новый запрос назначается на один из свободных потоков из этого пула, а если все потоки заняты, запрос попадает в очередь ожидания или отклоняется. Пул потоков ограничивает максимальное количество одновременно выполняемых запросов, предотвращая исчерпание системных ресурсов и обеспечивая предсказуемое поведение системы под нагрузкой. Размер пула потоков критически важен: слишком маленький пул приведет к неэффективному использованию ресурсов и большим очередям, слишком большой — к чрезмерному переключению контекста и деградации производительности. В современных высоконагруженных системах пулы потоков часто заменяются асинхронными неблокирующими моделями с event loop, но понимание работы пулов потоков остается важным для диагностики проблем производительности, особенно в системах с блокирующими операциями ввода-вывода.
p95 или 95-й перцентиль — это статистическая метрика, показывающая, что 95 процентов всех запросов в системе выполняются быстрее указанного значения, а только 5 процентов запросов могут занимать больше времени или быть медленнее этого порога. Эта метрика гораздо полезнее среднего значения, так как она отражает реальный опыт большинства пользователей и позволяет выявлять проблемы с долгими хвостами распределения времени отклика. Например, если p95 время ответа равно 200 миллисекундам, это означает, что из ста запросов только пять обрабатываются дольше 200 миллисекунд, а остальные 95 выполняются быстрее. В системах с асинхронной обработкой p95 часто используется для мониторинга производительности и установки SLA с потребителями, а отклонение p95 от среднего значения может указывать на наличие узких мест, проблем с блокировками или неравномерное распределение нагрузки между воркерами.
Синхронный путь уместен, если:
- операция укладывается в SLA (например, меньше 300 мс) и UI ждёт результат сразу;
- нужна атомарность в одной БД без распределённого согласования;
- нагрузка мала, а очередь дороже редкого таймаута;
- команда пока не готова сопровождать брокер (мониторинг, runbook, дежурства — инциденты).
Перенос простого CRUD в очередь "для масштабируемости" без цифр в требованиях часто добавляет только задержку согласованности и усложняет отладку. Сначала зафиксируйте NFR и профиль нагрузки.
Воркер — это автономный процесс или поток выполнения, который извлекает задачи из очереди, обрабатывает их и сохраняет или передает результаты обработки дальше по цепочке, действуя как исполнитель в системе асинхронной обработки. Воркеры являются основными потребителями ресурсов процессора и памяти в высоконагруженных системах, и их количество, конфигурация и эффективность напрямую определяют пропускную способность системы. Воркеры могут быть как пулом потоков внутри одного процесса, так и отдельными микросервисами, работающими на разных физических или виртуальных машинах, что обеспечивает горизонтальное масштабирование. Каждый воркер обычно специализируется на определенном типе задач, имеет собственные настройки ресурсов, политики повторов обработки в случае ошибок и механизмы graceful shutdown для корректного завершения текущих задач при остановке. Эффективное управление воркерами включает мониторинг их загрузки, автоматическое масштабирование при изменении нагрузки и балансировку задач между ними.
Два уровня асинхронности
Слово "асинхронность" в IT означает два разных механизма. Их часто путают; на практике они дополняют друг друга.
| Уровень | Что происходит | Инструменты | Масштаб |
|---|---|---|---|
| Внутри одного процесса | Пока сервис ждёт ответа от сети или диска, он обрабатывает другие запросы | async/await, event loop, goroutines | Один инстанс API |
| Между процессами и сервисами | Задача сохраняется в брокере; API и воркер работают независимо | Очередь, топик, outbox | Кластер, несколько команд |
Асинхронность внутри одного процесса — это способ организации выполнения операций в рамках одного приложения или процесса, при котором длительные задачи не блокируют основной поток выполнения и позволяют обрабатывать другие запросы параллельно, используя механизмы, такие как асинхронные функции, колбэки или корутины. В рамках одного процесса асинхронность обычно реализуется с помощью неблокирующего ввода-вывода и event loop, когда поток не ждет завершения операции ввода-вывода, а переключается на другие задачи, возвращаясь к результату, когда операция завершится. Это позволяет одному потоку обслуживать тысячи одновременных соединений и операций, значительно повышая эффективность использования процессора и памяти. Классическим примером является модель Node.js с event loop, которая позволяет обрабатывать десятки тысяч запросов на одном процессоре, или асинхронные функции в Python с asyncio, которые дают похожие преимущества. Асинхронность внутри одного процесса кардинально меняет ментальную модель разработчика, требуя нового подхода к организации кода, обработке ошибок и управлению состоянием.
Асинхронность между процессами и сервисами — это взаимодействие различных независимо выполняющихся процессов или микросервисов через механизмы асинхронного обмена сообщениями, где отправитель не ждет немедленного ответа от получателя, а продолжает свою работу, зная, что сообщение будет обработано позже. Такой подход позволяет компонентам системы развиваться независимо, масштабироваться по отдельности и быть устойчивыми к временным сбоям других компонентов. Взаимодействие реализуется через очереди сообщений, брокеры, шины событий или вебхуки, гарантирующие доставку и обработку сообщений даже при временной недоступности получателя. Асинхронность между сервисами является фундаментом построения распределенных систем, так как она позволяет избежать временных зависимостей, упрощает реализацию отказоустойчивости и обеспечивает естественное разделение ответственности. Однако она также приносит дополнительные сложности, такие как обеспечение согласованности данных, обработка дублирующихся сообщений, управление порядком обработки и мониторинг состояния распределенных процессов.
Неблокирующий ввод-вывод в приложении
Ввод-вывод или I/O — это операции обмена данными между программой и внешними устройствами, файловой системой, сетью или другими программами, которые характеризуются значительными задержками по сравнению со скоростью обработки данных процессором. Операции ввода-вывода являются основным источником задержек в высоконагруженных системах и включают чтение и запись на диск, сетевые запросы, обращение к базам данных, взаимодействие с периферийными устройствами и работу с очередями сообщений. Время выполнения операции ввода-вывода может варьироваться от микросекунд для кэшированных операций чтения до сотен миллисекунд для сетевых запросов через интернет, что на несколько порядков превышает время выполнения процессорных операций. Именно поэтому в высоконагруженных системах критически важно правильно организовать работу с вводом-выводом, используя асинхронные и неблокирующие подходы, чтобы эффективно утилизировать процессорное время и обеспечить высокую пропускную способность.
Неблокирующий ввод-вывод — это подход к выполнению операций ввода-вывода, при котором поток выполнения не приостанавливается в ожидании завершения операции, а немедленно возвращает управление вызывающему коду, позволяя ему продолжить выполнение других задач, пока операция выполняется в фоновом режиме. При использовании неблокирующего ввода-вывода операционная система немедленно возвращает управление и уведомляет программу о завершении операции через специальные механизмы, такие как сигналы, колбэки или опрос статуса. Это позволяет одному потоку обрабатывать множество одновременных операций ввода-вывода, что критически важно для высоконагруженных серверных приложений, работающих с тысячами клиентов. Неблокирующий ввод-вывод лежит в основе современных высокопроизводительных фреймворков и языков, таких как Node.js, Netty, Java NIO, Python asyncio и Go, обеспечивая высокую масштабируемость и эффективное использование системных ресурсов.
Event loop или цикл событий — это механизм управления асинхронным выполнением кода, представляющий собой бесконечный цикл, который постоянно проверяет наличие новых событий, готовых к обработке, и выполняет соответствующие колбэки, поддерживая очередь задач и управляя очередностью их выполнения. Event loop является сердцем асинхронных систем и работает по принципу неблокирующего ввода-вывода, позволяя одному потоку обрабатывать множество параллельных операций без необходимости создания отдельных потоков для каждой из них. В цикле событий все операции ввода-вывода и таймеры запускаются и выполняются асинхронно, а по завершении их колбэки помещаются в очередь, ожидая своей очереди на выполнение в основном потоке. Эта модель позволяет достичь исключительной эффективности в системах с интенсивным вводом-выводом, таких как веб-серверы или прокси-серверы, и лежит в основе многих популярных технологий, включая JavaScript в браузерах и Node.js, а также фреймворков для Python, Ruby и других языков.
Event loop (цикл событий) — механизм в рантайме (Node.js, Python asyncio, Go), который переключается между запросами, пока один ждёт I/O. Это повышает RPS на одном сервере.
Ограничение: если работа нагружает CPU (рендер PDF, транскодинг видео), одного event loop мало — нужны отдельные воркеры или параллельные вычисления.
Типичная ошибка новичка: контроллер объявлен async, но внутри вызывается синхронная библиотека без пула потоков — event loop блокируется, выигрыша нет. Подробнее — асинхронное выполнение.
Очередь между компонентами
Очередь между компонентами — это структурный элемент распределенной системы, представляющий собой промежуточное хранилище для сообщений, передаваемых между различными компонентами, сервисами или процессами, работающее по принципу FIFO и обеспечивающее временную независимость отправителя и получателя. Очереди выступают в роли буфера, сглаживающего разницу в производительности компонентов, позволяя отправителю продолжать работу даже если получатель временно недоступен или перегружен. Использование очередей является ключевым паттерном для построения слабосвязанных систем, так как компоненты взаимодействуют только через стандартизированные сообщения, не имея прямой зависимости друг от друга. Очереди обеспечивают надежность доставки, гарантируя, что сообщение не будет потеряно и будет доставлено получателю даже в случае сбоев, и предоставляют дополнительные возможности, такие как приоритизация сообщений, маршрутизация, дедупликация и управление порядком обработки.
Сообщение записывается на диск брокера (или в реплицированный журнал вроде Kafka). Если API упадёт сразу после публикации, задача дождётся воркера. Это другой уровень надёжности, чем фоновая goroutine или setTimeout в памяти процесса.
На практике часто сочетают оба уровня:
- на входе — неблокирующий HTTP (stateless API);
- на выходе в тяжёлую подсистему — брокер и пул воркеров.
Типовой продакшн-контур
Продакшн-контур или production environment — это рабочая среда, в которой программное обеспечение функционирует в реальных условиях эксплуатации, обслуживая реальных пользователей и обрабатывая реальные данные, в отличие от тестовых, стейджинговых или разработочных сред. Продакшн-контур характеризуется высокими требованиями к надежности, производительности, безопасности и доступности, так как любые сбои или ошибки непосредственно влияют на бизнес-процессы и репутацию компании. Асинхронные системы в продакшн-контуре должны быть особенно тщательно сконфигурированы, иметь резервирование всех критических компонентов, быть устойчивыми к сбоям и иметь возможности быстрого восстановления. Мониторинг продакшн-контура включает сбор метрик производительности, отслеживание ошибок, аудит операций и оповещение о нештатных ситуациях, а все изменения в этой среде проходят строгую процедуру согласования, тестирования и rollback-планирования.
Схема повторяется в отчётах, рассылках, медиа и ETL:
Тот же каркас описан в типовом контуре system design, email-рассылке и очередях в 12 концепциях.
| Компонент | Роль | Если компонента нет |
|---|---|---|
| Stateless API | Принимает запрос, валидирует, фиксирует намерение | — |
| Primary DB | Источник истины по задаче и данным | Статус только в памяти воркера |
| Outbox / job table | Атомарность "данные + задача" | Задачи без записи или записи без задач |
| Брокер | Буфер, повторы, рассылка подписчикам | Потеря задач при рестарте API |
| Workers | Тяжёлая CPU/I/O логика | API не масштабируется по фону |
| Status store | pending → running → done / failed | Клиент не видит прогресс |
Stateless API — сервис без локального состояния сессии: любой инстанс может обработать запрос; данные — в БД или кэше (Redis).
Ключевые компоненты
Очереди и брокеры
Очереди и брокеры — это программные системы, предназначенные для организации надежного обмена сообщениями между компонентами распределенной архитектуры, где очереди предоставляют структуру данных для временного хранения сообщений, а брокеры обеспечивают маршрутизацию, доставку, гарантии и управление этими очередями. Брокеры сообщений реализуют сложные протоколы обмена, поддерживают различные модели взаимодействия, такие как point-to-point через очереди и publish-subscribe через топики, обеспечивают гарантированную доставку, сохранность сообщений и восстановление после сбоев. Популярные реализации брокеров, такие как RabbitMQ, Apache Kafka, Amazon SQS и ActiveMQ, предоставляют различные гарантии и подходы к управлению сообщениями, от простых очередей для разгрузки синхронных операций до сложных систем потоковой обработки с возможностью воспроизведения истории сообщений. Выбор подходящего брокера и модели его использования является критическим архитектурным решением, влияющим на масштабируемость, надежность и сложность всей системы.
Брокеры сообщений — это специализированные промежуточные программные продукты, предоставляющие инфраструктуру для асинхронного обмена сообщениями между распределенными приложениями, берущие на себя ответственность за маршрутизацию, хранение, доставку и управление сообщениями. Брокеры работают по принципу приема сообщений от производителей и передачи их потребителям согласно заданным правилам маршрутизации, обеспечивая такие гарантии, как доставка ровно один раз, сохранение порядка сообщений, сохранность при сбоях и масштабирование за счет горизонтального расширения. Современные брокеры сообщений поддерживают различные протоколы взаимодействия, включая AMQP, MQTT, STOMP и собственные двоичные протоколы, и предоставляют богатые возможности мониторинга и управления. Брокеры играют роль универсальной шины данных в enterprise-архитектурах, позволяя интегрировать разнородные системы, реализовывать паттерны событийно-ориентированной архитектуры и строить сложные потоки обработки данных с сохранением слабой связанности компонентов.
Брокер сообщений (message broker) временно хранит задачи или события и передаёт их потребителям.
| Модель | Как устроено | Примеры задач | Технологии |
|---|---|---|---|
| Очередь задач | Одно сообщение обычно обрабатывает один воркер | Отчёты, письма, превью картинок | RabbitMQ, SQS |
| Журнал событий | Сообщения хранятся по политике retention; чтение с offset | Аналитика, CDC, повторное проигрывание | Kafka |
| Pub/Sub | Одно событие получают все подписчики топика | OrderCreated → склад, CRM, аналитика | Kafka topic, Rabbit fanout |
Сравнение брокеров
Подробные гайды — 114, 118, 119.
| Критерий | RabbitMQ | Apache Kafka | Amazon SQS | Redis Streams |
|---|---|---|---|---|
| Модель | Очереди, маршрутизация, TTL | Разделённый log | Управляемая очередь в облаке | Поток в памяти |
| Типичная задача | Workflow, фоновые job | Потоковая обработка, высокий throughput | Job без своего кластера | Лёгкие задачи при уже установленном Redis |
| Порядок сообщений | В пределах одной очереди | В пределах partition | Опциональная FIFO-очередь | В пределах stream |
| Хранение | До ack или по TTL | Дни и недели по настройке | До 14 дней | Ограничено RAM |
| Эксплуатация | Средняя (свой кластер) | Высокая | Низкая (managed) | Низкая; RAM — узкое место |
Выбор фиксируют в ADR с учётом команды, облака, объёма сообщений в секунду и необходимости replay.
Фоновые воркеры
Фоновые воркеры — это выделенные процессы или службы, работающие непрерывно в фоновом режиме и предназначенные для выполнения задач, которые не должны блокировать основной поток обработки запросов или выполняются асинхронно по отношению к основному пользовательскому взаимодействию. Фоновые воркеры используются для обработки длительных операций, таких как генерация отчетов, обработка загруженных файлов, отправка почтовых уведомлений, выполнение аналитических расчетов и синхронизация данных с внешними системами. Они работают независимо от веб-сервера или основного приложения, периодически опрашивая очереди задач или реагируя на события, и могут масштабироваться горизонтально для увеличения пропускной способности. Управление фоновыми воркерами включает мониторинг их состояния, обработку ошибок, перезапуск в случае падения и graceful shutdown при обновлении системы, чтобы не потерять задачи, находящиеся в процессе обработки.
Воркер — процесс или сервис, который:
- подписывается на очередь или топик;
- забирает сообщение;
- выполняет бизнес-логику;
- подтверждает обработку (ack) или откладывает повтор (nack).
Рекомендации по эксплуатации:
- Масштабирование — по длине очереди или consumer lag (KEDA, HPA); CPU API для этого слабый сигнал.
- Prefetch — сколько сообщений воркер берёт заранее; согласовать с временем обработки одного сообщения.
- Graceful shutdown — при SIGTERM перестать брать новые задачи, завершить текущие или вернуть их в очередь.
- Изоляция — тяжёлые job (FFmpeg) в отдельном deployment от лёгких уведомлений.
- Версионирование — поле
schema_versionв payload; новые поля добавлять как необязательные.
Воркер должен быть идемпотентным.
Планировщики и отложенные задачи
Планировщики и отложенные задачи — это механизмы, позволяющие выполнять определенные операции не немедленно, а в заданный момент времени или по расписанию, обеспечивая автоматизацию процессов, которые должны происходить регулярно или с задержкой. Планировщики позволяют настраивать периодическое выполнение задач через интервалы, по расписанию в определенное время дня или по сложным cron-выражениям, и широко используются для проведения регулярных операций, таких как создание бэкапов, отправка еженедельных отчетов, обновление кэша или очистка устаревших данных. Отложенные задачи позволяют запланировать выполнение операции через определенный промежуток времени после какого-либо события, например, отправка напоминания через час после создания заказа, автоматическое подтверждение действия через 24 часа или отмена заказа при отсутствии оплаты в течение 15 минут. В современных системах планировщики часто реализуются на основе распределенных очередей или специализированных сервисов, таких как Celery в Python, Quartz в Java или AWS EventBridge в облачных средах.
| Механизм | Задержка | Пример |
|---|---|---|
| Очередь с TTL / delayed exchange | Секунды и часы | Повтор через 5 мин, напоминание |
| Cron + batch | По расписанию | Ночной отчёт (системное администрирование) |
| Workflow engine (Temporal, Cadence) | Долгие цепочки с таймерами | Бронь с удержанием 15 минут |
Для "выполнить через N секунд" часто хватает TTL в RabbitMQ или таблицы scheduled_jobs с poller.
Publish-Subscribe
Publish-Subscribe — это архитектурный паттерн асинхронного взаимодействия, при котором издатели публикуют сообщения в определенные темы или топики, не зная о подписчиках, которые в свою очередь подписываются на интересующие их темы и получают все сообщения, опубликованные в этих темах. Этот паттерн обеспечивает слабую связанность компонентов и позволяет строить гибкие, расширяемые системы, в которых новые подписчики могут добавляться без изменений в издателях. В отличие от очередей с моделью point-to-point, где каждое сообщение потребляется одним потребителем, в модели publish-subscribe каждый подписчик получает копию каждого сообщения. Этот паттерн широко используется для реализации событийно-ориентированных архитектур, интеграции микросервисов и построения систем реального времени, где множество компонентов должны реагировать на одни и те же события.
Pub/Sub (издатель — подписчик) — паттерн, при котором один факт рассылается нескольким независимым сервисам.
- Очередь задач — каждое письмо из 100 000 обрабатывает ровно один воркер.
- Топик событий — событие "заказ создан" одновременно получают склад, аналитика и CRM.
Подробнее — событийная архитектура, типы взаимодействия.
Outbox, CDC и согласованность с БД
Outbox — это паттерн проектирования, решающий проблему надежной публикации событий в распределенных системах путем сохранения событий в отдельную таблицу базы данных в рамках той же транзакции, что и основное изменение состояния, с последующей асинхронной доставкой этих событий через отдельный процесс-релей. Когда система изменяет состояние агрегата и должна опубликовать событие, она сохраняет это событие в таблицу outbox в рамках той же транзакции, что гарантирует, что либо событие и изменение будут сохранены вместе, либо ничего не произойдет. Отдельный фоновый процесс периодически читает неотправленные события из таблицы outbox и публикует их в очередь или брокер сообщений, а после успешной публикации отмечает их как отправленные. Этот паттерн особенно важен в микросервисных архитектурах для обеспечения согласованности между сервисами без использования распределенных транзакций и гарантирует, что события будут опубликованы даже в случае сбоев приложения.
CDC или Change Data Capture — это техника отслеживания и захвата изменений в базе данных для их последующей синхронизации с другими системами, построения потоков событий или создания аналитических представлений, работающая на уровне журналов транзакций базы данных. CDC позволяет асинхронно реплицировать изменения данных из одной базы данных в другую, без необходимости модифицировать исходное приложение, что делает его идеальным решением для миграции данных, построения систем реального времени и интеграции унаследованных систем. В отличие от периодического опроса таблиц на предмет изменений, CDC обеспечивает близкий к реальному времени захват изменений с минимальной нагрузкой на базу данных, так как использует нативные механизмы репликации. Популярные реализации CDC включают Debezium, Amazon Database Migration Service с изменением захвата, и различные решения на основе триггеров баз данных, но наиболее эффективным является использование журналов транзакций таких систем, как PostgreSQL, MySQL и MongoDB.
Согласованность с БД — это свойство системы, гарантирующее, что состояние данных в базе данных соответствует бизнес-правилам и ожиданиям приложения, и все изменения данных согласованы, атомарны и изолированы друг от друга. В контексте асинхронных систем обеспечение согласованности с БД становится особенно сложной задачей, потому что изменения могут происходить в разных компонентах системы в разное время, а транзакции могут быть распределенными между несколькими сервисами и базами данных. Различают строгую согласованность, когда данные всегда находятся в актуальном состоянии для всех читателей сразу после записи, и конечную согласованность, когда данные могут быть временно несогласованными, но обязательно придут к согласованному состоянию через некоторое время. Выбор между этими моделями является фундаментальным архитектурным решением, влияющим на масштабируемость, доступность и сложность системы.
Если API сначала пишет в БД, а потом отдельным шагом шлёт в брокер, возможны рассинхроны:
- БД сохранилась, publish упал — задачи в очереди нет.
- Publish прошёл, БД откатилась — в очереди "лишняя" задача.
Transactional Outbox
Transactional Outbox — это конкретная реализация паттерна outbox, которая гарантирует атомарность сохранения бизнес-данных и соответствующих событий в рамках одной транзакции базы данных, используя специальный механизм, встроенный в систему управления базами данных. При использовании transactional outbox, в рамках одной транзакции сначала сохраняется основная сущность, например, заказ, и одновременно вставляется запись в таблицу outbox, содержащую событие, которое необходимо опубликовать. Затем отдельный фоновый процесс, работающий в том же экземпляре приложения или в отдельном сервисе, читает записи из таблицы outbox и публикует их в очередь сообщений, удаляя или помечая их после успешной публикации. Этот подход обеспечивает надежность публикации даже при сбоях приложения сразу после сохранения данных, так как неотправленные события остаются в таблице и будут обработаны при следующем запуске отправителя.
Outbox — таблица, куда в той же транзакции, что и бизнес-запись, добавляется строка "нужно опубликовать событие X". Отдельный процесс (relay или poller) читает outbox и шлёт в брокер; после успеха строка помечается опубликованной.
CREATE TABLE outbox (
id BIGSERIAL PRIMARY KEY,
aggregate_id UUID NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_at TIMESTAMPTZ
);
Скелет для иллюстрации; в проде нужны индексы и политика очистки старых строк.
CDC (Change Data Capture)
CDC — чтение журнала изменений БД (WAL в PostgreSQL) и передача в поток (Debezium → Kafka). Меньше кода в приложении; больше инфраструктуры стриминга. Связь с репликацией.
WAL или Write-Ahead Logging — это техника ведения журнала предзаписи в системах управления базами данных, при которой все изменения сначала записываются в защищенный журнал до их применения к основным файлам данных, что обеспечивает возможность восстановления данных в случае сбоев. WAL является основой для обеспечения атомарности и долговечности транзакций в современных СУБД и позволяет реализовывать такие функции, как точка восстановления после сбоя, репликация и CDC. При работе с асинхронными системами WAL часто используется как источник для захвата изменений данных, потому что он содержит полный и упорядоченный журнал всех изменений в базе данных. Также WAL может быть задействован для реализации паттерна transactional outbox, когда записи о событиях, сохраненные в той же транзакции, что и основные изменения, попадают в журнал и могут быть надежно обработаны даже после перезапуска системы.
Inbox на стороне consumer
Inbox — это паттерн, симметричный outbox, предназначенный для обеспечения идемпотентной обработки входящих сообщений в распределенных системах путем сохранения идентификаторов полученных сообщений в специальной таблице перед их обработкой. Когда сервис получает сообщение, он сначала проверяет таблицу inbox, не обрабатывал ли он уже это сообщение, и если нет, сохраняет его идентификатор в рамках транзакции вместе с результатами обработки. Этот подход гарантирует, что даже если сообщение будет доставлено дважды из-за сетевых проблем или повторных попыток брокера, оно будет обработано только один раз. Паттерн inbox критически важен для систем с гарантией доставки at-least-once и помогает избежать дублирования данных и повторного выполнения побочных эффектов.
Inbox — таблица у получателя с уникальным message_id. Повторная доставка того же id не запускает обработку второй раз. Вместе с outbox даёт устойчивость при семантике at-least-once (§ семантика).
Consumer или потребитель — это компонент системы, который извлекает сообщения из очереди или подписки, обрабатывает их и выполняет необходимые действия, являясь получателем в асинхронной архитектуре. Потребитель может быть реализован как отдельный воркер, как часть микросервиса или как функция в serverless-архитектуре, и его задача — обеспечить корректную обработку полученных сообщений с учетом всех бизнес-правил, обработки ошибок и гарантий доставки. Потребители должны быть спроектированы с учетом возможности повторной обработки сообщений, пауз в работе, Graceful shutdown и отказоустойчивости. Эффективность потребителей критически важна для пропускной способности всей системы, и их масштабирование часто является основным способом увеличения производительности асинхронной обработки.
At-least-once — это семантика доставки сообщений, при которой система гарантирует, что каждое отправленное сообщение будет доставлено получателю хотя бы один раз, но при этом может быть доставлено несколько раз в случае сбоев, повторных попыток или проблем с подтверждением. Эта семантика является стандартной для большинства брокеров сообщений и протоколов из-за своей простоты и надежности, но требует от потребителя реализации идемпотентности для корректной обработки дублирующихся сообщений. Преимущество at-least-once заключается в том, что она обеспечивает высокую надежность доставки и проста в реализации по сравнению с более строгой exactly-once, которая значительно сложнее и дороже. Потребители сообщений в системах с at-least-once должны быть спроектированы так, чтобы повторная обработка одного и того же сообщения не приводила к нежелательным побочным эффектам.
Типичные сценарии
| Сценарий | Поведение | Риски | Углубление |
|---|---|---|---|
| Фоновые задачи | 202 Accepted + id; PDF или видео в фоне | Дубликаты, потеря статуса | Паттерны с клиентом, state machine |
| Телеметрия и логи | События в Kafka → хранилище аналитики | Потеря при at-most-once | Пакетная работа |
| Микросервисы | OrderCreated → несколько подписчиков | Распределённая согласованность | Saga |
| ETL / bulk | Данные режут на chunk по 10k строк | OOM, долгие транзакции | Конвейеры |
| Входящий webhook | Быстрый 200, обработка в очереди | Таймаут и повторы у партнёра | Входящие webhooks |
Генерация отчёта
Пользователь нажимает "Скачать отчёт за год". Синхронный путь занимает 40–120 с и упирается в таймаут балансировщика (часто 60 с).
Шаги:
POST /reports— записьreports(id, status=pending)и строка outbox в одной транзакции.- Ответ
202и{ "task_id": "…" }. - Воркер строит CSV/PDF, кладёт в S3 или аналог, обновляет
status=ready. - UI опрашивает
GET /reports/{id}или слушает SSE.
Обработка видео
- Загрузка файла — синхронно (multipart → object storage).
- Транскодинг — цепочка очередей:
1080p→720p→thumbnails→notify-user. - Узкое место — CPU; воркеры на нодах с высоким CPU limit или GPU.
- Статус — конечный автомат на
video_id.
Создание заказа
Синхронно на критичном пути:
- резерв на складе;
- списание оплаты (или Saga с компенсацией).
Асинхронно после события OrderPaid:
- письмо с чеком;
- бонусы;
- аналитика;
- push.
Отдельное событие нужно, чтобы сбой почты не откатывал заказ. См. сценарий User/Order и email-рассылку.
Конечный автомат задачи
Статус фоновой задачи хранят в БД (таблица jobs / tasks). Память воркера для этого недостаточна — при рестарте процесса статус исчезнет.
| Статус | Для пользователя | Действия инженера |
|---|---|---|
pending | "Принято" | Проверить outbox relay |
queued | "В очереди" | Смотреть depth и lag |
running | "Выполняется" | Lease на случай падения воркера |
succeeded | Ссылка на результат | TTL артефакта в хранилище |
failed | Ошибка | Алерт; не бесконечный retry |
dead | "Обратитесь в поддержку" | Разбор DLQ |
Lease (аренда задачи) — при переходе в running воркер пишет locked_until = now() + 5 min. Другой воркер не берёт задачу, пока lock активен. После истечения — повторная доставка.
Аналогичная схема для письма — state machine в email-рассылке.
Паттерны взаимодействия с клиентом
Как клиент узнаёт, что фоновая задача завершена:
| Паттерн | Суть | Плюсы | Минусы | Когда уместен |
|---|---|---|---|---|
| Fire-and-forget | Задача ушла в очередь без статуса для клиента | Минимальная задержка | Нет обратной связи | Метрики, некритичные события |
| Polling | Периодический GET /tasks/{id} | Просто, работает везде | Лишние запросы | Мобильные клиенты, B2B |
| Long polling | Сервер держит соединение до смены статуса | Меньше опросов | Таймауты прокси | Умеренная нагрузка |
| SSE | Поток text/event-stream | Живой прогресс в браузере | Только server → client | Дашборды |
| WebSocket | Двусторонний канал | Чат и статус в одном UI | Сложнее инфраструктура | Интерактивные приложения |
| Webhook | POST на callback URL клиента | Без опроса | Нужны подпись и идемпотентность | B2B-интеграции |
Fire-and-forget — это модель асинхронного взаимодействия, при которой отправитель отправляет сообщение получателю без ожидания подтверждения о его получении или результате обработки, полагаясь на промежуточную инфраструктуру для надежной доставки. Этот подход характеризуется минимальной задержкой для отправителя и отсутствием блокировок, но требует уверенности в надежности системы доставки и принятия риска возможной потери сообщения. Fire-and-forget широко используется для операций, где временная задержка критична, а потеря сообщения допустима или может быть компенсирована другими механизмами, например, для отправки логов, метрик, необязательных уведомлений или аналитических событий. В некоторых реализациях fire-and-forget дополняется механизмами подтверждения на уровне брокера, но без ожидания обработки потребителем.
Polling — это механизм получения данных, при котором клиент периодически опрашивает сервер на предмет наличия новых данных или обновлений, активно запрашивая состояние системы через равные промежутки времени. Polling является простым в реализации и понимании методом, но создает нагрузку на сервер и сеть, особенно при коротких интервалах опроса, и не обеспечивает мгновенную доставку обновлений. Этот подход эффективен для сценариев, где обновления происходят редко или не критично время их получения, и часто используется в интеграциях с устаревшими системами, не поддерживающими современные асинхронные протоколы. Важным аспектом реализации polling является выбор оптимального интервала опроса, который должен балансировать между своевременностью получения данных и нагрузкой на систему.
Long polling — это улучшенная версия polling, при которой сервер удерживает HTTP-соединение открытым до тех пор, пока не появятся новые данные для клиента или не истечет таймаут, после чего клиент инициирует новое соединение. Этот подход позволяет эмулировать real-time взаимодействие без необходимости использования сложных протоколов и дает более быстрое получение обновлений по сравнению с классическим polling, уменьшая при этом нагрузку на сервер. В long polling клиент отправляет запрос к серверу, и если нет новых данных, сервер не возвращает ответ немедленно, а держит соединение открытым, возвращая ответ только когда появляются данные или истекает время ожидания. Long polling широко используется для реализации чат-приложений, систем уведомлений и других сценариев, где требуется near-real-time доставка данных без использования WebSocket или SSE.
SSE или Server-Sent Events — это технология, позволяющая серверу отправлять события клиенту через HTTP-соединение, которое устанавливается клиентом один раз и остается открытым для потоковой передачи обновлений от сервера. SSE использует простой текстовый протокол на основе HTTP и поддерживает автоматическое переподключение при разрыве соединения, что делает его надежным и простым в использовании для односторонней передачи событий от сервера к клиенту. Технология идеально подходит для сценариев, где клиенту необходимо получать обновления в реальном времени, такие как ленты новостей, курсы валют, уведомления о статусе заказов или показатели дашбордов. SSE поддерживается всеми современными браузерами и не требует специальных библиотек на клиентской стороне, но, в отличие от WebSocket, работает только в одном направлении — от сервера к клиенту.
WebSocket — это протокол полнодуплексной связи по одному TCP-соединению, обеспечивающий постоянный канал обмена сообщениями между клиентом и сервером в реальном времени в обоих направлениях. В отличие от HTTP, WebSocket устанавливает постоянное соединение, которое остается открытым, позволяя серверу отправлять данные клиенту без запроса и клиенту отправлять данные серверу в любой момент. WebSocket идеально подходит для приложений реального времени с активным двусторонним обменом, таких как онлайн-игры, торговые платформы, интерактивные чаты и системы совместной работы. Протокол WebSocket имеет низкую задержку и минимальные накладные расходы по сравнению с повторными HTTP-запросами, но требует более сложной инфраструктуры для поддержания множества долгоживущих соединений.
Webhook — это механизм, при котором приложение отправляет HTTP-запросы на заранее заданные URL-адреса других систем для уведомления о произошедших событиях, работая как обратный вызов, инициируемый сервером при наступлении определенного события. Webhook обычно используются для интеграции между различными сервисами, когда одно приложение хочет получать уведомления о событиях в другом приложении без необходимости постоянного опроса. При наступлении события система-источник формирует HTTP-запрос с данными о событии и отправляет его на URL, который был зарегистрирован получателем, который обрабатывает это уведомление и выполняет необходимые действия. Вебхуки являются простым, но мощным инструментом интеграции, используемым в платежных системах для уведомления о статусе транзакций, в CI/CD системах для уведомления о результатах сборки и во многих других сценариях интеграции.
Подробнее — Polling, SSE, Webhook, REST.
HTTP-контракт async API
HTTP-контракт async API — это формализованное описание асинхронного взаимодействия между системами через HTTP-протокол, определяющее форматы запросов и ответов, методы, заголовки, коды ошибок и бизнес-сценарии, закрепленные в контракте на уровне API. В отличие от синхронных REST-контрактов, async API контракты могут описывать асинхронные паттерны, такие как отправка запроса с немедленным подтверждением и получение результата позже через колбэк или вебхук. Такой контракт может быть документирован с использованием спецификаций AsyncAPI или OpenAPI с расширениями для асинхронных операций и включает определение всех возможных состояний операции, timeout'ов, политик повторных попыток и обработки ошибок. HTTP-контракты async API являются основой для построения надежных интеграций между распределенными системами, обеспечивая единообразное понимание взаимодействия всеми участниками.
| Метод | Код | Тело ответа | Смысл |
|---|---|---|---|
POST /tasks | 202 Accepted | task_id, status_url | Задача принята, ещё не готова |
GET /tasks/{id} | 200 | status, progress, result | Текущее состояние |
DELETE /tasks/{id} | 202 / 204 | — | Запрос отмены (best-effort) |
Практики:
- заголовок
Locationс URL статуса; Idempotency-Keyпри повторномPOST— идемпотентность;- коды ошибок и формат — проектирование API.
Требования к исходящему webhook
- подпись тела (HMAC-SHA256), проверка заголовка
X-Signature; - идемпотентность по
event_idу получателя; - быстрый
200 OKи своя очередь на обработку (иначе партнёр шлёт повторы); - timestamp и окно допустимого времени против replay.
Примеры — webhooks ESP, публичный API.
Семантика доставки
Семантика доставки — это набор гарантий, которые система обмена сообщениями предоставляет относительно того, будет ли сообщение доставлено получателю и сколько раз, определяя поведение системы при нормальной работе и при сбоях. Существует три основных вида семантики доставки: at-most-once, когда сообщение может быть потеряно, но никогда не будет доставлено более одного раза; at-least-once, когда сообщение будет доставлено обязательно, но может быть доставлено несколько раз; и exactly-once, когда сообщение будет доставлено ровно один раз и это гарантируется системой даже при сбоях. Выбор семантики доставки является важным архитектурным решением, так как at-most-once проще в реализации и быстрее, но не гарантирует доставку, at-least-once надежен, но требует идемпотентности от потребителя, а exactly-once наиболее надежен, но самый сложный и дорогой в реализации и снижает производительность.
Семантика доставки — ответ на вопрос, сколько раз сообщение будет обработано и может ли оно потеряться.
| Гарантия | Смысл | Цена |
|---|---|---|
| At-most-once | Может потеряться; дубликатов нет | Риск для критичных задач |
| At-least-once | Доставят минимум один раз; возможны дубликаты | Нужен идемпотентный воркер |
| Exactly-once (в рамках брокера) | Одна запись в транзакции producer | Сложность, связность компонентов |
| Effectively exactly-once | Пользователь видит один эффект | At-least-once + dedup + идемпотентность |
Подробнее — идемпотентность и семантика доставки.
Где теряются и дублируются сообщения
Сообщения — это структурированные блоки данных, которые передаются между компонентами системы в процессе асинхронного обмена, содержащие всю необходимую информацию для выполнения конкретного действия или уведомления о произошедшем событии. Каждое сообщение обычно состоит из заголовка с метаданными, такими как уникальный идентификатор, тип сообщения, временная метка, информация о маршрутизации и приоритете, а также тела, содержащего сами данные, которые могут быть представлены в различных форматах, включая JSON, XML, Protocol Buffers, Avro или бинарные данные. Сообщения также могут содержать информацию о контексте, такую как идентификатор корреляции для отслеживания потока обработки, данные об отправителе и получателе, а также различные атрибуты для управления доставкой, включая время жизни сообщения, количество попыток доставки и флаги для обработки. Сообщения являются основным носителем информации в событийно-ориентированных и потоковых системах, и их структура, размер, формат и способ сериализации существенно влияют на производительность, надежность и удобство отладки всей системы обмена.
| Точка сбоя | Риск | Митигация |
|---|---|---|
| API упал до commit БД | Задача не создана | Повтор POST с Idempotency-Key |
| Commit есть, relay не опубликовал | Задача в outbox, не в брокере | Poller/CDC, алерт на "застрявшие" строки |
| Publish OK, crash до ack | Повторная доставка | Идемпотентность |
| Обработка OK, crash до ack | Повторная доставка | Идемпотентность + inbox |
| Ack до commit в БД | Потеря при crash после ack | Ack после commit (manual ack) |
Backpressure и управление нагрузкой
Backpressure — это механизм управления нагрузкой в потоковых системах, при котором получатель данных сигнализирует отправителю о своей неспособности обрабатывать поступающие сообщения с текущей скоростью, вызывая замедление или временную приостановку отправки для предотвращения переполнения буферов и потери данных. Этот механизм действует как регулирующий клапан, позволяя системе самоадаптироваться к различным нагрузкам, выравнивая разницу в производительности между производителями и потребителями и предотвращая ситуации, когда медленный потребитель вызывает нестабильность всей системы из-за переполнения памяти или сетевых буферов. Backpressure может быть реализован различными способами, включая блокировку отправки при заполнении буфера, использование скользящих окон для контроля скорости, отправку специальных сигналов для уменьшения темпа отправки или применение стратегий управления потоком на уровне протокола, как в случае с TCP. В распределенных системах правильная реализация Backpressure критически важна для обеспечения стабильности и предсказуемости поведения, особенно при работе с нестабильными сетевыми соединениями или компонентами с переменной производительностью.
Backpressure — ситуация, когда задачи поступают в очередь быстрее, чем воркеры их разбирают.
Признаки:
- растёт queue depth (число сообщений в очереди);
- растёт age of oldest message (возраст самой старой задачи);
- растёт consumer lag в Kafka;
- нарушается SLA ("отчёт за 5 минут" систематически опаздывает);
- заканчивается память или диск брокера (нет TTL / max-length).
Стратегии:
- добавить воркеры или partition (autoscaling);
- ускорить handler (batch, меньше round-trip к БД);
- throttle на
POST /tasks(rate limiting); - отдельные очереди по приоритету (
critical/batch); - честный ответ "система перегружена" вместо молчаливого lag;
- отброс некритичных событий при перегрузке (только для некритичного трафика).
Идемпотентность
Идемпотентность — это свойство операции, при котором её многократное применение с одними и теми же входными данными дает тот же результат, что и однократное применение, и не вызывает нежелательных побочных эффектов, позволяя безопасно повторять операции в случае сбоев или повторной доставки сообщений. В контексте асинхронных систем идемпотентность является критически важным требованием для потребителей сообщений с семантикой доставки at-least-once, так как сообщение может быть обработано несколько раз из-за сетевых проблем, перезапусков или ручных повторов. Идемпотентность может быть реализована различными способами, включая использование уникальных идентификаторов запросов для отслеживания уже выполненных операций, сохранение состояния обработки в базе данных с проверкой дублей, использование механизмов версионирования или применение операций, которые по своей природе являются идемпотентными, например, установка конкретного значения, а не инкремент счетчика. Обеспечение идемпотентности требует дополнительных усилий при проектировании и может снижать производительность, но она является основой для построения надежных распределенных систем, где гарантия ровно одной обработки часто недостижима или слишком дорога.
При at-least-once повторная доставка неизбежна. Обработчик должен давать тот же эффект, что при первом запуске.
| Техника | Как работает | Пример |
|---|---|---|
| Естественная идемпотентность | Повтор безопасен сам по себе | SET status = 'done' WHERE id = ? |
| Ключ идемпотентности | Уникальный ключ в БД / Redis | UNIQUE(message_id) в inbox |
| Compare-and-set | Обновление только из допустимого статуса | WHERE status = 'queued' |
| Ключ внешнего API | Провайдер принимает idempotency key | Stripe, SES |
- Методы и ключ идемпотентности
- Кейс дубликата письма — 144
Чеклист code review воркера
- Есть стабильный
message_idили бизнес-ключ в payload? - Запись в БД защищена уникальным constraint?
- Вызов внешнего API — после фиксации "уже обработано"?
- Повтор с тем же ключом возвращает тот же результат, а не 500?
Упорядочивание и партиционирование
Упорядочивание — это аспект обработки сообщений, обеспечивающий доставку и обработку сообщений в том порядке, в котором они были отправлены, что критически важно для систем, где порядок событий имеет значение для корректности бизнес-логики. В распределенных системах сохранение глобального порядка между всеми сообщениями от всех производителей является непростой задачей, особенно при высокой нагрузке и горизонтальном масштабировании, поэтому обычно применяются различные стратегии, такие как упорядочивание в пределах одной партиции или одного ключа сообщения. Брокеры сообщений, такие как Kafka, обеспечивают строгий порядок сообщений внутри каждой партиции, но порядок между различными партициями не гарантируется, поэтому производители должны самостоятельно выбирать ключ партиционирования для группировки связанных сообщений. Поддержание порядка сообщений требует компромиссов между производительностью и гарантиями, так как строгое упорядочивание ограничивает параллелизм обработки и может приводить к проблемам, таким как Head-of-line blocking, когда одно проблемное сообщение задерживает обработку всех последующих сообщений в очереди.
Партиционирование — это техника разделения данных или сообщений на отдельные логические или физические части, называемые партициями, которые могут обрабатываться параллельно, что позволяет достичь горизонтальной масштабируемости и увеличить пропускную способность системы. При партиционировании сообщения распределяются между партициями на основе некоторого ключа, такого как идентификатор пользователя или категория события, и все сообщения с одинаковым ключом попадают в одну и ту же партицию, что гарантирует их упорядоченную обработку. Этот подход является фундаментом масштабируемости таких систем, как Apache Kafka, где топики разделены на партиции, каждая из которых может обслуживаться разными брокерами и читаться разными потребителями параллельно. Выбор правильной стратегии партиционирования критически важен для балансировки нагрузки, так как неравномерное распределение ключей может привести к образованию горячих партиций и снижению общей производительности, а изменение количества партиций в работающей системе часто требует перераспределения данных и является сложной операцией.
| Требование | Решение | Цена |
|---|---|---|
| Строгий порядок всех событий | Одна очередь / один partition | Сложнее масштабировать consume |
| Порядок в рамках одной сущности | Partition key = order_id | "Горячие" ключи перегружают partition |
| Порядок не важен | Много consumers и partitions | Проще горизонтальный scale |
В Kafka порядок гарантирован внутри partition. События одного user_id часто кладут в одну partition, если нужна последовательная обработка профиля.
Head-of-line blocking — одно "застрявшее" сообщение блокирует всю partition. Поэтому нужны DLQ после N попыток и отдельные очереди для тяжёлых job.
Head-of-line blocking — это ситуация в системах с очередями, когда обработка одного проблемного сообщения в начале очереди блокирует или задерживает обработку всех последующих сообщений, даже если они не зависят от проблемного и могли бы быть обработаны быстрее. Такое поведение возникает в системах с одним потоком обработки или с строгим сохранением порядка, когда все сообщения в очереди должны обрабатываться последовательно в порядке поступления. В асинхронных системах Head-of-line blocking проявляется особенно остро, когда сообщение требует длительной обработки, вызывает ошибки и требует множества повторных попыток, или когда запрос к внешнему сервису занимает непредсказуемое время. Для борьбы с этой проблемой применяются различные стратегии, включая использование нескольких параллельных очередей для разных типов сообщений, реализацию системы приоритетов, где срочные сообщения могут обгонять проблемные, использование паттерна DLQ для изоляции проблемных сообщений, а также применение неблокирующих архитектур и асинхронных обработчиков, которые не задерживают всю очередь при возникновении проблем с отдельным сообщением.
Retry, DLQ и poison messages
Retry или повторная попытка — это стратегия повышения надежности обработки сообщений, при которой система автоматически повторяет операцию, завершившуюся ошибкой, через определенные промежутки времени в надежде, что временная проблема будет устранена и операция завершится успешно. Повторные попытки могут быть реализованы с фиксированной задержкой между попытками, с экспоненциально возрастающей задержкой для снижения нагрузки на проблемный ресурс, или с использованием случайной задержки для избежания эффекта синхронизации и образования нового пика нагрузки. Важными параметрами стратегии Retry являются максимальное количество попыток, интервалы между ними, а также критерии для определения, стоит ли повторять операцию вообще, так как некоторые ошибки, например, нарушение бизнес-правил, не могут быть решены повторными попытками. В распределенных системах Retry часто комбинируется с механизмами Circuit Breaker и является важной частью обеспечения отказоустойчивости, но неправильно настроенная стратегия повторных попыток может привести к эффекту Retry Storm, когда множество клиентов одновременно начинают повторять операции, создавая дополнительную нагрузку и усугубляя проблему.
Retry (повтор) — повторная попытка после ошибки. Часть инженерии устойчивости; не бесконечный цикл while.
| Параметр | Типичное значение | Заметка |
|---|---|---|
| Max attempts | 3–10 | Зависит от побочных эффектов |
| Backoff | Экспоненциальный + jitter (случайный разброс) | Без jitter — "стадный" всплеск повторов |
| DLQ | Отдельная очередь / topic | Ручной разбор, алерт |
Poison messages или ядовитые сообщения — это сообщения в системе обмена, которые не могут быть обработаны корректно из-за поврежденного формата, несовместимой структуры данных или содержат данные, вызывающие постоянные ошибки в логике обработки, что приводит к бесконечным сбоям при каждой попытке их обработки. Такие сообщения представляют серьезную проблему в асинхронных системах, потому что они могут блокировать обработку очереди, вызывать бесконечные ретраи, потреблять ресурсы процессора и памяти, а также нарушать мониторинг системы множеством повторяющихся ошибок. Обнаружение poison messages осуществляется через мониторинг количества ошибок при обработке, анализ содержимого сообщений и специальные механизмы валидации, а их обработка обычно включает изоляцию в DLQ, логирование для дальнейшего анализа, отправку уведомлений разработчикам для исправления причины появления таких сообщений. Предотвращение попадания poison messages в систему включает строгую валидацию входных данных, использование схем и контрактов для проверки структуры сообщений, а также внедрение механизмов graceful degradation при обнаружении необрабатываемых данных.
Poison message — сообщение, которое всегда падает (битый JSON, несуществующий user_id). Без DLQ оно крутится в retry и блокирует очередь.
| Ситуация | Повторять? |
|---|---|
| Таймаут внешнего API, 5xx | Да |
| 4xx, ошибка валидации | Нет → сразу DLQ |
| Дубликат после успеха | Нет (идемпотентность) |
| Брокер недоступен | Повтор publish из outbox |
Saga и длинные процессы
Saga — это паттерн управления распределенными транзакциями, который разбивает длительную операцию, затрагивающую несколько сервисов, на последовательность локальных транзакций, каждая из которых выполняется в отдельном сервисе и, в случае ошибки, компенсируется обратными операциями для обеспечения конечной согласованности данных во всей системе. В отличие от традиционных ACID-транзакций, которые используют двухфазную фиксацию и блокирующие механизмы, Saga использует асинхронность и события для координации, обеспечивая высокую доступность и масштабируемость, но с переходом к модели eventual consistency. Существует два основных подхода к координации Sagas: хореография, где каждый сервис знает, на какие события реагировать и какие компенсирующие действия выполнять, и оркестрация, где центральный координатор определяет последовательность шагов и обработку ошибок. Паттерн Saga широко применяется в микросервисных архитектурах для обеспечения согласованности операций, таких как обработка заказов в интернет-магазине, включающая резервирование товара, списание денег, формирование доставки и отправку уведомлений, где каждый шаг может быть компенсирован в случае сбоя, возвращая систему в согласованное состояние.
Когда операция затрагивает несколько сервисов без общей БД, одной очереди мало. Saga — цепочка локальных шагов с компенсацией при сбое (например, отмена брони, если оплата не прошла).
| Тип | Как координируется | Плюс | Минус |
|---|---|---|---|
| Choreography | События в брокере | Меньше центрального сервиса | Сложнее увидеть всю цепочку |
| Orchestration | Центральный координатор | Явный граф шагов | Ещё один сервис |
Пример: бронирование → оплата → письмо. Подробно — Saga. Outbox на каждом шаге связывает commit в БД и публикацию следующего события.
Отладка, безопасность и версии сообщений
Сложность отладки
Цепочка API → брокер → воркер → внешний API → ещё один топик. В логах одного сервиса виден только фрагмент.
Что помогает:
trace_id/correlation_idиз HTTP в payload и логи воркера;- structured logging (JSON) с
task_id,partition,offset; - distributed tracing (OpenTelemetry, Jaeger, Zipkin);
- показ
task_idв UI для поддержки.
Отказоустойчивость брокера
- кластер с репликацией (quorum queues, Kafka ISR);
- мониторинг disk, memory, connections;
- runbook при потере лидера — алгоритмы выбора лидера.
Версионирование схемы
- новые поля — необязательные (backward-compatible);
- schema registry (Avro, Protobuf) для Kafka;
- поле
schema_versionв конверте; - dual write / dual read при миграции.
Безопасность
| Угроза | Мера |
|---|---|
| Подмена сообщения | TLS, ACL на топики (ИБ) |
| PII в топике | Минимальный payload; шифрование at rest |
| Replay webhook | Подпись + timestamp |
| Переполнение очереди | Rate limit, auth на API (уязвимости API) |
Наблюдаемость
Без метрик очередь "слепая": lag замечают пользователи, а не дашборд.
Основные метрики
| Метрика | Зачем смотреть | Алерт |
|---|---|---|
| Queue depth | Нехватка воркеров | depth выше порога N минут |
| Consumer lag (Kafka) | Отставание от головы log | lag выше SLA |
| Age of oldest message | Реальная задержка job | больше 5 мин для transactional |
| Publish rate и consume rate | Накопление | consume меньше publish 10 мин |
| DLQ size | Битые сообщения / баги | любой рост |
| Processing time p95 | Медленный handler | рост после деплоя |
| Retry count | Нестабильная зависимость | всплеск |
| Outbox unpublished | Сломан relay | больше 0 долго |
Сквозной trace
Один task_id проходит через:
- access log API;
- запись outbox;
- publish (offset/partition);
- start/finish воркера;
- исходящий webhook (если есть).
Антипаттерны
| Антипаттерн | Почему плохо | Что делать |
|---|---|---|
| In-memory queue в API | Потеря при рестарте | Персистентный брокер или job table |
| Ack до commit в БД | Потеря при crash | Manual ack после commit |
| Бесконечный retry | Очередь забита | Max attempts + DLQ |
| Огромный payload в сообщении | Нагрузка на брокер и сеть | URL в S3 + metadata |
| Синхронный RPC в цикле consumer | Блокировка partition | Отдельные очереди, batch |
| Одна очередь на всё | Head-of-line, нет приоритетов | Разделение по SLA |
| Нет идемпотентности | Дубликаты после инцидента | inbox + unique keys |
| "Exactly-once" без обоснования | Ложная уверенность | Явный at-least-once + design |
Выбор технологий
| Ситуация | Частый выбор | Почему |
|---|---|---|
| Kubernetes, job с retry | RabbitMQ / Redis Streams | Знакомая эксплуатация, низкая latency |
| Streaming, replay, много consumers | Kafka | Log, retention, экосистема |
| Минимум ops, AWS | SQS + Lambda / ECS | Managed |
| Отложенные задачи, Redis уже в стеке | Bull / Celery / Sidekiq | Быстрый старт; RAM — лимит |
| Долгие саги с таймерами | Temporal, Cadence | Workflow как код |
Экосистема MSA · продакшн-стек
Оценка ёмкости
Грубая формула для планирования воркеров:
время_разбора ≈ (глубина_очереди × среднее_время_задачи) / число_воркеров
Пример: 10 000 задач, 2 с на задачу, 50 воркеров → около 400 с (6,7 мин) в худшем случае без нового притока.
Если приходит 100 msg/s, а обрабатывается 80 msg/s, очередь растёт линейно — нужен scale или throttle.
Конверт сообщения
В очередь кладут конверт (envelope) с метаданными. Сырой JSON всего заказа в сообщение обычно не помещают.
| Поле | Назначение |
|---|---|
message_id | Уникальный id (UUID); dedup, inbox |
correlation_id | Связь с HTTP-запросом / task_id |
event_type | Тип для маршрутизации |
schema_version | Версия контракта |
occurred_at | Время факта (ISO 8601) |
payload | Бизнес-данные |
{
"message_id": "550e8400-e29b-41d4-a716-446655440000",
"correlation_id": "task_8f3a2b",
"event_type": "ReportRequested",
"schema_version": 1,
"occurred_at": "2026-06-15T14:30:00Z",
"payload": {
"report_id": "rpt_991",
"user_id": "usr_42",
"format": "pdf"
}
}
Большие файлы передают по ссылке (S3 key). Base64 в теле сообщения раздувает брокер и сеть.
Приоритеты и несколько очередей
Одна FIFO-очередь на всё — простой старт. Минус: тяжёлый отчёт на 20 минут задерживает SMS с кодом входа.
| Подход | Как устроено |
|---|---|
| Отдельные очереди по SLA | jobs-critical, jobs-batch |
| Приоритет в RabbitMQ | x-max-priority |
| Отдельный кластер Kafka | transactional и analytics |
| Weighted fair queue | Доля CPU на классы задач |
Критичный путь (OTP, оплата) не делит очередь с batch-ETL без записи в ADR.
Деплой воркеров
| Модель | Плюс | Минус |
|---|---|---|
| Воркер в том же репо, отдельный entrypoint | Общие модели, один CI | Риск перепутать деплой API и worker |
| Отдельный сервис / репо | Независимый scale и релиз | Версионирование контрактов |
| Serverless (Lambda) | Оплата за вызов | Cold start, лимит времени |
| Kubernetes CronJob | Batch по расписанию | Не для постоянного consume |
Долгие job (видео) — отдельный deployment, autoscaling по queue depth (Kubernetes).
terminationGracePeriodSeconds в K8s должен превышать worst-case время обработки сообщения, иначе SIGKILL оборвёт работу без ack.
Circuit Breaker в async-контуре
Circuit Breaker (предохранитель) — паттерн, который временно прекращает вызовы к "больному" сервису (§7 в 141).
| Ситуация | Поведение |
|---|---|
| Воркер вызывает внешний API | При состоянии open — nack с delay или retry-очередь |
| Брокер недоступен | Outbox копится; relay повторяет publish |
| Лавина retry на мёртвую зависимость | Exponential backoff + DLQ |
Bulkhead — отдельные пулы соединений к разным внешним API, чтобы сбой одного не исчерпал все исходящие вызовы.
Расширенные кейсы
Пайплайн изображений (e-commerce)
POST /products/{id}/images→ файл в storage,image_id, статусuploaded.- Очередь
resize→ воркеры 150×150, 800×800, WebP. - Параллельно очередь
moderation(ML API). - Статус
ready_for_catalog— когда все derivative готовы.
Идемпотентность по ключу (image_id, variant).
Ночной биллинг
Cron в 02:00 кладёт N сообщений в invoices (по контракту). Workers масштабируются; ошибка по одному контракту → DLQ без остановки batch.
- Пакетная работа
- checkpoint по
contract_id
Поисковый индекс
Запись в PostgreSQL — синхронно. Elasticsearch — асинхронно через outbox → ProductUpdated. Пользователь 1–2 с может видеть старый снимок в поиске — осознанный trade-off (PACELC).
Импорт CSV
- Multipart → S3.
ImportJob+ сообщение "начать импорт".- Воркер читает потоком (chunk 5k строк), пишет в staging, обновляет
progress_percent. - UI — SSE или polling.
Мультитенантность
| Уровень | Реализация | Когда |
|---|---|---|
| Логическая | tenant_id в payload + ACL | Большинство B2B SaaS |
| Очередь на tenant | jobs-tenant-{id} | Жёсткий SLA |
| Кластер на tenant | Отдельный брокер | Enterprise, compliance |
Один tenant не должен забить общую очередь — per-tenant rate limit и квота на параллельные job.
Локальная разработка
| Подход | Плюс | Минус |
|---|---|---|
| Docker Compose (API + Rabbit + worker) | Близко к проду | Тяжелее на ноутбуке |
| Testcontainers в CI | Реальный брокер в тесте | Медленнее unit |
| In-memory queue в dev | Быстрый старт | Не ловит баги ack/retry |
На проде Rabbit, локально только asyncio.create_task — семантика разъедется при первом инциденте.
Минимум parity: outbox, manual ack, DLQ в dev-dashboard (RabbitMQ Management, Kafka UI).
Данные в очереди и compliance
Сообщения часто содержат PII (персональные данные). Очередь — ещё одно хранилище.
- минимальный payload (id вместо полного профиля);
- шифрование at rest (KMS);
- TTL согласован с GDPR;
- логи без полного тела сообщения.
Для аудита в конверте: occurred_at, actor_id, correlation_id.
Дерево решений
Пример метрик для дашборда
| Метрика | Тип | Идея PromQL |
|---|---|---|
queue_messages_ready | gauge | rabbitmq_queue_messages_ready{queue="jobs"} |
kafka_consumer_lag | gauge | kafka_consumergroup_lag_sum |
job_processing_seconds | histogram | histogram_quantile(0.95, …) |
outbox_unpublished_total | gauge | SQL count |
dlq_messages_total | counter | рост за час |
Алерт: queue_messages_ready > 1000 и consume rate меньше publish rate 10 минут.
Миграция на асинхронный путь
| Фаза | Действие | Риск |
|---|---|---|
| 1 | Вынести логику в handler; вызывать синхронно | Низкий |
| 2 | Таблица jobs; poller в том же процессе | Средний |
| 3 | Outbox + отдельные worker-процессы | Средний |
| 4 | Клиент на 202 + polling | Контракт API |
| 5 | Scale workers, DLQ, алерты | Ops |
Feature flag для отката без деплоя.
Чек-лист system design interview
Порядок ответа — каркас в 143:
- NFR — RPS, задержка job, нужен ли один эффект для пользователя.
- Что синхронно в HTTP, что в фоне.
- High-level — API, DB, outbox, broker, workers, storage, статус.
- Deep dive — идемпотентность, state machine, retry/DLQ, partition key.
- Failure modes — падение API, воркера, брокера; дубликаты.
- Scale — API и workers отдельно; bottleneck.
- Observability — depth, lag, DLQ, trace_id.
Уточняющие вопросы интервьюеру:
- нужен ли прогресс в реальном времени;
- допустима ли потеря некритичных событий;
- нужен ли строгий порядок по
order_id.
Пример ответа (сервис отчётов)
NFR: 10k отчётов/день, пик 500/ч, p95 API <300 мс, отчёт за 15 мин, дубликат PDF недопустим.
Схема: Client → LB → API → PostgreSQL + outbox → RabbitMQ → workers → S3 → обновление tasks.status. Клиент polling GET /tasks/{id}.
Детали:
- outbox в той же TX, что
INSERT INTO reports; UNIQUE(report_id)в inbox воркера;- DLQ после 5 retry;
- метрики
queue_depth, p95job_duration,dlq_size.
Отказы: worker упал — сообщение вернётся после visibility timeout; Rabbit недоступен — растёт outbox, алерт; S3 down — retry, без ack.
Бюджет задержки
| Этап | Синхронный путь (job 60 с) | Асинхронный путь |
|---|---|---|
| Приём запроса | 50 мс | 50 мс |
| Запись намерения | — | 30 мс (DB + outbox) |
| Ответ клиенту | 60 000 мс | 80 мс (202) |
| Генерация PDF | в HTTP-потоке | 45 с в worker |
| Результат | в теле ответа | polling / webhook |
В async-модели две задержки: быстрый ack и отдельно время до succeeded. UX должен это показывать ("отчёт готовится").
Стоимость и FinOps
| Статья | Комментарий |
|---|---|
| Брокер | Kafka с длинным retention дороже SQS pay-per-request |
| Workers 24/7 | Постоянный pool или scale-to-zero на Lambda |
| Object storage | Lifecycle для артефактов |
| Операции | On-call, мониторинг, апгрейды |
Экономия бывает, когда без очереди пришлось бы сильно увеличить число API-инстансов под редкие пики CPU.
Синхронный вызов и событие между сервисами
| Критерий | HTTP/gRPC напрямую | Событие через брокер |
|---|---|---|
| Связность | Вызывающий знает адрес callee | Издатель не знает подписчиков |
| Задержка для пользователя | Сумма всех hop | Только критичный путь синхронен |
| Отказ зависимости | 503 клиенту | Буфер, retry |
| Согласованность | Проще read-after-write | Eventual consistency |
| Отладка | Один request-id | Trace через топик |
| Версии | Жёсткий контракт API | Несколько consumer на topic |
Три и более синхронных вызова на критичном пути — сигнал вынести хвост в событие. Пример: POST /orders резервирует склад и списывает оплату; OrderPaid запускает письмо, CRM и аналитику.
Мини-postmortem (рост очереди)
Симптом: queue_depth с 200 до 6 000 за час; отчёты опаздывают на 40+ минут.
Хронология:
- Деплой воркера: обработка 8 с вместо 2 с.
- Пик enqueue +30% от рассылки.
- HPA смотрел на CPU, а не на depth; scale с задержкой 10 мин.
- Нет DLQ — битый
report_idкрутится в retry.
Действия: KEDA по depth; алерт oldest_message_age > 300s; DLQ + runbook; canary на воркерах.
CQRS и read-модели
CQRS — разделение модели записи (command) и чтения (query).
- Command — принимает намерение, пишет write-модель, публикует событие.
- Projector — consumer обновляет read-модель (таблица, Elasticsearch, Redis).
После POST на GET данные могут обновиться с задержкой в секунды. UI — optimistic update или индикатор "обновляется".
| Вопрос продукта | Ответ |
|---|---|
| Почему счётчик не сразу +1? | Read model отстаёт |
| Нужна мгновенная согласованность? | Read-after-write из primary — дороже |
| События в неправильном порядке? | Partition по aggregate_id, version в событии |
FAQ
Вопрос. Чем async в коде отличается от очереди?
Ответ. Async I/O — внутри одного процесса; задача пропадёт при рестарте сервера. Очередь — в брокере между процессами; задача переживёт рестарт API. См. два уровня асинхронности и раздел 4.05.
Вопрос. Нужен ли Kafka для 1000 писем в день?
Ответ. Обычно нет. Достаточно outbox, RabbitMQ или SQS и воркеров. Kafka уместен при потоковой аналитике, replay и множестве consumers на один поток.
Вопрос. Как тестировать воркеры?
Ответ. Unit-тест handler без брокера; интеграционный тест с Testcontainers; дважды один payload — проверка идемпотентности. Тестирование, нагрузочное тестирование.
Вопрос. Celery / Sidekiq — это архитектура?
Ответ. Это библиотеки воркеров поверх брокера (часто Redis). Outbox, семантика доставки и DLQ по-прежнему проектируете вы.
Вопрос. Можно ли без брокера — только таблица jobs в PostgreSQL?
Ответ. Да при умеренной нагрузке: SELECT … FOR UPDATE SKIP LOCKED, poller-воркеры. Fan-out и replay слабее, чем у Kafka; зато меньше компонентов. SQL, управление РСУБД.
Вопрос. Что такое visibility timeout в SQS?
Ответ. Время, пока сообщение скрыто от других consumers после получения. Если не подтвердить обработку — сообщение снова станет видимым (аналог nack + retry). Должно быть больше worst-case времени обработки.
Вопрос. Eventual consistency — это баг?
Ответ. Нет, если это записано в требованиях. Баг — когда продукт обещает мгновенную согласованность без синхронного чтения после записи. PACELC.
Вопрос. Где граница между очередью и data pipeline?
Ответ. Очередь задач — "выполни X один раз". Pipeline — непрерывный поток с checkpoint. Конвейеры данных, пакетная работа.
Вопрос. Нужен ли API Gateway перед async API?
Ответ. Не обязателен на старте. Полезен для auth, rate limit и TLS. Тяжёлую работу gateway не выполняет — только POST и 202. API Gateway в 12 концепциях.
См. также
| Тема | Материал |
|---|---|
| Шпаргалка по инфраструктурным идеям | 141 |
| System design | 143 |
| Сквозной кейс outbox + webhooks | 144 |
| RabbitMQ и Kafka | 114, 118, 119 |
| Async в коде | 4.05 |
| Saga, outbox | 2124 |
| Идемпотентность | 213 |
| Событийная архитектура | 2127 |
| Устойчивость | 2136 |
| Микросервисы | 8.05 intro |
| HTTP и статусы | 118 |
- Что произойдёт при дубликате сообщения?
- Где хранится статус для клиента?
- Какой лимит retry и куда попадёт poison message?
- Как измеряете depth, lag, age?
- Как связаны запись в БД и publish (outbox)?
- Какой контракт с клиентом — 202, polling, webhook?
- Есть ли runbook при росте DLQ?