Содержание урока · 10 разделов
- kafka.NewReader: ReaderConfig и первый kafka consumer
- ReadMessage: блокирующий вызов и поля kafka.Message
- Цикл чтения консьюмера: где выход и как не зациклиться
- Что Reader берёт на себя
- SetOffset, Lag и Stats: позиция и метрики читателя
- Методы Reader одним столом
- Локальный запуск консьюмера: песочница против Docker
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Ситуация до боли знакомая: платежи идут в топик payments, события копятся, а читателя нет. Данные лежат на диске брокера и никуда не денутся — в этом сила журнала из первого урока, — но чтобы из лога что-то получить, нужен процесс, который ходит к брокеру и вычитывает партицию по оффсетам. В kafka-go для этого есть Reader: высокоуровневый клиент чтения, который сам держит соединение, сам запрашивает батчи сообщений и сам переживает разрывы сети. Мы напишем консьюмера, читающего одну партицию напрямую — это самый честный способ увидеть механику: где лежит сообщение, какой у него оффсет и как двигается позиция чтения. Группы консьюмеров добавят сверху распределение партиций, а коммиты — память о прочитанном; об этом уроки седьмой и восьмой. Здесь же — фундамент: конфиг, блокирующий вызов, цикл и метрики. Ключи и партиционирование из пятого урока уже разложили события по партициям — теперь разберём, как одну партицию вычитать целиком и что произойдёт с позицией при рестарте.
kafka.NewReader: ReaderConfig и первый kafka consumer#
Консьюмер начинается с kafka.NewReader, который принимает ReaderConfig и паникует, если конфиг невалиден. Минимальный набор — Brokers, Topic и либо Partition, либо GroupID: это два режима работы, взаимоисключающие на уровне Validate. Чтение конкретной партиции — режим без состояния: вы говорите, откуда читать, и Reader не коммитит ничего. Чтение группой — режим с координацией, о нём следующий урок. Дефолты конфига стоят того, чтобы их знать: MinBytes равен 1 — брокер отвечает, как только есть хоть байт данных; MaxBytes — примерно мегабайт на батч, чтобы ответ брокера не разорвал память; MaxWait — 10 секунд ожидания данных в незаполненном батче. QueueCapacity внутренней очереди — 100 сообщений: они выравнивают скорость fetch и скорость вашего обработчика. Рядом стоят и менее заметные поля: MaxAttempts ограничивает тремя число попыток соединения, прежде чем ошибка уйдёт наружу, а ReadBatchTimeout — десятью секундами выборку из уже полученного батча. Эти цифры объясняют поведение, которое без них выглядит загадкой: почему консьюмер на пустом топике просыпается раз в десять секунд и почему большой MinBytes на низконагруженном топике добавляет задержку — брокер копит данные до минимума, прежде чем ответить. Для первого kafka reader оставьте дефолты как есть: они разумны, а тюнинг батчей — дело измерений, о которых поговорим в десятом уроке на продюсерской стороне.
Консьюмер одной партиции целиком
GOpackage main
import (
"context"
"fmt"
"log"
"github.com/segmentio/kafka-go"
)
func main() {
ctx := context.Background()
reader := kafka.NewReader(kafka.ReaderConfig{
Brokers: []string{"localhost:9092"},
Topic: "payments",
Partition: 0,
})
defer reader.Close()
if err := reader.SetOffset(kafka.FirstOffset); err != nil {
log.Fatal(err)
}
for {
m, err := reader.ReadMessage(ctx)
if err != nil {
log.Printf("reading stopped: %v", err)
break
}
fmt.Printf("offset=%d partition=%d key=%s value=%s\n",
m.Offset, m.Partition, m.Key, m.Value)
}
}Каркас чтения без группы: ReaderConfig требует адресов, топика и номера партиции, а MinBytes, MaxBytes и MaxWait kafka-go подставит сам. SetOffset с kafka.FirstOffset отправляет позицию к самому старому живому сообщению — как в predict второго урока. Дальше вечный for: ReadMessage блокируется до события, ошибка любого рода, включая io.EOF после Close, выводится в лог и гасит цикл. На пустом топике вызов просто ждёт.
ReadMessage: блокирующий вызов и поля kafka.Message#
ReadMessage читает и возвращает следующее сообщение, блокируя горутину, пока его нет: на живом потоке это микросекунды, на пустой партиции — хоть до утра. Отменить ожидание можно контекстом — так останавливают читателя при выключении сервиса. Возвращаемое значение — kafka.Message, и его поля — это адрес события в логе: Topic и Partition говорят, откуда, Offset — позиция, Key и Value — байты, Time — время записи брокером, Headers — служебные пары ключ-значение, которые продюсер приложил сам: трейс-идентификаторы и метки маршрутизации живут именно там, а не в Value. Время записи брокером, а не обработки: на нём строится задержка сквозной доставки. Поле HighWaterMark показывает последний оффсет, видимый брокеру, — из него считается лаг. Ошибку ReadMessage оборачивает в префикс fetching message, поэтому проверяйте через errors.Is и errors.As, а не сравнением строк. По устройству метод — тонкая обёртка: внутри это FetchMessage, который при группе сопровождается CommitMessages, и ошибка коммита приедет с префиксом committing message. Особый случай — io.EOF: его Reader возвращает после Close, и это штатный сигнал остановки, а не сбой. Если программа прочитала лог до конца, ReadMessage не вернёт управление с ошибкой — он повиснет и будет ждать новых данных, потому что чтение журнала в Kafka бесконечно по определению. Отсюда практическое правило вычитки истории: ограничивайте её не чтением до конца, а оффсетом или контекстом с дедлайном из восемнадцатого урока — иначе процесс застрянет в бесконечном ожидании после последнего события.
Проверка: что вернёт ReadMessage после Close
Reader прочитал поток, затем main вызвал reader.Close() в другой горутине. Что вернёт заблокированный ReadMessage?
Цикл чтения консьюмера: где выход и как не зациклиться#
Цикл консьюмера живёт весь срок жизни сервиса, и единственная ветка выхода в нём — ошибка от ReadMessage. Обработка этой ошибки — место, где новички ломают продакшен. Первый грех: логировать ошибку и продолжать, не отличая io.EOF от сбоя. После Close такой цикл превращается в горловину: Reader мёртв, ReadMessage мгновенно возвращает io.EOF, лог раздувается на гигабайты, процесс жжёт CPU впустую. Второй грех — выход по любой ошибке без стратегии: сеть мигнула, брокер перезапустился, и читатель умер вместе с сервисом, хотя kafka-go умеет переподключаться сам. Разумная схема выглядит так: io.EOF — тихий выход, контекст отменён — тихий выход, остальное — лог и либо выход с падением процесса, чтобы оркестратор перезапустил, либо пересоздание Reader с паузой. Контекст тут — главный инструмент: отменённый контекст гасит блокирующий вызов мгновенно, поэтому выключение сервиса выглядит как отмена, а не как убийство горутин с риском недоделанной обработки. Внутри Reader между попытками fetch уже стоит бэкофф от 100 миллисекунд до секунды, так что при живом Reader шторма запросов не будет. Определять политику перезапуска заранее — часть контракта консьюмера: ошибка в этой ветке видна только в час ночи, и смена решения на ходу стоит дороже десяти минут проектирования.
Как только в ReaderConfig появился GroupID, ReadMessage начинает коммитить оффсет сам — документация честно предупреждает, что оффсет может закоммититься раньше, чем сообщение обработано. Для быстрых обработчиков это незаметно, для медленных и дорогих — риск потерять события при падении. На словах «закоммитился раньше» держится вся тема девятого урока про гарантии: кто подтвердил позицию до работы, тот выбрал at-most-once, как бы он ни был удивлён после инцидента. Полный разбор коммитов, CommitInterval и FetchMessage — восьмой урок; сейчас достаточно знать, что поведение чтения зависит от наличия GroupID.
Что Reader берёт на себя#
- Соединения: реконнекты к брокерам и повторные попытки fetch внутри себя.
- Батчи: запросы MinBytes/MaxBytes/MaxWait и очередь QueueCapacity на 100 сообщений.
- Протокол: разбор fetch-ответов, оффсетов и меток времени без вашего участия.
- Группы: при GroupID — heartbeats, join и синхронизацию, об этом следующий урок.
- Метрики: счётчики Stats и лаг из коробки, без внешних инструментов.
- Позицию: SetOffset и Offset без группы — сдвигайте чтение в обе стороны.
SetOffset, Lag и Stats: позиция и метрики читателя#
Без группы вы владеете позицией напрямую, и это стоит потренировать на живом топике. SetOffset принимает kafka.FirstOffset, kafka.LastOffset или конкретное число — это переигрывание истории из урока про устройство брокера в чистом виде. Offset возвращает текущую позицию — следующий оффсет, который будет прочитан. Lag — расстояние до конца лога, вычисленное по последнему fetch: на живом потоке цифра достаточно свежая, на пустом устаревает, потому что fetch не выполняется. Для честной цифры есть ReadLag — отдельный запрос к брокеру, который вернёт актуальное отставание; ценой будет лишний round-trip, поэтому зовут его по таймеру, а не на каждое сообщение. С GroupID картина другая: SetOffset и ReadLag вернут ошибку unavailable when GroupID is set, а Offset и Lag — минус единицу, потому что позиция группы живёт на брокере, и клиенту её не отдают. Наконец, Stats отдаёт структуру ReaderStats со счётчиками Fetches, Messages, Bytes, Errors и Rebalances — их стоит экспортировать в мониторинг, прежде чем городить свой учёт: встроенные метрики уже считают то же самое, а параллельный учёт в своих обёртках — только источник расхождений. Разница между ReadLag по таймеру и встроенным Lag видна на графике за минуты — на тихом топике встроенная цифра стоит на месте и обманывает, пока не придёт новое сообщение.
Меню: позиция и лаг без группы
GO// следующий оффсет и отставание по последнему fetch
fmt.Println(reader.Offset(), reader.Lag())
// точный лаг отдельным запросом к брокеру
lag, err := reader.ReadLag(ctx)
if err != nil {
log.Printf("lag: %v", err)
} else {
fmt.Println("lag:", lag)
}
// переиграть историю заново — позиция крутится назад
err = reader.SetOffset(kafka.FirstOffset)
fmt.Println("rewind:", err)Чтение партиции без группы даёт прямой доступ к позиции: Offset — следующий оффсет, Lag — отставание по последнему fetch, ReadLag — свежая цифра отдельным запросом ListOffsets. SetOffset двигает позицию в обе стороны, поэтому повторное чтение истории — штатная операция. С GroupID все три приёма недоступны: у группы позиция одна и живёт на брокере.
История из код-ревью: конфиг консьюмера копировался из двух примеров, и в ReaderConfig остались и Partition: 0, и GroupID: billing. В рантайме это паника NewReader с текстом either Partition or GroupID may be specified, but not both — Validate честно ругается, но только при старте процесса, а не в IDE. Симптом узнаваем: сервис не поднимается вообще, и стек указывает в конструктор. Симметричная ловушка — SetOffset при живой группе: ошибка unavailable when GroupID is set возвращается уже в рантайме, посреди работающего цикла, если кто-то добавил подстройку позиции, забыв про режим группы. Правило простое: определились с режимом — уберите противоположное поле из конфига вовсе, а не закомментируйте.
Методы Reader одним столом#
| Метод | Что делает | С GroupID |
|---|---|---|
| ReadMessage(ctx) | Читает следующее сообщение; с группой — ещё и коммитит | да — коммит внутри вызова |
| FetchMessage(ctx) | Читает без коммита — ручное подтверждение | да — парная к CommitMessages |
| SetOffset(int64) | Ставит позицию чтения: FirstOffset, LastOffset, число | нет — вернёт ошибку |
| Lag() / ReadLag(ctx) | Отставание от конца лога: кэш против точного запроса | нет: Lag вернёт -1, ReadLag — ошибку |
| Stats() | Счётчики Fetches, Messages, Bytes, Errors, Rebalances | да — включая ребалансы |
| Close() | Останавливает Reader; ReadMessage вернёт io.EOF | нет явного выхода: координатор заметит по SessionTimeout |
Локальный запуск консьюмера: песочница против Docker#
Честно о формате: песочница сайта сеть не поднимает, поэтому этот код запускается локально: Go и Docker с брокером из третьего урока, два терминала — в одном продюсер из четвёртого, в другом этот Reader. Вы увидите своими глазами, как Offset растёт, а Lag тает, и что ReadMessage на пустом топике молчит, пока вы не пришлёте сообщение. Интерактивы ниже — тематические программы на чистом Go: они тренируют позицию, ошибки и лаг на эмуляции лога, и их вывод совпадает с тем, что покажет настоящий брокер на тех же данных. Если брокер ещё не поднят — интерактивов хватит для проверки в конце урока, а живой прогон останется пятиминуткой вместо вечера непонимания.
Проверка: Partition и GroupID вместе
В ReaderConfig заданы одновременно Partition: 0 и GroupID: billing. Что произойдёт?
С какого оффсета пойдёт чтение?
Прочитайте код и выберите, что он выведет:
reader := kafka.NewReader(kafka.ReaderConfig{
Brokers: []string{"localhost:9092"},
Topic: "payments",
Partition: 0,
})
reader.SetOffset(5)
m1, _ := reader.ReadMessage(ctx)
m2, _ := reader.ReadMessage(ctx)
fmt.Println(m1.Offset, m2.Offset)Соберите консьюмера партиции
Допишите конструктор Reader, его закрытие и метод чтения.
Соберите цикл чтения по строкам
Расставьте строки консьюмера в порядке исполнения: создание Reader, отложенное закрытие, цикл, чтение, обработка ошибки, печать сообщения.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: Reader изнутри
Один консьюмер на партицию — рабочая машина для вычитки истории и точечных потоков. Дальше масштаб: в уроке про группы консьюмеров GroupID превратит набор Reader'ов в группу, брокер начнёт делить партиции между членами, а нагрузка — расползаться по инстансам. Затем в уроке про коммиты разберём, куда девается позиция чтения при рестарте и почему ReadMessage с группой — это уже решение о гарантиях доставки. Проверьте домашнее задание на живом брокере: без пары прогонов в терминале поля kafka.Message остаются абстракцией. Ключи решают партицию, партиция решает порядок — Reader лишь честно показывает результат этих решений, и теперь вы умеете это показание читать без лога координатора и догадок.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Reader — высокоуровневое чтение: реконнекты, fetch-батчи и очередь внутри, вам — цикл и обработка ошибок.
- Partition и GroupID взаимоисключающие: NewReader паникует, если заданы оба.
- ReadMessage блокируется до сообщения; io.EOF после Close — штатный сигнал остановки цикла.
- kafka.Message — адрес события: Topic, Partition, Offset плюс байты Key и Value.
- Без группы позиция ваша: SetOffset двигает её, Offset показывает, Lag считает отставание.
- Дефолты MinBytes 1, MaxBytes 1 МБ, MaxWait 10 секунд объясняют паузы на тихих топиках.
- Код урока — локальный Go и брокер в Docker; песочница показывает тематические программы на чистом Go.
Домашнее задание
Против брокера из третьего урока запустите консьюмер партиции из урока и продюсер из четвёртого. Отправьте десять платежей и убедитесь, что Reader видит их с оффсетами 0-9. Затем перезапустите консьюмера с SetOffset(5): первые пять событий должны приехать повторно — это и есть переигрывание истории. После этого снимите Stats до и после минуты чтения и выпишите, какие счётчики выросли: Fetches, Messages, Bytes. Финал — сломайте конфиг: добавьте в него GroupID рядом с Partition и убедитесь, что NewReader паникует с сообщением either Partition or GroupID. Заодно запишите, какой из методов таблицы выше оказался самым полезным: этот выбор пригодится в уроке про мониторинг лага.
Личные заметкиПусто
Вопросы и ответы по уроку
ReadMessage и FetchMessage — когда какой?
ReadMessage — чтение с самокоммитом при группе: короткий цикл, готовые гарантии по умолчанию. FetchMessage — чтение без коммита: им пользуются, когда подтверждение нужно после обработки. Без GroupID оба метода ведут себя одинаково — коммитить просто нечего, поэтому выбор между ними — вопрос читаемости, а не семантики.
Почему консьюмер на тихом топике отвечает с задержкой?
Сработали дефолты fetch: MinBytes 1 байт, но брокер держит запрос до MaxWait 10 секунд, ожидая данных для батча. Если задержка критична, уменьшайте MaxWait или MinBytes — ценой станет больше пустых запросов к брокеру.
Можно ли читать несколько партиций одним Reader без группы?
Нет: режим без группы привязан к одной партиции через Partition. Несколько партиций — либо Reader на каждую в своей горутине, либо GroupID: группу распределит партиции между членами сама, и Reader переключится на новый режим чтения после ребаланса. Дублей при этом не будет — каждая партиция достанется ровно одному члену.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Ebitengine: 2D-игры на Go
Курс Ebitengine для Go: игровой цикл Update/Draw, спрайты и анимации, ввод, коллизии, тайлмапы, камера, звук, сцены и сборка на платформы. 25 уроков.
- Понимать игровой цикл Update/Draw и Layout
- Рисовать спрайты, атласы и примитивы
- Обрабатывать ввод: клавиатура, мышь, тач
- Строить движение, коллизии и анимации