Сайт нашего магазина каждую минуту порождает тысячи событий: Leyla открыла каталог, Aysel положила товар в корзину, Murad дошёл до оплаты. Аналитика считает популярные страницы, а завтра команде понадобится сервис рекомендаций, которому нужны все просмотры за неделю. Очередь RabbitMQ тут не поможет: сообщение исчезает после ack, перечитать его нельзя. Для таких задач создан Apache Kafka. Разберём, чем журнал отличается от очереди, что такое топик, партиция и смещение, запустим Kafka в Docker и напишем на Node.js первых producer и consumer.

Очередь или журнал

Очередь RabbitMQ похожа на стопку бланков заказов: повар взял бланк, выполнил и выбросил. Kafka — это журнал (log): тетрадь, в которую записи только добавляют в конец, ничего не стирая. У каждого читателя своя закладка, и читает он с того места, которое выберет сам. Прочитанная запись остаётся на месте: ту же страницу откроет другой читатель или тот же самый завтра.

КритерийОчередь (RabbitMQ)Журнал (Kafka)
После чтениясообщение удаляется после ackсобытие хранится до конца срока хранения
Читателиконкурируют за сообщенияу каждого своя закладка (offset)
Повторное чтениеневозможнодостаточно сдвинуть закладку назад
Порядокв очереди, пока потребитель одинвнутри каждой партиции
Типичная задачазадачи и команды: письмо, обработка файлапотоки событий: клики, заказы, логи

Топик, партиция, смещение

События дописываются в конец партиций, и у каждого события в партиции свой offset
События дописываются в конец партиций, и у каждого события в партиции свой offset

Вся Kafka стоит на четырёх понятиях:

  • Событие (event, record) — запись журнала: ключ key, значение value (обычно JSON), время и необязательные заголовки. Для Kafka это просто байты.
  • Топик (topic) — именованный журнал событий одного вида, например page-views.
  • Партиция (partition) — часть топика: независимый журнал, куда события только дописываются. Это единица параллелизма и единица порядка.
  • Смещение (offset) — номер события внутри партиции: 0, 1, 2… Номера не переиспользуются. Адрес события — «топик + партиция + offset».

Offset считается в каждой партиции отдельно. Модель журнала — несколько строк JavaScript:

js
// 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);

Вывод программы:

text
offset 0: Leyla opened /home
offset 1: Aysel opened /cart
offset 2: Leyla opened /cart
replay from 1: Aysel