Sync vs async в микросервисах: как выбрать способ связи

Sync vs async в микросервисах: как выбрать способ связи Записи вебинаров

Синхронное взаимодействие между микросервисами — это вызов, при котором сервис-инициатор отправляет запрос и ждет ответ, прежде чем продолжить работу (типичные протоколы — REST поверх HTTP, gRPC). Асинхронное взаимодействие — это передача сообщения через очередь или шину событий (например, RabbitMQ, Kafka), при которой отправитель публикует сообщение и продолжает работу сразу, не дожидаясь обработки.

Ниже — чем эти два способа отличаются на практике, таблица сравнения по критериям и правило выбора под конкретную задачу.

Синхронная связь: запрос-ответ

При синхронном вызове сервис A обращается к сервису B напрямую (по HTTP или gRPC) и блокируется до получения ответа или истечения таймаута. Это самая простая для понимания модель: вызов похож на обычный вызов функции, только через сеть.

Плата за простоту — временная связанность (temporal coupling): оба сервиса должны быть доступны одновременно. Если сервис B недоступен или отвечает медленно, это напрямую влияет на сервис A — вызов падает по таймауту или зависает, если таймаут не задан.

REST обычно используют для внешних и межкомандных API, где важна читаемость контракта. gRPC — там, где важна скорость и типизация внутри одной инфраструктуры: он работает поверх HTTP/2, использует бинарный формат Protocol Buffers и поддерживает не только простой запрос-ответ (unary), но и потоковую передачу (streaming). Из-за поддержки стриминга сам gRPC-канал не сводится целиком к «синхронному» — но для типичного вызова между сервисами (unary-запрос с ожиданием ответа) речь идет о той же модели запрос-ответ, что и в REST.

Асинхронная связь: очереди и события

При асинхронной связи сервис A не вызывает сервис B напрямую. Вместо этого он публикует сообщение в брокер (очередь в RabbitMQ или топик в Kafka), а сервис B читает это сообщение независимо, когда готов.

Отправитель и получатель разорваны во времени: получателя может не быть онлайн в момент отправки, и это нормально — сообщение подождет в очереди или партиции топика. Это снимает временную связанность, но добавляет новую сложность: нужно думать о порядке обработки, повторной доставке и согласованности данных между сервисами.

RabbitMQ — брокер очередей, хорошо подходит для задач и уведомлений, обычно с моделью «сообщение забрали — сообщение удалено из очереди». Kafka — распределенный журнал событий: сообщения хранятся в топике заданное время (retention), и несколько независимых потребителей могут читать один и тот же поток каждый в своем темпе. Выбор между ними зависит от задачи: если нужен именно журнал событий с повторным чтением и высокой пропускной способностью — смотрят в сторону Kafka; если нужна классическая очередь задач с гибкой маршрутизацией — в сторону RabbitMQ.

Различие в двух словах

Чтобы не путать понятия дальше по тексту:

  • Синхронный вызов (request-response) — вызывающий сервис ждет ответ в том же взаимодействии.
  • Асинхронное сообщение через очередь — сообщение попадает к одному получателю, обычно раз (модель «задача»).
  • Событие через шину/топик (publish-subscribe) — одно событие может прочитать несколько независимых подписчиков (fan-out); это тоже асинхронная связь, но не то же самое, что очередь задач для одного обработчика.

Sync vs async: сравнение по критериям

Критерий Синхронно (REST/gRPC) Асинхронно (очереди/события)
Связь во времени Оба сервиса должны быть доступны одновременно Отправитель и получатель работают независимо друг от друга
Что получает вызывающий Ответ в том же взаимодействии Подтверждение публикации; сам результат — позже, отдельным событием или через опрос статуса
Поведение при недоступности получателя Вызов завершается ошибкой или таймаутом Сообщение остается в очереди/топике и обрабатывается после восстановления получателя (при персистентной очереди)
Связность (coupling) Сервис знает адрес и контракт другого сервиса напрямую Сервисы связаны только через формат сообщения, не знают друг о друге
Типичный протокол/формат HTTP плюс JSON, gRPC плюс Protocol Buffers AMQP (RabbitMQ), протокол репликации Kafka, MQTT
Порядок обработки Определяется порядком вызовов Нужно обеспечивать явно, например партиционированием по ключу в Kafka
Число получателей одного сообщения Один — тот, кого вызвали Может быть несколько, если это publish-subscribe

Когда что выбирать: по задаче

Задача читателя Что взять Когда иначе
Нужен ответ прямо сейчас, чтобы продолжить сценарий (проверить остаток товара перед оплатой, авторизовать пользователя) Синхронный REST- или gRPC-вызов Если операция объективно долгая (минуты), синхронный вызов держит клиента впустую — лучше сразу вернуть async-задачу и отдельный статус
Действие можно выполнить с задержкой и оно не должно тормозить основной сценарий (отправить письмо, обновить витрину аналитики) Асинхронное событие через очередь или топик Если результат обязателен в том же ответе клиенту, асинхронность добавит сложность без пользы
Несколько сервисов должны узнать об одном факте (заказ создан — нужно уведомить склад, биллинг, аналитику) Публикация события в топик, модель publish-subscribe (Kafka) Если подписчик один и связь простая, можно обойтись прямым sync-вызовом без брокера
Всплески нагрузки, которые нужно сгладить Очередь как буфер между приемом запроса и обработкой (RabbitMQ или Kafka) Если получатель и так быстрее источника нагрузки, буфер не даст выигрыша, а добавит задержку
Нужна гарантированная последовательность обработки событий одного и того же объекта Kafka с партиционированием по ключу объекта Без ключа партиционирования Kafka гарантирует порядок только внутри одной партиции, не между ними

Как ведут себя sync и async при сбое получателя

Ниже — схематичный псевдокод, который показывает разницу в поведении, а не готовое к продакшену решение.

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

# Синхронный вызов: Order Service ждет ответ Payment Service
def create_order(order_data):
    order = save_order(order_data)
    response = http_client.post(
        "http://payment-service/api/payments",
        json={"order_id": order.id, "amount": order.amount},
        timeout=5,
    )
    if response.status_code != 200:
        # Payment Service недоступен или отвечает с ошибкой -
        # вызов падает по таймауту/статусу здесь и сейчас
        raise PaymentServiceUnavailable()
    order.status = "paid"
    return order

Если Payment Service в этот момент не отвечает, вызов create_order завершится исключением, и вызывающей стороне (например, API-шлюзу) нужно решать, что делать: повторить попытку, вернуть ошибку клиенту или включить откат заказа.

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

# Асинхронное взаимодействие: Order Service публикует событие
def create_order(order_data):
    order = save_order(order_data, status="pending")
    event_bus.publish(
        topic="order.created",
        key=order.id,
        payload={"order_id": order.id, "amount": order.amount},
    )
    return order  # клиент получает ответ сразу, статус - pending

# Payment Service подписан на топик и обрабатывает событие своим темпом
def on_order_created(event):
    payment = process_payment(event["order_id"], event["amount"])
    event_bus.publish(topic="payment.processed", payload=payment)

Если Payment Service в этот момент недоступен, событие остается в топике (в Kafka — в партиции, в RabbitMQ — в очереди) и будет обработано, когда сервис снова начнет читать сообщения. Order Service не падает и не ждет; клиент сразу получает ответ со статусом pending, а финальный статус заказа обновится позже отдельным событием.

Типичные ошибки и границы упрощения

Async — это не «бесплатная надежность». Большинство брокеров по умолчанию гарантируют доставку «минимум один раз» (at-least-once): сообщение может прийти получателю повторно, например после сбоя перед подтверждением обработки. Значит, обработчик события должен быть идемпотентным — повторная обработка того же сообщения не должна приводить к двойному списанию денег или двойной отправке письма. Сообщения, которые не удалось обработать после нескольких попыток, обычно уводят в отдельную очередь ошибок (dead letter queue), а не теряют молча.

Синхронная цепочка вызовов из нескольких сервисов подряд — тоже не автоматически надежна. Если сервис A синхронно вызывает B, а тот синхронно вызывает C, отказ C может каскадно уронить всю цепочку и держать потоки A и B занятыми до истечения таймаутов. Для таких цепочек нужны отдельные механизмы устойчивости — таймауты на каждом шаге, повторные попытки с задержкой и circuit breaker, который перестает бить в уже упавший сервис, а не сама по себе синхронность или асинхронность.

Отдельный вопрос — что делать, если бизнес-процесс должен согласованно пройти через несколько асинхронных сервисов (например, оформление заказа сразу списывает деньги, резервирует товар и запускает доставку). Здесь встает выбор между хореографией (каждый сервис реагирует на чужие события самостоятельно, без единого координатора) и оркестрацией (отдельный сервис-координатор явно вызывает шаги по порядку). Это отдельный уровень дизайна процесса поверх выбранного способа связи, типично решаемый паттерном Saga, и в объеме этой статьи не разбирается подробно.

Выводы

  • Синхронная связь (REST, gRPC) дает немедленный ответ, но связывает сервисы во времени: оба должны быть доступны одновременно.
  • Асинхронная связь (очереди, события: RabbitMQ, Kafka) разрывает эту связанность, но требует отдельно решать идемпотентность, порядок и повторную доставку.
  • Выбор делают по задаче: нужен ответ сейчас для продолжения сценария — синхронно; действие можно отложить или нужно уведомить нескольких подписчиков — асинхронно.
  • Kafka дает журнал событий с несколькими независимыми читателями и порядок внутри партиции; RabbitMQ — классическую очередь задач.
  • Ни один из способов не заменяет отдельную работу над отказоустойчивостью: таймауты и circuit breaker для sync, идемпотентность и dead letter queue для async.

Где применяется / связь с практикой

Выбор между синхронной и асинхронной связью — это одно из базовых архитектурных решений при проектировании микросервисов, и его обычно принимают отдельно для каждой пары сервисов, а не одним правилом на весь проект. На практике встречаются системы, где часть вызовов идет через gRPC, а интеграционные события — через Kafka, и это нормальная гибридная схема.

Освойте тему на практике

Как спроектировать межсервисное взаимодействие целиком — с версионированием API, паттерном Saga для распределенных транзакций и выбором между оркестрацией и хореографией — разбирают на курсе Microservice Architecture. Если пока нужно только присмотреться к теме, можно начать с открытых уроков Otus — там разбирают похожие практические кейсы.

FAQ

Можно ли сочетать sync и async в одном сервисе?
Да, это обычная практика: например, сервис отвечает клиенту синхронно по REST, а внутри параллельно публикует событие для других сервисов через Kafka или RabbitMQ. Важно только не смешивать модели внутри одного вызова так, чтобы клиент случайно начал ждать асинхронную обработку как синхронную.

Что делать, если сообщение из очереди не обработалось?
Нужны повторные попытки с задержкой (retry с backoff) и обработчик, устойчивый к повторной доставке (идемпотентный). Сообщения, которые не обработались после нескольких попыток, обычно направляют в отдельную очередь ошибок (dead letter queue) для ручного разбора, а не теряют.

gRPC — это синхронный или асинхронный протокол?
Обычный unary-вызов gRPC (запрос-ответ) относится к синхронной модели, как и REST. При этом gRPC поддерживает потоковую передачу (streaming), где клиент и сервер могут обмениваться сообщениями по мере готовности — эта часть уже ближе к асинхронному взаимодействию. Для типичной межсервисной связи речь чаще идет именно об unary-вызовах.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)