Содержание урока · 10 разделов
- Reader API без группы: Partition вместо GroupID
- kafka.Reader.SetOffset: три формы оффсета
- Offset, Lag и ReadLag: измерения без группы
- Сценарии без группы: реплей, аудит, ручная раздача
- Сценарии groupless Reader одним списком
- Ниже Reader: DialLeader и сырой Conn
- Способы стартовать чтение одним столом
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Задача вне штатного потока: группа закоммитила всё до последнего сообщения и уехала вперёд, а вам нужно перечитать вчерашние события — миграция данных, расследование инцидента, пересчёт агрегата. Группа для этого не приспособлена: её позиция принадлежит ей, и менять её по ходу дела нечестно по отношению к остальным членам. Для таких задач у kafka-go есть второй режим Reader — без GroupID, с прямым указанием партиции и полным контролем позиции. Это тот же Reader из шестого урока, но в режиме «одна партиция, один владелец»: вы задаёте стартовую точку, двигаете её когда угодно между чтениями и видите лаг партиции без посредников. В этом уроке разберём конфигурацию без группы, все формы kafka setoffset — константы, числа и время, — измерения Offset и ReadLag и честный список сценариев, где прямой доступ к партиции выигрывает у группы, и где проигрывает.
Reader API без группы: Partition вместо GroupID#
Конфигурация зеркальна групповой: в ReaderConfig вместо GroupID указывается Partition — конкретный номер партиции топика. Правило взаимной исключённости жёсткое: NewReader паникует, если заданы и Partition, и GroupID одновременно — документация формулирует «either Partition or GroupID may be specified, but not both», и паника на старте это лучший способ узнать о конфликте. Один reader в этом режиме читает ровно одну партицию: параллелизм получаете вручную, по reader на партицию, и распределение партиций по инстансам — тоже ваша работа, а не работа брокера. Взамен исчезают все групповые механики: никаких пульсов, генераций и перетасовок; процесс умер — чтение остановилось, поднялся — продолжили с той позиции, которую сами установили. Для разовой стартовой точки есть и поле конфига StartOffset — оно принимает те же константы FirstOffset и LastOffset, его дефолт FirstOffset, и оно экономит вызов SetOffset, когда позиция известна ещё до создания reader'а. Именно за предсказуемость groupless-режим любят в утилитах: реплей-джобы, аудиты, переливы между топиками. Цена — самостоятельность: никто не закоммитит позицию за вас, и если сервис упал, продолжение с места остановки нужно организовать явно — хранить оффсет там же, где и результат обработки.
Партиционный reader с перемоткой на начало
GOreader := kafka.NewReader(kafka.ReaderConfig{
Brokers: []string{"localhost:9092"},
Topic: "payments",
Partition: 2,
})
defer reader.Close()
if err := reader.SetOffset(kafka.FirstOffset); err != nil {
log.Fatalf("set offset: %v", err)
}
for {
m, err := reader.ReadMessage(ctx)
if err != nil {
break
}
fmt.Println(m.Partition, m.Offset, string(m.Value))
}ReaderConfig содержит Topic и Partition и не содержит GroupID — reader привязан ко второй партиции. SetOffset с константой FirstOffset переводит позицию на самый старый доступный оффсет: с учётом ретеншена это может быть не ноль, если старые сегменты уже удалены. Дальше обычный цикл ReadMessage: сообщения идут подряд по оффсетам, а m.Partition подтверждает, откуда они приехали.
Инцидент из реальной практики: инженер хотел перемотать группового консьюмера на начало топика, нашёл SetOffset в API, вызвал его, ошибку не проверил — и удивился, почему реплей не начался. А не начался он потому, что SetOffset с GroupID возвращает errNotAvailableWithGroup: позицию группового reader'а задаёт коммит, а не ручная перемотка. Ошибка была не в API, а в представлении: перемотать группу штатным API kafka-go нельзя — либо новый GroupID со стартом с FirstOffset, либо отдельный groupless reader для перечитывания. Проверяйте возвращаемую ошибку: она в одном случае злит, а в другом экономит вечер разбирательства.
kafka.Reader.SetOffset: три формы оффсета#
SetOffset принимает три вида значений, и все три полезны в разных задачах. Константа kafka.FirstOffset — минус два — переводит позицию на самый старый доступный оффсет партиции; с включённым ретеншеном «старый» значит «самый ранний из живых», а не ноль. Константа kafka.LastOffset — минус один — ставит reader на хвост: следующий ReadMessage заблокируется и вернёт первое новое сообщение, а не последнее лежащее; чтобы перечитать лежащее, нужен конкретный номер. Третья форма — число: SetOffset(42) позиционирует точно на оффсет 42, и следующий ReadMessage вернёт именно его. Метод работает на лету: позицию можно двигать между чтениями, двигаясь по истории туда-обратно, и каждое перемещение видно в логе: reader печатает «setting the offset of the kafka reader … from … to …», так что мухлеж с позицией сам себя выдаёт в логах. Два пограничных случая из исходников: после Close SetOffset возвращает io.ErrClosedPipe, а позиция меньше первого живого оффсета при чтении аккуратно доезжает до него — reader печатает в лог, сколько сообщений он пропустил прыжком. Историческая ремарка для тех, кто встретит старые примеры: в версии 0.2.0 значения минус единицы и минус двойки менялись местами, поэтому константы по именам — не стиль, а защита от копипасты с призраками.
Проверка: хвост партиции
В партиции лежат сообщения с оффсетами 0..99. Выполнен reader.SetOffset(kafka.LastOffset). Что вернёт следующий ReadMessage?
Offset, Lag и ReadLag: измерения без группы#
У groupless reader'а есть полный комплект измерений, который групповому недоступен. Offset() возвращает текущую позицию — абсолютный оффсет, на котором reader стоит. Lag() отдаёт кэшированный лаг: разницу между концом партиции и позицией, обновляемую по ReadLagInterval, который в конфиге можно отключить отрицательным значением. ReadLag(ctx) — свежий замер по сети: метод сходит к брокеру, узнаёт последний оффсет и считает лаг прямо сейчас; удобно для точечных проверок и метрик. Нюанс: при отрицательной позиции ReadLag возвращает ноль — «лаг неопределён, пока чтение не началось», и это стоит помнить, вызывая метод до первого чтения. Для группового режима всё это недоступно честно и по-разному: Offset и Lag возвращают минус единицу, а ReadLag — ошибку errNotAvailableWithGroup; лаг группы считается по агрегированным данным брокера, о чём говорит двадцать первый урок. Из полезных полей сообщения в этом режиме отметим HighWaterMark: каждое прочитанное сообщение несёт конец партиции на момент чтения — дешёвый способ оценить остаток без отдельного запроса. Все эти измерения превращают groupless reader в прибор: скрипт мониторинга, отладочный хвост, счётчик непрочитанного — всё это десять строк кода.
Хвост партиции с замером лага
GOreader := kafka.NewReader(kafka.ReaderConfig{
Brokers: []string{"localhost:9092"},
Topic: "payments",
Partition: 0,
})
defer reader.Close()
reader.SetOffset(kafka.LastOffset)
lag, err := reader.ReadLag(ctx)
if err != nil {
log.Fatalf("lag: %v", err)
}
log.Printf("tailing, lag=%d", lag)
for {
m, err := reader.ReadMessage(ctx)
if err != nil {
break
}
fmt.Printf("%d/%d %s
", m.Offset, m.HighWaterMark, m.Value)
}Reader встаёт на хвост партиции и печатает каждое новое сообщение в формате оффсет/конец-партиции. ReadLag перед циклом даёт свежий замер по сети — для хвостового наблюдателя он почти всегда ноль, что и подтверждает работу. HighWaterMark в каждом сообщении позволяет не делать лишних запросов: разница между ним и текущим оффсетом — грубая оценка остатка на лету.
Lag() не ходит в сеть: он отдаёт кэшированное значение, которое фоновая горутина reader'а обновляет раз в ReadLagInterval — по умолчанию раз в минуту. Для дешёвого периодического опроса этого хватает, но на быстром потоке цифра устаревает между обновлениями, а свежий замер даёт только ReadLag(ctx) с походом к брокеру. Интервал настраивается в ReaderConfig, а отрицательное значение отключает фоновое обновление вовсе: тогда единственными источниками лага остаются ReadLag и HighWaterMark прочитанных сообщений.
Проверка: SetOffset с GroupID
У reader задан GroupID. Что вернёт вызов reader.SetOffset(42)?
Какой оффсет у следующего сообщения?
Прочитайте код и выберите, что он выведет:
// партиция содержит сообщения с оффсетами 0..9
reader.SetOffset(kafka.FirstOffset)
m1, _ := reader.ReadMessage(ctx) // оффсет 0
m2, _ := reader.ReadMessage(ctx) // оффсет 1
reader.SetOffset(5)
m3, _ := reader.ReadMessage(ctx)
fmt.Println(m3.Offset)Сценарии без группы: реплей, аудит, ручная раздача#
Соберём список задач, где groupless reader — правильный инструмент, а не костыль. Реплей и backfill: перечитать диапазон оффсетов или день по времени после миграции или смены правил; группа для этого не годится — её позиция нельзя трогать без последствий. Аудит и отладка: пройти партицию с начала с чужой логикой, посмотреть живые события через хвост, сверить, что реально лежит в топике. Карантин: перечитать события из карантинного топика девятнадцатого урока с новыми правилами обработки — позиция карантинного джоба полностью под его контролем. Ручная раздача: когда по reader на партицию и своё распределение дают контроль, которого группа не даёт, — например, привязка партиций к мощностям инстансов. Мониторинг: ReadLag по каждой партиции в метрики — прибор из двадцать первого урока. Общая деталь всех сценариев — чекпоинт: утилита реплея обычно печатает последний обработанный оффсет в файл или переменную процесса запуска, чтобы после сбоя продолжить с него через SetOffset с числом, а не с начала; это тот же контракт, что и коммиты группы, только хранилище позиции — ваше. И обратный список: штатный поток событий с масштабированием — работа групп из седьмого урока; писать свой раздальщик партиций поверх groupless readers, чтобы избежать группы, — вот это уже костыль, который придётся поддерживать самому.
Сценарии groupless Reader одним списком#
- Реплей: перечитать диапазон оффсетов или день по SetOffsetAt после миграции.
- Отладка: догнать хвост через LastOffset и посмотреть живые события топика.
- Аудит: пройти партицию с FirstOffset до конца с собственной логикой.
- Карантин: повторно обработать события карантинного топика с новыми правилами.
- Ручная раздача: по reader на партицию, когда нужны свои правила распределения.
- Мониторинг: ReadLag по каждой партиции в метрики без агрегаторов.
Ниже Reader: DialLeader и сырой Conn#
Иногда нужен уровень ниже: kafka.DialLeader открывает прямое соединение с лидером конкретной партиции — DialLeader(ctx, "tcp", "localhost:9092", topic, partition) возвращает сырой *Conn. У него есть свои Read, ReadMessage, ReadBatch и своя навигация по оффсетам: Seek с Offset, а также ReadFirstOffset, ReadLastOffset и ReadOffsets — по сути, это транспорт без планировщика, пульсов и прочей обвязки Reader. Годится для точечных инструментов: быстро прочитать несколько сообщений, снять оффсеты, сделать разовую миграцию. Для сервисного кода Reader остаётся правильным выбором: он переживает переезды лидера, переконнекты и держит батчинг чтения — сырой Conn ничего этого не делает за вас. Практический ориентир: если утилита одноразовая и живёт секунды, DialLeader быстрее собрать и проще выбросить; если инструмент живёт в проде дольше минуты — берите Reader и получайте отказоустойчивость бесплатно.
Способы стартовать чтение одним столом#
| Механизм | Что делает | Когда брать |
|---|---|---|
| StartOffset в ReaderConfig | Стартовая позиция нового reader, дефолт FirstOffset | Задать старт один раз при создании |
| SetOffset(kafka.FirstOffset) | Перевести позицию на самый старый живой оффсет | Полный реплей партиции |
| SetOffset(kafka.LastOffset) | Встать на хвост: ждать следующее новое сообщение | Догнать поток, отладка вживую |
| SetOffset(число) | Перепрыгнуть на конкретный оффсет | Повтор с известного места |
| SetOffsetAt(ctx, время) | Найти оффсет по метке времени | Перечитать вчерашний день |
| CommitMessages с GroupID | Позицию задаёт коммит группы | Штатный поток событий |
Reader API теперь полный: групповой режим для штатного потока и прямой доступ к партиции — для всего остального. Дальше по курсу: второй урок про концепции брокера объяснит, откуда в партиции берутся оффсеты; шестой урок про первого консьюмера напомнит базу Reader; двадцать первый урок про мониторинг лага вырастит ReadLag в систему метрик. Для практики нужен локальный брокер из Docker: заведите утилиту реплея и пройдите ею свой учебный топик — прямое чтение партиции превращается из абстракции в привычку за один вечер. Формат честности прежний: сеть песочница не поднимает, поэтому живой запуск — только у вас, а интерактивы ниже тренируют формы SetOffset на чистом Go — песочница справится с ними без брокера.
Соберите groupless reader с перемоткой
Допишите партицию в конфиге и перемотку на начало.
Соберите хвостовой наблюдатель
Расставьте строки: конфиг без GroupID, закрытие, встать на хвост, чтение, печать оффсета.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: Reader API и оффсеты
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Reader без группы: Partition вместо GroupID, один reader — одна партиция.
- NewReader паникует, если заданы и Partition, и GroupID: либо группа, либо прямое чтение.
- SetOffset принимает FirstOffset, LastOffset или конкретный оффсет и работает на лету между чтениями.
- LastOffset ставит reader на хвост: следующий ReadMessage ждёт новое сообщение, а не возвращает лежащее.
- SetOffsetAt ищет оффсет по времени: реплей дня без ручного подбора числа.
- Offset, Lag и ReadLag — полный комплект измерений только без группы: с ней Offset и Lag дают -1.
- Lag кэшируется и обновляется раз в ReadLagInterval; свежий замер — только ReadLag с походом к брокеру.
- Реплей, аудит, карантин и ручная раздача — работа groupless Reader; штатный поток живёт в группах.
Домашнее задание
Возьмите топик payments из прошлых уроков и напишите утилиту реплея: SetOffsetAt на вчерашний момент, печать партиция/оффсет/значение до конца, счётчик событий. Затем хвостовой режим: SetOffset с LastOffset, ReadLag до и после — убедитесь, что лаг на хвосте около нуля, и что утилита ждёт новые события. Третье упражнение: создайте reader с GroupID и попытайтесь вызвать SetOffset — опишите полученную ошибку словами и объясните, почему API так устроен. Финал: посчитайте ReadLag по всем партициям топика groupless-ридерами и сверьте сумму с тем, что вы узнаете в двадцать первом уроке.
Личные заметкиПусто
Вопросы и ответы по уроку
Группа тоже читает с оффсетов — зачем отдельный режим?
Группа читает с закоммиченной позиции, и её позиция — общая собственность членов: перематывать её для реплея значит ломать поток. Штатного API перемотки группы в kafka-go нет: SetOffset с GroupID возвращает ошибку. Реплей делают groupless reader'ом или временной группой с новым GroupID и StartOffset — второй способ удобен, когда перечитывать надо многими инстансами. Третий, тонкий вариант: существующую группу можно сдвинуть коммитом — утилита с тем же GroupID коммитит желаемый оффсет, и группа продолжит с него; операция штатная, но с последствиями для всех членов, поэтому делается на паузе или по регламенту.
Сколько groupless readers можно держать в одном процессе?
По одному на партицию — и это разумный потолок: reader привязан к партиции, и два reader на одну партицию дадут два независимых потока одинаковых сообщений. Параллелизм масштабируется числом reader'ов, каждый со своим циклом обработки. Помните, что позицию при остановке придётся хранить самим: groupless reader не коммитит оффсеты в брокер.
Что вернёт ReadMessage, когда партиция дочитана?
Заблокируется в ожидании новых сообщений — чтение партиции по определению бесконечно, конец сейчас означает «пока конец». Завершение цикла организуется контекстом: отмена прервёт ReadMessage с ошибкой, и утилита сможет завершиться аккуратно. Если нужно ограничить объём — сравнивайте m.Offset с границей или считайте сообщения и выходите по счётчику. Отдельный пограничный случай — старт с LastOffset на пустой партиции: позиция резолвится в ноль, и первое же сообщение станет первым прочитанным.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Ebitengine: 2D-игры на Go
Курс Ebitengine для Go: игровой цикл Update/Draw, спрайты и анимации, ввод, коллизии, тайлмапы, камера, звук, сцены и сборка на платформы. 25 уроков.
- Понимать игровой цикл Update/Draw и Layout
- Рисовать спрайты, атласы и примитивы
- Обрабатывать ввод: клавиатура, мышь, тач
- Строить движение, коллизии и анимации