1. Смена парадигмы: почему Kafka — это не очередь, а распределённый лог
Смена парадигмы: почему Kafka — это не очередь, а распределённый лог
Представьте, что вы разрабатываете банковскую систему. Вы используете RabbitMQ: сервис транзакций отправляет событие о переводе средств, сервис антифрода его успешно считывает, подтверждает обработку, и... сообщение навсегда удаляется из брокера. А теперь представьте, что через месяц к вам приходит отдел Data Science и просит: «Выгрузите нам все транзакции за последние 30 дней, мы хотим обучить новую нейросеть». В парадигме классических очередей вам нечего им дать — данные исчезли в момент прочтения.
Чтобы решить эту проблему, нам придется полностью изменить взгляд на то, как системы обмениваются данными. Мы переходим от концепции «временного буфера» к концепции «вечной летописи».
Иллюзия очереди: почему RabbitMQ не подходит для хранения
В предыдущем курсе мы глубоко изучили RabbitMQ. Мы выяснили, что это невероятно гибкий маршрутизатор (Exchange) и надежный буфер (Queue). Но фундаментальная философия RabbitMQ и протокола AMQP заключается в разрушающем чтении (destructive read).
Как только Consumer забирает сообщение и отправляет подтверждение, брокер физически удаляет это сообщение из памяти и с диска. Очередь в RabbitMQ стремится быть пустой. Идеальное состояние RabbitMQ — это ноль сообщений в очередях, что означает, что система справляется с нагрузкой.
> Традиционный брокер сообщений (RabbitMQ) — это почтовое отделение. Письмо лежит там ровно до тех пор, пока за ним не придет адресат. После вручения почта забывает о существовании письма.
Если вам нужно, чтобы одно и то же событие (например, order.created) получили три разных сервиса, RabbitMQ решает это через паттерн Publish/Subscribe (Fanout или Topic Exchange). Он создает три физические копии сообщения (или ссылки на него) в трех разных очередях. Каждый сервис читает и опустошает свою личную очередь.
Но что, если сервисов станет тридцать? А если новый сервис появится через год и захочет прочитать исторические данные с самого начала?
Распределенный лог: пишем в камень
Apache Kafka изначально создавалась в LinkedIn не как очередь сообщений, а как распределенный журнал событий (Distributed Commit Log).
Лог (журнал) — это самая простая структура данных в информатике. Это файл, в который записи добавляются строго последовательно, одна за другой.
У лога есть два железных правила:
!Сравнение архитектуры Queue и Log
В Kafka поток сообщений определенного типа называется Topic (топик). Логически топик — это и есть тот самый бесконечный лог. Когда Producer отправляет сообщение в Kafka, брокер просто приписывает его в конец файла на диске.
И здесь кроется главное отличие: Kafka не удаляет сообщения после того, как их прочитали. Данные лежат на диске днями, неделями или годами, пока не сработает глобальная политика очистки (Retention policy), например: «удалять всё, что старше 7 дней».
Инверсия ответственности: от умного брокера к умному клиенту
В RabbitMQ брокер был «умным»: он помнил, какое сообщение выдано, какое находится в состоянии Unacked, а какое пора отправить в Dead Letter Exchange. Клиент (Consumer) был относительно «глупым» — он просто просил «дай мне следующее сообщение».
В Kafka всё наоборот. Брокер «глупый» — это просто высокопроизводительное хранилище файлов на диске. А вот Consumer становится «умным».
Поскольку сообщения не удаляются, как Consumer понимает, что он уже прочитал, а что нет? С помощью концепции Offset (смещение).
Offset — это просто порядковый номер сообщения в логе. Первое сообщение получает offset 0, второе — 1, сотое — 99. Каждый Consumer сам (или с помощью клиентской библиотеки) запоминает: «Я прочитал этот лог вплоть до offset 42. В следующий раз я начну читать с offset 43».
!Анимация добавления в лог и движения курсоров Offset
Такая архитектура дает колоссальные преимущества:
!Проверка понимания неразрушающего чтения
Машина времени: переигровка событий
Самая мощная возможность, которую открывает парадигма лога — это Event Replay (переигровка событий).
Поскольку данные лежат на диске и не удаляются после чтения, курсор (Offset) можно двигать не только вперед, но и назад. Если вы выкатили новую версию сервиса аналитики и поняли, что в коде была ошибка, из-за которой последние сутки данные считались неверно, в RabbitMQ это была бы катастрофа — оригинальные сообщения уже удалены, их не вернуть.
В Kafka вы просто останавливаете Consumer, сбрасываете его Offset на значение, которое было 24 часа назад, исправляете баг в коде и запускаете сервис снова. Consumer заново прочитает все вчерашние события из лога, как будто видит их впервые, и пересчитает данные правильно.
Сравнительная таблица парадигм
| Характеристика | RabbitMQ (Message Queue) | Apache Kafka (Distributed Log) | | :--- | :--- | :--- | | Судьба сообщения | Удаляется после подтверждения (ACK) | Хранится на диске до истечения срока (Retention) | | Маршрутизация | Сложная, на стороне брокера (Exchanges, Bindings) | Простая, Producer пишет напрямую в Topic | | Отслеживание прогресса | Брокер помнит статус каждого сообщения (Unacked) | Consumer помнит свой порядковый номер (Offset) | | Множественные читатели | Требуется дублирование сообщений в разные очереди | Читают один и тот же файл, используя разные Offset | | Переигровка событий | Невозможна (без сложных костылей) | Встроена в архитектуру (перемотка Offset назад) |
Что дальше?
Мы выяснили, что топик в Kafka — это последовательный лог. Но если все данные одного топика (например, миллиарды кликов пользователей) писать в один файл на одном сервере, мы быстро упремся в лимиты жесткого диска и пропускную способность сети.
Чтобы Kafka могла обрабатывать миллионы сообщений в секунду и распределять нагрузку на множество серверов, этот гигантский лог нужно разрезать на части. В следующей главе мы разберем концепцию Partitions (партиций) — главного механизма масштабирования в Apache Kafka.