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

Так как кон­тей­не­ры изо­ли­ро­ва­ны друг от дру­га, то выбор транс­пор­та для сооб­ще­ний силь­но огра­ни­чен, и, ско­рее все­го, это будет сеть и 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. Пол­но­мас­штаб­ные, такие как RabbitMQMSMQIBM MQ или Kafka, кото­рые мас­шта­би­ру­ют­ся, под­дер­жи­ва­ют тран­зак­ции, под­твер­жде­ния о достав­ке и дру­гие полезности.
  3. Облач­ные реше­ния, напри­мер Amazon SQSAzure Queue и Service Bus Queue или Google Cloud Pub/Sub.

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

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

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

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

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

Отпра­вить-и-забиыть (он же fire-and-forget). Рабо­та­ет соглас­но назва­нию. После отправ­ки сооб­ще­ния сер­вис не ожи­да­ет ниче­го в ответ.

Запрос-ответ (Request-response). Это пат­терн уже немно­го напо­ми­на­ет RPC. После отправ­ки сооб­ще­ния сер­вис ожи­да­ет, что будет какая-нибудь ответ­ная реак­ция. Для это­го нуж­но уже две MQ — одна для исхо­дя­щих и одна для вхо­дя­щих сообщений.

Publish-subscribe (я сло­мал­ся, пере­во­дя publish). В этом слу­чае сооб­ще­ние полу­чат сра­зу несколь­ко под­пис­чи­ков. Каж­дый под­пис­чик заве­дёт свою оче­редь сооб­ще­ний, в кото­рые «глав­ная» оче­редь будет ложить копии сообщений.

Итог

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