Две системы должны обменяться данными. Первая мысль — один сервис вызывает REST API другого и получает ответ. Это работает, пока второй сервис доступен, быстро отвечает и выдерживает нагрузку. Как только хотя бы одно из условий нарушается, синхронная цепочка начинает падать целиком. Асинхронная интеграция через очереди и события снимает часть проблем, но приносит свои: задержки, дубли, порядок сообщений, сложность отладки. Разберём, как выбирать между этими подходами и чем каждый из них расплачивается за свои преимущества.
Синхронная интеграция
При синхронном взаимодействии клиент отправляет запрос и ждёт ответа, прежде чем продолжить работу. Типичные варианты — REST, gRPC, SOAP, GraphQL. Модель понятна: вызвал функцию — получил результат или ошибку.
Плюсы:
- немедленный результат: клиент сразу знает, получилось или нет;
- простая модель согласованности: после успешного ответа данные уже изменены;
- лёгкая отладка: один запрос, один ответ, понятная трассировка;
- не нужна дополнительная инфраструктура — брокер, мониторинг очередей.
Минусы:
- временная связанность: если поставщик недоступен, клиент тоже не может выполнить операцию;
- каскадные отказы: медленный сервис в конце цепочки «подвешивает» потоки во всех сервисах выше по цепочке;
- суммирование задержек: цепочка из пяти вызовов по 100 мс — это уже полсекунды;
- перенос нагрузки: всплеск запросов у клиента сразу становится всплеском у поставщика.
Доступность синхронной цепочки — произведение доступностей её звеньев. Пять сервисов с доступностью 99,9% дают вместе около 99,5% — это уже не три с половиной часа простоя в год, а почти двое суток.
Асинхронная интеграция
При асинхронном взаимодействии отправитель передаёт сообщение посреднику — брокеру сообщений — и продолжает работу, не дожидаясь обработки. Получатель забирает сообщение, когда готов. Типичные брокеры: Apache Kafka, RabbitMQ, ActiveMQ/Artemis, облачные очереди (Amazon SQS, Yandex Message Queue), IBM MQ в банковском секторе.
Плюсы:
- развязка по времени: получатель может быть недоступен — сообщения дождутся его в брокере;
- сглаживание пиков: очередь поглощает всплеск, а получатель обрабатывает сообщения в своём темпе;
- масштабирование: несколько экземпляров получателя делят поток сообщений между собой;
- одно событие — много получателей: отправитель не знает, кто и сколько подписчиков обработают сообщение.
Минусы:
- итоговая согласованность (eventual consistency): данные в системах какое-то время расходятся;
- доставка «хотя бы один раз»: сообщение может прийти повторно, получатель обязан это выдерживать;
- порядок не гарантирован в общем случае, а там, где гарантирован, — только в пределах партиции или очереди;
- сложность отладки: путь бизнес-операции разбит на несколько независимых шагов в разных системах;
- инфраструктура: брокер нужно эксплуатировать, мониторить и масштабировать.
Очередь и топик: два способа доставки
| Очередь (point-to-point) | Топик (publish-subscribe) | |
|---|---|---|
| Кто получает | один получатель из группы конкурирующих | каждый подписчик получает свою копию |
| Назначение | распределение работы | оповещение заинтересованных сторон |
| Пример | очередь задач на формирование PDF-счетов | событие «Заказ оплачен» для склада, доставки и аналитики |
| В RabbitMQ | очередь с несколькими консьюмерами | fanout- или topic-exchange с очередью на каждого подписчика |
| В Kafka | одна consumer group | несколько consumer group на одном топике |
Kafka стоит отдельно: это не очередь, а журнал событий. Сообщения не удаляются после чтения, а хранятся заданное время; каждый потребитель сам помнит, до какого места дочитал (offset), и может перечитать историю. Это удобно для восстановления данных и подключения новых потребителей, но меняет модель мышления: «прочитано» не значит «удалено».
Команда, событие, документ
Сообщения различаются по смыслу, и это различие важно зафиксировать в контракте.
- Команда — просьба что-то сделать: «Сформируй счёт», «Отправь SMS». У неё один адресат, и отправитель ожидает, что действие будет выполнено. Название — в повелительном наклонении.
- Событие — уведомление о свершившемся факте: «Заказ оплачен», «Клиент сменил адрес». Отправитель не знает, кто его обработает, и не ожидает действий. Название — в прошедшем времени.
- Документ (event-carried state transfer) — событие, которое несёт полное состояние объекта, чтобы получателям не нужно было обращаться за подробностями к источнику.
Частая ошибка — события, которые на деле являются скрытыми командами: «ЗаказНуждаетсяВДоставке» с единственным подписчиком. Это синхронная связанность, переодетая в асинхронность, со всеми минусами обоих миров.
Тонкое или толстое событие
Тонкое событие несёт только идентификатор и тип: {"type": "OrderPaid", "orderId": "42"}. Подписчик сам идёт за деталями в API источника. Плюс — маленький контракт; минус — синхронная зависимость возвращается, а данные к моменту запроса могли уже измениться.
Толстое событие несёт всё, что нужно подписчикам: сумму, состав, адрес. Плюс — подписчики автономны; минус — контракт события шире и его изменения затрагивают больше потребителей. На практике выбирают середину: в событие кладут данные, которые нужны большинству подписчиков, и фиксируют их в контракте.
Как выбрать
Удобно идти от вопросов, а не от технологий.
| Вопрос | Скорее синхронно | Скорее асинхронно |
|---|---|---|
| Нужен ли ответ, чтобы продолжить? | да: проверка лимита, расчёт цены, авторизация | нет: уведомления, отчёты, синхронизация справочников |
| Ждёт ли пользователь результат на экране? | да, и результат быстрый | нет, или результат долгий (минуты, часы) |
| Сколько получателей у информации? | один | несколько, и их число будет расти |
| Допустима ли временная рассогласованность? | нет | да, секунды или минуты допустимы |
| Что будет, если получатель недоступен? | операцию можно отклонить | операцию нельзя потерять |
| Нагрузка равномерная или пиковая? | равномерная, поставщик справляется | пиковая, нужен буфер |
Полезная эвристика: запросы на чтение и проверки — синхронно, изменения, интересные другим системам, — событиями. Сервис принимает заказ по REST, сразу отвечает клиенту «принято» и публикует событие, на которое реагируют остальные.
Гибридные схемы
Асинхронный запрос — ответ
Клиент отправляет запрос в очередь с указанием, куда прислать ответ (reply-to) и идентификатором корреляции (correlation-id). Поставщик обрабатывает запрос и кладёт ответ в указанную очередь. Так работают многие банковские интеграции на IBM MQ. Важно описать таймаут ожидания ответа и поведение при его превышении.
Принять синхронно — обработать асинхронно
API принимает запрос, проверяет его, сохраняет и сразу возвращает 202 Accepted с идентификатором задачи. Обработка идёт в фоне, а клиент узнаёт результат одним из способов:
- опрос (polling):
GET /tasks/{id}с интервалом; - обратный вызов (webhook): поставщик сам вызывает API клиента по завершении;
- событие: клиент подписан на топик результатов;
- push в интерфейс: WebSocket или Server-Sent Events.
Это стандартное решение для долгих операций: формирование отчётов, импорт файлов, проверки в сторонних системах.
Синхронный фасад над событиями
Для внешних партнёров, которые не умеют работать с брокером, асинхронный процесс оборачивают REST API, а внутри сервисы общаются событиями. Аналитику важно не забыть описать, что видит партнёр в промежуточных состояниях.
Надёжность: о чём договориться заранее
Асинхронная интеграция надёжна только тогда, когда о надёжности договорились явно.
- Гарантии доставки. Почти всегда это «хотя бы один раз» (at-least-once). «Ровно один раз» на уровне бизнес-эффекта достигается идемпотентной обработкой на стороне получателя.
- Порядок. Если важно, чтобы события по одному заказу обрабатывались по порядку, в Kafka их отправляют с одним ключом (id заказа), тогда они попадут в одну партицию. Порядок между разными заказами при этом не гарантирован — и обычно не нужен.
- Ошибки обработки. Что делать с сообщением, которое не удаётся обработать: сколько повторов, с какими паузами, куда его отложить (dead letter queue), кто и как разбирает отложенные.
- Атомарность публикации. Сохранить данные в БД и отправить событие нужно согласованно. Наивное «сначала commit, потом send» теряет события при сбое; стандартное решение — шаблон Transactional Outbox.
- Срок жизни сообщений. Сколько хранится сообщение, если получатель недоступен долго.
- Версионирование. Как меняется схема сообщения, как долго поддерживаются старые версии.
У синхронной интеграции свой список: таймауты на каждом вызове, повторы с экспоненциальной паузой, ограничение числа одновременных запросов, предохранитель (circuit breaker), который перестаёт вызывать упавший сервис и даёт ему восстановиться.
Что фиксировать в постановке
Для синхронной интеграции:
- контракт в OpenAPI (или WSDL, .proto): операции, структуры, ошибки;
- ожидаемая нагрузка: запросов в секунду в среднем и в пике;
- требования ко времени ответа: например, 95% запросов быстрее 300 мс;
- таймауты и политика повторов на стороне клиента;
- поведение при недоступности поставщика: отказ пользователю, деградация, данные из кэша.
Для асинхронной интеграции:
- контракт в AsyncAPI: каналы (топики, очереди), сообщения, заголовки, схемы данных;
- тип сообщения: команда или событие, кто отправитель и кто получатели;
- ключ партиционирования и требования к порядку;
- идентификатор сообщения для дедупликации, идентификатор корреляции для трассировки;
- политика повторов и dead letter;
- допустимая задержка доставки: секунды, минуты;
- объём: сообщений в секунду и средний размер сообщения.
Обязательно — диаграмма последовательности сквозного процесса, где видно, какие шаги синхронные, а какие асинхронные, и где система может оказаться в промежуточном состоянии.
Типичные ошибки
- Синхронная цепочка через всю систему. Сервис A вызывает B, B вызывает C, C вызывает D — и всё это в рамках запроса пользователя. Любой сбой в конце цепочки становится сбоем для пользователя.
- Брокер как синхронный вызов. Отправитель кладёт сообщение и блокируется в ожидании ответа без таймаута — получаем минусы обоих подходов.
- Нет идемпотентности. Повторная доставка приводит к двойному списанию, двойному письму, двойной отгрузке.
- Нет плана на «ядовитые» сообщения. Одно сообщение, которое всегда падает при обработке, блокирует всю очередь или партицию.
- События без владельца. Никто не отвечает за схему события, её меняют без согласования, подписчики ломаются.
- Асинхронность там, где пользователь ждёт ответа. Кнопка «Оплатить» показывает «готово», а через минуту приходит отказ — сценарий нужно было проектировать с промежуточным статусом.
Итог
Синхронная интеграция проще и даёт немедленный результат, но связывает системы по времени и доступности. Асинхронная развязывает системы и выдерживает пики, но требует продуманной надёжности: идемпотентности, повторов, порядка, мониторинга. Зрелая архитектура использует оба подхода: синхронные вызовы — для запросов, на которые нужен ответ здесь и сейчас, события — для распространения изменений между системами.
Синхронные контракты удобно описывать в редакторе OpenAPI, асинхронные — в редакторе AsyncAPI (см. гайд «AsyncAPI онлайн»), а сквозной процесс с синхронными и асинхронными шагами — на диаграмме последовательности PlantUML. О выборе между REST, gRPC и GraphQL — в статье «gRPC, GraphQL и REST».
