Содержание урока · 10 разделов
- Судьба события: лог против очереди с ack
- Повторы: nack из коробки против DLQ своими руками
- Порядок: партиция против одного консьюмера
- Fanout: группы читают один лог против exchanges
- Retention и replay: история, которую можно перечитать
- Пять кейсов одним столом
- Вердикты по кейсам
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Оба брокера принимают сообщения от продюсеров и отдают их консьюмерам, и на этом сходство заканчивается. 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 топик руками: повтор без остановки группы
GOfor {
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 — раз за разом перечитывал бы отказ.
Команда, пришедшая с RabbitMQ, «отклоняет» сообщение в Kafka так: не коммитит и ждёт, что брокер отдаст его другому. Но kafka-группа не перекидывает непрокоммиченную позицию соседу по желанию — сообщение просто остаётся непрочитанным для этой группы и всплывает после рестарта с FirstOffset или перечитывания. Вторая привычка опаснее: писать в DLT и коммитить в случайном порядке без учёта ошибок WriteMessages — сообщение теряется молча, и в DLT оно не попадает никогда. Повтор на Kafka — это транзакция из двух шагов, и коммитить оригинал можно только после успешной записи в DLT.
Порядок: партиция против одного консьюмера#
Третий кейс: обработка заказов одного клиента не должна перемешиваться. 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: две группы на одном логе
GObilling := 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, тогда как очередь сама освобождается по мере разбора. Выбор, таким образом, снова о модели данных: архив за подписку или почтовый ящик без архива.
Все пять кейсов воспроизводятся локально без кластера: Docker-брокер из третьего урока для kafka-go и локальный RabbitMQ в соседнем контейнере. Запустите два Reader с разными GroupID из примера выше на одном топике и убедитесь, что оба получили весь поток; затем остановите биллинг, отправьте пачку и посмотрите, как аудит продолжает, а биллинг догоняет после старта. Интерактивы этого урока работают в чистом Go без сети — сравнение моделей не требует брокера, но руками прогнать fanout стоит.
Пять кейсов одним столом#
| Кейс | Kafka | RabbitMQ |
|---|---|---|
| Судьба после обработки | Сообщение остаётся в логе до истечения retention | Ack удаляет сообщение: очередь хранит незавершённую работу |
| Повторы при ошибке | Руками: 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 членом группы, и константу старта с самого старого сообщения.
Соберите повтор с DLT в правильном порядке
Расставьте строки consumer-цикла: чтение, выход по ошибке, обработка, запись отказа в dead-letter топик, коммит, закрытие цикла.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: две модели в одной памяти
Пять кейсов сошлись в один вывод: выбирать нужно не «что лучше», а что за данные у вас на проводе. Сообщение-факт, который завтра понадобится кому-то ещё, — это 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 как задачную очередь с ретраями и приоритетами?
Технически можно, экономически — редко оправдано. Ретраи собираются руками: DLT-топик, отдельный ретраер, заголовки с числом попыток — весь механизм из dead-letter exchange придётся воспроизвести кодом. Приоритетов в логе нет в принципе: партиция читается строго последовательно, «важное раньше» потребует отдельного топика и отдельной группы с приоритетом планировщика. Для задачных очередей честнее RabbitMQ или asynq на Redis: там ретраи и веса — часть модели, а не надстройка. Kafka оправдывает себя, когда задача после выполнения остаётся фактом, который нужно перечитать.
Как мигрировать с RabbitMQ на Kafka, не сломав текущих потребителей?
Мигрировать стоит кейсами, а не системой целиком. Определите, какие очереди хранят задачи, а какие — события: задачи оставьте или уведите в asynq, события переведите на Kafka. Для перехода ставится мост: консьюмер RabbitMQ публикует полученные письма в kafka-топик, старые потребители работают, новые читают лог. Опасное место — двойная запись при переключении продюсера: пока не проверена запись в оба брокера и сверка потока, старый путь не выключайте. После переключения новые подписчики подключаются группами, и мост демонтируют.
Правда ли, что RabbitMQ медленнее Kafka, и стоит ли это учитывать в выборе?
На бенчмарках Kafka выигрывает в пропускной способности на порядок, но выбор от цифр зависит меньше, чем кажется. Очередь с ack и подтверждениями на каждую операцию не может быть такой же дешёвой, как последовательная запись в лог, — это цена разных моделей, а не плохой реализации. Для задачной очереди с тысячами задач в секунду RabbitMQ избыточно быстр, для потока событий в миллионы сообщений он упрётся раньше Kafka. Сначала определите модель данных по кейсам урока, затем проверьте объёмы на своём железе: локальный стенд покажет, где реально узкое место — в брокере или в вашем обработчике.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Ebitengine: 2D-игры на Go
Курс Ebitengine для Go: игровой цикл Update/Draw, спрайты и анимации, ввод, коллизии, тайлмапы, камера, звук, сцены и сборка на платформы. 25 уроков.
- Понимать игровой цикл Update/Draw и Layout
- Рисовать спрайты, атласы и примитивы
- Обрабатывать ввод: клавиатура, мышь, тач
- Строить движение, коллизии и анимации