Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 23 из 25 ~20 мин +20 XP

Kafka и RabbitMQ: выбор под задачу

Kafka и RabbitMQ постоянно стоят в одном ряду в требованиях к Go-сервисам, и всё же это инструменты с разной моделью данных: распределённый лог с офсетами против очереди с подтверждениями. Сравнивать их списком фич бессмысленно — сравнивать нужно кейсами: что случится с событием после обработки, как устроены повторы, кто гарантирует порядок, как раздаётся копия потока нескольким подписчикам и можно ли перечитать неделю назад. В этом уроке пройдём пять таких кейсов подряд, покажем каждый на коде kafka-go и сведём вердикты в таблицу: где лог выигрывает, где очередь, а где ни то ни другое и пора доставать asynq.

Содержание урока · 10 разделов

Оба брокера принимают сообщения от продюсеров и отдают их консьюмерам, и на этом сходство заканчивается. Kafka хранит всё, что записано: сообщение попадает в лог партиции и остаётся там до истечения retention, а консьюмеры лишь двигают собственные офсеты — сам журнал от чтения не меняется. RabbitMQ устроен как почта: сообщение кладётся в очередь и выдаётся одному подписчику, а ack консьюмера — это команда «удали навсегда»; не забрал или отклонил — очередь начинает ретраить и в конце отправляет в dead-letter. Из этой разницы вытекает всё остальное: у Kafka повторы делаются руками, зато есть история, у RabbitMQ повтор и отказ — встроенный механизм, зато после ack следа не остаётся. Дальше — пять кейсов, в каждом один и тот же вопрос и два разных ответа.

Судьба события: лог против очереди с ack#

Первый кейс: платежный сервис обработал событие и что дальше. В RabbitMQ путь короткий: консьюмер вызвал Ack — брокер удалил сообщение из очереди, и система вернулась к состоянию «до события». Это идеально для задач в духе «отправить письмо и забыть»: очередь — журнал незавершённой работы, а не архив. В Kafka сообщение после обработки никуда не девается: WriteMessages положил его в партицию, консьюмер сдвинул коммит офсета, но запись живёт до истечения retention. Любой новый читатель начинает с StartOffset и видит те же байты, что видели первые. Отсюда практическое следствие для выбора: если ценность сообщения умирает в момент обработки — это очередь; если событие остаётся фактом, который завтра понадобится новому подписчику, аналитике или отладке, — это лог. Ошибка выбора всплывает не сразу: очередь с задачами на Kafka заставит городить удаление и компакцию из семнадцатого урока, а аудит событий на RabbitMQ потребует отдельного хранилища, потому что брокер ничего не помнит. Отметьте для себя асимметрию хранения: Kafka расходует диск всегда, RabbitMQ — только на незавершённую работу, и в мирном режиме очередь выглядит экономнее ровно до момента, когда понадобится история.

Проверка: ack в RabbitMQ

Консьюмер RabbitMQ успешно обработал сообщение и отправил ack. Что происходит с сообщением?

Повторы: nack из коробки против DLQ своими руками#

Второй кейс: обработчик упал на конкретном сообщении. В RabbitMQ вся механика повтора встроена: nack с requeue возвращает сообщение в очередь, а связка с dead-letter exchange собирает отказавшие письма в отдельную очередь, где их ретраят по расписанию или разбирает человек. Консьюмер только честно сообщает результат. В Kafka повторов из коробки нет: consumer-протокол не умеет «отклонить» одно сообщение, потому что офсет — позиция в логе, а не номер задачи. Стандартный паттерн — сделать всё самому: отказавшее сообщение уводим в отдельный dead-letter топик с заголовком-причиной и коммитим оригинал, иначе группа застрянет на этой позиции навсегда. Такой паттерн разобран построчно в девятнадцатом уроке; здесь важно другое — сравнить трудозатраты. На RabbitMQ retry-политика — настройка, на Kafka — собственный код, тесты и мониторинг DLT-топика. Зато ваш DLQ описан на Go, а не в терминах чужого плагина.

Dead-letter топик руками: повтор без остановки группы

GO
for {
        m, err := reader.FetchMessage(ctx)
        if err != nil {
                log.Fatal(err)
        }
        if err := process(m); err != nil {
                // повтор руками: отказавшее сообщение уходит в DLT
                dlt.WriteMessages(ctx, kafka.Message{
                        Topic:   "orders-dlt",
                        Key:     m.Key,
                        Value:   m.Value,
                        Headers: append(m.Headers, kafka.Header{
                                Key:   "x-error",
                                Value: []byte(err.Error()),
                        }),
                })
        }
        reader.CommitMessages(ctx, m)
}

Классическая схема повтора в kafka-go, которую в RabbitMQ дал бы один nack: отказавшее сообщение копируется в dead-letter топик с причиной в заголовке, после чего офсет коммитится и группа двигается дальше. Без WriteMessages в DLT цикл завис бы на позиции ошибки, а без CommitMessages — раз за разом перечитывал бы отказ.

Порядок: партиция против одного консьюмера#

Третий кейс: обработка заказов одного клиента не должна перемешиваться. Kafka гарантирует порядок в пределах партиции: ключ отправителя через Hash попадает всегда в один отсек, и события внутри него читаются ровно в порядке записи; масштабирование добавляет консьюмеров в группу, но партиция по-прежнему одна на ключ — это механика пятого и седьмого уроков. В RabbitMQ честный порядок стоит дороже: очередь отдаёт сообщения нескольким консьюмерам параллельно, и про глобальный порядок внутри неё говорить не приходится; гарантия появляется только в вырожденной конфигурации — одна очередь, один канал, один консьюмер, prefetch равен единице, — и эта же конфигурация убивает пропускную способность. Есть нюанс и у Kafka: порядок держится, пока число партиций неизменно, и сломается при добавлении новых — маппинг ключа пересчитается. На кейсе это значит: поток событий с требованием «сначала создан, потом оплачен» укладывается в Kafka естественно, а очередь задач, где порядок неважен, а важна скорость разбора, выигрывает от параллелизма RabbitMQ. RabbitMQ в этой роли не проваливается, а честно признаёт: упорядочивать и масштабироваться одновременно он не умеет, и выбирать придётся одно из двух.

Проверка: строгий порядок обработки

Сервису нужен строгий порядок обработки событий по ключу. Какая конфигурация даёт его честно?

Fanout: группы читают один лог против exchanges#

Четвёртый кейс: одно событие заказа нужно биллингу, аудиту и уведомлениям. В RabbitMQ это работа exchanges: публикуете в fanout-обменник, он копирует письмо во все привязанные очереди, и каждая команда читает свою копию. В Kafka той же служит структура консьюмер-групп: лог один, а независимые читатели — это группы с разными GroupID. Каждая группа ведёт собственные коммиты офсетов и получает весь поток целиком; внутри группы партиции делятся между членами, между группами — никогда. Это механика седьмого урока, и на сравнении она даёт Kafka ощутимое преимущество: добавить нового подписчика к существующему потоку — значит запустить ещё одну группу, без изменения продюсера и без новых очередей в топологии. В RabbitMQ каждая новая команда — это новая очередь и новая привязка на обменнике, то есть правка конфигурации брокера. Для Go-кода разница тоже на поверхности: Reader с другим GroupID — четыре строки, тогда как топология exchanges — команда или консоль брокера. Обратная сторона: группа в Kafka читает лог с той позиции, где договорились, а обменник RabbitMQ раздаёт только то, что опубликовано после привязки.

Fanout в kafka-go: две группы на одном логе

GO
billing := kafka.NewReader(kafka.ReaderConfig{
        Brokers: []string{"localhost:9092"},
        GroupID: "billing",
        Topic:   "orders",
})
defer billing.Close()

audit := kafka.NewReader(kafka.ReaderConfig{
        Brokers: []string{"localhost:9092"},
        GroupID: "audit",
        Topic:   "orders",
})
defer audit.Close()

Два Reader с разными GroupID — это fanout без exchanges: каждый получает полный поток заказов и хранит коммиты независимо, биллинг отстанет — аудит не заметит. Внутри одной группы партиции разделились бы между членами, но здесь группы разные, поэтому пересечений нет. Новый подписчик добавляется ещё одним Reader с новым GroupID.

Что получит новая группа audit-2024?

Прочитайте код и выберите, что он выведет:

// топик orders существует месяц, retention семь дней,
// группы billing и audit давно читают поток.
reader := kafka.NewReader(kafka.ReaderConfig{
        Brokers:     []string{"localhost:9092"},
        GroupID:     "audit-2024",
        Topic:       "orders",
        StartOffset: kafka.FirstOffset,
})

Retention и replay: история, которую можно перечитать#

Пятый кейс — следствие первого: что делать, когда нужно перечитывание. Kafka хранит лог заданный срок или объём, и новый консьюмер с StartOffset равным kafka.FirstOffset начинает с самого старого живого сообщения: отладка инцидента недели назад, восстановление проекции, подключение нового сервиса к старому потоку — всё это replay без дополнительной инфраструктуры, детали в семнадцатом уроке. RabbitMQ не хранит обработанное в принципе: ack стёр сообщение, и «перечитать неделю» на очереди означает, что неделю назад кто-то обязан был скопировать поток в архив — отдельное хранилище, отдельный код. В копилку RabbitMQ честно запишем обратное: лог, который растёт семь дней, требует диска и внимания к retention, тогда как очередь сама освобождается по мере разбора. Выбор, таким образом, снова о модели данных: архив за подписку или почтовый ящик без архива.

Пять кейсов одним столом#

КейсKafkaRabbitMQ
Судьба после обработкиСообщение остаётся в логе до истечения retentionAck удаляет сообщение: очередь хранит незавершённую работу
Повторы при ошибкеРуками: DLT-топик, запись и коммит из кодаВстроено: nack, requeue, dead-letter exchange
ПорядокГарантирован в партиции; ключ через Hash держит один отсекЧестно только одна очередь, один канал, один консьюмер
FanoutГруппы с разными GroupID читают один лог независимоExchanges копируют письма в привязанные очереди
Replay историиНовый консьюмер с FirstOffset перечитывает retentionНевозможно: после ack сообщения нет

Вердикты по кейсам#

  • Событийный аудит, поток данных, ETL и интеграции по событиям — Kafka: лог хранит факты, группы дают fanout, replay выручает новых читателей.
  • Платёжные события и любые потоки, где порядок по ключу важен, — Kafka с Hash-балансером и трезвым отношением к числу партиций.
  • Задачные очереди с ретраями, приоритетами и разбором отказов — RabbitMQ: nack и dead-letter exchange из коробки.
  • Фоновые задачи внутри Go-сервиса — asynq на Redis: та же модель задач без второго брокера в инфраструктуре.
  • Нужны и поток, и задачи — берите оба: Kafka для событий, очередь для работы; граница проходит по судьбе сообщения после ack.

Соберите replay-ридер

Допишите конфиг новой группы: поле, которое делает Reader членом группы, и константу старта с самого старого сообщения.

reader := kafka.NewReader(kafka.ReaderConfig{ Brokers: []string{"localhost:9092"}, Topic: "orders", GroupID: "replay-2024", StartOffset: kafka., }) defer reader.Close()

Соберите повтор с DLT в правильном порядке

Расставьте строки consumer-цикла: чтение, выход по ошибке, обработка, запись отказа в dead-letter топик, коммит, закрытие цикла.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: две модели в одной памяти

Термин

Распределённый лог

нажмите, чтобы перевернуть
Определение

Модель Kafka: сообщения остаются в партиции до истечения retention, консьюмеры лишь двигают собственные офсеты.

1 / 5

Пять кейсов сошлись в один вывод: выбирать нужно не «что лучше», а что за данные у вас на проводе. Сообщение-факт, который завтра понадобится кому-то ещё, — это Kafka: лог, офсеты, группы, replay. Задача, которая исчезает в момент выполнения, — это очередь: RabbitMQ с его nack и dead-letter exchange, а для фоновых задач внутри одного Go-приложения — asynq на Redis. Разбор повтора в kafka-go построчно ждёт в уроке про ошибки консьюмера, механика групп, на которой построен fanout, — в уроке про группы консьюмеров; когда вердикт по вашему кейсу — очередь, держите под рукой курс по RabbitMQ и курс по asynq. Прежде чем выбирать, посчитайте судьбу каждого сообщения вашего домена: что с ним после обработки, кто его перечитает, кто его отклонит при ошибке — ответы на эти три вопроса дают вердикт быстрее любого бенчмарка. В песочнице это проверяется за вечер: один Docker-брокер, два Reader с разными GroupID и честный список кейсов вашего домена. Следующий урок — обратная сторона этого: грабли продакшена Kafka, на которых лог превращается из преимущества в источник инцидентов.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Kafka — распределённый лог: сообщения остаются в партиции до истечения retention, RabbitMQ — очередь с ack: сообщение удаляется после обработки.
  • Повторы: у RabbitMQ встроены nack и dead-letter exchange, в Kafka DLT-топик, запись и коммит делаются руками.
  • Порядок: Kafka гарантирует его в партиции по ключу; RabbitMQ — только одна очередь с одним консьюмером ценой пропускной способности.
  • Fanout: в Kafka независимые группы читают один лог, в RabbitMQ exchanges копируют письма в очереди.
  • Replay: новый консьюмер с kafka.FirstOffset перечитывает retention; на очереди с ack перечитывать нечего.
  • Кейс-вердикт: аудит, потоки, ETL — Kafka; задачные очереди с ретраями — RabbitMQ; фоновые задачи в Go — asynq на Redis.
  • Ошибка выбора проявляется поздно: задачная очередь на Kafka требует удаления и компакции, аудит на RabbitMQ — внешнего архива.

Домашнее задание

Поднимите Docker-стенд из третьего урока и локальный RabbitMQ, затем прогоните три кейса из урока на своих данных. Первый: создайте топик orders, запустите два Reader с GroupID billing и audit — убедитесь, что fanout работает и обе группы получили весь поток. Второй: сломайте process на одном сообщении, соберите consumer-цикл с DLT-топиком из разбора выше и убедитесь, что группа не застряла, а отказ лег в orders-dlt с заголовком x-error. Третий: запустите новую группу replay с kafka.FirstOffset и зафиксируйте, с какого момента она получила данные. В финале выпишите три кейса вашего текущего проекта и для каждого вердикт: лог, очередь или asynq — с обоснованием по судьбе сообщения после обработки.

Личные заметкиПусто
Хранятся только в вашем браузере (Kafka · Kafka и RabbitMQ: выбор под задачу)

Вопросы и ответы по уроку

Можно ли использовать Kafka как задачную очередь с ретраями и приоритетами?

Технически можно, экономически — редко оправдано. Ретраи собираются руками: DLT-топик, отдельный ретраер, заголовки с числом попыток — весь механизм из dead-letter exchange придётся воспроизвести кодом. Приоритетов в логе нет в принципе: партиция читается строго последовательно, «важное раньше» потребует отдельного топика и отдельной группы с приоритетом планировщика. Для задачных очередей честнее RabbitMQ или asynq на Redis: там ретраи и веса — часть модели, а не надстройка. Kafka оправдывает себя, когда задача после выполнения остаётся фактом, который нужно перечитать.

Как мигрировать с RabbitMQ на Kafka, не сломав текущих потребителей?

Мигрировать стоит кейсами, а не системой целиком. Определите, какие очереди хранят задачи, а какие — события: задачи оставьте или уведите в asynq, события переведите на Kafka. Для перехода ставится мост: консьюмер RabbitMQ публикует полученные письма в kafka-топик, старые потребители работают, новые читают лог. Опасное место — двойная запись при переключении продюсера: пока не проверена запись в оба брокера и сверка потока, старый путь не выключайте. После переключения новые подписчики подключаются группами, и мост демонтируют.

Правда ли, что RabbitMQ медленнее Kafka, и стоит ли это учитывать в выборе?

На бенчмарках Kafka выигрывает в пропускной способности на порядок, но выбор от цифр зависит меньше, чем кажется. Очередь с ack и подтверждениями на каждую операцию не может быть такой же дешёвой, как последовательная запись в лог, — это цена разных моделей, а не плохой реализации. Для задачной очереди с тысячами задач в секунду RabbitMQ избыточно быстр, для потока событий в миллионы сообщений он упрётся раньше Kafka. Сначала определите модель данных по кейсам урока, затем проверьте объёмы на своём железе: локальный стенд покажет, где реально узкое место — в брокере или в вашем обработчике.

Термины урока

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

Следующая ступень учебного пути

Ebitengine25 уроков~12 чпрактика25 уроков уже доступны

Ebitengine: 2D-игры на Go

Курс Ebitengine для Go: игровой цикл Update/Draw, спрайты и анимации, ввод, коллизии, тайлмапы, камера, звук, сцены и сборка на платформы. 25 уроков.

  • Понимать игровой цикл Update/Draw и Layout
  • Рисовать спрайты, атласы и примитивы
  • Обрабатывать ввод: клавиатура, мышь, тач
  • Строить движение, коллизии и анимации