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

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

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

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

Первый консьюмер: Reader

Первый kafka reader собирается из трёх частей: kafka.NewReader с конфигом ReaderConfig, блокирующий ReadMessage и вечный цикл чтения консьюмера — каркас, на котором стоит вся читающая сторона Kafka. Продюсеры из прошлых уроков уже пишут события в брокер — пора их забирать. Заодно разберём поля kafka.Message, SetOffset, Lag и Stats — метрики, без которых чтение остаётся слепым. К концу урока у вас будет каркас, в который вписывается любой kafka consumer golang-сервиса, от демона телеметрии до биллинга.

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

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

Консьюмер одной партиции целиком

GO
package 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 шторма запросов не будет. Определять политику перезапуска заранее — часть контракта консьюмера: ошибка в этой ветке видна только в час ночи, и смена решения на ходу стоит дороже десяти минут проектирования.

Что 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 все три приёма недоступны: у группы позиция одна и живёт на брокере.

Методы 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 := kafka.(kafka.ReaderConfig{ Brokers: []string{"localhost:9092"}, Topic: "payments", Partition: 0, }) defer reader.() m, err := reader.(ctx) if err != nil { log.Printf("stop: %v", err) return }

Соберите цикл чтения по строкам

Расставьте строки консьюмера в порядке исполнения: создание Reader, отложенное закрытие, цикл, чтение, обработка ошибки, печать сообщения.

Строки

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

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

Карточки: Reader изнутри

Термин

kafka.NewReader

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

Конструктор Reader с ReaderConfig: Brokers, Topic и Partition либо GroupID. Паникует на невалидном конфиге — взаимоисключающие поля ловит Validate.

1 / 6

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

Личные заметкиПусто
Хранятся только в вашем браузере (Kafka · Первый консьюмер: Reader)

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

ReadMessage и FetchMessage — когда какой?

ReadMessage — чтение с самокоммитом при группе: короткий цикл, готовые гарантии по умолчанию. FetchMessage — чтение без коммита: им пользуются, когда подтверждение нужно после обработки. Без GroupID оба метода ведут себя одинаково — коммитить просто нечего, поэтому выбор между ними — вопрос читаемости, а не семантики.

Почему консьюмер на тихом топике отвечает с задержкой?

Сработали дефолты fetch: MinBytes 1 байт, но брокер держит запрос до MaxWait 10 секунд, ожидая данных для батча. Если задержка критична, уменьшайте MaxWait или MinBytes — ценой станет больше пустых запросов к брокеру.

Можно ли читать несколько партиций одним Reader без группы?

Нет: режим без группы привязан к одной партиции через Partition. Несколько партиций — либо Reader на каждую в своей горутине, либо GroupID: группу распределит партиции между членами сама, и Reader переключится на новый режим чтения после ребаланса. Дублей при этом не будет — каждая партиция достанется ровно одному члену.

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

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

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

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

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

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

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