---
title: "Топики, партиции и оффсеты"
description: "Как устроен брокер Kafka: топики и партиции как append-only лог, офсеты сообщений, реплики и лидеры, границы порядка и что видно консьюмеру в kafka.Message."
course: "Kafka в Go: продюсеры и консьюмеры на kafka-go"
url: "https://golangmaster.ru/courses/kafka-go/lessons/02-koncepty-brokera/"
published: "2026-09-19"
updated: "2026-09-29"
minutes: 18
---

# Топики, партиции и оффсеты

*Курс «Kafka в Go: продюсеры и консьюмеры на kafka-go», урок 2 из 25. ~18 минут. Обновлено 29.09.2026. Источник: https://golangmaster.ru/courses/kafka-go/lessons/02-koncepty-brokera/*

Топики и партиции kafka — три слова, без которых код на kafka-go не читается: непонятно, почему сообщения уходят не туда, откуда приходят и за что отвечает оффсет. В этом уроке спускаемся на уровень брокера: append-only лог, партиции как единицы параллелизма и порядка, офсеты как адреса сообщений, реплики как страховка от падения брокера.

Первый контакт с Kafka почти всегда выглядит одинаково: сервис пишет заказы в топик, консьюмеров становится больше, а в логах — загадочное «порядок нарушился» и цифры вроде partition 2, offset 5193, которые никто не просил. Проблема не в коде, а в пропущенном слое: Kafka — не почта с ящиками, а журнал, разрезанный на партиции, где у каждого сообщения есть точный адрес. Пока этот слой не разобран, Writer и Reader превращаются в чёрные ящики: кажется, что брокер сам разрулит порядок и доставку, и каждая пара дней уходит на отладку того, что объяснялось бы одной схемой. Разберём схему один раз и целиком: что такое топик, зачем его режут на партиции, что такое оффсет и почему реплики — не лишняя сложность, а условие выживания. Всё дальнейшее в курсе — от Writer до мониторинга лага — стоит именно на этих четырёх понятиях. Поэтому не торопитесь: это единственный урок без живого кода, зато каждый следующий будет опираться на схему, которую здесь соберём.

### Топики и партиции kafka: как брокер хранит поток

Топик — именованный канал событий: orders, payments, clicks. Технически это не один файл и не одна очередь, а просто имя, под которым живёт набор партиций. Партиция — осколок топика: независимый append-only журнал, который дописывается с конца и живёт на диске брокера отдельным набором сегментов. При создании топика вы задаёте число партиций — в kafka-go это NumPartitions в TopicConfig, а из консоли флаг --partitions. Почему разрез обязательный? Первая причина — скорость: одна партиция пишется последовательно и ограничена одним диском и одним брокером; десять партиций раскладываются по десяти брокерам и пишутся одновременно. Вторая — параллелизм чтения: именно партиции, а не топики, распределяются между консьюмерами группы. Третье — масштабирование: добавить брокеров бесполезно, пока все события текут в одну партицию. Число партиций выбирается при создании и потом увеличивается с осторожностью: от него зависит, куда попадут сообщения с одинаковым ключом, и эта ловушка получит отдельный урок. Пока запомните формулу: топик — логика, партиции — физика. Отсюда и вопросы «куда писать» и «откуда читать» в коде всегда решаются через партиции: Writer выбирает их балансировщиком, Reader получает назначение от брокера, и оба возвращают номер партиции в каждом сообщении.

### Партиция: append-only лог и единица порядка

Внутри партиция — предельно простая структура: сообщения дописываются в конец и никогда не меняются. Нет обновлений, нет удалений по одному, нет перестановок — только добавление в хвост. Простота и делает Kafka быстрой: последовательная запись на диск — самый дешёвый режим работы носителя, по скорости приближающийся к записи в сеть, поэтому один брокер спокойно переваривает сотни тысяч небольших сообщений в секунду, если нагрузка ложится по партициям равномерно. Из append-only модели растёт и главное правило порядка: сообщения упорядочены внутри партиции и ни в каком другом месте. Записали в партицию 0 «заказ создан», потом «заказ оплачен» — читатель этой партиции увидит их строго в этом порядке. Но продюсер волен раскладывать события по разным партициям, и тогда между партициями никакого порядка нет и не будет: читатель видит перемешанный поток. Отсюда инженерный вывод, который определил весь API клиента: всё, что должно происходить в правильной последовательности, обязано попадать в одну партицию — за это отвечает ключ, и его разбор впереди. Число партиций при этом становится потолком параллелизма: больше читателей в группе, чем партиций, — лишние стоят без работы. Сюда же примыкает и цена хранения: каждая партиция — набор сегментных файлов с индексами, и их суммарное число умножается на количество топиков, поэтому сотни партиций «на всякий случай» бьют по брокеру.

### Адрес сообщения глазами kafka-go

```go
reader := kafka.NewReader(kafka.ReaderConfig{
        Brokers:   []string{"localhost:9092"},
        Topic:     "orders",
        Partition: 0,
        MinBytes:  1,
        MaxBytes:  10e6,
})
defer reader.Close()

m, err := reader.ReadMessage(context.Background())
if err != nil {
        log.Fatal(err)
}
fmt.Printf("%s [%d] @%d: %s\n",
        m.Topic, m.Partition, m.Offset, m.Value)
```

Здесь Reader читает конкретную партицию без группы — так проще всего увидеть адресацию. ReadMessage возвращает kafka.Message, в котором заполнены Topic, Partition и Offset: вы всегда знаете, откуда пришло сообщение и какая у него позиция в логе. MinBytes и MaxBytes ограничивают размер порции, которую брокер отдаёт за одну поездку по сети; почему 1 и 10e6 — разберём в уроке про Reader.

> **Порядок между партициями не существует.** Живое место из реального проекта: заказ и его оплата ушли без ключа, попали в партиции 1 и 2, и биллинг обработал оплату раньше создания заказа — счёт списался, а заказ ещё числился незавершённым, на что сработали алерты. Чинить это после факта больно: события уже разъехались, и потребителю пришлось бы буферизовать и сортировать их самому. Решение всегда одно и ставится на стороне продюсера: события одной сущности — один ключ — одна партиция. Об этом целиком пятый урок курса.

### Офсеты kafka: адрес сообщения внутри партиции

Оффсет — порядковый номер сообщения в партиции: монотонно растущее целое, которое брокер выдаёт при записи. Оффсеты не перемешиваются и не переиспользуются: 0, 1, 2, 3 и дальше; слово «офсеты kafka» в документации всегда об этих номерах. Практические детали, которые экономят вечера. Первое: оффсеты уникальны в пределах партиции — «offset 5193» без указания партиции ничего не значит. Второе: в последовательности бывают пропуски — брокер резервировал номера под внутренние записи вроде транзакционных маркеров, и это нормально, а не потеря данных. Третье: начало лога движется — старые сегменты удаляются по сроку или размеру целиком, поэтому самый старый живой оффсет может быть, например, 4 582 001, а не 0; попытка прочитать стёртое вернёт ошибку вне диапазона. Четвёртое: оффсет читателя — его личная позиция, которую он подтверждает брокеру и получает обратно после рестарта; в топике __consumer_offsets брокер хранит эти позиции для всех групп. Понимание оффсета делает осмысленным весь API чтения: FirstOffset и LastOffset, SetOffset и коммиты — всё это операции с номерами позиций в логе. Наконец, оффсеты объясняют и механику гарантий: чтобы не потерять позицию после сбоя, консьюмер подтверждает её брокеру — коммиты получат отдельный урок, а здесь важно, что позиция хранится не в памяти читателя, а в кластере.

> **Стёртое прочитать нельзя — и не нужно.** Ретеншен удаляет сообщения физически: сегменты с истёкшим сроком стираются с диска, и оффсеты в их диапазоне больше не существуют. Это не баг и не потеря: срок жизни задан владельцем топика осознанно. Если данные нужны дольше, чем живёт лог, источник правды — база или отдельное хранилище, а топик — транспорт и журнал интеграции. Про настройку ретеншена и лог-компакцию — семнадцатый урок. Для учебного топика типовой срок — неделя по умолчанию; руками его трогать не нужно до тех пор, пока вы не поймёте цену решения.

### Партиции и офсеты одним списком

- Партиция — независимый append-only журнал: пишем только в конец, не меняем и не удаляем по одному.
- Порядок живёт внутри партиции; между партициями брокер его не даёт никогда.
- Число партиций — потолок параллелизма группы: читателей больше, чем партиций, — кто-то простаивает.
- Оффсет — номер позиции в партиции: монотонный, уникальный в её пределах, с допустимыми пропусками.
- Начало лога движется после чистки старых сегментов — нулевой оффсет в долгоживущем топике встречается редко.
- Позиции групп брокер хранит во внутреннем топике __consumer_offsets.

### Реплики и лидер: за счёт чего партиция не теряется

Партиция на одном брокере — точка отказа: диск умер, события недоступны. Поэтому у каждой партиции есть фактор репликации — число копий на разных брокерах. Одна копия назначается лидером: именно он принимает все записи и чтения партиции, а остальные брокеры — фолловеры — подтягивают его данные себе. Набор синхронно догоняющих реплик называется ISR, и от него считается кворум подтверждения записи: сколько реплик обязаны получить сообщение, чтобы продюсер считал его записанным. Если брокер с лидером умирает, контроллер кластера назначает лидером одну из догнавших реплик — продюсеры и консьюмеры переподключаются и продолжают работу, пользователь этого даже не заметит, кроме короткой паузы. Для локальной разработки фактор репликации один — брокер всего один, и это нормально; в продакшене чаще три копии с кворумом из двух. Полная механика подтверждений и цены надёжности — урок про durability, здесь важна картина: партиция живёт не на брокере, а в кластере. Отсюда и правило локальной разработки: ReplicationFactor: 1 в TopicConfig — не экономия, а честное отражение одиночной ноды; поднять фактор выше на одном брокере просто некуда.

### Анатомия брокера: термин и где он виден в коде

| Термин | Что это | Где встречается в kafka-go |
| --- | --- | --- |
| Топик | Именованный набор партиций | WriterConfig.Topic, ReaderConfig.Topic |
| Партиция | Осколок топика, отдельный лог | m.Partition, ReaderConfig.Partition |
| Оффсет | Номер сообщения в партиции | m.Offset, kafka.FirstOffset / LastOffset |
| Лидер | Реплика, принимающая записи и чтения | выбирается кластером, в коде не задаётся |
| ISR | Реплики, догнавшие лидера | влияет на RequiredAcks продюсера |
| Ретеншен | Срок жизни сообщений | настройка топика, Admin API |

### Офсеты в коде: FirstOffset, LastOffset и SetOffset

kafka-go выставляет адресацию наружу честно, без магии. Константа kafka.FirstOffset означает «с самого старого живого сообщения партиции», kafka.LastOffset — «с конца, ждать новые». Для чтения отдельной партиции оффсет двигается методом reader.SetOffset: ноль — читать с абсолютно первой позиции, LastOffset — подписка на хвост. Именно так строится переигрывание истории: сдвиньте оффсет назад — и партиция отдаст старые сообщения снова, пока они не стёрты ретеншеном. С группами механика другая: GroupID берёт позиции из брокера, SetOffset там не нужен, а стартовая точка без сохранённого оффсета задаётся полем StartOffset — этот путь разберём в уроках про группы и коммиты. Сейчас важна карта: любая операция чтения сводится к вопросу «с какого оффсета какой партиции», и у вас есть два уровня контроля — сырой по партициям и групповой по оффсетам. Запускается всё это локально против брокера из следующего урока: песочница сайта сеть не поднимает, зато интерактивы ниже позволяют щупать адресацию без кластера. Рекомендую привычку: каждый новый Reader запускать с печатью Topic, Partition и Offset — адресация в консоли объясняет больше, чем любой лог.

### Меню: управление стартовой позицией

```go
reader := kafka.NewReader(kafka.ReaderConfig{
        Brokers:   []string{"localhost:9092"},
        Topic:     "orders",
        Partition: 0,
})

// начать с самого старого живого сообщения
reader.SetOffset(kafka.FirstOffset)

// подписаться на хвост: только новые сообщения
reader.SetOffset(kafka.LastOffset)

// перечитать историю с конкретной позиции
reader.SetOffset(5193)
```

Три режима одной партиции без группы: FirstOffset проигрывает всё, что пережило ретеншен, LastOffset превращает Reader в подписку на новые события, а жёсткий оффсет — в точку возврата после сбоя. SetOffset работает только в сыром режиме, без GroupID: у группы позициями управляет брокер, и вмешиваться вручную не требуется.

### Проверка: запись в партицию

**Вопрос:** Продюсер дописал сообщение в партицию. Что происходит с его оффсетом?

- ✅ Брокер выдал монотонно следующий номер позиции в этой партиции
- Оффсет выбирает продюсер из свободных номеров
- Оффсет один на весь топик, без привязки к партиции
- Оффсет появляется только после чтения консьюмером

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

### Проверка: лидер партиции

**Вопрос:** Брокер с лидером партиции упал, копия на другом брокере догнала. Что будет?

- Партиция станет недоступной до возврата брокера
- ✅ Кластер назначит лидером догнавшую реплику, работа продолжится
- Продюсер обязан пересоздать топик
- Оффсеты читателей обнулятся

Репликация существует именно для этого: контроллер выбирает нового лидера из синхронных копий, клиенты переподключаются, оффсеты групп остаются в брокере. Пауза короткая; данные не теряются, если записи подтверждались с учётом реплик — эту цену разберём в уроке про durability.

### Что вернёт первый ReadMessage?

```go
// В партиции 0 топика orders уже лежат сообщения
// с оффсетами 0..999.

reader := kafka.NewReader(kafka.ReaderConfig{
        Brokers:   []string{"localhost:9092"},
        Topic:     "orders",
        Partition: 0,
})
reader.SetOffset(kafka.LastOffset)

m, err := reader.ReadMessage(ctx)
fmt.Println(m.Offset, err)
```

**Что выведет программа?**

- 999 <nil> — чтение вернёт последнее существующее сообщение
- 0 <nil> — без указания позиции Reader стартует с нуля
- ✅ ReadMessage заблокируется и вернётся только после нового сообщения с оффсетом 1000
- ошибка: оффсет вне диапазона

LastOffset означает «конец лога»: читатель садится на хвост и ждёт следующее сообщение, а не вычитывает старые. Оффсет 999 остаётся непрочитанным для этого Reader — если он нужен, ставьте FirstOffset или конкретный номер. Это частая ловушка при первом запуске консьюмера: программа «висит», потому что новых событий просто нет.

### Соберите чтение партиции

Допишите конструктор Reader, его конфиг и установку хвостовой позиции.

```go
reader := kafka.___(kafka.___{
        Brokers:   []string{"localhost:9092"},
        Topic:     "orders",
        Partition: 0,
})
defer reader.Close()

reader.SetOffset(kafka.___)
```

Подсказка: Конструктор называется NewReader, конфиг — ReaderConfig, а хвост лога обозначает LastOffset.

Правильные ответы в пропусках: `NewReader`, `ReaderConfig`, `LastOffset`

### Соберите переигрывание истории

Расставьте строки в порядке выполнения: конфиг чтения, Reader, закрытие, установка позиции, чтение, вывод адреса.

Правильный порядок строк:

1. `reader := kafka.NewReader(kafka.ReaderConfig{Brokers: []string{"localhost:9092"}, Topic: "orders", Partition: 0})`
2. `defer reader.Close()`
3. `reader.SetOffset(kafka.FirstOffset)`
4. `m, err := reader.ReadMessage(context.Background())`
5. `if err != nil { log.Fatal(err) }`
6. `fmt.Printf("[%d] @%d: %s\n", m.Partition, m.Offset, m.Value)`

### Карточки: устройство брокера

- **Топик** — Именованный канал событий; физически — набор партиций. Число партиций задаётся при создании и определяет потолок параллелизма.
- **Партиция** — Append-only журнал-осколок топика на диске брокера. Порядок сообщений существует только внутри неё.
- **Оффсет** — Монотонный номер сообщения в партиции, который раздаёт брокер при записи. Адрес, по которому читатель возвращается к сообщению.
- **Ретеншен** — Срок жизни сообщений в топике. Старые сегменты стираются целиком, поэтому нулевой оффсет в живом топике — редкость.
- **Лидер и ISR** — Лидер принимает записи и чтения партиции, ISR — реплики, успевшие за ним. При падении лидера его место занимает догнавшая копия.
- **FirstOffset / LastOffset** — Константы kafka-go: читать с самого старого живого сообщения или сесть на хвост и ждать новые.

Схема брокера сложилась: топики и партиции — хранилище потока, оффсеты — адреса в нём, реплики — страховка. Дальше по порядку: в [уроке установки](/courses/kafka-go/lessons/03-ustanovka-brokera/) поднимем локальный брокер в Docker и создадим первый топик с тремя партициями; в [уроке про ключи](/courses/kafka-go/lessons/05-klyuchi-particionirovanie/) разберём, как отправлять события одной сущности в одну партицию и что ломается при изменении числа партиций. Следующий слой — чтение: [первый консьюмер Reader](/courses/kafka-go/lessons/06-pervyj-consumer/) ждёт вас в шестом уроке. Пока практика одна: запустите у себя Reader из урока и посмотрите на реальные Topic, Partition и Offset своими глазами — эта картина остаётся с вами до конца курса. Если какая-то из ролей ещё путается — вернитесь к таблице выше: пять строк там заменяют половину страниц документации.

## Коротко: что запомнить

- Топик — логика, партиции — физика: осколки-логи пишутся параллельно и распределяются между читателями.
- Порядок гарантирован только внутри партиции; между партициями его нет — события одной сущности должен сводить вместе ключ.
- Оффсет — номер позиции в партиции: монотонный, с пропусками, раздаётся брокером при записи.
- Начало лога движется после чистки: стёртые оффсеты прочитать нельзя, и это осознанная цена ретеншена.
- Реплики и лидер делают партицию живучей: падение брокера — пауза, а не потеря.
- В kafka-go адресация видна напрямую: m.Topic, m.Partition, m.Offset плюс FirstOffset, LastOffset и SetOffset.
- Позиция группы хранится в кластере, а не в памяти процесса: после рестарта читатель возвращается на подтверждённое место.
- Код урока запускается локально против брокера в Docker; песочница показывает тематические программы на чистом Go.

## Домашнее задание

Возьмите брокер из следующего урока, создайте топик с тремя партициями и отправьте в него десять сообщений без ключей. Затем прочитайте их через Reader с Partition: 0 и напечатайте m.Partition и m.Offset каждого — убедитесь, что события размазались по партициям, а внутри каждой идут по оффсетам. Вторая часть: поставьте reader.SetOffset(kafka.FirstOffset) и перечитайте всё снова, чтобы почувствовать переигрывание истории. В конце запишите, какой оффсет был у первого прочитанного сообщения — он ещё пригодится в уроке про Reader API.

## Вопросы и ответы по уроку

### Сколько партиций делать для нового топика?

Для сервисного топика типовой старт — от трёх до двенадцати: меньше трёх не даёт запаса по параллелизму, больше — усложняет ребалансировку и создание мелких файлов. Точная арифметика зависит от планируемой скорости записи и числа потребителей; ход с увеличением после запуска возможен, но связан с переигрыванием ключей, о чём отдельно в пятом уроке. Если сомневаетесь, начните с шести: и параллелизм есть, и ребалансировка не превращается в марафон.

### Почему оффсеты начинаются не с нуля?

В живом топике начало лога движется: старые сегменты удаляются по сроку или размеру целиком, и самый старый живой оффсет уходит вверх. Пропуски в середине последовательности тоже нормальны — номера резервируются под внутренние записи брокера. Ноль увидите только в свежесозданной партиции.

### Можно ли удалить одно конкретное сообщение?

Штатной операции нет: партиция — append-only лог, сообщения не удаляются по одному. Есть ретеншен по времени или размеру, стирание старых сегментов и лог-компакция, которая оставляет последнее значение по ключу. Удалять точечно не получается по дизайну — и обычно не нужно: проблема решается оффсетом читателя, а не чисткой лога.
