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 — это "сделай действие".
Примеры:
ReserveInventorySendEmail
Свойства:
- направлен на конкретного исполнителя;
- ближе к imperative style;
- часто подразумевает, что кто-то обязан выполнить действие.
Event¶
Event — это "факт уже произошел".
Примеры:
OrderCreatedPaymentCompletedUserRegistered
Свойства:
- описывает факт прошлого;
- может интересовать много разных подписчиков;
- слабее связывает системы.
Практически:
- Kafka особенно хорошо ложится на event model;
- использовать Kafka как "удаленный RPC через сообщения" можно, но это часто плохой design smell.
Event Notification vs Event-Carried State Transfer¶
Есть две популярные модели событий.
1. Event Notification¶
Событие сообщает:
- "что-то произошло"
Пример:
Потребитель дальше сам идет за данными в другой сервис.
Плюсы:
- событие маленькое;
- меньше дублирования данных.
Минусы:
- появляется дополнительный network hop;
- растет связанность;
- падает автономность consumer'а.
2. Event-Carried State Transfer¶
Событие несет достаточно данных, чтобы consumer мог сработать автономно.
Пример:
Плюсы:
- consumer меньше зависит от синхронных вызовов;
- лучше для асинхронной интеграции;
- проще строить projections.
Минусы:
- больше размер сообщений;
- нужна дисциплина в schema evolution.
Чаще всего на практике нужен разумный баланс:
- событие должно нести достаточно данных для основного downstream use case;
- но не превращаться в бесконтрольную копию всей БД.
Outbox Pattern¶
Один из самых важных паттернов для Kafka.
Проблема:
Что если между этими шагами приложение упадет?
Получим рассинхрон:
- данные в БД есть, а события нет;
- или наоборот.
Идея outbox¶
В той же транзакции БД:
- сохраняем бизнес-изменение;
- пишем запись в outbox table.
Потом отдельный publisher / CDC:
- читает outbox;
- публикует в Kafka;
- помечает запись как отправленную или опирается на CDC-механику.
Схема:
Почему это важно:
- не нужен 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:
orderIduserIdaccountIdaggregateId
Плохие варианты:
- случайная 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.