Перейти к содержанию

Kafka Design Patterns

Kafka в архитектуре

Kafka обычно используют не "просто как транспорт", а как backbone событийной архитектуры.

Типовые сценарии:

  • integration events между микросервисами;
  • event-driven workflow;
  • CDC из БД;
  • сбор логов и телеметрии;
  • stream processing;
  • feed для аналитики и ML pipeline;
  • buffer между producer и медленным downstream.

Event vs Command

Это важное различие.

Command

Command — это "сделай действие".

Примеры:

  • ReserveInventory
  • SendEmail

Свойства:

  • направлен на конкретного исполнителя;
  • ближе к imperative style;
  • часто подразумевает, что кто-то обязан выполнить действие.

Event

Event — это "факт уже произошел".

Примеры:

  • OrderCreated
  • PaymentCompleted
  • UserRegistered

Свойства:

  • описывает факт прошлого;
  • может интересовать много разных подписчиков;
  • слабее связывает системы.

Практически:

  • Kafka особенно хорошо ложится на event model;
  • использовать Kafka как "удаленный RPC через сообщения" можно, но это часто плохой design smell.

Event Notification vs Event-Carried State Transfer

Есть две популярные модели событий.

1. Event Notification

Событие сообщает:

  • "что-то произошло"

Пример:

{
  "eventType": "OrderCreated",
  "orderId": "o-123"
}

Потребитель дальше сам идет за данными в другой сервис.

Плюсы:

  • событие маленькое;
  • меньше дублирования данных.

Минусы:

  • появляется дополнительный network hop;
  • растет связанность;
  • падает автономность consumer'а.

2. Event-Carried State Transfer

Событие несет достаточно данных, чтобы consumer мог сработать автономно.

Пример:

{
  "eventType": "OrderCreated",
  "orderId": "o-123",
  "userId": "u-7",
  "amount": 4500,
  "items": 3
}

Плюсы:

  • consumer меньше зависит от синхронных вызовов;
  • лучше для асинхронной интеграции;
  • проще строить projections.

Минусы:

  • больше размер сообщений;
  • нужна дисциплина в schema evolution.

Чаще всего на практике нужен разумный баланс:

  • событие должно нести достаточно данных для основного downstream use case;
  • но не превращаться в бесконтрольную копию всей БД.

Outbox Pattern

Один из самых важных паттернов для Kafka.

Проблема:

1. Сохранили заказ в БД
2. Отправили событие в Kafka

Что если между этими шагами приложение упадет?

Получим рассинхрон:

  • данные в БД есть, а события нет;
  • или наоборот.

Идея outbox

В той же транзакции БД:

  1. сохраняем бизнес-изменение;
  2. пишем запись в outbox table.

Потом отдельный publisher / CDC:

  1. читает outbox;
  2. публикует в Kafka;
  3. помечает запись как отправленную или опирается на CDC-механику.

Схема:

App transaction:
  business table update
  + outbox insert

Outbox relay / CDC:
  outbox -> Kafka

Почему это важно:

  • не нужен distributed transaction между БД и Kafka;
  • локальная транзакция БД дает надежную точку истины;
  • рассинхронов становится меньше.

Retry topics и DLQ

Если consumer не смог обработать сообщение, нельзя бездумно делать бесконечный retry в том же потоке.

Иначе получишь:

  • блокировку partition;
  • endless poison message;
  • лаг по всей группе.

Практический подход:

Retry topic

  • временно перекладываем сообщение в retry topic;
  • читаем его позже;
  • можно сделать несколько уровней с increasing delay.

DLQ (Dead Letter Queue / Topic)

  • если после N попыток не удалось обработать;
  • отправляем сообщение в отдельный topic для ручного разбора или спец-обработки.

Важно:

  • DLQ не должна быть "кладбищем, куда никто не смотрит";
  • нужен процесс разбора, алертинг и понимание, как re-drive делать безопасно.

Выбор key

key — одно из самых недооцененных архитектурных решений в Kafka.

Нужно продумать:

  • для какой сущности нужен локальный порядок;
  • по какому ключу удобно делать дедупликацию;
  • не создаст ли key hot partition;
  • не приведет ли отсутствие key к потере порядка там, где он нужен.

Хорошие варианты key:

  • orderId
  • userId
  • accountId
  • aggregateId

Плохие варианты:

  • случайная key там, где нужен порядок;
  • constant key для всего потока;
  • отсутствие key без осознания последствий.

Схемы данных и versioning

Событие — это публичный контракт.

Поэтому важно:

  • фиксировать schema;
  • версионировать изменения;
  • не ломать старых consumer'ов;
  • думать о backward/forward compatibility.

Типичные форматы:

  • JSON
  • Avro
  • Protobuf

Практические правила:

  • не удаляй поля без необходимости;
  • старайся добавлять новые поля как optional;
  • держи стабильные идентификаторы события;
  • клади в событие eventId, eventType, eventVersion, occurredAt.

Размер сообщений

Kafka не любит превращаться в хранилище гигантских payload.

Плохая идея:

  • пихать в event большие бинарники;
  • отправлять огромные snapshot'ы без необходимости;
  • переносить файлы через Kafka как основной канал.

Почему это плохо:

  • растет нагрузка на сеть и диск;
  • хуже batching и replication;
  • медленнее recovery;
  • тяжелее consumer'ам.

Чаще лучше:

  • положить большой объект в object storage;
  • в Kafka передать ссылку и метаданные.

Kafka vs RabbitMQ / классическая очередь

Очень грубо:

Kafka сильна в:

  • high throughput event streams;
  • durable log;
  • replay;
  • независимых consumer groups;
  • stream processing;
  • хранении потока событий некоторое время.

Классический message broker / queue сильнее в:

  • task dispatch;
  • per-message ack/nack workflow;
  • сложной маршрутизации;
  • work queue сценариях;
  • более "очередной" семантике обработки.

Практический вывод:

  • Kafka не "лучше всего";
  • Kafka хороша для своих задач;
  • использовать ее как молоток для любого обмена сообщениями — ошибка.

Когда Kafka подходит плохо

  • нужен простой cron-like job queue;
  • сообщений мало и нет потребности в replay;
  • архитектура не событийная и не stream-oriented;
  • команде не нужна операционная сложность Kafka;
  • нужен строгий sync request/response.

Типичные анти-паттерны

  • Использовать Kafka как RPC-шину.
  • Ожидать глобальный порядок по topic.
  • Не продумывать key и partitioning.
  • Писать неидемпотентный consumer и удивляться дублям.
  • Смешивать бизнес-критичные и шумовые события в одном topic без причин.
  • Не думать о schema evolution.
  • Делать DLQ и никогда ее не разбирать.
  • Надеяться, что "exactly-once все решит".

Что запомнить

  • Kafka лучше всего чувствует себя как backbone событий и потоков.
  • Событие — это контракт, а не просто JSON в topic.
  • Outbox pattern часто нужен, когда есть БД + Kafka publish.
  • Retry topics и DLQ должны быть осознанной частью дизайна.
  • Правильный key и нормальный schema/versioning часто важнее, чем тонкий config tuning.