Спор «Kafka или RabbitMQ» обычно начинают с вопроса, какая из систем мощнее. Для команды из трёх-пяти человек с одним сервером он почти бесполезен. Полезнее другой вопрос, и он грамматический: какой глагол стоит за вашим сообщением? Если сообщение говорит «сделай», то это работа, у неё есть исполнитель, результат и момент, когда о ней можно забыть. Если оно говорит «это произошло», то это факт, и факт нужен не одному получателю, а всем, кто захочет к нему вернуться: сегодня, завтра и после того, как напишут новый сервис.
Из этого различия вытекает почти всё остальное: как подтверждается доставка, что происходит с прочитанным сообщением, зачем нужно хранение и сколько места на диске придётся отдать. Разберём две конкретные задачи, потом посчитаем, во что обходится одиночный узел, и посмотрим, что меняется, когда проект подрастает.
Две задачи, которые легко принять за одну
Первая задача. Интернет-магазин принимает заказ, и нужно, чтобы кто-то один выставил счёт, отправил письмо и зарезервировал товар. Заказ ушёл в обработку, обработчик справился, сообщение больше не нужно. Если обработчик упал на середине, сообщение должно достаться другому экземпляру или вернуться в очередь. Если обработать заказ не получается совсем, его надо куда-то отложить, чтобы он не крутился по кругу и не мешал остальным.
Вторая задача. Тот же магазин решил, что события «заказ создан», «заказ оплачен», «заказ отменён» нужны сразу нескольким системам: аналитике, рекомендациям, складу, а через полгода ещё и новому модулю антифрода. Новый модуль хочет начать не с этой секунды, а с начала прошлого месяца, чтобы обучиться на реальной истории. Здесь важно не то, что «кто-то обработал». Важно, что запись осталась, у каждого читателя своя позиция в ней, и позицию можно сдвинуть назад.
Первая задача про очередь работ, вторая про журнал событий. Снаружи оба случая выглядят как «сервис А отправил сообщение, сервис Б получил», поэтому их и путают. Различие проявляется позже: при сбое, при подключении нового потребителя, при вопросе «а что мы отправляли вчера в три ночи».
Заказ, который нужно выполнить: как мыслит RabbitMQ
В модели брокера сообщений, к которой относится RabbitMQ, продюсер отправляет сообщение в точку обмена, оттуда по правилам маршрутизации оно попадает в одну или несколько очередей, а потребители забирают сообщения из очереди. Ключевая договорённость такая: потребитель берёт сообщение, обрабатывает и подтверждает. После подтверждения сообщение из очереди уходит. Если потребитель оборвал соединение, не подтвердив, брокер вернёт сообщение в очередь и отдаст другому. Это и есть «работа с исполнителем»: сообщение живёт, пока работа не сделана.
Для небольшой команды в этом много практической пользы. Несколько одинаковых обработчиков читают одну очередь и делят работу между собой, а добавить ещё одного воркера можно, не меняя ничего в продюсере. Очередь сама показывает состояние дел: сколько заказов ждёт, сколько в обработке. Рост очереди лучше любой метрики говорит, что обработчики не успевают.
Теперь про неприятную сторону, о которой в первый день не думают: что делать с сообщением, которое обработать не получается. Битый формат, несуществующий клиент, внешний сервис, который неделю отвечает ошибкой. Если вернуть такое сообщение в очередь сразу, оно будет бесконечно возвращаться и занимать обработчиков. Поэтому для «ядовитых» сообщений заводят отдельное место, куда их переводят после неудачи. В RabbitMQ для этого служит механизм dead letter exchanges. В его документации, RabbitMQ: Dead Letter Exchanges, описано, что dead lettering перенаправляет сообщения, а циклы маршрутизации могут приводить к их удалению. Там же сказано: по умолчанию внутреннее повторное опубликование происходит без publisher confirms и может терять сообщения, если цель недоступна, а для quorum queues есть отдельный режим at-least-once dead lettering.
Для читателя из этого следуют три практических вывода. Во-первых, «страховочная» очередь для неудач сама требует проектирования: куда она ведёт, не замыкается ли маршрут в кольцо, кто и как разбирает накопившееся. Во-вторых, режим по умолчанию не равен «ничего не потеряется»: в нём есть оговорка про недоступную цель. В-третьих, выбор типа очереди влияет на гарантии. Документация указывает, что более надёжный режим связан именно с quorum queues, а значит, для очередей, где потеря сообщения недопустима, тип очереди нужно выбирать осознанно и сверяться с документацией своей версии. Какую версию поставит читатель и какие там значения по умолчанию, зависит от него, поэтому проверять это нужно на своём стенде, а не по памяти.
Как выглядит повторная попытка в живой системе
Допустим, обработчик счетов вызывает платёжный сервис и получает таймаут. Дальше есть несколько стратегий, и все они определяются моделью «работа с исполнителем». Можно вернуть сообщение сразу: быстро, но если сервис лежит, вы получите шквал бесполезных повторов. Можно отправить сообщение в отдельную очередь ожидания с задержкой и потом вернуть в основную, так повторы расходятся во времени. Можно после нескольких неудач отправить заказ в очередь для ручного разбора. Все три варианта естественно выражаются средствами брокера сообщений, и ни одному из них не нужна история старых заказов. Нужна только судьба одной работы.
Эту особенность стоит запомнить: в задачах первого типа повторное чтение прошлого не просто не нужно, оно вредно. Если бы вы случайно «перечитали» вчерашние заказы, вы бы повторно выставили счета.
История, которую читают несколько систем: как мыслит Kafka
Kafka устроена вокруг другой идеи. В введении Kafka события организованы в topics и partitions. Topic можно представить как именованный журнал, а partition как его независимую часть, в которую записи дописываются одна за другой. Читатель не «забирает» запись, а двигается по журналу и помнит, до какого места дошёл. Запись при этом остаётся на месте, пока её не удалят по правилам хранения, которые вы настроили.
Отсюда следует то, что трудно получить в модели очереди работ. Новый потребитель подключается и начинает с нужной позиции, в том числе с самой ранней из сохранившихся. Два независимых потребителя читают один и тот же поток, не мешая друг другу: у каждого своя позиция. Ошибка в аналитике, обнаруженная через два дня, лечится сдвигом позиции назад и пересчётом, без просьб к продюсеру «отправьте всё ещё раз». Для нашего магазина это ровно сценарий антифрода, который хочет учиться на прошлом месяце.
Цена за это тоже есть, и платить её нужно сразу. Журнал надо хранить, а значит, диск становится не фоном, а центральным ресурсом. Нужно решить, сколько времени или какой объём истории вы держите. И нужно понять, как ваши данные делятся на partitions, потому что от этого зависит порядок.
Что на самом деле обещано про порядок
Порядок событий важен почти всем, и именно вокруг него накапливаются недоразумения. Введение Kafka говорит прямо: для одной partition чтение сохраняет порядок записи, а общий порядок между partitions не обещается. Это надо понимать буквально. Если события «заказ создан» и «заказ оплачен» по одному заказу попали в разные partitions, потребитель вполне может увидеть оплату раньше создания.
Практический способ избежать этого — класть все события одной сущности в одну partition, например выбирая её по идентификатору заказа. Тогда порядок внутри заказа сохраняется, а между разными заказами он вам и не нужен. Это ваше проектное решение, а не волшебство брокера, и принимать его нужно до того, как в журнале накопится история, потому что менять схему деления на ходу неудобно.
Обратная сторона: если вы разделили поток на несколько partitions, чтобы читать параллельно, вы получили параллелизм ценой общего порядка. Если же вам действительно нужен строгий общий порядок по всему потоку, придётся жить с одной partition и, соответственно, с одним читающим экземпляром в группе. Команде полезно записать на бумаге: «порядок нужен внутри чего?» Ответ почти всегда «внутри заказа» или «внутри пользователя», а не «во всём мире».
В очереди работ с несколькими обработчиками тоже нет общего порядка: два воркера берут разные сообщения и заканчивают в произвольной последовательности. Если порядок критичен, придётся ограничивать параллелизм или проектировать обработку так, чтобы порядок не имел значения. Это не недостаток RabbitMQ, а следствие самой идеи «много исполнителей одной работы».
Дубликаты неизбежны в обеих моделях
Обе системы можно настроить так, чтобы сообщение не пропадало, и цена такой настройки — возможные повторы. Обработчик сделал работу, но упал до подтверждения: брокер вернёт сообщение, и работа будет сделана дважды. В журнале событий потребитель успел выполнить действие, но не успел сохранить позицию: после перезапуска он прочитает то же событие снова. Механика разная, итог одинаков.
Поэтому настоящий вопрос не «как получить доставку ровно один раз», а «что произойдёт, если это сообщение придёт дважды». Если обработчик выставляет счёт, то повтор нужно распознать по стабильному идентификатору заказа и не выставлять второй счёт. Заметьте: речь о стабильном идентификаторе самой операции, а не о новом идентификаторе каждой попытки. Свежий идентификатор на каждый повтор ничего не защищает, он только делает дубликаты неузнаваемыми. И не стоит рассчитывать, что транзакция вашей базы данных сама охватит внешний вызов: запись в свою базу и обращение к платёжному сервису остаются двумя разными действиями, и согласовывать их вы должны отдельно.
Это рассуждение одинаково для RabbitMQ и Kafka, и оно хорошо показывает, как мало выбор брокера решает за вас. Он определяет форму проблемы, но не снимает её.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Арифметика одного узла
Теперь про то, что обычно оставляют на потом: сколько ресурсов съедает выбранная модель. Возьмём условные числа, они нужны только чтобы показать порядок рассуждения, а не как замер или рекомендация. Допустим, магазин публикует 50 событий в секунду, каждое весит около 1 КБ вместе со служебными данными. Это около 50 КБ в секунду, или примерно 4,4 ГБ в сутки. Если держать историю неделю, получится порядка 31 ГБ только полезных данных. Добавьте служебные файлы, запас на всплески и место, которое операционной системе нужно просто чтобы дышать, и окажется, что неделя истории при таком потоке заметно давит на диск в 40 ГБ.
Для очереди работ арифметика другая и зависит от поведения потребителей, а не от желаемой глубины истории. Пусть в очередь приходят 20 сообщений в секунду по 2 КБ, то есть около 3,5 ГБ в сутки. Пока обработчики справляются, очередь почти пуста и диск почти не растёт. Но если обработчик сломался в пятницу вечером и никто не заметил до понедельника, очередь превратится в склад. Три дня простоя при тех же числах дадут порядка 10 ГБ накопленного. Значит, для очереди нужен не большой диск «на всякий случай», а мониторинг глубины очереди и понятное правило: что происходит, если она достигла определённого размера.
В этом и состоит главное различие в расходе диска. Журнал событий растёт по расписанию, которое вы выбрали сами через срок хранения. Очередь работ растёт только когда что-то идёт не так. Первое планируется заранее, второе требует тревоги вовремя.
Теперь про процессор и память. Не берусь утверждать, какой тариф подойдёт: это зависит от размера сообщений, числа потребителей, настроек надёжности и вашего кода, и без собственного замера на своих данных любые числа будут гаданием. Скорости из чужих тестов тоже мало помогают: они сняты на другом оборудовании, с другим размером сообщений и другими настройками подтверждений. Разумный порядок действий такой: поднять брокер на небольшом тарифе, пустить в него поток, похожий на ваш, и смотреть, что упирается раньше всего: диск, память или сеть. У MAATRIX тарифы линейки Ba, Uab, Heka, Maat и Djed отличаются числом vCPU, объёмом памяти и NVMe-диска. Например, Ba даёт 1 vCPU, 2 ГБ RAM и 40 ГБ, Heka даёт 4 vCPU, 8 ГБ и 160 ГБ. Но это описание ресурсов, а не обещание ёмкости брокера, и переходить на следующий тариф стоит по итогам замера, а не по таблице.
Одно наблюдение всё же можно сделать без измерений. Журнал событий читает и пишет диск постоянно, и скорость диска там важна. NVMe, который указан в публичном предложении MAATRIX, подходит такой нагрузке по своей природе, но сам по себе он не заменяет расчёт объёма хранения.
Что на самом деле значит «один узел»
Здесь легко обмануть себя красивыми словами. Один VPS с одним брокером — это один процесс на одном сервере. Если сервер недоступен, недоступен и брокер. Если диск повреждён, вместе с ним уходят и сообщения, и накопленная история. Никакая настройка внутри одного узла не превращает его в отказоустойчивый кластер.
У Kafka это видно особенно ясно. В описании Kafka сказано, что репликация partitions по нескольким brokers обеспечивает дополнительные копии данных. Ключевые слова — «по нескольким brokers». Если брокер один, копиям просто некуда лечь: можно написать в конфигурации любой коэффициент репликации, но физически данные останутся в одном месте. Поэтому Kafka на одном узле — это нормальная и законная конфигурация для разработки, для обучения, для проекта, где потерю истории можно пережить или восстановить из первичной базы. Но это не «маленькая отказоустойчивая Kafka». И Kafka не следует воспринимать как «большой RabbitMQ»: у неё другая модель, другое хранение, и сложность эксплуатации растёт вместе с ней.
У RabbitMQ картина похожая, хотя и менее наглядная. Один узел — одна точка отказа. Quorum queues, о которых мы говорили в связи с dead lettering, по самой идее работают через согласие нескольких узлов, поэтому их преимущества раскрываются только в кластере. На одиночном узле вы получаете удобный и простой брокер, но не повышенную живучесть. Более подробно о развёртывании RabbitMQ на отдельном сервере рассказано в статье RabbitMQ на VPS: брокер сообщений.
Полезно поэтому сформулировать вслух, что вы готовы потерять. Три вопроса помогают не обманывать себя:
- Если сервер исчезнет сегодня в три часа ночи, можно ли восстановить сообщения из другого места, например из первичной базы заказов?
- Сколько работы потеряется или повторится при перезапуске, и справится ли с этим идемпотентная обработка?
- Где лежит копия данных, которая переживёт потерю самого диска, и когда вы в последний раз пробовали из неё восстановиться?
Последний вопрос касается резервных копий, и тут важно не додумывать за поставщика: автоматические резервные копии не заявлены как часть тарифов MAATRIX, так что копирование данных брокера, если оно вам нужно, придётся организовывать самостоятельно. Для очереди работ часто достаточно того, что настоящие данные живут в первичной базе, а очередь можно наполнить заново. Для журнала событий, который и есть источник правды, вопрос стоит острее.
Сопровождение: сколько внимания требует каждая модель
Небольшая команда платит не только серверами, но и вниманием. У очереди работ эксплуатационные задачи довольно ясные: следить за глубиной очередей, за числом потребителей, за очередью неудач, за свободным местом. Это знакомые вещи, и их можно объяснить новому разработчику за вечер.
У журнала событий внимания требуется больше и другого рода. Нужно выбрать число partitions и ключ деления, решить срок хранения, понимать, как потребители хранят свои позиции, что делать, когда потребитель отстал настолько, что нужные записи уже удалены по сроку. Нужно помнить, что вместе с историей вы сохраняете и ошибки: событие с неверным форматом останется в журнале и будет прочитано каждым новым потребителем. Всё это не страшно, но это отдельный объём знаний, и если в команде нет человека, который хочет им заниматься, механизм начнёт жить своей жизнью.
Честная оценка для малой команды звучит так. Если вам не нужно перечитывать историю и независимо кормить несколько систем одним потоком, то сложность журнала событий вы покупаете впустую. Если же эта возможность составляет суть задачи, то попытка изобразить журнал средствами очереди обойдётся дороже: придётся самим складывать копии сообщений в таблицы, самим помнить позиции и самим придумывать повторное чтение. Тогда вы построите маленькую Kafka хуже настоящей.
Когда модель правильная, а узла уже мало
Теперь представим, что выбор сделан верно и проект растёт. Очередь работ обслуживает уже не сотни, а десятки тысяч заказов в час, и однажды сервер с брокером становится узким местом или, что чаще, становится страшно, что он единственный. Заметьте: модель при этом не стала неправильной. Заказ по-прежнему работа, а не факт для архива. Просто изменились требования к живучести и ёмкости.
Возможные шаги зависят от того, что именно упёрлось. Если не хватает ресурсов одной машины, помогает переход на более крупный тариф: у линейки есть варианты от Ba до Djed, и изменение размера иногда решает проблему без перестройки архитектуры. Если же беспокоит потеря единственного узла, ресурсов недостаточно: нужен второй и третий узел, на которых данные физически продублированы. Здесь пригодится осторожность: несколько VPS сами по себе не гарантируют, что они стоят на разном оборудовании и что приложение стало отказоустойчивым. Отказоустойчивость даёт схема репликации, настроенная и проверенная вами, а не само число серверов. Расположение узлов в разных локациях (в публичном предложении названы UK, US, RU и FR) добавляет свои соображения: задержки между узлами, доступность образов и ОС в конкретной локации нужно сверять в форме заказа.
Для Kafka рост выглядит как добавление brokers и увеличение числа partitions, и тогда репликация впервые обретает смысл: копии партиций можно разместить на разных узлах. Для RabbitMQ рост выглядит как кластер из нескольких узлов и перенос важных очередей на тип, рассчитанный на кворум. В обоих случаях сохраняется то, с чего мы начали: Kafka остаётся журналом событий, RabbitMQ остаётся очередью работ. Рост меняет число машин, но не меняет смысл сообщения.
Бывает и другое: проект вырос так, что в нём появились оба типа задач. Заказы нужно выполнять, а их история нужна аналитике. Такое сочетание нормально, и вариантов немного. Можно оставить очередь для работ и отдельно вести журнал событий для аналитики. Можно сперва вести журнал в базе данных, пока потребителей два, и не поднимать новую систему ради двух читателей. Решать надо по потребности: «нужен ли нам отдельный журнал с независимыми позициями чтения» — вопрос, на который можно ответить только на собственном потоке.
Как проверить выбор на собственном проекте
Ниже небольшая таблица не про «быстро» и «гибко», а про поведение системы в конкретных ситуациях. Читать её нужно как подсказку к вопросам, а не как приговор.
| Ситуация | Модель очереди работ (RabbitMQ) | Модель журнала событий (Kafka) |
|---|---|---|
| Подключили новый сервис, которому нужна история | Историю брать неоткуда: сообщения подтверждены и ушли. Нужен отдельный источник. | Новый потребитель читает журнал с нужной позиции, пока записи не удалены по сроку хранения. |
| Обработчик упал на середине работы | Неподтверждённое сообщение вернётся в очередь и достанется другому обработчику, возможен повтор. | После перезапуска чтение продолжится с сохранённой позиции, часть событий может быть обработана повторно. |
| Одно сообщение невозможно обработать | Нужен отдельный маршрут для неудач; его надо спроектировать и проверить, как показано в документации по DLX. | Запись остаётся в журнале; решать, пропустить её или остановить чтение, должен потребитель. |
| Важен порядок | Параллельные обработчики не дают общего порядка; нужно ограничивать параллелизм. | Порядок гарантирован внутри одной partition, между partitions нет. |
| Что растёт на диске | Накопленная очередь, когда потребители отстали. | Журнал в пределах выбранного срока хранения, даже при исправных потребителях. |
Если пройтись по таблице со своими данными, картина обычно проясняется быстро. Попробуйте ответить на несколько вопросов письменно, по одному на каждый поток сообщений в вашей системе:
- Сообщение — это команда или факт? Нужно ли его забывать после выполнения или хранить как запись о случившемся?
- Сколько независимых систем будет читать один и тот же поток, и появятся ли новые?
- Нужно ли когда-нибудь перечитать прошлое, и насколько далеко?
- Внутри чего должен сохраняться порядок: внутри заказа, пользователя или всего потока?
- Что произойдёт, если одно и то же сообщение придёт дважды?
- Что вы готовы потерять вместе с единственным сервером, и откуда это можно восстановить?
Где всё это запускать
Для проверки модели на реальных данных не нужен большой кластер. Небольшой VPS с root-доступом позволяет поставить брокер, пустить в него поток, похожий на ваш, и посмотреть на глубину очереди, рост диска и поведение при перезапуске. У MAATRIX в публичном предложении указаны NVMe-диски, процессоры AMD EPYC и root-доступ, а оплата принимается картой РФ и через СБП, после чего VPS создаётся автоматически за пару минут. Для такого пробного стенда этого достаточно; конкретный образ ОС и его наличие в нужной локации стоит уточнить в форме заказа. Только не принимайте пробный стенд за боевую схему: это один узел со всеми ограничениями, о которых шла речь выше.
И последнее соображение. Название продукта должно следовать за ответом на вопросы выше, а не предшествовать им. Если большая часть ваших сообщений — это работа, которую нужно выполнить один раз и забыть, вам нужна модель очереди, и подойдёт брокер, построенный вокруг подтверждений и маршрутов неудач. Если же главное — сохранить поток случившегося, чтобы разные системы читали его в своём темпе и возвращались назад, вам нужна модель журнала, и тогда стоит смотреть на Kafka вместе с её партициями, сроками хранения и честной арифметикой диска. Сначала напишите на листе одну фразу о своём сообщении, начинающуюся с глагола. Остальное станет вопросом настройки.