📨 Раздел 15 · Вопрос #2

Что такое партиция (partition) и зачем она нужна

Каждая партиция представляет собой строго упорядоченную, неизменяемую последовательность сообщений (Append-Only Log), в которую новые события дописываются исключительно в конец,...


🟢 Junior Level

30-секундный ответ

Партиция (Partition) — это фундаментальная физическая единица масштабирования, хранения и параллелизма в Apache Kafka.

Каждая партиция представляет собой строго упорядоченную, неизменяемую последовательность сообщений (Append-Only Log), в которую новые события дописываются исключительно в конец, получая монотонно возрастающий порядковый номер — Offset.

Зачем нужны партиции:

  1. Параллелизм обработки: это ключевой закон Kafka: одну партицию в рамках одной Consumer Group может читать только ОДИН поток/инстанс консьюмера. Если у топика 10 партиций, вы можете запустить 10 параллельных инстансов микросервиса и ускорить чтение в 10 раз.
  2. Горизонтальное масштабирование: топик размером в 10 терабайт невозможно уместить на диск одного физического сервера. Партиции распределяются по разным серверам-брокерам в кластере.
  3. Отказоустойчивость: каждая партиция независимо реплицируется на другие брокеры (Leader $\to$ Followers).

Наглядная аналогия из жизни

Партиции — это многополосная платная автомагистраль с кассами оплаты:

  • Топик — это вся трасса целиком.
  • Партиции — это отдельные выделенные полосы движения (Полоса 0, Полоса 1, Полоса 2).
  • Консьюмеры — это кассиры на пунктах взимания платы.
  • Если у вас всего 1 полоса (1 партиция), то сколько бы кассиров (консьюмеров) вы ни наняли, на полосе сможет работать только один кассир, а за ним выстроится гигантская пробка. Сделав 6 полос, вы ставите 6 кассиров и пропускаете в 6 раз больше машин одновременно!

Базовый пример структуры топика

[ Топик "payments" (3 партиции) ]
  ├── [ Партиция 0 ]: [offset 0] ──► [offset 1] ──► [offset 2] ──► Consumer App Instance A
  ├── [ Партиция 1 ]: [offset 0] ──► [offset 1] ──► [offset 2] ──► Consumer App Instance B
  └── [ Партиция 2 ]: [offset 0] ──► [offset 1] ──► [offset 2] ──► Consumer App Instance C
# Создание топика с явным указанием 6 партиций
kafka-topics.sh --bootstrap-server localhost:9092 \
  --create \
  --topic payments \
  --partitions 6 \
  --replication-factor 3

🟡 Middle Level

Анатомия партиции: Offset, LEO и High Watermark

Внутри каждой партиции существует чёткое разграничение состояний сообщений:

[ Физический лог Партиции 0 ]
 Offset:    0     1     2     3     4     5     6     7
         ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
         │ msg │ msg │ msg │ msg │ msg │ msg │ msg │ msg │
         └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
                                       ▲                 ▲
                                       │                 │
                             [ High Watermark (HW) ]   [ Log End Offset (LEO) ]
                              (Подтверждено ISR)        (Записано на Лидере)
                                       ▲
                                       │
                        Консьюмерам ДОСТУПНО только ДО HW!
  1. Offset: уникальный 64-битный порядковый номер сообщения внутри данной партиции. В партиции 0 есть offset=42, и в партиции 1 есть offset=42 — они не имеют друг к другу никакого отношения.
  2. Log End Offset (LEO): смещение следующего сообщения, которое будет записано в лог лидера партиции.
  3. High Watermark (HW): смещение последнего сообщения, которое было успешно скопировано всеми репликами из пула ISR (In-Sync Replicas).
    • Критическое правило: консьюмеры никогда не увидят сообщения с offset между HW и LEO! Это гарантирует, что клиенты не прочитают «грязные» данные, которые могут быть потеряны в случае падения текущего лидера.

Как брокер распределяет данные по партициям

Когда продюсер отправляет ProducerRecord<Key, Value>:

  • Если ключ указан (key != null): брокер вычисляет хэш по алгоритму Murmur2: \(\text{partition} = \text{murmur2}(\text{key}) \pmod{\text{totalPartitions}}\) Все сообщения с одинаковым ключом (например, userId = "usr-889") гарантированно попадают в одну и ту же партицию, обеспечивая строгий порядок FIFO.
  • Если ключ отсутствует (key == null): в современных версиях Kafka (с 2.4+) работает алгоритм Sticky Partitioner: продюсер пакует сообщения пачками (batches) в одну партицию до её заполнения, после чего переключается на следующую, многократно снижая задержки сети и повышая степень сжатия.

Правило сопоставления консьюмеров и партиций

В рамках одной группы потребителей (Consumer Group):

  • Если Partitions == Consumers (например, 4 партиции и 4 консьюмера) $\to$ идеальный баланс: каждому достается ровно по 1 партиции.
  • Если Partitions > Consumers (например, 6 партиций и 2 консьюмера) $\to$ каждый консьюмер обрабатывает по 3 партиции.
  • Если Consumers > Partitions (например, 3 партиции и 5 консьюмеров) $\to$ 2 консьюмера будут бессмысленно простаивать в холостом режиме (Idle), так как Kafka запрещает параллельное чтение одной партиции двумя инстансами одной группы!

🔴 Senior Level

Файловая структура сегментов на файловой системе Linux

На сервере брокера каждая партиция представляет собой обычную директорию в log.dirs (например, /var/lib/kafka/data/orders-0/):

/var/lib/kafka/data/orders-0/
├── 00000000000000000000.log         # Фактические сжатые бинарные записи
├── 00000000000000000000.index       # Разреженный индекс смещений: Offset -> File Position
├── 00000000000000000000.timeindex   # Временной индекс: Timestamp -> Offset
├── leader-epoch-checkpoint          # Контрольные точки смены лидеров (JEP/KIP-101)
└── partition.metadata               # UUID партиции топика

Разреженный индекс (Sparse Index):

Kafka не индексирует каждое сообщение. Запись в .index вносится через каждые index.interval.bytes (по умолчанию 4096 байт / 4 КБ):

  1. Консьюмер запрашивает сообщение с offset = 1530.
  2. Брокер открывает файл .index (отображённый в память через mmap) и выполняет бинарный поиск ($O(\log N)$), находя ближайшую метку (например, offset 1500 = байт 61440).
  3. Брокер переходит к смещению 61440 в файле .log и производит короткое линейное сканирование всего 4 КБ данных до нужного смещения.

Защита от потери данных: Leader Epoch против High Watermark Truncation

До версии Kafka 0.11 репликация опиралась исключительно на High Watermark (HW). Это приводило к скрытым катастрофам при сбоях брокеров:

Сценарий сбоя без Leader Epoch:

  1. Реплика A (Лидер) и Реплика B (Follower) записали сообщение с offset=1, но Реплика B упала до того, как обновила свой HW.
  2. При перезагрузке Реплика B усекала свой лог до старого известного HW (удаляла offset=1).
  3. Если в этот момент Лидер A падал, Реплика B становилась новым лидером и записывала новое сообщение в offset=1.
  4. В результате лог расходился (Log Divergence), а подтверждённые данные безвозвратно терялись!

Решение — Leader Epoch (leader-epoch-checkpoint):

Каждая смена лидера инкрементирует монотонный номер эпохи (Leader Epoch, например Epoch 1, Epoch 2). Вместо автоматического усечения лога по HW, вернувшийся из сбоя Follower опрашивает текущего Лидера: «Какое последнее смещение было записано в твоей эпохе?». Лидер отвечает точным значением, и фолловер усекает лог только до границы эпохи, исключая случайную потерю согласованных записей.


Проблема «Горячей партиции» (Hot Partition & Key Skew)

В продакшене часто возникает ситуация, когда один ключ генерирует 80% всех транзакций (например, в маркетплейсе ключ крупного продавца «Apple» или «Samsung» генерирует миллионы заказов, а мелкие ИП — по 5 заказов в день):

  • Все сообщения с ключом «Apple» попадают строго в одну партицию (например, партицию 2).
  • Брокер, обслуживающий партицию 2, упирается в 100% CPU и IOPS диска.
  • Консьюмер, читающий партицию 2, накапливает огромный Consumer Lag, в то время как остальные 15 консьюмеров простаивают без работы!

Архитектурные решения проблемы Key Skew:

  1. Соление ключа (Key Salting): к популярному ключу добавляется случайный префикс или суффикс: apple_0, apple_1, apple_2. Сообщения равномерно разлетаются по 3 партициям.
  2. Compound Keys (Составные ключи): использование в качестве ключа связки sellerId_orderId вместо одного sellerId.

4 Tricky Questions

1. В чём фундаментальная разница между Log End Offset (LEO) и High Watermark (HW), и почему консьюмер не может прочитать сообщения между ними?

Ответ:

  • LEO (Log End Offset): смещение последнего сообщения, физически записанного на диске лидера партиции (включая сообщения, которые ещё не успели реплицироваться на фолловеров).
  • HW (High Watermark): смещение последнего сообщения, которое было успешно скопировано и подтверждено ВСЕМИ репликами из синхронного пула ISR (In-Sync Replicas).
  • Почему консьюмер не видит сообщения между HW и LEO: если бы консьюмер прочитал сообщение, находящееся выше HW (например, записанное на лидере, но ещё не отреплицированное на узел B), а лидер в этот момент физически сгорел, новым лидером стал бы узел B. На узле B этого сообщения нет, и оно будет заменено другим сообщением при следующей записи продюсера. Клиент столкнулся бы с иллюзией чтения «сообщения-призрака» (Dirty Read / Data Loss). Поэтому изоляция по High Watermark гарантирует согласованность чтения (Read Committed Isolation).

2. Почему наличие 50 000 партиций на одном брокере способно парализовать весь кластер Kafka, особенно в архитектурах с ZooKeeper?

Ответ: Большое количество партиций создаёт колоссальные накладные расходы по 4 направлениям:

  1. Дескрипторы файлов ОС (ulimit -n): каждая партиция содержит минимум 3 открытых файла на сегмент (.log, .index, .timeindex). 50 000 партиций мгновенно съедают сотни тысяч дескрипторов файлов ОС.
  2. Память ядра (OS Page Cache & JVM Metaspace): каждый индекс партиции удерживается в оперативной памяти через mmap. Брокер исчерпывает виртуальную память.
  3. Паузы при перевыборах лидера (Leader Election Storm): при падении брокера контроллер должен выбрать новых лидеров для 50 000 партиций. В кластерах с ZooKeeper это вызывало шквал ZK-запросов и заморозку кластера на несколько минут! (Проблема частично решена в KRaft с аппаратным батчингом метаданных, но лимит в 2000–4000 партиций на брокер остаётся лучшей практикой).
  4. Задержки репликации: потокам ReplicaFetcherThread приходится поочерёдно опрашивать тысячи очередей, что резко увеличивает задержки сквозной репликации (Replication Lag).

3. Что произойдет, если в топик с 4 партициями подключить Consumer Group из 6 инстансов микросервиса?

Ответ:

  • 4 инстанса получат по 1 партиции и будут полноценно обрабатывать поступающие события.
  • 2 оставшихся инстанса перейдут в состояние бездействия (Idle/Cold Standby) и не получат ни одного сообщения.
  • Kafka фундаментально запрещает одновременное разделение одной партиции между несколькими консьюмерами одной группы, так как это мгновенно разрушило бы строгий порядок смещений (offset order).
  • Однако эти 2 простаивающих пода служат «горячим резервом»: если один из работающих 4 инстансов упадет по OOM или выйдет из строя сервер, координатор группы инициирует Rebalance, и освободившаяся партиция моментально переназначится на один из свободных инстансов без необходимости ожидания запуска нового пода в Kubernetes.

4. Можно ли принудительно направить сообщение в конкретный номер партиции в обход стандартного Partitioner?

Ответ: Да, можно. В классе ProducerRecord в Java Kafka Client конструктор перегружен и принимает явный номер партиции:

// Явное указание целевой партиции: Integer partition = 2
ProducerRecord<String, String> record = 
    new ProducerRecord<>("orders", 2, System.currentTimeMillis(), "order-123", jsonPayload);
kafkaTemplate.send(record);

Если номер партиции передан явно (partition != null), стандартный DefaultPartitioner (хэширование ключа или sticky batching) полностью игнорируется, и сообщение отправляется строго в указанную партицию. Однако это считается жестким архитектурным антипаттерном (code smell), так как зашивает топологию кластера в исходный код бизнес-логики и приведёт к ошибкам IllegalArgumentException при переносе сервиса в окружение с другим числом партиций.


🎯 Шпаргалка для интервью

Главное о партициях

  • Суть: фундаментальная единица параллелизма, физического шардирования и хранения в Kafka.
  • Лог FIFO: неизменяемый append-only лог со строгим порядком смещений (Offset) внутри каждой партиции.
  • Закон консьюмера: 1 партиция = максимум 1 консьюмер в рамках одной Consumer Group.
  • High Watermark (HW): граница видимости данных консьюмерами; сообщения выше HW клиентам недоступны до подтверждения репликами.
  • Leader Epoch: предотвращает потерю подтвержденных данных и расхождение логов при смене лидеров.
  • Key Skew (Hot Partition): перекос нагрузки из-за одного популярного ключа; решается солением ключей (Salting).
  • Разреженный индекс (.index): бинарный поиск по памяти mmap каждые 4 КБ лога.
  • Ограничения: уменьшить число партиций нельзя; рекомендуемый лимит — до 2 000–4 000 партиций на брокер.

Сводка метрик партиции

| Термин | Определение | Доступность для чтения | | :— | :— | :— | | Offset | Монотонный номер события в партиции | Да (если $\le$ HW) | | LEO (Log End Offset) | Смещение следующей записи на Лидере | Нет (если > HW) | | HW (High Watermark) | Смещение, подтверждённое всеми ISR | Граница чтения консьюмеров | | ISR | Пул реплик, успевающих за лидером | Участвуют в формировании HW |

Красные флаги на собеседовании (Чего говорить нельзя)

  • ❌ «Если консьюмеры не справляются, мы просто запустим 20 инстансов на топик из 4 партиций» — 16 инстансов будут простаивать в Idle.
  • ❌ «Offset сообщения уникален во всем топике» — он уникален только в рамках конкретной партиции.
  • ❌ «Консьюмер сразу читает любое сообщение, как только лидер записал его к себе на диск» — консьюмер видит только сообщения до High Watermark (HW).

Связанные вопросы