2. Topics, partitions и offsets
Вот фундаментальная статья, детально разбирающая Topics, Partitions и Offsets — ядро модели хранения и параллелизма Apache Kafka.
Если в традиционных СУБД фундаментальной единицей декомпозиции выступает таблица со строками, а в RabbitMQ — очередь сообщений, то в Apache Kafka весь мир держится на триаде: Топик, Партиция и Оффсет. Именно это триединство превращает Kafka из банального брокера в распределённую нервную систему современного бэкенда, способную переваривать терабайты телеметрии в реальном времени.
Топики: категории бесконечного потока данных
Если смотреть на архитектуру Kafka с высоты птичьего полёта, Topic (топик) — это именованный логический поток данных, к которому продюсеры публикуют события, а консьюмеры оформляют подписку. Разработчик присваивает топику осмысленное доменное имя (orders, user-events, page-views), и с этого момента он выступает точкой схождения для всех событий определенного контракта.
Однако за этим привычным абстрактным фасадом скрывается радикальное архитектурное решение: в отличие от RabbitMQ, где топик — это алгоритм маршрутизации (Topic Exchange), не хранящий ни байта данных, в Kafka топик — это логический контейнер для набора физических, неизменяемых, строго упорядоченных логов — партиций.
Структура топика и его физическое воплощение
Когда системный инженер или сервис инициализирует топик в кластере, он обязан зафиксировать две базовые характеристики:
- Количество партиций (
num.partitions): Степень горизонтального дробления потока данных. - Фактор репликации (
replication.factor): Количество узлов брокеров, на которых физически дублируются журналы каждой партиции.
Топик сам по себе не содержит ни одного байта на диске — это виртуальный агрегатор. Все физические операции записи и чтения адресуются в конкретные партиции этого топика.
flowchart TD
subgraph TopicLogical ["Логический топик: 'orders'"]
T["Topic: orders<br>(num.partitions=3, replication.factor=3)"]
end
subgraph PartitionsPhysical ["Физические партиции (Append-Only Commit Logs)"]
P0["Партиция 0<br>Лидер: Broker 1<br>Followers: Broker 2, 3"]
P1["Партиция 1<br>Лидер: Broker 2<br>Followers: Broker 1, 3"]
P2["Партиция 2<br>Лидер: Broker 3<br>Followers: Broker 1, 2"]
end
T --> P0
T --> P1
T --> P2
PR["Go Producer"] -->|"hash/round-robin/явный"| P0
PR -->|"hash/round-robin/явный"| P1
PR -->|"hash/round-robin/явный"| P2
CG["Consumer Group: 'order-service'"] -->|"Назначена P0"| P0
CG -->|"Назначена P1"| P1
CG -->|"Назначена P2"| P2
classDef top fill:#312e81,stroke:#818cf8,stroke-width:2px,color:#f8fafc;
classDef part fill:#064e3b,stroke:#34d399,stroke-width:2px,color:#f8fafc;
classDef client fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#f8fafc;
class T top;
class P0,P1,P2 part;
class PR,CG client;
Партиции: единица параллелизма и порядка
Партиция (Partition) — это фундаментальная единица масштабирования, отказоустойчивости и гарантии порядка в Kafka.
Именно количество партиций задает абсолютный теоретический потолок горизонтального параллелизма: одна партиция в рамках конкретной Consumer Group может обрабатываться не более чем одним консьюмером одновременно.
Почему партиция гарантирует строгий порядок
Каждая партиция представляет собой линейно упорядоченный лог с монотонно возрастающим смещением (offset).
Как только брокер зафиксировал запись в партиции, ее позиция относительно соседей цементируется навсегда:
- Консьюмер, подключенный к партиции №0, видит поток событий строго в том хронологическом порядке, в котором их подтвердил лидер этой партиции (гарантия FIFO).
- Однако между независимыми партициями (например, между партицией №0 и партицией №1) никакой синхронизации порядка не существует.
flowchart LR
subgraph P0 ["Партиция 0 (Строгий порядок FIFO внутри)"]
direction LR
m0["Offset 0<br>Order 1 Created"] --> m1["Offset 1<br>Order 1 Paid"] --> m2["Offset 2<br>Order 1 Shipped"]
end
subgraph P1 ["Партиция 1 (Строгий порядок FIFO внутри)"]
direction LR
m3["Offset 0<br>Order 2 Created"] --> m4["Offset 1<br>Order 2 Cancelled"]
end
classDef p fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc;
class m0,m1,m2,m3,m4 p;
Физическая структура: сегменты и файлы
На дисковом накопителе сервера партиция разворачивается не как бесконечный непрерывный файл, а как директория, заполненная набором упорядоченных сегментов (segments).
Активный сегмент, куда в данный момент пишет брокер, постепенно растет. При достижении предельного размера (параметр segment.bytes, по умолчанию 1 ГБ) или временного порога (segment.ms, по умолчанию 7 дней) сегмент закрывается (становится неизменяемым), и создается новый активный сегмент.
Каждый сегмент на диске состоит из трех специализированных файлов:
.log(Журнал данных): Непосредственный поток сжатых бинарных пакетов сообщений (RecordBatch)..index(Смещение $\to$ Позиция): Разреженный позиционный индекс (Sparse Offset Index), связывающий логическийoffsetс физическим смещением в байтах внутри.log-файла..timeindex(Время $\to$ Смещение): Разреженный временной индекс, сопоставляющий timestamp сообщения с его логическимoffset.
# Пример файловой структуры партиции orders-0
/var/lib/kafka/data/orders-0/
├── 00000000000000000000.log # Сегмент: сообщения с offset 0 по 12344
├── 00000000000000000000.index # Позиционный индекс смещений
├── 00000000000000000000.timeindex # Временной индекс
├── 00000000000000012345.log # Активный сегмент: offset с 12345 и далее...
├── 00000000000000012345.index
└── 00000000000000012345.timeindex
Зачем нужно разделение на сегменты?
- Мгновенное удаление устаревших данных: Операционной системе не нужно вырезать байты из середины терабайтного файла. Брокер просто вызывает системный вызов
unlink()на старейшие файлы сегментов, целиком освобождая блоки на диске за доли микросекунды. - Быстрый бинарный поиск: Разреженный индекс загружается прямо в оперативную память. Поиск физического смещения для любого offset занимает $O(\log N)$ операций бинарного поиска в памяти, после чего выполняется единственное прямое чтение блока с диска.
- Минимизация фрагментации: Файлы фиксированного размера идеально укладываются в экстенты файловых систем Linux (ext4, XFS).
Offset: «курсор» в бесконечном логе
Каждому сообщению, попадающему в партицию, брокер присваивает Offset — монотонно возрастающее 64-битное целое число (int64), однозначно определяющее позицию записи в рамках этой партиции.
Оффсет в Kafka концептуально отличается от очередей задач:
- Оффсет нельзя переставить, перепрыгнуть или удалить выборочно.
- Оффсет не привязан к конкретному воркеру — это объективная координата сообщения в координатной сетке времени топика.
- Консьюмер хранит лишь числовое значение оффсета, отмечая, до какого места лог уже успешно прочитан.
flowchart LR
subgraph PartitionLog ["Partition 0 Log"]
direction LR
O0["Offset 0"] --> O1["Offset 1"] --> O2["Offset 2"] --> O3["Offset 3"] --> O4["Offset 4"] --> O5["Offset 5 (Active)"]
end
C_Lag["Current Consumer Offset: 2<br>(Lag: 3 сообщения)"] -.-> O2
classDef logBox fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc;
classDef lagBox fill:#1e293b,stroke:#f59e0b,stroke-width:1px,color:#f8fafc;
class O0,O1,O2,O3,O4,O5 logBox;
class C_Lag lagBox;
Управление смещениями и Consumer Group
Вся мощь параллельной обработки раскрывается в механизме фиксации прогресса (Commit Offsets):
- Консьюмер вычитывает пакет сообщений из партиции.
- После успешной бизнес-обработки консьюмер отправляет брокеру команду фиксации (
OffsetCommitRequest). - Брокер записывает зафиксированный номер оффсета во внутренний системный топик
__consumer_offsets.
Топик __consumer_offsets представляет собой уплотняемый журнал (Compacted Topic). Он хранит пары вида [ConsumerGroup, Topic, Partition] -> CommittedOffset. Благодаря этому при перезапуске пода в Kubernetes или масштабном сбое сети новый консьюмер мгновенно считывает свое последнее зафиксированное состояние и продолжает работу без дублей и пропусков.
Политика фиксации оффсетов бывает двух видов:
- Auto-Commit (Автоматический): Драйвер консьюмера раз в $N$ секунд фоново коммитит последний вычитанный оффсет. Удобно, но чревато потерей данных при аварии воркера до завершения обработки в базе данных.
- Manual Commit (Ручной): Консьюмер явно подтверждает оффсет строго после завершения записи в целевую базу данных. Это фундамент построения гарантий At-least-once и Exactly-Once (см. 6. Exactly once в Kafka и 10. Idempotency в message processing).
Как консьюмер находит начальную точку
Если новая Consumer Group впервые подключается к существующему топику (в __consumer_offsets для нее еще нет записей), брокер определяет стартовую позицию на основе параметра auto.offset.reset:
earliest: Начать с самого первого доступного сообщения в логе (перечитать всю историю).latest: Начать с самого конца (игнорировать историю и получать только новые события, поступающие после подключения).
// Идиоматичная настройка клиента franz-go на чистом Go
package main
import (
"context"
"log"
"github.com/twmb/franz-go/pkg/kgo"
)
func createKafkaConsumer() (*kgo.Client, error) {
return kgo.NewClient(
kgo.SeedBrokers("localhost:9092"),
kgo.ConsumerGroup("my-billing-group"),
kgo.ConsumeTopics("orders"),
// Отключаем автокоммит: мы будем подтверждать оффсеты руками!
kgo.DisableAutoCommit(),
// При отсутствии сохраненного оффсета читаем с самого начала истории
kgo.ConsumeResetOffset(kgo.NewOffset().AtStart()),
)
}
Стратегия маршрутизации: как продюсер выбирает партицию
Отправитель сообщения (Producer) играет ключевую роль в распределении нагрузки между партициями топика. Алгоритм маршрутизации выбирается на основе структуры сообщения:
flowchart TD
Msg["Входящее сообщение продюсера"] --> Check{"Есть ли у сообщения Key?"}
Check -->|"Да: Key != nil"| Hash["Key-Based Partitioner<br>partition = murmur2(key) % num_partitions"]
Check -->|"Нет: Key == nil"| Sticky["Sticky / Round-Robin Partitioner<br>Батчинг сообщений по партициям"]
Check -->|"Явный номер"| Manual["Manual Partition Assignment<br>Явный выбор разработчика"]
Hash --> TargetP["Гарантированное попадание<br>одного ключа в одну партицию (FIFO)"]
Sticky --> UniformP["Идеально равномерное<br>распределение по кластеру (High RPS)"]
Manual --> FixedP["Точечная запись"]
classDef decision fill:#1e293b,stroke:#818cf8,stroke-width:2px,color:#f8fafc;
classDef action fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc;
class Check decision;
class Hash,Sticky,Manual,TargetP,UniformP,FixedP action;
-
Маршрутизация по ключу (Key-based Partitioning):
Если сообщение снабжено непустым ключом (например,user_idилиorder_id), стандартный партиционер Kafka вычисляет 32-битный хэш с помощью алгоритма murmur2 и вычисляет индекс:partition = hash(key) % num_partitions.
Это гарантирует, что все события одного пользователя или заказа всегда попадут строго в одну и ту же партицию, гарантируя идеальный порядок их обработки. -
Равномерное распределение (Round-robin / Sticky Partitioner):
Если ключ не задан (key == nil), продюсер распределяет сообщения между всеми партициями. В современных версиях Kafka используется Sticky Partitioner: он заполняет пакет для одной партиции до лимитаbatch.size, затем переключается на следующую, что максимизирует сжатие и сетевую утилизацию. -
Явное указание партиции:
Приложение может вручную задать конкретный индекс партиции в коде, переопределив стандартную логику.
Механическая симпатия: как партиции взаимодействуют с железом
Каждая партиция, будучи линейным неизменяемым логом, идеально согласована с физическими свойствами современных накопителей.
Последовательная запись в активный сегмент позволяет дискам (SSD/NVMe) работать на пиковой пропускной способности, эффективно заполняя erase-блоки и минимизируя усиление записи (Write Amplification).
Чтение консьюмерами с использованием sendfile и страничного кеша означает, что даже при тысячах потребителей данные копируются только один раз — из кеша ядра в сокетную буферную память NIC, вообще не попадая в пространство пользователя.
Именно эта синергия между неизменяемой моделью хранения, сегментированием, zero-copy и последовательным вводом-выводом позволяет брокеру обслуживать миллионы сообщений в секунду на обычном оборудовании.
Практические выводы и подготовка к собеседованию
Понимание топиков, партиций и оффсетов необходимо для ответа на ключевые архитектурные вопросы на Middle+/Senior собеседованиях:
Дальнейшие шаги
Мы детально исследовали фундаментальные строительные блоки Kafka: топики, партиции, оффсеты и файлы сегментов.
Следующий логический шаг — погрузиться в механику работы клиентов: как продюсер собирает батчи, сжимает данные и как консьюмер опрашивает брокер через длинные пулы (Long Polling).
В следующей лекции мы детально разберем: 3. Producer и consumer.