Streaming foundations: Kafka и log-based архитектура
В уроках 01-05 мы говорили про batch: данные приходят пачкой, Spark/CH прожевал, результат лёг в Parquet или MergeTree. В этом уроке другая половина мира: streaming, где данные текут непрерывно, и каждое событие надо обработать в момент появления, а не через час.
Аудитория: знаком с базами данных и REST API; работал с очередями (RabbitMQ, SQS, Redis). Понимаешь, чем producer отличается от consumer. Урок не учит “поднимать Kafka в Kubernetes” и не описывает каждую ручку конфига; цель, разобрать архитектурные принципы log-based систем, чтобы дальше уверенно проектировать pipeline.
Параллельный материал: раздел 24 · микросервисы рассказывает про Kafka со стороны event-driven архитектуры приложения. Этот урок смотрит со стороны data engineering: Kafka как backbone для аналитики и stream processing. Темы пересекаются по основам, но фокус разный.
0. Зачем event streaming
Классический pipeline 2010-х: транзакционная БД (Postgres) -> ночной ETL -> DWH. Лаг от события до отчёта, часы или сутки. Это работало, пока бизнес жил в неделях.
Современный спрос: real-time recommendations, fraud detection за секунды, операционные dashboard, A/B-тестирование с feedback в реальном времени. ETL раз в сутки больше не подходит.
Решение: между OLTP и OLAP кладётся поток событий. Событие, immutable запись о факте: OrderPlaced(orderId, userId, amount, ts). Источник пишет события в Kafka; consumers читают и реагируют. Это разрывает источник и приёмник: одно событие может одновременно идти в DWH (для отчётов), в систему рекомендаций, в fraud-detection, в audit log.
Эту идею Jay Kreps (LinkedIn, потом Confluent) сформулировал в 2013 году в статье “The Log: What every software engineer should know about real-time data’s unifying abstraction”. Дальше идея проросла в Kafka, Pulsar, Redpanda, в архитектурные стили event sourcing и CQRS, в коммерческие продукты Snowpipe и BigQuery streaming inserts.
1. Лог как первичная абстракция
Kafka log, это append-only последовательность сообщений. Каждому сообщению назначается возрастающий 64-битный offset. Лог никогда не модифицируется, только дополняется.
offset: 0 1 2 3 4 5 6 7 ...
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ A │ B │ C │ D │ E │ F │ G │ H │ ──-> append
└────┴────┴────┴────┴────┴────┴────┴────┘
↑ ↑
consumer-A consumer-B
(committed offset 1) (committed offset 4)
Ключевые свойства:
- Immutable. Однажды записанное сообщение не меняется и не удаляется (до retention policy).
- Total order внутри лога. Offset монотонно растёт; порядок гарантирован.
- Consumer читает с любого offset. Не “получил и забыл”, а “читаю с offset N”. Можно перечитать историю, можно стартовать с самого начала.
- Replay, native операция. Один и тот же лог может одновременно читать 10 разных consumer-ов, каждый со своим offset.
Это отличается от обычной message queue (RabbitMQ, SQS), где сообщение удаляется после ack. В очереди сообщение принадлежит одному consumer-у; в Kafka, лог принадлежит топику, consumers, читатели, не владельцы.
1.1 Topic, partition, broker
Topic: orders
├── Partition 0 ──> хранится на broker-1 (leader), broker-2, broker-3 (followers)
├── Partition 1 ──> broker-2 (leader), broker-3, broker-1 (followers)
├── Partition 2 ──> broker-3 (leader), broker-1, broker-2 (followers)
└── ...
- Topic, логическое имя потока (
orders,clicks,audit-log). Topic, это коллекция партиций. - Partition, физическая единица распределения и параллелизма. Каждая партиция, отдельный лог-файл на брокере.
- Broker, сервер Kafka. Кластер, набор брокеров. Партиции распределены между брокерами; каждая партиция реплицируется на N брокеров для отказоустойчивости.
Партиция, минимальная единица параллелизма для consumer-ов. Один consumer в группе обрабатывает одну или несколько партиций; партиций в топике должно быть >= числа consumer-ов.
2. Партиции и порядок
Главный design-вопрос Kafka, partition key. Producer вычисляет hash от key, ставит сообщение в partition = hash(key) % N. Все сообщения с одним key, в одной партиции; внутри одной партиции порядок сохранён.
Producer:
send(topic="orders", key="user-42", value={...})
│
├─ hash("user-42") % 8 = 3
▼
Partition 3 ──-> broker-X
2.1 Свойство порядка
Внутри партиции порядок гарантирован (offset 0, 1, 2, …). Между партициями, нет. Это значит:
- Все события одного пользователя, на одной партиции (если key=user_id), порядок гарантирован.
- События разных пользователей могут прийти consumer-у в разном порядке.
2.2 Выбор partition key
Эмпирические правила:
- По бизнес-сущности, для которой важен порядок. user_id, order_id, account_id. Все события одной сущности обрабатываются одним consumer-ом в порядке.
- Не используй timestamp как key. Все свежие события упадут на одну партицию (hot partition), остальные простаивают.
- Не используй random / UUID, если порядок важен. Это уничтожает гарантию порядка для бизнес-сущности.
- Учитывай cardinality. Если key имеет 10 уникальных значений, а партиций 100, 90 партиций пустые. Cardinality key должна быть >> числа партиций.
Антипаттерн: hot partition, когда из-за плохого key 80% трафика идёт в одну партицию. Симптом: один broker нагружен на 95% CPU, остальные на 10%. Лечится перерасчётом hash через добавление случайного компонента (для key, требующего меньше строгий ordering): key = order_id + ":" + (id % 4).
2.3 Сколько партиций
Главный tuning: число партиций топика. Слишком мало, упрёшься в потолок параллелизма consumer-ов. Слишком много, накладные расходы (метаданные на брокере, файловые дескрипторы, latency replication).
Эмпирические значения:
- Маленький топик (десятки тысяч событий/секунду): 6-12 партиций.
- Большой топик (сотни тысяч): 30-60.
- Очень большой (миллионы): 100-200.
Увеличить число партиций можно (через kafka-topics --alter), уменьшить, нельзя (требует пересоздания топика). Поэтому стартуй чуть с запасом, но не забегай в сотни на пустом старте.
2.4 Демо: producer-consumer rate
Подними producer выше consumer, буфер заполняется.
Когда длина достигает hwm, producer переходит в paused:
это и есть backpressure. Опустится буфер ниже половины hwm, producer
возвращается в flowing. Это упрощённая модель: в Node hwm,
это порог для возврата false у write(), в Web
Streams похожая логика реализована через возврат промиса из pull.
Виджет показывает базовый принцип streaming: если producer пишет быстрее, чем consumer читает, очередь растёт. В Kafka это lag (см. ниже). Двигай слайдеры, увидишь, как меняется размер буфера в зависимости от соотношения rate-ов.
В Kafka буфер виртуально безграничен (до retention size), поэтому “переполнения” в смысле RabbitMQ не бывает; вместо этого consumer накапливает lag, и видишь это в метрике consumer_lag.
3. Replication: leader/follower, ISR
Каждая партиция реплицируется на N брокеров (replication factor, RF). Один из replica назначен leader, остальные, followers. Все read и write идут к leader; followers пассивно подтягивают лог с leader-а.
3.1 ISR: in-sync replicas
ISR, set реплик, у которых лог совпадает с leader-ом (или отстаёт незначительно, максимум 30 секунд по дефолту). Если follower упал или сильно отстал, его выбрасывают из ISR.
Ключевая гарантия: если acks=all, producer получает подтверждение только когда все ISR-реплики записали сообщение. Это значит, при потере leader новый leader (выбранный из ISR) не теряет подтверждённые данные.
3.2 acks: контракт producer-broker
acks=0 producer не ждёт ответа -> может потерять при крэше
acks=1 leader подтвердил запись -> может потерять, если leader упадёт до replication
acks=all все ISR подтвердили запись -> durability максимальная
В data engineering пайплайнах только acks=all. Скорость на 10-30% ниже, но без неё ты не имеешь права гарантировать “событие записано”.
3.3 min.insync.replicas
Параметр на уровне топика: минимальное число ISR, при котором producer может писать с acks=all. Если ISR упал ниже этого порога (например, два broker-а из трёх упали при RF=3 и min.insync.replicas=2), producer получает NotEnoughReplicasException и не может писать.
Это сознательный trade-off: лучше отказать клиенту в записи, чем потерять данные при следующем сбое. Стандартная конфигурация для важных топиков: RF=3, min.insync.replicas=2, acks=all. Это переживает падение одного broker-а без потери данных и без блокировки записи.
урок из курса
Урок из курса «Data Engineering». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.