Чем потоковые очереди в RabbitMQ похожи на топики Apache Kafka, в чем между ними разница и как это проявляется на больших нагрузках. Почему масштабируемость и производительность стриминговых очередей RabbitMQ все-таки ниже, чем у топиков Kafka.
Как потоковые очереди в RabbitMQ пытаются приблизить его к Kafka
Недавно я писала про типы очередей в RabbitMQ, которые бывают не только классические, когда брокер удаляет сообщение из очереди после того, как потребитель подтвердил получение данных, но и кворумные, а также стриминговые (потоковые), которые появились в версии 3.9. Потоки (stream queues) представляют собой распределенные логи для добавления данных (AO, Append-Only), откуда потребленные сообщения не удаляются даже после их получания потребителем. Поэтому из потоковой очереди потребитель может считать данные повторно. Этим потоковые очереди RabbitMQ отличаются от кворумных.
Как и кворумные очереди, потоковые сохраняются и реплицируются, обеспечивая доступность данных даже при сбое узла. Масштабируемость потока обеспечивается благодаря шардированию очереди по сегментам (шардам), на которых физически сохраняется часть сообщений из одной очереди, которая представляет собой единую логическую сущность. Распределение сообщений по шардам выполняется по заданному алгоритму шардирования, например, по круговому перебору или хэшу от ключа сообщения. Шарды размещаются на разных узлах кластера RabbitMQ. Потребители могут читать сообщения параллельно из разных шардов, что увеличивает производительность и позволяет масштабировать обработку нагрузки. Поток похож на раздел топика Kafka.
Еще один способ масштабирования публикации и потребления потоков реализуется через суперпотоки, когда один большой логический поток делится на потоки разделов, разделяя хранилище и трафик на несколько узлов кластера. По своей сути, это похоже на сам топик Kafka.
Таким образом, потоковые очереди делают RabbitMQ похожим на Apache Kafka, позволяя хранить миллионы и даже миллиарды сообщений, а также обеспечивая параллельное чтение одного и того же сообщения разными потребителями и повторное потребление данных. Это полезно в сценариях потоковой обработки большого объема событий пользовательского поведения или телеметрии, аналитики в (почти) реальном времени и асинхронной интеграции приложений, когда данные нужно хранить долго и читать их несколькими группами потребителей.
Что вместо смещений потребителей: механизмы отслеживания доставки сообщений
Модель потребления сообщений в потоковых очередях RabbitMQ тоже схожа на Kafka: приложения-потребители сами управляют позициями смещения (offsets) для упорядоченного потребления данных. При этом сама концепция хранения смещения потребителей, как в Apache Kafka, в потоковых очередях RabbitMQ отсутствует. Отслеживание доставки сообщений с гарантией at-least-once (по крайней мере, один раз) в потоковых очередях RabbitMQ реализуется следующим образом:
- когда сообщение попадает в очередь, оно остаётся там до тех пор, пока потребитель не подтвердит его обработку, отправив положительное подтверждение ack;
- если подтверждение не получено из-за сбоя потребителя или разрыва соединения, сообщение возвращается в очередь и может быть доставлено другому потребителю – RabbitMQ повторяет доставку неподтверждённых сообщений.
Таким образом, в RabbitMQ состояние доставки сообщений реализовано через механизм подтверждений (ack/nack) на уровне очереди, а не через смещение потребителей, как в Kafka. Вся информация о доставленных, но не подтверждённых сообщениях, хранится в памяти RabbitMQ или на диске, если очередь долговечная (по умолчанию для потоковых очередей). При этом в RabbitMQ нет централизованного хранения смещений типа таблицы или файла с offset-позициями для каждого потребителя: есть только список сообщений в очереди и их состояние подтверждения.
Если сообщение нужно считать из потоковой очереди повторно, это реализуется на уровне потребителя. Для этого приложение должно хранить идентификаторы последних обработанных сообщений в отдельном хранилище, например, своей локальной базе данных.
Масштабируемость и производительность: Kafka начинает и выигрывает
Помимо этого отличия, потоковые очереди RabbitMQ, хоть и работают быстрее классических, все же уступают Apache Kafka по масштабируемости. Особенно это заметно на больших кластерах и высоких нагрузках. Причины этого кроются как в архитектуре самих систем, так и в деталях реализации. Хотя стриминговые очереди RabbitMQ представляют собой AO-лог, они все равно основаны на внутренних концепциях этого брокера сообщений, таких как обменники и привязки для маршрутизации сообщений. Проверка метаданных сообщений для направления в нужную очередь занимает время.
Кроме того, разные языки программирования, на которых реализованы RabbitMQ и Kafka, влияют на производительность этих популярных брокеров сообщений. Будучи изначально разработанным для создания распределённых, отказоустойчивых телекоммуникационных систем, функциональный язык Erlang, на котором написан RabbitMQ, отлично подходит для параллельной обработки легковесных задач с высокой отказоустойчивостью и эффективной работы с тысячами независимых процессов. Erlang хорошо справляется с легковесными процессами внутри одного ядра ЦП, но не всегда эффективно использует современные многопроцессорные серверы для однопоточных нагрузок, таких как длинные последовательные записи на диск, что характерно для потоковых очередей. Встроенный механизм сборки мусора, т.е. очистки памяти в Erlang-процессах может приводить к непредсказуемым задержкам, особенно при большом количестве сообщений и длительном хранении их в памяти.
Примечательно, что в потоковых очередях RabbitMQ сам диск становится узким местом из-за роста накладных расходы на обслуживание файлов очередей, поскольку оптимизации последовательной записи и чтения, реализованы не так, как в Kafka на основе максимально эффективного использования страничного кэша.
Kafka, написанная на Java, обеспечивает высокую производительность при обработке больших объёмов данных благодаря зрелым механизмам дискового ввода-вывода, использованию страничного кэша операционной системы, буферизации и многоэтапной сборке мусора. Поэтому она хорошо справляется с большими потоками данных, требующими высокой пропускной способности.
Таким образом, RabbitMQ подходит для сценариев с большим количеством параллельных соединений и легковесных независимых задач с небольшой нагрузкой. В свою очередь, Kafka отлично масштабируется как по горизонтали, так и по вертикали, обеспечивая очень высокую пропускную способность на больших объёмах данных.
В заключение подчеркну, что потоковые очереди, появившиеся в RabbitMQ примерно в 2022 году, существенно расширяют набор сценариев применения этого популярного брокера сообщений, добавляя к распределенные логи к традиционной модели очереди заданий для потоковой аналитики с повторным чтением событий. Однако из-за архитектурных особенностей (движок Erlang, управление памятью, работа с диском) масштабируемость и производительность потоковых очередей RabbitMQ все равно существенно уступает Kafka на больших нагрузках с петабайтными объемами данных, огромным количеством клиентов и многоузловыми кластерами.
Кроме того, разнообразие и зрелость экосистемы потоковой обработки данных на основе Kafka намного выше. А стриминговые очереди в RabbitMQ до сих пор считаются экзотикой, о которой знают не только лишь все)). Однако, эта функция может быть полезна, когда надо расширить набор возможностей уже существующей системы, построенной на основе этого брокера сообщений без смены технологического стека.
Научиться работать с RabbitMQ, а также с другими технологиями проектирования архитектуры и интеграции информационных систем вы сможете на моих курсах Школы прикладного бизнес-анализа и проектирования информационных систем в нашем лицензированном учебном центре обучения и повышения квалификации системных и бизнес-аналитиков в Москве:
- Проектирование потокового конвейера на RabbitMQ с разработкой спецификации AsyncAPI
- Основы архитектуры и интеграции информационных систем