Раздел 26 · Data Engineering

Streaming foundations: Kafka и log-based архитектура

middle-senior~70 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Перед этим уроком

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)

Ключевые свойства:

  1. Immutable. Однажды записанное сообщение не меняется и не удаляется (до retention policy).
  2. Total order внутри лога. Offset монотонно растёт; порядок гарантирован.
  3. Consumer читает с любого offset. Не “получил и забыл”, а “читаю с offset N”. Можно перечитать историю, можно стартовать с самого начала.
  4. 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

Backpressure: producer 12/s, consumer 5/s, hwm = 8
producer
flowing
buffer
0/8
consumer
0
0 produced0 consumed0 paused0 tps

Подними 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 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.