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. Сначала решаешь модель, потом протокол.