Kafka topics, partitions и гарантии порядка
Самая частая ошибка в Kafka - ожидать глобальный порядок сообщений в topic. Kafka гарантирует порядок только внутри одной partition. Если topic разбит на 12 partitions, это 12 независимых логов, которые пишутся и читаются параллельно.
topic: orders
partition 0: [o-1 created][o-1 paid][o-1 shipped]
partition 1: [o-2 created][o-2 cancelled]
partition 2: [o-3 created][o-4 created][o-3 paid]
Порядок есть внутри каждой линии.
Глобального порядка между линиями нет.
Это не недостаток, а компромисс ради масштабирования. Чтобы Kafka могла обрабатывать много данных, она делит topic на partitions. Ваша задача как backend-разработчика - выбрать key так, чтобы нужный вам порядок оказался внутри одной partition.
Topic - имя потока, partition - единица параллелизма
Topic задаёт доменную категорию: orders.events, payments.events, users.changelog. Partition задаёт физическое разделение потока.
| Количество partitions | Что меняется |
|---|---|
| 1 | Простой порядок во всём topic, но низкий параллелизм. |
| 3 | Можно читать тремя consumers внутри одной group. |
| 24 | Больше throughput, но больше файлов, metadata и rebalancing-стоимости. |
Партиции нельзя выбирать "на всякий случай бесконечно много". Каждая partition - это логи на диске, replication, metadata в cluster, участие в leader election и rebalance.
Практическое правило: partitions выбирают под ожидаемый throughput, количество consumers и запас роста. Увеличить partitions обычно можно. Уменьшить - нет, только пересоздавать topic или переносить данные.
Keying: как producer выбирает partition
Producer отправляет record с key или без key. Если key есть, partition обычно выбирается хешем:
partition = hash(key) % number_of_partitions
Это значит:
- одинаковый key обычно попадает в одну partition;
- порядок для одного key сохраняется;
- разные keys могут попадать в разные partitions;
- при увеличении количества partitions распределение новых сообщений может измениться.
Последний пункт опасен для production. Если hash(key) % partitions начал давать другую partition, новые события того же key могут поехать в новую partition, пока старые ещё лежат в старой. Между двумя partitions Kafka не даёт порядка, поэтому увеличение partitions для topic с жёстким per-key ordering требует отдельного плана: окно остановки, новый topic, миграция, dual write/read или осознанное принятие риска.
Примеры key:
| Topic | Хороший key | Почему |
|---|---|---|
orders.events | order_id | Все события заказа идут последовательно. |
users.events | user_id | События пользователя не перемешиваются. |
payments.events | payment_id или order_id | Зависит от нужного порядка. |
audit.events | tenant_id:user_id | Можно сохранить порядок действий пользователя внутри tenant. |
Плохой key - случайный UUID для каждого события, если бизнесу нужен порядок по заказу. Плохой key - константа "all", если вы случайно отправляете весь поток в одну partition и убиваете параллелизм.
Отдельная проблема - hot key. Если 40% событий приходят по одному tenant_id, то одна partition будет отставать, даже если topic имеет 24 partitions и cluster выглядит свободным. Иногда key приходится делать составным: tenant_id:entity_id, provider:currency_pair или другой вариант, который сохраняет нужный порядок, но распределяет нагрузку.
Ordering guarantees
Kafka даёт несколько уровней порядка.
| Гарантия | Работает? | Условие |
|---|---|---|
| Порядок внутри partition | Да | Records записаны в partition последовательно. |
| Порядок для одного key | Обычно да | Key стабильно попадает в одну partition. |
| Порядок между partitions | Нет | Partitions независимы. |
| Порядок после retry producer | Да, если правильно настроено | Idempotent producer, bounded in-flight requests. |
| Порядок после consumer parallel processing | Не автоматически | Нужна осторожная модель обработки. |
Consumer может сломать порядок, даже если Kafka его сохранила. Например, вы читаете partition последовательно, но отправляете сообщения в worker pool, где второе событие обработалось раньше первого. Для событий одного key это может быть критично.
Kafka partition: A1 -> A2 -> A3
Worker pool: A2 finished first, A1 still running
Результат в БД может стать неконсистентным.
Если порядок важен, обрабатывайте partition последовательно или шардируйте worker pool по key.
Replication factor
Replication factor показывает, сколько копий partition хранит Kafka.
topic orders, partition 0, replication.factor=3
Broker 1: leader orders-0
Broker 2: follower orders-0
Broker 3: follower orders-0
Leader принимает read/write для partition. Followers копируют данные с leader. Если broker с leader умер, Kafka выбирает нового leader из актуальных replicas.
Replication factor обычно ставят 3 в production. Это не "магическая защита от всего", а компромисс: можно пережить потерю одного broker без потери доступности при нормальных настройках.
ISR: in-sync replicas
ISR - это replicas, которые не отстали от leader сильнее допустимого. Не все followers автоматически считаются безопасными кандидатами.
orders-0
leader: broker-1 offset=1050
follower broker-2 offset=1050 -> ISR
follower broker-3 offset=1001 -> out of sync
ISR важен для durability. Если producer требует подтверждения от всех in-sync replicas, Kafka может гарантировать, что запись есть не только на leader.
Но ISR может сжиматься. Если followers не успевают за leader из-за сети, диска или нагрузки, они выпадают из ISR. Тогда cluster становится более хрупким.
Producer acks
acks определяет, когда broker отвечает producer "запись принята".
acks | Когда producer получает успех | Риск |
|---|---|---|
0 | Не ждёт broker вообще | Максимальный риск потери. |
1 | Leader записал record | Потеря возможна, если leader умер до replication. |
all / -1 | Leader и ISR подтвердили запись | Лучший durability, выше latency. |
Для production-событий обычно выбирают acks=all вместе с min.insync.replicas.
min.insync.replicas
min.insync.replicas говорит, сколько replicas должно быть в ISR, чтобы запись с acks=all считалась успешной.
Типичная production-связка:
replication.factor=3
min.insync.replicas=2
producer acks=all
Что это даёт:
- запись успешна, если её подтвердили минимум две in-sync replicas;
- если доступна только одна replica, producer получит ошибку;
- система выбирает отказ записи вместо тихой потери durability.
Это важная мысль: хорошие настройки Kafka иногда ломают запись, чтобы не принять данные в небезопасном состоянии. Backend должен уметь обработать такую ошибку: retry, circuit breaker, outbox, degradation.
Leader/follower и failover
Каждая partition имеет leader. Producer и consumer работают через leader. Followers догоняют leader.
Client
|
v
Broker 1 leader for orders-0
| replication
+--> Broker 2 follower
+--> Broker 3 follower
Если broker 1 падает:
Broker 2 becomes leader for orders-0
Producer refreshes metadata
Consumer reconnects
В этот момент возможны временные ошибки: NOT_LEADER_OR_FOLLOWER, timeout, disconnect. Нормальный Kafka client умеет обновлять metadata и повторять операции, но ваш код всё равно должен иметь context timeout и понятную обработку ошибок.
Как выбирать partitions
Простой ориентир:
- Оцените throughput: сколько MB/s пишем и читаем.
- Оцените максимальное количество consumers внутри одной group.
- Подумайте о key cardinality: достаточно ли разных keys, чтобы равномерно распределиться.
- Заложите рост, но не создавайте тысячи partitions без необходимости.
Пример:
orders.events:
partitions: 12
replication.factor: 3
min.insync.replicas: 2
key: order_id
Такой topic можно параллельно читать до 12 consumers внутри одной group. События одного заказа будут идти в одну partition.
Для RateDesk событие RateUpdated выглядит простым, но key всё равно является архитектурным решением. Key по currency_pair сохраняет порядок обновлений пары из разных источников хуже, если источники конфликтуют. Key по provider:currency_pair лучше изолирует источники, но downstream должен сам решать, какой provider сейчас главный. Это не задача Kafka, а доменное правило.
Capacity и стоимость partitions
Partitions влияют не только на throughput:
- больше partitions = больше открытых файлов, metadata и controller work;
- rebalance занимает дольше, если group владеет тысячами partitions;
- leader election и recovery становятся тяжелее;
- слишком много маленьких partitions ухудшают batching и compression;
- слишком мало partitions ограничивают consumer parallelism и могут создать lag.
Хороший capacity plan связывает expected MB/s, размер record, retention, replication factor, количество consumer groups и допустимый lag. "Поставим 100 partitions на всякий случай" часто так же плохо, как "оставим одну".
Типичные компромиссы
| Нужно | Компромисс |
|---|---|
| Глобальный порядок | 1 partition, но низкий throughput. |
| Высокий throughput | Много partitions, но нет глобального порядка. |
| Высокая durability | acks=all, больше latency и возможные ошибки при деградации ISR. |
| Низкая latency | Меньшие batch и слабее durability-настройки. |
| Равномерная нагрузка | Хороший key, но иногда сложнее сохранить доменный порядок. |
| Будущий рост partitions | Легче масштабировать throughput, но можно сломать порядок для key при изменении mapping. |
Kafka почти всегда про осознанный баланс. "Поставим 100 partitions, acks=1, потом разберёмся" - плохая стратегия для доменных событий.
Вопросы на собеседовании
- Где Kafka гарантирует порядок сообщений?
- Почему одинаковый key обычно попадает в одну partition?
- Что произойдёт с порядком при worker pool на стороне consumer?
- Чем leader partition отличается от follower?
- Что такое ISR?
- Как связаны
replication.factor,acks=allиmin.insync.replicas? - Почему
acks=1может потерять данные при падении leader? - Можно ли уменьшить количество partitions в существующем topic?
- Почему увеличение partitions может быть опасно для key-based ordering?
- Как hot key проявится в consumer lag?
Практика
- Для topic
orders.eventsвыберите key и объясните, какие события должны сохранять порядок. - Нарисуйте схему topic с 6 partitions и consumer group из 4 consumers: кому сколько partitions достанется?
- Объясните поведение producer при
replication.factor=3,min.insync.replicas=2,acks=all, если жив только один broker. - Придумайте пример, где глобальный порядок важнее throughput, и topic стоит сделать с одной partition.
Интерактивная практика
Где Kafka гарантирует порядок сообщений?
Что выведет этот код?
package main
import "fmt"
func partitionMapping(sameKey bool) string {
if sameKey {
return "same"
}
return "maybe-different"
}
func main() {
fmt.Println(partitionMapping(true))
fmt.Println(partitionMapping(false))
}
Реализуй WriteDecision: при acks=all запись можно принять, только если число in-sync replicas не меньше minISR.