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

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

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

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

Мониторинг: лаг консьюмера

Лаг консьюмера — главная метрика любого kafka-потока: она отвечает не на вопрос «работает ли код», а на вопрос «успевает ли система за данными», и в kafka-go за неё отвечают ReadLag и Lag у Reader плюс Stats у Writer. Разберём, что именно означает число лага, почему ноль тоже сигнал, как получить лаг для одиночного ридера и почему для группового консьюмера API честно откажется работать, что показывать статистика Writer и как собрать фоновую горутину с тикером, которая ведёт лог и питает алерты на отставание.

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

Продюсер исправно пишет, консьюмер жив и даже коммитит офсеты — а вечером выясняется, что заказы обрабатывались с задержкой в час. Код не падал, ошибок не было, потребитель просто работал медленнее потока событий. Единственная метрика, которая ловит такую ситуацию заранее, — лаг консьюмера: разница между последним офсетом партиции и офсетом, на котором стоит группа или ридер. Это число означает «столько сообщений ещё не разобрано», и оно говорит о здоровье потока больше, чем любой лог. Седьмой урок строил группы, восьмой — коммиты офсетов, тринадцатый разбирал ребалансировку; все они манипулировали механизмом, но не отвечали на вопрос владельца сервиса «как быстро мы догоняем конец очереди прямо сейчас». Здесь отвечаем: сначала арифметика лага, потом инструменты kafka-go для его измерения, затем паттерн фонового мониторинга и правила алертов, при которых сообщение приходит до того, как позвонил клиент.

Лаг консьюмера: разница двух офсетов#

Формула лага двухсложная. У каждой партиции есть лог с последним офсетом — high watermark, число записей, которое брокер уже принял. У консьюмера есть собственная позиция — офсет последнего разобранного сообщения, который он же и коммитит. Лаг партиции равен разности: последний офсет минус текущий офсет группы. Ридер без GroupID из четырнадцатого урока хранит эту позицию сам, групповой консьюмер из седьмого — в координаторе, но смысл числа одинаков: столько событий легло в топик, а в хендлер ещё не попало. Из формулы следуют обе стороны жизни лага. Растущий лаг означает, что в единицу времени в партицию пишется больше, чем консьюмер успевает разобрать: не хватает партиций, упала скорость хендлера, задерживает внешняя система. Отрицательного лага не бывает, а вот лаг, застрявший на нуле, — не всегда победа: он говорит лишь, что очередь пуста в момент замера, и ничего — о том, жив ли сам потребитель и коммитит ли он позиции. Именно поэтому лаг смотрят в динамике, а не одним срезом, и всегда вместе с признаками жизни процесса.

Kafka lag мониторинг: что считать нормой#

В сыром виде лаг — штуки, и порог «лаг больше тысячи» ничего не значит: для топика с десятью событиями в сутки это осень, для платежного потока — полминуты. Kafka lag мониторинг начинается с перевода лага во время: лаг, умноженный на среднюю скорость обработки одной партиции, даёт минуты задержки, и алерт ставится именно на задержку — «фоновая обработка отстаёт больше чем на пять минут». Второй обязательный график — скорость изменения лага. После деплоя лаг, выросший ступенькой и вернувшийся к нулю, означает паузу на рестарт и ребаланс из тринадцатого урока; лаг, выросший и не возвращающийся, означает, что новый код медленнее старого. Ноль, продержавшийся часы при живом потоке продюсера, — тоже не повод для радости, а сигнал смотреть коммиты: либо консьюмер перечитывает то же место с кривым стартовым офсетом, либо коммиты задерживаются, и позиция группы врёт. Отдельно честный вывод: единственного числа «лаг» мало — мониторят лаг, скорость его роста и факт коммитов, и только связка этих линий отличает живую систему от тихо заглохшей.

ReadLag: лаг одиночного ридера

GO
reader := kafka.NewReader(kafka.ReaderConfig{
        Brokers:   []string{"localhost:9092"},
        Topic:     "payments",
        Partition: 0,
        // GroupID не задан: методы лага доступны
})
defer reader.Close()

lag, err := reader.ReadLag(ctx)
if err != nil {
        log.Printf("lag: %v", err)
}
log.Printf("лаг партиции: %d, кэш: %d", lag, reader.Lag())

ReadLag синхронно спрашивает у брокера последний офсет партиции и вычитает из него офсет последнего сообщения, отданного ReadMessage, — результат свежий, но стоит сетевого запроса. Lag() читает внутренний кэш, который Reader обновляет в фоне, поэтому бесплатен, но отстаёт на интервал обновления. Пока текущий офсет ридера отрицательный, ReadLag возвращает ноль.

Проверка: ReadLag у группового консьюмера

Reader создан с GroupID: "payments-group". Что произойдёт при вызове ReadLag?

errNotAvailableWithGroup: лаг и группы#

Ограничение из квиза — не досадная недоделка, а следствие архитектуры. Ридер без GroupID знает свою позицию сам: он хранит текущий офсет в памяти и потому способен посчитать лаг в один запрос. Групповой консьюмер из седьмого урока позицию не выбирает — её назначает координатор, коммиты лежат в брокере, и после ребалансировки партиции переехали бы к другим членам в любой момент. Число в памяти отдельного Reader ничего бы не значило, поэтому kafka-go возвращает errNotAvailableWithGroup, а Lag() — минус единицу. Что делать, если поток живёт группой, а лаг видеть нужно? Три пути. Первый — снять статистику топика напрямую через Client: разность последнего офсета партиции и закоммиченного офсета группы, метод, который мы разберём в восемнадцатом уроке про контексты и таймауты. Второй — вывести лаг на сторону брокера: kafka-lag-exporter и консольные утилиты кластера считают его из коммитов. Третий — держать для наблюдения отдельный ридер без GroupID, читающий тот же топик с LastOffset: он не мешает группе, потому что не состоит в ней. Практическое правило: ReadLag — для ручных и тестовых потоков, групповой лаг — за пределами kafka-go, в метриках кластера.

Writer.Stats: статистика стороны записи

GO
st := writer.Stats()
log.Printf("topic %s: writes=%d msgs=%d errors=%d retries=%d",
        st.Topic, st.Writes, st.Messages, st.Errors, st.Retries)
log.Printf("batch avg=%d write avg=%v",
        st.BatchSize.Avg, st.WriteTime.Avg)

Stats() снимает срез WriterStats: Writes — записи, Messages — сообщения, Bytes — объём, Errors — счётчик ошибок, Retries — ретраи, BatchSize и BatchBytes — сводка размеров батчей, WriteTime — распределение времени записи с полями Avg, Min, Max. Счётчики агрегируются внутри Writer, поэтому метрику снимают периодически, а не по событию.

Проверка: что растёт в WriterStats

После деплоя Writer.Stats показывает: Errors растёт, Messages стоит на месте. Какой вывод корректен?

Что вернёт ReadLag у свежего ридера?

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

// Reader без GroupID, только что создан:
// ReadMessage ни разу не вызывался,
// StartOffset по умолчанию — дефолтный.
lag, err := reader.ReadLag(ctx)
log.Printf("lag=%d err=%v", lag, err)

Фоновый мониторинг: горутина с тикером#

Лаг, который смотрят руками, мониторингом не является: его забудут посмотреть именно в тот вечер, когда он вырастет. Паттерн фонового мониторинга — отдельная горутина с тикером, живущая в контексте приложения. Она периодически снимает лаг всех наблюдаемых ридеров и партиций, пишет его в лог или отправляет в систему метрик, и умирает вместе с контекстом, не оставляя утечек. Три детали делают горутину рабочей, а не декоративной. Во-первых, свой таймаут на каждый замер: ReadLag ходит в брокер, и замер не должен зависеть от чужих таймаутов — приёмы из восемнадцатого урока. Во-вторых, ошибка замера — тоже метрика: недоступный брокер для мониторинга выглядит как всплеск ошибок, а не как ноль лага, иначе дежурный увидит обманчивое благополучие. В-третьих, замер должен обходить все партиции потока, а не одну. Занятно, что внутри Reader уже живёт похожая механика: ReadLagInterval запускает фоновую горутину, которая с тикером обновляет кэш для Lag(), — наш паттерн повторяет её, но во внешний мир. Для Async-Writer из десятого урока та же горутина снимает Stats: без неё ошибки записи невидимы в принципе.

Правила мониторинга лага#

  • Лаг измеряется в сообщениях, алерты ставятся на минуты задержки: лаг умножить на среднюю скорость обработки.
  • Мониторят три линии: абсолютный лаг, скорость его роста и факт коммитов — по одной линии диагноз не ставится.
  • ReadLag работает только у ридера без GroupID; для групп лаг снимают на стороне брокера или отдельным ридером.
  • Замер лага получает свой таймаут: недоступный брокер — это метрика ошибок, а не ноль лага.
  • Writer.Stats снимают по тикеру для Async-Writer: иначе ошибки записи не видны никому.
  • Рост лага после деплоя — первичный сигнал, что новый код медленнее предыдущего.

Алерты: лаг в минутах и рост после деплоя#

Арифметика алерта: средняя скорость обработки партиции — десять событий в секунду, лаг пять тысяч — восемь минут задержки. Порог настраивается на время, а не на штуки: «задержка больше пяти минут в течение трёх проверок подряд» — правило, которое не сработает от случайного всплеска и не промолчит при реальном отставании. Второе правило — ступенька после деплоя: короткий рост лага на рестарте с последующим возвратом к нулю — норма ребаланса; рост, который не возвращается за таймаут выката, открывает инцидент ещё до жалоб пользователей. Третье — зеркальный алерт на подозрительный ноль при живом продюсере: он ловит и застрявший консьюмер, и задержавшиеся коммиты, потому что и то и другое выглядит как позиция, не двигающаяся с места. Свяжем с ошибками чтения: если лаг растёт вместе с ретраями и ошибками из девятнадцатого урока, потребитель не медленный, а падающий — яд-сообщение, от которого группа циклится, живёт в списке пунктов о ядовитых сообщениях. Мониторинг лага без мониторинга ошибок чтения половину таких случаев примет за «просто не успеваем».

Инструменты мониторинга одним столом#

ИнструментЧто показываетНюанс
Reader.Lag()Кэш лага после последнего ReadMessageМинус один при GroupID; отстаёт на ReadLagInterval
Reader.ReadLag(ctx)Свежая разность конца партиции и позиции ридераОшибка errNotAvailableWithGroup при GroupID; ноль при отрицательном офсете
ReadLagIntervalЧастота фонового обновления кэша лагаДефолт — минута; задаётся в ReaderConfig
Writer.Stats()Writes, Messages, Errors, Retries, размеры батчей, время записиДля Async-Writer — единственное окно в судьбу сообщений
Метрики брокераЛаг групповых консьюмеров по коммитамВне kafka-go: exporter или утилиты кластера

Соберите замер лага

Допишите частоту фонового обновления лага в конфиге Reader и имя метода, который даёт свежую разность офсетов.

reader := kafka.NewReader(kafka.ReaderConfig{ Brokers: []string{"localhost:9092"}, Topic: "payments", Partition: 0, ReadLagInterval: 30 * time., }) lag, err := reader.(ctx) if err != nil { log.Printf("lag: %v", err) }

Соберите горутину мониторинга лага

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

Строки

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

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

Карточки: лаг и статистика

Термин

Лаг консьюмера

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

Разность последнего офсета партиции и текущего офсета группы или ридера: столько сообщений ещё не разобрано.

1 / 6

Итог части: лаг — не прихоть observability-команд, а единственное число, показывающее, успевает ли система за своими данными. В kafka-go ридер без группы даёт его двумя методами — свежим ReadLag и кэшным Lag(), групповой консьюмер отвечает errNotAvailableWithGroup и отправляет за лагом к метрикам брокера, а Writer.Stats закрывает сторону записи теми же счётчиками и распределениями. Дальше по курсу: урок про группы объяснил, почему позиция группы не принадлежит Reader; урок про коммиты — как закоммиченный офсет становится второй половиной формулы лага; урок про ребаланс — откуда на графике берутся ступеньки после деплоя; а урок про таймауты — как снимать метрики так, чтобы замер не зависал вместе с замеряемым. Начните в песочнице: поднимите локально брокер, запустите Writer, погасите Reader на минуту — и посмотрите, как число лага честно расскажет о вашей паузе.

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

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

Вывод

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

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

  • Лаг консьюмера — разность последнего офсета партиции и офсета группы: столько сообщений ещё не разобрано.
  • Главный порог алерта — минуты задержки: лаг умножить на среднюю скорость обработки партиции.
  • Ноль лага при живом продюсере — тоже сигнал: застрявший ридер или задержавшиеся коммиты.
  • ReadLag даёт свежий лаг и отказывается работать у группы: errNotAvailableWithGroup при заданном GroupID.
  • Lag() читает кэш, обновляемый фоном раз в ReadLagInterval, и возвращает минус единицу для группы.
  • Writer.Stats отдаёт WriterStats: Writes, Messages, Errors, Retries, BatchSize, WriteTime — окно в сторону записи.
  • Фоновый мониторинг — горутина с тикером и собственным таймаутом замера по всем партициям.
  • Рост лага после деплоя и лаг с ошибками чтения — разные диагнозы: медленный код против яд-сообщения.

Домашнее задание

Поднимите локальный стенд из третьего урока и соберите пару Writer и Reader на топик payments с одной партицией, Reader — без GroupID. Напишите горутину monitorLag по образцу урока с тикером в тридцать секунд и контекстом приложения. Остановите Reader на две минуты, затем запустите снова: убедитесь, что лог лага вырос во время паузы и вернулся к нулю после рывка, и объясните по формуле «лаг на скорость», сколько это секунд задержки. Затем добавьте второй Writer с Async: true и снимайте Stats в том же тикере: добейтесь, чтобы Errors в WriterStats оказались на графике рядом с лагом. Отдельно создайте Reader с GroupID и зафиксируйте ответ ReadLag — ошибку errNotAvailableWithGroup, — записав в комментариях, где в таком случае живёт правда о лаге группы.

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

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

Почему ReadLag не работает с GroupID и это не починят?

Потому что позиция группового консьюмера не принадлежит Reader: её назначает координатор, коммит хранится у брокера, а после ребаланса партиция вообще переедет к другому члену группы. Любое число в памяти конкретного Reader было бы вымыслом, поэтому library возвращает errNotAvailableWithGroup, а Lag() — минус единицу. Лаг группы считается из закоммиченных офсетов и текущих концов партиций — то есть на стороне брокера: exporter или утилиты кластера, либо самописный замер через Client, который мы разберём в восемнадцатом уроке.

Lag() показывает ноль, но сообщения точно есть. Так как оно вообще работает?

Lag() — это кэш, а не запрос: он возвращает разность, которую Reader посчитал в последний раз, когда отдал вам сообщение или когда фоновая горутина обновила её по ReadLagInterval. Пока ReadMessage не вызывался или брокер недоступен, кэш ничего не знает. Для решения, а не для индикации, используйте ReadLag — он ходит к брокеру за свежими офсетами в момент вызова. И проверьте партицию: у ридера с фиксированной партицией кэш отвечает только за неё.

Какой порог лага ставить в алерте?

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

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

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

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

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

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

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

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