awesome-everything EN
↑ Обратно к восхождению

Очереди, потоки, события

Kafka partitions: единица параллелизма, порядка и дверь в одну сторону

Суть Topic разбит на N partitions; хеш ключа выбирает одну; порядок держится только внутри partition; один consumer на partition ограничивает параллелизм. Количество partitions — решение, которое не отыграть назад.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 17 min

Topic order-events отставал, и кто-то поднял его с 6 partitions до 12, чтобы «добавить пропускную способность». Десять минут всё работало. Потом повалил саппорт: cancel для заказа обработался раньше create. Один orderId, два события, теперь летят в разные partitions, потому что hash(orderId) % 12 больше не равен hash(orderId) % 6. Порядок по ключу — единственная гарантия, на которую опирался весь пайплайн — тихо испарился ровно в момент, когда изменилось количество partitions. Отката, который вернёт старые ключи на места, не существует.

Topic — это N независимых логов, и ключ выбирает лог

Topic в Kafka — это не один поток, а N partitions, каждая — независимый append-only лог со своими offsets. Когда producer шлёт запись с ключом, дефолтный partitioner вычисляет murmur2(key) % N, чтобы выбрать, в какую именно partition запись попадёт. Один ключ — одна partition, всегда — пока N не меняется. Записи без ключа размазываются по partitions (round-robin или sticky-батчинг), потому что им нечем закрепиться.

Этот единственный механизм делает сразу три работы, и их смешение — там, где дизайны ломаются:

  • Распределение — раскладка нагрузки по brokers и дискам.
  • Порядок — Kafka гарантирует порядок только внутри одной partition, никогда не по всему topic. Глобального порядка нет.
  • Co-location — все записи одного ключа в одном месте, поэтому consumer видит всю историю этого ключа последовательно.

Значит «порядок» в Kafka на деле означает порядок по ключу, и достаётся он бесплатно лишь потому, что все записи ключа хешируются в одну partition. Выбор ключа — это и есть выбор того, что останется упорядоченным: ключ по orderId — события заказа выстроены; ключ по customerId — ты сериализуешь всё для одного клиента (и рискуешь, что один крупный клиент перегрузит partition — об этом ниже).

Consumer groups: одна partition — один consumer

На стороне чтения consumer group делит partitions между своими членами так, что каждая partition в любой момент принадлежит ровно одному consumer в группе. Это назначение — источник параллелизма Kafka и его самый жёсткий потолок. Если у topic 6 partitions, полезную работу делают максимум 6 consumers в группе. Запусти 7-й — он простаивает без назначения. Максимальный параллелизм потребления навсегда ограничен количеством partitions.

Поэтому количество partitions — решение по планированию ёмкости, а не ручка тюнинга. Ты выбираешь N заранее под параллелизм, который понадобится на пике, потому что поднять его потом — тот самый опасный ход, к которому этот урок всё время возвращается.

Если у тебя……тоСледствие
6 partitions, 4 consumersЧасть consumers держит по 2 partitionsНормально — есть запас расти до 6
6 partitions, 6 consumersМаппинг 1:1Максимум параллелизма достигнут
6 partitions, 8 consumers2 consumers не получают ничегоПростаивающая ёмкость — partitions ограничивают
Горячий ключ на 1 partition1 consumer перегружен, остальные простаиваютПерекос — добавление consumers не поможет

Дверь в одну сторону: смена N перехеширует всё

Количество partitions у topic можно увеличить. Уменьшить нельзя, и увеличение редко обходится так дёшево, как кажется. Проблема в partitioner: hash(key) % N — функция от N. Поменяй N с 6 на 12, и результат по модулю меняется для большинства ключей, поэтому ключ, который всегда шёл в partition 2, теперь идёт в partition 8. Будущие записи ключа маршрутизируются в новую partition, а его история лежит в старой — два потока больше не упорядочены друг относительно друга, и любой consumer, построивший состояние по ключу (текущий баланс, позиция стейт-машины), теперь читает разорванную, неупорядоченную историю.

Для stateful-приложений Kafka Streams это хуже, чем икота: changelog/state store ключуется по partition, поэтому смена количества partitions фактически способна повредить локальное состояние. Поэтому стандартный ход сеньора — не ресайзить на месте. Ты заранее закладываешь partitions с запасом под ожидаемый рост или — когда действительно надо масштабироваться — создаёшь новый topic с большим количеством и мигрируешь (dual-write в старый и новый, дренаж старого, перевод consumers). Это миграция, а не правка конфига.

Почему это работает

Почему нельзя уменьшить partitions? Partition — это упорядоченный лог с зафиксированными offsets и, возможно, компактным состоянием. Слияние двух логов должно было бы вплести их записи в одну временную линию, но корректного вплетения нет — их offsets и порядки независимы. Kafka отказывает, вместо того чтобы тихо разрушить порядок, поэтому счётчик только растёт. Считай это необратимым.

Горячие partitions, ребалансы и цена «слишком много»

Доминируют два прод-сценария отказа. Первый — перекос (skew): если один ключ (кит-клиент, вирусный товар) забирает основной трафик, весь он хешируется в одну partition, поэтому один consumer упёрт в 100%, а остальные простаивают. Больше partitions и больше consumers не дают ничего — узкое место это один ключ на одной partition. Фикс на уровне ключа (солить ключ, разбить горячую сущность), а не на уровне partition.

Второй — ребаланс. Когда consumer входит, уходит или считается мёртвым (пропущенный heartbeat, долгая пауза GC, медленный деплой), группа переназначает partitions. Классический «eager»-протокол — stop-the-world: каждый consumer отзывает все свои partitions, и потребление встаёт по всей группе на время процесса — часто на секунды, иногда дольше в шторме ребалансов. Важны два смягчения: incremental cooperative rebalancing (Kafka 2.4+) переназначает только partitions, которые обязаны переехать, поэтому большинство consumers продолжают работать, и static membership (KIP-345) даёт каждому consumer стабильный group.instance.id, чтобы быстрый рестарт вообще не запускал ребаланс.

Цифры заземляют трейдофф. Одна partition тянет порядка десятков MB/s, поэтому пропускная способность растёт с количеством partitions — до предела. Перевали за ~1000 partitions на broker, и пропускная способность producer и p99 latency резко деградируют (собственные тесты Confluent показывают обвал throughput по мере роста от сотен к 10K partitions). При легаси-плоскости метаданных ZooKeeper отказ broker означал выборы лидеров, масштабирующиеся с количеством partitions, что загоняло controller failover в диапазон 5–7 секунд при тысячах partitions; KRaft срезал это до менее секунды и поднимает практический потолок в сотни тысяч partitions на cluster. Баланс сеньора: достаточно partitions для параллелизма и запаса, но не так много, чтобы failover latency и накладные расходы на partition стали доминировать.

Выбери лучший вариант

Topic order-events (ключ по orderId, 6 partitions) отстаёт на пике, и нужна большая пропускная способность consumers. Порядок по заказу должен держаться. Выбери ход.

Викторина

У topic 8 partitions. Consumer group с 12 consumers всё равно отстаёт. В чём настоящее ограничение?

Викторина

Почему подъём keyed-topic с 6 до 12 partitions рискует сломать порядок?

Расставь шаги по порядку

Расставь по порядку, что происходит, когда producer шлёт запись с ключом, а consumer group её читает:

  1. 1 Producer вычисляет hash(key) % N, чтобы выбрать одну partition
  2. 2 Запись добавляется в лог этой partition на следующий offset
  3. 3 Группа назначает эту partition ровно одному consumer
  4. 4 Этот consumer читает partition в порядке offset, видя историю ключа последовательно
  5. 5 Если consumer умирает, ребаланс переназначает его partitions другому члену
Вспомните перед уходом
  1. 01
    Коллега хочет поднять живой keyed-topic с 6 до 24 partitions, чтобы починить лаг consumers. Объясни риск и более безопасный путь.
  2. 02
    Что такое stop-the-world ребаланс, почему он больно бьёт и что его снижает?
Итог

Topic в Kafka — это N независимых partitions, и дефолтный partitioner отправляет каждую запись с ключом в hash(key) % N — поэтому ключ всегда попадает в одну и ту же partition, и поэтому порядок гарантирован только внутри partition и никогда по всему topic. На стороне чтения consumer group отдаёт каждую partition ровно одному consumer, поэтому количество partitions — жёсткий потолок параллелизма потребления: лишние consumers просто простаивают. Это делает количество решением по ёмкости, и почти необратимым — поднять можно, опустить нельзя, а подъём перехеширует большинство ключей, разрывая историю каждого ключа между старой и новой partition и тихо ломая порядок по ключу (и повреждая состояние Kafka Streams). Поэтому закладывают запас заранее или мигрируют в новый topic, а не ресайзят на месте. В проде боль проявляется как перекос горячих partitions (один ключ упирает один consumer, остальные простаивают — фикс на уровне ключа) и ребалансы (stop-the-world паузы в секунды, укрощаемые incremental cooperative rebalancing и static membership). Partition тянет десятки MB/s, но после ~1000 partitions на broker throughput и p99 деградируют, а на легаси ZooKeeper controller failover шёл 5–7 секунд при тысячах partitions — KRaft срезает это до менее секунды. Вся игра в том, чтобы выбрать N, дающее параллелизм и порядок, с которыми можно жить долго.

Продолжить восхождение ↑Partition в Kafka: тест с множественным выбором
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.