Очередь сообщений и асинхронное взаимодействие

В прошлых постах я несколько раз использовал фразу «взаимодействие контейнеров», но особо не вдаваясь в то, как на самом деле оно происходит, и, возможно, подавая ложную надежду, что речь идёт о телепатии.

Так как контейнеры изолированы друг от друга, то выбор транспорта для сообщений сильно ограничен, и, скорее всего, это будет сеть и TCP/UDP протоколы. Но при этом есть уйма вариантов, как этой сетью пользоваться, и об этом мы сейчас и поговорим.

Паттерны межсервисного общения

Всё просто. Их два — это синхронные и асинхронные сообщения.

Синхронные

Отправляя синхронное сообщение мы ожидаем, что тут же что-то придёт в ответ. Это может быть удалённый вызов процедуры (Remote Procedure Call, далее — RPC), или HTTP запрос к RESTful сервису, либо что-нибудь другое. Но в любом случае за запросом должен последовать ответ.

Синхронные сообщения

Синхронное общение заходит лучше всего. С удачным API создаётся впечатление, что на самым деле мы вызываем методы локального объекта, или отправляем запросы к локальной же базе данных. К несчастью, это всё ложь: на самом деле вызываемый объект лежит где-то далеко в сети, а сеть, господа, имеет тенденцию падать. Так уж она сделана. Но на дизайне приложения эта реальность никак не сказывается. Может, добавим дополнительный try-catch, и всё.

Еще одна проблема при использовании синхронного подхода кроется в том, что RPC и REST предполагают какое-то знание о вызываемой стороне. Но сервисам полагается быть независимыми — так в умных книгах пишется. В общем, проблема.

Асинхронное

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

Вот более атеистический пример этой же идеи. Например, пусть в нашем приложении будут два сервиса: UI и сервис обработки заказов. При синхронном подходе, как только UI-сервис заметит, что пользователь нажал кнопку «Заказать», он вызовет PlaceOrder метод в удалённом обработчике заказов через какой-нибудь RPC или REST.

При асинхронном подходе UI просто отправит сообщение «Тут приходили… Есть заказ..», но при этом ему не особо интересно, кто получит это сообщение и что он будет с этим делать.

Очередь сообщений

Но просто так отправить сообщение в космос не получится. У Маска не всегда получается, куда уж простому сельскому программисту. Поэтому выбирают цель поближе — очередь сообщений (Message Queue, MQ). Задача очереди — получить сообщение и держать его у себя, пока кто-нибудь не заберет. Как почтовый ящик. В ней могу быть дополнительные свистелки и ароматизаторы, но, в целом, основная идея именно такая.

очередь сообщений

И очередей на выбор — как медалей у позднего Брежнева:

  1. Простые, такие как ZeroMQ. Они даже не являются брокерами (посредниками), и работают прямо в сервисе.
  2. Полномасштабные, такие как RabbitMQ, MSMQIBM MQ или Kafka, которые масштабируются, поддерживают транзакции, подтверждения о доставке и другие полезности.
  3. Облачные решения, например Amazon SQS, Azure Queue и Service Bus Queue или Google Cloud Pub/Sub.

При таком изобилии выбор MQ для конкретного проекта может затянуться, поэтому стоит начать с простых наводящих вопросов, для чего же она нужна, и уже потом смотреть, что подходит. Например, если процесс с MQ скоропостижно скончался, а в нём оставались необработанные сообщения, важно ли, чтобы они сохранились? Если да, то ищем слово durable в характеристиках MQ. ZeroMQ, например, уходя в страну вечной охоты, уходит туда вместе со всеми сообщениями. MSMQ разрешает выбирать, что делать с сообщениями в проблемных случаях, а облачные MQ не умирают в принципе.

Еще один правдоподобный сценарий — сообщение всё-таки доставилось получателю, но он не успел его обработать и отключился. Стоит ли отправлять такое сообщение назад в очередь? Если да, то MSMQ, Kafka и другие поддерживают либо транзакции, либо подтверждения о доставке, с которыми сообщение можно спасти.

И, наконец, иногда сообщения отправляются… не туда. Например, очередь сообщений, куда целился отправитель, не существует, либо она была переполнена. Некоторые MQ поддерживают так называемую dead-letter queue (DLQ), которая создавалась именно под такие случаи.

Паттерны асинхронных сообщений

Кроме того, что сообщения можно отправлять, их можно отправлять по-разному. Например:

  1. Отправить-и-забиыть (он же fire-and-forget). Работает согласно названию. После отправки сообщения сервис не ожидает ничего в ответ.Fire and forget pattern
  2. Запрос-ответ (Request-response). Это паттерн уже немного напоминает RPC. После отправки сообщения сервис ожидает, что будет какая-нибудь ответная реакция. Для этого нужно уже две MQ — одна для исходящих и одна для входящих сообщений.Request-response pattern
  3. Publish-subscribe (я сломался, переводя publish). В этом случае сообщение получат сразу несколько подписчиков. Каждый подписчик заведёт свою очередь сообщений, в которые «главная» очередь будет ложить копии сообщений.Publisher-subscriber pattern

Заключение

Очереди сообщений — классные. Иногда громоздкие, часто нетривиально настраиваемые, но всё равно приятные. Но никогда не стоит забывать, что MQ — это всё еще инструмент для решение конкретных задач, а не серебряная пуля или лекарство от рака. В каких-то задачах он полезен, в других — вызывает геморрой. В работе программиста гемора и так хватает, так что выбирайте осторожно.

Очередь сообщений и асинхронное взаимодействие: 4 комментария

    1. Я бы с удовольствием, но в таких примерах полагается приводить случай из жизни, типа «а вот как-то в тайге…», а у меня таких нет. А истории, вычитанные у кого-то, не считаются.
      Зато в следующий раз будет ZeroMQ и даже куски кода!

  1. Павел. Интересный блог.
    попал к вам случайно … а тут так много итересного…
    есть вопросы готов меняться на пиво 🙂

    можно как то с вами связаться?
    За ранее спасибо .

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *