Сайт нашего магазина каждую минуту порождает тысячи событий: Leyla открыла каталог, Aysel положила товар в корзину, Murad дошёл до оплаты. Аналитика считает популярные страницы, а завтра команде понадобится сервис рекомендаций, которому нужны все просмотры за неделю. Очередь RabbitMQ тут не поможет: сообщение исчезает после ack, перечитать его нельзя. Для таких задач создан Apache Kafka. Разберём, чем журнал отличается от очереди, что такое топик, партиция и смещение, запустим Kafka в Docker и напишем на Node.js первых producer и consumer.
Очередь или журнал
Очередь RabbitMQ похожа на стопку бланков заказов: повар взял бланк, выполнил и выбросил. Kafka — это журнал (log): тетрадь, в которую записи только добавляют в конец, ничего не стирая. У каждого читателя своя закладка, и читает он с того места, которое выберет сам. Прочитанная запись остаётся на месте: ту же страницу откроет другой читатель или тот же самый завтра.
| Критерий | Очередь (RabbitMQ) | Журнал (Kafka) |
|---|---|---|
| После чтения | сообщение удаляется после ack | событие хранится до конца срока хранения |
| Читатели | конкурируют за сообщения | у каждого своя закладка (offset) |
| Повторное чтение | невозможно | достаточно сдвинуть закладку назад |
| Порядок | в очереди, пока потребитель один | внутри каждой партиции |
| Типичная задача | задачи и команды: письмо, обработка файла | потоки событий: клики, заказы, логи |
Топик, партиция, смещение
Вся Kafka стоит на четырёх понятиях:
- Событие (event, record) — запись журнала: ключ
key, значениеvalue(обычно JSON), время и необязательные заголовки. Для Kafka это просто байты. - Топик (topic) — именованный журнал событий одного вида, например
page-views. - Партиция (partition) — часть топика: независимый журнал, куда события только дописываются. Это единица параллелизма и единица порядка.
- Смещение (offset) — номер события внутри партиции: 0, 1, 2… Номера не переиспользуются. Адрес события — «топик + партиция + offset».
Offset считается в каждой партиции отдельно. Модель журнала — несколько строк JavaScript:
// one partition = an array that only grows
const partition = [];
function append(event) {
partition.push(event);
return partition.length - 1; // the offset of the new event
}
append({ user: 'Leyla', page: '/home' });
append({ user: 'Aysel', page: '/cart' });
append({ user: 'Leyla', page: '/cart' });
// a reader remembers only a bookmark: the next offset to read
let offset = 0;
while (offset < partition.length) {
const { user, page } = partition[offset];
console.log(`offset ${offset}: ${user} opened ${page}`);
offset++;
}
// reading removed nothing: move the bookmark back and read again
offset = 1;
console.log(`replay from ${offset}:`, partition[offset].user);Вывод программы:
offset 0: Leyla opened /home
offset 1: Aysel opened /cart
offset 2: Leyla opened /cart
replay from 1: Aysel