Содержание урока · 10 разделов
- Топики и партиции kafka: как брокер хранит поток
- Партиция: append-only лог и единица порядка
- Офсеты kafka: адрес сообщения внутри партиции
- Партиции и офсеты одним списком
- Реплики и лидер: за счёт чего партиция не теряется
- Анатомия брокера: термин и где он виден в коде
- Офсеты в коде: FirstOffset, LastOffset и SetOffset
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Первый контакт с 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
GOreader := 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 — адресация в консоли объясняет больше, чем любой лог.
Меню: управление стартовой позицией
GOreader := 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, закрытие, установка позиции, чтение, вывод адреса.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: устройство брокера
Схема брокера сложилась: топики и партиции — хранилище потока, оффсеты — адреса в нём, реплики — страховка. Дальше по порядку: в уроке установки поднимем локальный брокер в 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.
Личные заметкиПусто
Вопросы и ответы по уроку
Сколько партиций делать для нового топика?
Для сервисного топика типовой старт — от трёх до двенадцати: меньше трёх не даёт запаса по параллелизму, больше — усложняет ребалансировку и создание мелких файлов. Точная арифметика зависит от планируемой скорости записи и числа потребителей; ход с увеличением после запуска возможен, но связан с переигрыванием ключей, о чём отдельно в пятом уроке. Если сомневаетесь, начните с шести: и параллелизм есть, и ребалансировка не превращается в марафон.
Почему оффсеты начинаются не с нуля?
В живом топике начало лога движется: старые сегменты удаляются по сроку или размеру целиком, и самый старый живой оффсет уходит вверх. Пропуски в середине последовательности тоже нормальны — номера резервируются под внутренние записи брокера. Ноль увидите только в свежесозданной партиции.
Можно ли удалить одно конкретное сообщение?
Штатной операции нет: партиция — append-only лог, сообщения не удаляются по одному. Есть ретеншен по времени или размеру, стирание старых сегментов и лог-компакция, которая оставляет последнее значение по ключу. Удалять точечно не получается по дизайну — и обычно не нужно: проблема решается оффсетом читателя, а не чисткой лога.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Ebitengine: 2D-игры на Go
Курс Ebitengine для Go: игровой цикл Update/Draw, спрайты и анимации, ввод, коллизии, тайлмапы, камера, звук, сцены и сборка на платформы. 25 уроков.
- Понимать игровой цикл Update/Draw и Layout
- Рисовать спрайты, атласы и примитивы
- Обрабатывать ввод: клавиатура, мышь, тач
- Строить движение, коллизии и анимации