---
title: "Kafka и RabbitMQ: выбор под задачу"
description: "Повторы, порядок, fanout: сравнение на кейсах. Лог с офсетами против очереди с ack: где сильнее Kafka, где RabbitMQ и как выбирать брокера под Go-сервис."
course: "Kafka в Go: продюсеры и консьюмеры на kafka-go"
url: "https://golangmaster.ru/courses/kafka-go/lessons/23-kafka-vs-rabbitmq/"
published: "2026-09-19"
updated: "2026-09-29"
minutes: 20
---

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

*Курс «Kafka в Go: продюсеры и консьюмеры на kafka-go», урок 23 из 25. ~20 минут. Обновлено 29.09.2026. Источник: https://golangmaster.ru/courses/kafka-go/lessons/23-kafka-vs-rabbitmq/*

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

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

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

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

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

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

- ✅ Брокер удаляет его из очереди: следа не остаётся, перечитать нельзя
- Оно остаётся в очереди до истечения retention и читается новыми подписчиками
- Оно автоматически переносится в dead-letter exchange как обработанное
- Оно хранится в логе партиции до истечения срока хранения

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

### Повторы: 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 — раз за разом перечитывал бы отказ.

> **Живое место: nack-привычки переносятся в Kafka.** Команда, пришедшая с RabbitMQ, «отклоняет» сообщение в Kafka так: не коммитит и ждёт, что брокер отдаст его другому. Но kafka-группа не перекидывает непрокоммиченную позицию соседу по желанию — сообщение просто остаётся непрочитанным для этой группы и всплывает после рестарта с FirstOffset или перечитывания. Вторая привычка опаснее: писать в DLT и коммитить в случайном порядке без учёта ошибок WriteMessages — сообщение теряется молча, и в DLT оно не попадает никогда. Повтор на Kafka — это транзакция из двух шагов, и коммитить оригинал можно только после успешной записи в DLT.

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

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

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

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

- ✅ Kafka: топик с Hash по ключу — порядок внутри партиции гарантирован
- RabbitMQ: одна очередь с десятью консьюмерами и prefetch равным ста
- RabbitMQ: fanout exchange на пять очередей с независимыми обработчиками
- Kafka: RoundRobin-балансер без ключей и восемь партиций

Порядок в Kafka живёт в партиции: одинаковый ключ попадает в один отсек и читается последовательно. В RabbitMQ параллельные консьюмеры обрабатывают сообщения вперемешку; честный порядок возможен только на одной очереди с одним консьюмером — ценой пропускной способности. RoundRobin без ключей размазывает события по партициям и порядок рвёт.

### 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?

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

**Что выведет программа?**

- ✅ Весь лог за retention: неделю событий с самого начала, независимо от других групп
- Только новые события: группа стартует с текущего конца лога
- Копию офсетов группы audit: имя группы отдаёт её позицию новому читателю
- Ошибку: вторая группа на топике невозможна, fanout требует exchanges

Новая группа не наследует ничьих офсетов и начинается с FirstOffset — минус двух, самой старой доступной записи. Лог хранит неделю, столько же она и получит; в RabbitMQ перечитывать нечего — после ack сообщения нет. Это и есть replay, которого у очереди с ack не бывает.

### 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 членом группы, и константу старта с самого старого сообщения.

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

Подсказка: Константа FirstOffset равна минус двум и означает самую старую доступную запись партиции — именно с неё начинается перечитывание истории.

Правильные ответы в пропусках: `FirstOffset`

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

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

Правильный порядок строк:

1. `reader, _ := kafka.NewReader(kafka.ReaderConfig{Brokers: []string{"localhost:9092"}, GroupID: "billing", Topic: "orders"})`
2. `dlt := kafka.NewWriter(kafka.WriterConfig{Brokers: []string{"localhost:9092"}, Topic: "orders-dlt"})`
3. `for {`
4. `m, err := reader.FetchMessage(ctx)`
5. `if err != nil { break }`
6. `if err := process(m); err != nil {`
7. `dlt.WriteMessages(ctx, kafka.Message{Topic: "orders-dlt", Key: m.Key, Value: m.Value})`
8. `} // отказ ушёл в DLT`
9. `reader.CommitMessages(ctx, m)`
10. `} // конец цикла`

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

- **Распределённый лог** — Модель Kafka: сообщения остаются в партиции до истечения retention, консьюмеры лишь двигают собственные офсеты.
- **Очередь с ack** — Модель RabbitMQ: сообщение выдаётся одному подписчику и удаляется после подтверждения; очередь хранит незавершённую работу.
- **Dead-letter exchange** — Встроенный в RabbitMQ механизм: отклонённые nack-ом письма попадают в отдельную очередь. В Kafka аналог — DLT-топик своими руками.
- **Fanout в Kafka** — Консьюмер-группы с разными GroupID читают один лог независимо; новый подписчик — новая группа без правки продюсера.
- **Replay** — Перечитывание истории: новый консьюмер со StartOffset kafka.FirstOffset получает весь лог за retention. Очередь с ack так не умеет.

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

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

- 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. Сначала определите модель данных по кейсам урока, затем проверьте объёмы на своём железе: локальный стенд покажет, где реально узкое место — в брокере или в вашем обработчике.
