По мере приближения к пределу производительности на одном вычислительном узле мы приходим к тому, что проще разделить наше приложение на несколько частей, микросервисов, расположенных на разных узлах
Поэтому появилась микросервисная архитектура, позволяющая разделить приложения и независимо масштабировать отдельные части. К тому же повышается устойчивость к сбоям - узлы могут дублировать друг друга, а сами узлы использовать разные технологии: один написан на Java, другой на C#, а третий на Python
Однако, если же в монолитной архитектуре общение слоев происходило через функции, то как общаться микросервисам, которые могут располагаться в разных местах и иметь несовместимые интерфейсы?
Очевидным решением является общением по сети: мы можем сделать для каждого узла интерфейс, который отправляет/принимает нужные ему события. Определение этого интерфейса включает выбор транспорта или протокола и согласование форматов сообщений, которыми будут обмениваться системы.
Пока формат сообщений и порядок их отправки между системами согласованы, они смогут взаимодействовать друг с другом, не заботясь о реализации другой системы. До тех пор, пока поддерживается сам контракт, взаимодействие может продолжаться без изменений с другой стороны. Эти две системы эффективно разделены этим интерфейсом
Разделяют 2 типа связи между микросервисами:
Поэтому появились брокеры сообщений (или системы обмена сообщениями).
Брокеры являются независимой прослойкой в общении между микросервисами и в этот же момент базой данных, хранящей все пришедшие сообщения. Брокеры сообщений предоставляют свой интерфейс и свои библиотеки для разных языков, что позволяет объединить микросервисы с разными технологиями
Рассмотрим основные подходы в отправке и получении сообщений:
Подход Точка-Точка (или Point-to-Point) использует очереди сообщений между отправителем и получателем. Очередь действует как буфер, на который могут подписаться получатели, а очередь пытается справедливо распределить сообщения между потребителями. Таким образом, одно сообщение дойдет только одному потребителю
Очереди являются надежными - то есть очередь гарантирует, что будет сохранять сообщения до тех пор, пока потребители не подпишутся на нее. Тогда при появившейся возможности очередь отправит сообщения найденным потребителям. Очереди предоставляют гарантию, что сообщение будет доставлено хотя бы один раз
Надежность часто путают с персистентностью. Персистентность определяет, записывает ли сообщение система обмена сообщениями в какого-либо рода хранилище между получением и отправкой его потребителю
Сообщения, отправляемые в очередь, могут быть или не быть персистентными. Обмен сообщениями типа Точка-точка используется, когда требуется однократное действие с сообщением. В качестве примера можно привести внесение средств на счет или выполнение заказа на доставку
Подход Издатель-Подписчик (Publisher-to-Subscriber) состоит в следующем: потребители могут подписаться на так называемые топики, в которых издатели отправляют свои сообщения. Издатель транслирует свои сообщения в топик, а подписчики их получают. Подписчики по своему желанию могут отписаться от топика, но тогда до них не дойдут сообщения издателей
Топики являются ненадежными - подписчики после повторной подписки не получат пропущенные сообщения. По этой причине можно сказать, что топики предоставляют гарантию доставки не более одного раза для каждого потребителя.
Издатель-Подписчик полезен, когда сообщения носят чисто информационный характер, например, температура градусника
Сценарии использования часто требуют совмещения моделей обмена сообщениями Издатель-Подписчик и Точка-Точка, например, когда нескольким системам требуется копия сообщения, и для предотвращения потери сообщения требуется как надежность, так и персистентность.
В этих случаях требуется адресат (общий термин для очередей и топиков), который распределяет сообщения в основном как топик, так, что каждое сообщение отправляется очереди, предназначенный для конечных потребителей (в этом случае ими могут быть группа потребителей, конкурирующих за сообщение, или один потребитель). Тип чтения в этом случае - один раз для каждой заинтересованной стороны. Эти гибридные адресаты часто требуют надежности, так что, если потребитель отключается, сообщения, которые отправляются в это время, принимаются после повторного подключения потребителя.
Гибридные модели не новы и могут применяться в большинстве систем обмена сообщениями, включая как ActiveMQ (через виртуальные или составные адресаты, которые объединяют топики и очереди), так и Kafka (неявно, как фундаментальное свойство дизайна её адресата).
В 2004 году появился брокер сообщений ActiveMQ с открытым кодом. ActiveMQ была разработана как реализация спецификации Java Message Service (JMS).
JMS же описывает абстракции для отправки и получения сообщений в асинхронном режиме по подписочной модели.
В ней изложены четкие указания по обязанностям клиента системы обмена сообщениями и брокера, с которым он общается, отдавая предпочтение обязательству брокера распределять и доставлять сообщения.
Основная обязанность клиента - взаимодействовать с адресатом (очередью или топиком) отправляемых им сообщений. Сама спецификация направлена на то, чтобы сделать взаимодействие API с брокером относительно простым.
Хотя API и ожидаемое поведение были хорошо определены в спецификации JMS, фактический протокол связи между клиентом и брокером был намеренно исключен из спецификации, чтобы существующие брокеры могли быть сделаны JMS-совместимыми. Таким образом, ActiveMQ был свободен в определении своего собственного протокола взаимодействия. Поэтому протокол OpenWire используется реализацией клиентской библиотеки ActiveMQ JMS. Помимо протокола OpenWire брокер ActiveMQ поддерживает:
AMQP (Advanced Message Queuing Protocol) - не имеет понятия клиентов или брокеров и включает в себя такие функции, как управление потоками, транзакции и различные QoS (Quality-of-Service, качество сервиса): не более одного раза, не менее одного раза и точно один раз
MQTT (Message Queuing Telemetry Transport) - протокол для отправки телеметрии
ну и другие, такие как STOMP
При работе со сторонними библиотеками и внешними компонентами необходимо учитывать, что они могут быть несовместимы с функциями, предоставляемыми в ActiveMQ.
Сам JMS описывает несколько сущностей:
ConnectionFactory
для создания подключения к брокеруConnection
для создания сессии для взаимодействия с брокеромSession
для создания MessageProducer
и MessageConsumer
MessageProducer
для отправки сообщенияMessageConsumer
для получения сообщенияMessage
ConnectionFactory
- это интерфейс, используемый для установления соединений с брокером. В типичном приложении реализация этого интерфейса представляет собой единственный экземпляр. Для ActiveMQ - это ActiveMQConnectionFactory
.
Connection
- это долгоживущий объект, после создания он обычно существует в течение всего жизненного цикла приложения до его закрытия. Connection
- потокобезопасный и может работать с несколькими потоками одновременно.
Session
- это дескриптор потока при взаимодействии с брокером. Объекты Session
не являются потокобезопасными, что означает, что они не могут быть доступны нескольким потокам одновременно. Session
- это основной транзакционный дескриптор, с помощью которого программист может закоммитить и откатить (rollback) операции обмена сообщениями, если он работает в транзакционном режиме. Используя этот объект, вы создаете объекты Message, MessageConsumer и MessageProducer, а также получаете указатели (дескрипторы) на объекты Topic
и Queue
.
MessageProducer
- интерфейс позволяющий отправлять сообщение адресату.
MessageConsumer
- интерфейс позволяющий получать сообщения. Существует два механизма получения сообщения:
Своя реализация MessageListener
, у которой при получении будет вызываться метод onMessage(Message message)
(push-модель):
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
try {
if (message instanceof TextMessage) {
TextMessage textMsg = (TextMessage) message;
System.out.println("Получено текстовое сообщение: " + textMsg.getText());
} else {
System.out.println("Получено сообщение другого типа: " + message);
}
} catch (JMSException e) {
e.printStackTrace();
}
}
});
Опрос на наличие сообщений с помощью методов consumer.receive()
, consumer.receive(long timeout)
или consumer.receiveNoWait()
Message
- самая важная структура. Сообщение содержит заголовки, свойства, а также само тело сообщения (полезная нагрузка, payload)
Заголовки - это хорошо известные элементы, определенные спецификацией JMS и доступные напрямую через API, такие как JMSDestination
и JMSTimestamp
. Свойства - это произвольные фрагменты информации о сообщении, которые задаются для упрощения обработки или маршрутизации сообщений без необходимости считывания самого тела сообщения.
Из Session может быть создано несколько различных типов сообщений в зависимости от типа содержимого, которое будет отправлено в теле - наиболее распространенными из которых являются TextMessage
для строк и BytesMessage
для двоичных данных.
Рассмотрим на примере ActiveMQ, как работает отправка сообщения брокеру:
Это ожидание подтверждения персистентных сообщений является базой гарантии, предоставляемой JMS API - если вы хотите, чтобы сообщение было сохранено, для вас, вероятно, также важно, было ли сообщение принято брокером в первую очередь.
Существует ряд причин, по которым это может оказаться невозможным, например, достигнут предел памяти. Вместо сбоя, брокер либо заставляет издателя ждать, пока не освободится память для обработки сообщения (процесс называется Producer Flow Control), либо он кинет исключение издателю - подобное поведение настраивается в брокере
Обычно, в простом базовом сценарии, никого не интересует, как там себя чувствует брокер сообщений и используют его по сценарию.
Если брокер записывает сообщения в файл, то перед тем как попасть на диск, данные могут оказаться в буферном кеше, поэтому в этом случае подтверждение получения необязательно означает непосредственную запись
Кстати, именно поэтому ваш компьютер жалуется, когда вы небезопасно извлекаете USB-накопитель - файлы, которые вы скопировали, на самом деле, возможно, не были записаны! Как только данные выходят за пределы буферного кэша, они попадают на следующий уровень кэширования, на этот раз на аппаратном уровне - кэш контроллера диска.
ActiveMQ включает в себя буфер записи, в который записываются сообщения. Затем данные из буфера записывается в одно действие. После завершения издатели получают уведомление. Таким образом, брокер максимизирует использование пропускной способности хранилища, а время, потраченное на открытие/закрытие файла, минимизируется
Как же работает отправка сообщения?
Брокер может читать сообщения из памяти страницами, а может использовать механизм курсора между принимающей и перенаправляющей частями брокера для минимизации взаимодействия с хранилищем. Постраничное чтение является одним из режимов, используемым в этом механизме. Курсоры можно рассматривать, как кэш уровня приложения, который необходимо поддерживать в синхронизированном состоянии с хранилищем брокера.
Также в действительности брокер может не удалять сообщения - в журнал может записаться запись о подтверждении. Фактическое удаление файла журнала, содержащего сообщение, будет выполнено сборщиком мусора в фоновым потоке на основе этой информации