← назад к разделу

Когда сервисов больше одного, они должны как-то общаться. Самый очевидный путь — один сервис вызывает другой по HTTP и ждёт ответа. Но есть и другой способ: сервис публикует сообщение в очередь и идёт дальше, не ожидая никого.

Разберём, что означает каждый подход, в чём реальная разница и как не ошибиться с выбором.

Синхронный вызов: спросил — жди ответа

Представьте, что вы звоните в службу доставки и ждёте на линии, пока оператор проверит, доступен ли ваш адрес. Пока оператор не ответил — вы ничего не можете продолжать делать.

Именно так работает синхронный вызов между сервисами. Сервис A отправляет HTTP-запрос к сервису B и блокируется в ожидании ответа. Только получив результат, он продолжает работу.

Сервис A  ──── HTTP GET /users/42 ────►  Сервис B
          ◄─── { "name": "Иван" } ──────

Это привычно и просто: видно, что происходит, ошибки возвращаются сразу, результат есть здесь и сейчас.

Но есть цена: оба сервиса должны работать одновременно. Если сервис B недоступен — операция в сервисе A тоже падает. Если B тормозит — A тоже тормозит. Их судьбы связаны.

Асинхронный вызов: опубликовал — живи дальше

Теперь другой сценарий. Вы не звоните оператору, а оставляете сообщение на автоответчике: «Оформите заказ № 42». И тут же занимаетесь другими делами — оператор перезвонит, когда обработает.

Так работает взаимодействие через брокер сообщений (Kafka, RabbitMQ и подобные). Сервис A публикует событие в очередь и не ждёт: он уже завершил свою работу. Сервис B читает это событие тогда, когда готов.

Сервис A  ──── OrderPaid ────►  [ очередь ]  ────►  Сервис B
          (продолжает работу немедленно)

Если сервис B временно недоступен — событие лежит в очереди и никуда не девается. Когда B поднимется, он его обработает.

Обратная сторона: ответа нет. A не знает, когда B обработает событие и обработает ли вообще. Результат появится через секунды, а то и позже.

Главный вопрос выбора

Задайте себе один вопрос: нужен ли результат соседнего сервиса прямо сейчас, чтобы продолжить операцию?

Если да — синхронный вызов. Примеры:

  • проверить остаток на счёте перед списанием;
  • получить стоимость доставки, чтобы показать итог в корзине;
  • авторизовать платёж в платёжном шлюзе.

Если нет — асинхронное событие. Примеры:

  • уведомить пользователя по email после оформления заказа;
  • обновить поисковый индекс, когда товар изменился;
  • начислить бонусные баллы;
  • передать данные в аналитику.

Хороший тест: что покажет пользователь экран, если сосед не ответил? Если «операция не завершилась» — нужен синхронный вызов. Если «то же самое, что и обычно» — подойдёт асинхронное событие.

Что происходит, когда сосед недоступен

Это самое важное практическое отличие.

При синхронном вызове недоступность соседа — ваш отказ. Если платёжный шлюз лежит, оплата не проходит. Это честно: без шлюза оплата всё равно бессмысленна.

При асинхронном взаимодействии сосед может быть недоступен — это не ваша проблема. Заказ оформляется успешно, событие уходит в очередь, сервис уведомлений обработает его позже, когда поднимется.

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

Один получатель или несколько

При синхронном вызове у вас один адресат: сервис A знает и вызывает конкретный сервис B.

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

Главное: издатель события не знает, кто его читает. Когда появится новый потребитель, сервис заказов не нужно трогать — новый сервис просто подписывается на очередь.

При синхронных вызовах добавление нового потребителя требует изменений в вызывающем коде.

Нагрузка и буферизация

Синхронный вызов передаёт нагрузку напрямую. Если на сервис A пришли 10 000 запросов в секунду, столько же запросов прилетит в сервис B. Его лимиты становятся вашими лимитами.

Брокер сообщений работает как буфер. Сервис A публикует события быстро, сервис B читает их в своём темпе. Пиковые нагрузки сглаживаются естественным образом.

Типичные ошибки

Цепочка синхронных вызовов. Оформление заказа последовательно вызывает склад, биллинг, доставку и уведомления. Если каждый сервис доступен на 99% времени, цепочка из пяти шагов доступна уже на 95%. Латентности складываются. Отказ четвёртого сервиса отменяет работу трёх предыдущих. Такие сценарии с несколькими шагами естественно ложатся на события, а не на цепочку вызовов.

Команда в очереди. Событие «SendEmailRequested» — это замаскированная команда конкретному сервису, не факт домена. Это худшее из двух миров: связность команды плюс сложность асинхронности. События называют свершившиеся факты: «OrderPaid», «UserRegistered», «InvoiceIssued».

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

События без гарантии доставки. Публикация события в брокер после коммита транзакции простым вызовом: если приложение упало между коммитом и отправкой — событие потеряно, данные разъехались. Решение — паттерн outbox: событие сначала сохраняется в ту же базу в рамках той же транзакции, отдельный процесс его отправляет.

Брокер ради брокера. Если между двумя сервисами одной команды один потребитель и ровная нагрузка — добавлять Kafka ради «правильной архитектуры» не стоит. REST с повторными попытками проще в отладке и наблюдении.

Как они сочетаются

В одной системе оба подхода используются одновременно — и это нормально.

Синхронные вызовы там, где пользователь ждёт интерактивного ответа. Асинхронные события там, где домены сообщают друг другу о случившихся фактах.

Подозрительны крайности. Система только на HTTP-вызовах — это сцепленные сервисы, которые падают вместе. Система только на событиях, включая сценарии оплаты и авторизации, — это пользователи, смотрящие на бесконечный спиннер.

Коротко

  • Синхронный вызов: A вызывает B и ждёт ответа. Оба должны работать одновременно.
  • Асинхронное событие: A публикует факт в очередь и идёт дальше. B обработает, когда готов.
  • Главный критерий: нужен ли результат соседа прямо сейчас?
  • Синхронно: проверить баланс, авторизовать платёж, посчитать итог в корзине.
  • Асинхронно: уведомления, аналитика, бонусы, обновление индексов.
  • Недоступность соседа при синхронном вызове — ваш отказ. При событии — его проблема, он догонит потом.
  • События — для фактов домена, не для команд конкретному сервису.
  • События без outbox теряются при сбое между записью в БД и отправкой в брокер.
  • Оба подхода сочетаются в одной системе — по задаче, а не по принципу.

Что почитать дальше

  • Распределённые паттерны — saga, outbox, идемпотентность: инструментарий асинхронной стороны.
  • Паттерны отказоустойчивости — таймауты, повторные попытки, circuit breaker: инструментарий синхронной стороны.
  • AMQP vs Kafka — следующий выбор, если решили использовать события.