Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 2 из 25 ~18 мин +20 XP

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

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

Содержание урока · 10 разделов

Первый контакт с 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.

Офсеты 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: у группы позициями управляет брокер, и вмешиваться вручную не требуется.

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

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

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

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

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

Прочитайте код и выберите, что он выведет:

// В партиции 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)

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

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

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

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

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

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

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

Термин

Топик

нажмите, чтобы перевернуть
Определение

Именованный канал событий; физически — набор партиций. Число партиций задаётся при создании и определяет потолок параллелизма.

1 / 6

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

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • Топик — логика, партиции — физика: осколки-логи пишутся параллельно и распределяются между читателями.
  • Порядок гарантирован только внутри партиции; между партициями его нет — события одной сущности должен сводить вместе ключ.
  • Оффсет — номер позиции в партиции: монотонный, с пропусками, раздаётся брокером при записи.
  • Начало лога движется после чистки: стёртые оффсеты прочитать нельзя, и это осознанная цена ретеншена.
  • Реплики и лидер делают партицию живучей: падение брокера — пауза, а не потеря.
  • В 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.

Личные заметкиПусто
Хранятся только в вашем браузере (Kafka · Топики, партиции и оффсеты)

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

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

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

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

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

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

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

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Ebitengine25 уроков~12 чпрактика25 уроков уже доступны

Ebitengine: 2D-игры на Go

Курс Ebitengine для Go: игровой цикл Update/Draw, спрайты и анимации, ввод, коллизии, тайлмапы, камера, звук, сцены и сборка на платформы. 25 уроков.

  • Понимать игровой цикл Update/Draw и Layout
  • Рисовать спрайты, атласы и примитивы
  • Обрабатывать ввод: клавиатура, мышь, тач
  • Строить движение, коллизии и анимации