Перейти к содержанию

Sync и async

Когда система распадается на отдельные deployable units, появляется вопрос, который раньше решал компилятор: как один кусок зовёт другой. Внутри процесса это вызов функции: наносекунды, типобезопасно, либо отработало, либо стектрейс. Через сеть всё иначе, и первый выбор тут не протокол, а модель: ждать ответ или не ждать.


Synchronous request/response

Synchronous request/response - вызывающий шлёт запрос и блокируется, пока не придёт ответ.

Так работают REST поверх HTTP и gRPC. A зовёт B, держит открытое соединение и ждёт. Пока B думает, логическая операция A занята этим ожиданием.

   ┌─────┐   request    ┌─────┐
   │  A  │ ───────────► │  B  │
   │     │              │     │  B обрабатывает
   │ wait│ ◄─────────── │     │
   └─────┘   response   └─────┘
   A заблокирован всё время, пока B не ответит

Ключевое свойство - temporal coupling, связность во времени:

  • A и B должны быть живы одновременно. B лежит - A не получает ответ, операция падает прямо сейчас.
  • Отказ вызываемого = отказ цепочки. A зовёт B, B зовёт C, C тормозит - тормозит вся цепочка.
  • Связность не только по доступности, но и по скорости. Медленный B делает медленным A, даже если сам A быстрый.

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

Когда подходит:

  • нужен немедленный результат, без него дальше нельзя: логин, проверка баланса, валидация на лету
  • запрос-ответ по своей природе: «дай пользователя по id», «посчитай и верни»
  • цепочка короткая, вызываемый надёжный

Грабли:

  • Длинные синхронные цепочки A -> B -> C -> D: latency складывается, вероятности отказа перемножаются.
  • Без таймаутов, retry и circuit breaker один медленный сервис подвешивает всех, кто его зовёт (см. Устойчивость).
  • Соблазн звать синхронно там, где ответ на самом деле не нужен сразу.

Asynchronous messaging

Asynchronous messaging - отправитель кладёт сообщение в broker и идёт дальше, не дожидаясь, пока его кто-то обработает.

Между отправителем и получателем встаёт посредник: очередь или broker (Kafka, RabbitMQ, SQS). Отправитель делает fire-and-forget: положил и забыл. Получатель забирает, когда готов.

   ┌─────┐  publish   ┌──────────┐   consume   ┌─────┐
   │  A  │ ─────────► │  broker  │ ──────────► │  B  │
   └─────┘            │ ┌──┬──┬─┐│             └─────┘
   A свободен сразу   │ └──┴──┴─┘│   B читает в своём
                      │  буфер   │   темпе, когда жив
                      └──────────┘

Broker разрывает temporal coupling:

  • A и B не обязаны быть живы одновременно. B лежит - сообщения копятся в очереди, B поднимется и разгребёт.
  • Broker работает буфером. Прилетел пик нагрузки - A не падает, очередь растёт, B доедает в своём темпе. Backpressure вместо каскадного отказа.
  • Связность низкая: A не знает, кто и сколько получателей читает. Подписался новый consumer - A не трогаешь.

Цена:

  • Eventual consistency. В момент отправки результат ещё не случился, состояние системы сходится позже. Под это проектируют осознанно.
  • Сложнее отладка и трассировка: поток разорван во времени, причинно-следственную связь не видно в одном стектрейсе.
  • At-least-once, дубли и переупорядочивание - обычная реальность брокеров. Получатель почти всегда должен быть идемпотентным.

Когда подходит:

  • интеграция между bounded contexts: «заказ оформлен» - пусть реагирует кто хочет
  • развязка: producer не должен знать и ждать consumer-ов
  • сглаживание пиков: загрузка, рассылки, обработка, которую не жалко отложить на секунды
  • fan-out: одно событие - много независимых реакций

См. брокеры и протоколы и Event-Driven Architecture.


Как выбирать

Нужен ответ прямо сейчас, чтобы продолжить, - sync. Нужно сообщить о факте и развязать стороны - async. Большинство реальных систем смешивают обе модели, а не выбирают одну навсегда.

Критерий Sync (request/response) Async (messaging, events)
Связность во времени temporal coupling, оба живы сразу развязка через broker
Результат немедленный eventual consistency
Отказ вызываемого падает вся цепочка сообщения копятся в очереди
Пик нагрузки каскадный отказ буфер, backpressure
Связность сторон A знает B A не знает получателей
Отладка один стектрейс разорванный во времени поток
Типичный кейс запрос-ответ, валидация интеграция, события, fan-out

Где в async живёт логика, в сервисах (smart endpoints, dumb pipes) или в шине (ESB), см. SOA и микросервисы.

Итого: sync связывает во времени, async развязывает ценой eventual consistency. Сначала решаешь модель, потом протокол.