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

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

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

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

Продюсер: батчи и производительность

Kafka batch producer копит сообщения по партициям и уезжает одним запросом — главный рычаг производительности kafka go, без которого продюсер с поодиночными отправками не выдержит даже средний поток. Разберём BatchSize, BatchTimeout, Async и RequiredAcks в Writer, посчитаем цену каждой настройки и увидим, где асинхронность молча теряет события.

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

Нагрузочный тест, который сводит с ума: сервис телеметрии шлёт тысячу метрик в секунду, Writer создан по четвёртому уроку, ошибок нет, а CPU процесса красный и брокер потеет. Разбор показывает тысячу produce-запросов в секунду — по одному на сообщение: каждый WriteMessages с одиночным сообщением едет до брокера и обратно, платя сетевым round-trip и конвейером брокера за каждое событие отдельно. Умножьте на десять сервисов — и половина мощности Kafka уходит на обработку оверхеда. Лечение известно с времён первых брокеров: накапливать сообщения и отправлять пачкой, один запрос на сотню событий. В kafka-go накопление встроено в Writer и управляется тремя полями конфига — BatchSize, BatchTimeout и BatchBytes, — а пятый урок уже показал, что пачка разлетается по партициям через балансировщик. Здесь разберём рычаги по одному: что каждый из них меняет, где пределы и какую цену платит Async — самый быстрый и самый опасный режим продюсера.

Батчи kafka batch producer: как Writer копит сообщения#

Writer в kafka-go не шлёт сообщения поодиночке, даже когда WriteMessages вызывается с одним событием. Внутри каждый Writer держит на каждую топик-партицию аккумулятор: пришедшие сообщения докладываются в текущий батч, полный батч уходит в очередь на отправку, а неполный ждёт — либо новых сообщений, либо таймаута. Пороги задаются в WriterConfig: BatchSize — цель по числу сообщений в батче, по умолчанию 100; BatchBytes — потолок в байтах, дефолт 1048576, то есть мегабайт; BatchTimeout — срок дожития неполного батча, по умолчанию секунда. Аккумуляторы живут между вызовами WriteMessages: пять вызовов по двадцать сообщений в одну партицию соберутся в стобалльный батч. Пороги — не единственные ручки: Writer сам ретраит доставку до десяти попыток по умолчанию, с бэкоффом от ста миллисекунд до секунды, поэтому короткое моргание сети батч переживает без вашего участия. Для kafka batch producer это означает, что оптимум зависит от трафика: на живом потоке батчи наполняются сами, а редкие события ждут таймаута, чтобы не зависнуть в памяти навсегда. Формула простая: пропускная способность растёт с размером батча, задержка растёт с ожиданием его наполнения, и между ними всегда стоит ваш профиль трафика.

Writer с явными порогами батчей

GO
writer := kafka.NewWriter(kafka.WriterConfig{
        Brokers:      []string{"localhost:9092"},
        Topic:        "metrics",
        Balancer:     &kafka.RoundRobin{},
        BatchSize:    500,             // цель: 500 сообщений в батче
        BatchTimeout: 50 * time.Millisecond, // но не дольше 50 мс
        BatchBytes:   1 << 20,         // потолок батча в байтах
})
defer writer.Close()

msgs := make([]kafka.Message, 0, 500)
for i := 0; i < 500; i++ {
        msgs = append(msgs, kafka.Message{
                Key:   []byte(fmt.Sprintf("metric-%d", i%10)),
                Value: []byte(fmt.Sprintf("value=%d", i)),
        })
}

err := writer.WriteMessages(ctx, msgs...)
log.Printf("flushed: %v", err)

Три поля решают всё: BatchSize 500 копит полтысячи сообщений, BatchTimeout 50 миллисекунд не даёт хвосту висеть дольше этого срока, BatchBytes страхует от гигантских батчей. Пачка из 500 сообщений уходит одним вызовом WriteMessages и — при одной партиции — одним produce-запросом. Ключи здесь декоративные: для метрик порядок не нужен, поэтому RoundRobin из пятого урока, а при живом трафике аккумуляторы Writer'а сожмут и эту пачку с соседними.

BatchSize и BatchTimeout: два рычага одной цены#

Рычаги работают в паре, и настраивать их по одному — ошибка. BatchSize задаёт цель: батч уходит, как только набрал это число сообщений. BatchTimeout — предохранитель: неполный батч не живёт дольше срока, поэтому редкий трафик не копится вечно. Поднимите BatchSize до тысячи на потоке из десяти событий в секунду — и почти каждый батч будет уезжать по таймауту, добавляя секунду задержки каждому сообщению без выигрыша в запросах. Опустите до десяти на потоке из десяти тысяч — и Writer износит сеть тысячей запросов, хотя мог бы десятком. Здоровая конфигурация держит два сценария: пиковый трафик наполняет батчи по размеру, ночной провал дожимает их по таймауту. Практический приём — мерить фактический размер батчей, а не догадываться: Stats отдаёт BatchSize и BatchBytes как summary-статистики с минимумом, средним и максимумом, и перекос сразу видно. Дефолты — 100 сообщений и секунда — разумны для сервисных потоков; тюнинг начинается там, где либо задержка критична, либо счёт запросов бьёт по брокеру, и всегда по измерениям, а не по вкусу.

Проверка: редкий трафик и большой батч

BatchSize: 1000, BatchTimeout: time.Second. Трафик — одно сообщение в секунду. Как уйдут сообщения?

WriteMessages пачкой: один вызов, много партиций#

Второй рычаг производительности — сам вызов: WriteMessages принимает вариадик сообщений, и одна пачка едет эффективнее, чем серия одиночных вызовов. Внутри Writer раскладывает сообщения по партициям через балансировщик, собирает на каждую свой батч и отправляет параллельно. Ошибка возвращается с индексом виновника: MessageError хранит позицию сбойного сообщения в исходном срезе, так что частичная обработка возможна. Особый случай — сообщение, превышающее лимит: если событие больше BatchBytes, WriteMessages возвращает MessageTooLargeError, не отправив ничего, — ожидание поведения такое, что вы отбросите слишком крупное сообщение и повторите отправку остального. Повтор упростит само исключение: ошибка несёт поля Message и Remaining — виновника и пачку без него, — так что пересборка среза не нужна. Пачка заодно экономит на блокировках: один вызов с тысячей сообщений проходит путь планирования один раз, а не тысячу. Отсюда правило сервисного кода: собирайте события в срез и отправляйте одним вызовом за тик — таймер или размер среза, что наступит раньше, — а Writer'овые батчи наложатся сверху и сожмут трафик до брокера.

Меню: Async и ловля ошибок через Stats

GO
asyncWriter := kafka.NewWriter(kafka.WriterConfig{
        Brokers: []string{"localhost:9092"},
        Topic:   "metrics",
        Async:   true, // WriteMessages не ждёт и ошибок не возвращает
})

// мгновенный возврат: err здесь всегда nil при Async
err := asyncWriter.WriteMessages(ctx,
        kafka.Message{Value: []byte("tick=1")})
fmt.Println("err:", err)

// ошибки не пропали — они в статистике
go func() {
        for range time.Tick(time.Second) {
                s := asyncWriter.Stats()
                log.Printf("writes=%d errors=%d", s.Writes, s.Errors)
        }
}()

Async превращает WriteMessages в постановку в очередь: вызов возвращается мгновенно, ошибки доставки уходят в Stats, поле Errors, а не вызывающему. Горутина с таймером — минимальный мониторинг асинхронного Writer'а: без неё недоставленные записи невидимы. Для критичных потоков этот режим запрещён: нет ни возврата ошибки, ни способа узнать судьбу конкретного сообщения без Completion.

Проверка: куда уходят ошибки при Async

Async: true, брокер недоступен, WriteMessages вызван 50 раз. Что увидит код?

Сколько produce-запросов сделает Writer?

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

writer := kafka.NewWriter(kafka.WriterConfig{
        Brokers:      []string{"localhost:9092"},
        Topic:        "metrics",   // одна партиция
        BatchSize:    100,
        BatchTimeout: time.Second,
})

for i := 0; i < 250; i++ {
        writer.WriteMessages(ctx,
                kafka.Message{Value: []byte("x")})
}
// WriteMessages с Async: false

RequiredAcks и цена надёжности на пути батча#

Третий рычаг — сколько подтверждений ждёт продюсер. Поле RequiredAcks в WriterConfig задаёт уровень acks: значение минус один — ожидание всех синхронизированных реплик, единица — подтверждение лидера, ноль — fire-and-forget. Важная деталь kafka-go: NewWriter при нулевом значении подставляет RequireAll — историческое поведение конструктора, поэтому дефолтный Writer ждёт весь ISR и является самым надёжным, а не самым быстрым. Константы для поля — RequireNone, RequireOne и RequireAll, они же ноль, единица и минус единица: в конфиге пишите их по именам, чтобы нулевой RequiredAcks не читался как «ноль подтверждений». Снижение до RequireOne уменьшает задержку на пути подтверждения, но ослабляет гарантию: при падении лидера до репликации последние записи теряются — trade-off, который вы уже оценивали в девятом уроке, только теперь со стороны продюсера. Батч усиливает эффект любой настройки: одна секунда ожидания на батч из пятисот сообщений — это два миллисекунды на событие, против секунды на каждое при одиночной отправке. Для потоков, где критична пропускная способность, а не задержка, крупный батч с RequireAll часто быстрее мелкого с RequireOne — мерите и выбираете сами: Stats отдаёт WriteTime и Retries по факту.

Правила тюнинга батчей одним списком#

  • Меряйте фактические размеры батчей через Stats: summary с минимумом, средним и максимумом честнее догадок.
  • Держите пару размер-таймаут согласованной: цель по наполнению плюс предохранитель по времени.
  • На редком трафике таймаут становится фактическим таймером отправки — проверяйте задержку на провалах, а не только на пике.
  • Пачку для WriteMessages собирайте на стороне сервиса: срез за тик экономит путь планирования и блокировки.
  • Async — только там, где потеря легальна; для остального синхронный Writer с Completion или мониторингом Stats.
  • Сообщение крупнее BatchBytes возвращается MessageTooLargeError до отправки: поле Remaining даст пачку без виновника.

Рычаги производительности Writer одним столом#

НастройкаДефолтЧто меняет
BatchSize100Цель по сообщениям в батче: меньше запросов при большем размере
BatchTimeout1 секундаСрок дожития неполного батча: потолок задержки на тихом трафике
BatchBytes1048576Потолок батча в байтах; крупнее — MessageTooLargeError
AsyncfalseМгновенный возврат без ошибок; судьба сообщений — только в Stats
RequiredAcksRequireAll у NewWriterЧисло подтверждений: весь ISR против одного лидера

Локальный замер батчей: как увидеть выигрыш#

Про формат честно: песочница сеть не поднимает, поэтому замеры делаются локально против брокера из Docker — третий урок ещё пригодится. Против брокера прогоните одинаковый поток метрик через Writer с дефолтами и с вашими порогами и сравните Stats: счётчик Writes покажет число produce-запросов, BatchSize-статистика — фактические размеры батчей, WriteTime — время записи. Разница между тысячей запросов и пятью видна невооружённым глазом и в цифрах, и в нагрузке CPU. Интерактивы ниже тренируют механику накопления на программах в чистом Go: тот же аккумулятор, те же пороги, тот же таймаут — без сети, зато мгновенно и с предсказуемым выводом, который совпадает с поведением брокера на тех же числах.

Соберите батч-продюсер

Допишите поля конфига, задающие размер и таймаут батча, и метод отправки пачки.

writer := kafka.NewWriter(kafka.WriterConfig{ Brokers: []string{"localhost:9092"}, Topic: "metrics", : 500, : 50 * time.Millisecond, }) err := writer.(ctx, msgs...) log.Printf("flushed: %v", err)

Соберите асинхронный Writer с мониторингом

Расставьте строки: конфиг с Async, мгновенная отправка, вывод nil-ошибки, горутина со Stats, лог счётчиков.

Строки

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

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

Карточки: батчи и производительность

Термин

BatchSize

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

Цель по числу сообщений в батче, дефолт 100. Батч уезжает при наполнении; на редком трафике срабатывает таймаут.

1 / 6

Продюсер теперь масштабируется вместе с потоком: батчи собираются сами, пороги стоят по измерениям, а асинхронность — осознанный выбор с мониторингом ошибок, а не копипаста. Из урока в код уезжают два решения: пороги под измеренный профиль трафика и честный ответ на вопрос, можно ли этому потоку терять события, — первое числом, второе словом Async. Дальше по курсу: урок про ошибки продюсера разберёт, что делать с непосланным — ретраи, таймауты и цену RequiredAcks на падениях; а четырнадцатый урок про Reader API вернёт нас на читательскую сторону, к сырым оффсетам без группы. Для консьюмера той же природы — задержка против пропускной способности — держите в голове MinBytes и MaxBytes из шестого урока: зеркальные рычаги fetch-батчей. Замеряйте свой Writer локально против брокера из Docker — пять минут на прогон Stats заменят вечер споров о настройках.

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

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

Вывод

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

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

  • Writer копит батчи по партициям между вызовами: пороги — BatchSize, BatchBytes и BatchTimeout.
  • Дефолты: 100 сообщений, мегабайт и секунда; тюнинг начинается по измерениям Stats, а не по вкусу.
  • Пачка в WriteMessages эффективнее серии одиночных вызовов: путь планирования проходится один раз.
  • Неполный батч дожимает таймаут: редкий трафик не висит в памяти и не ждёт наполнения до вечности.
  • Async даёт мгновенный возврат без ошибок отправки; единственный источник правды — Stats().Errors.
  • NewWriter при RequiredAcks 0 подставляет RequireAll: дефолтный Writer надёжен, а не быстр.
  • Completion отчитывается о судьбе каждой пачки: в Async это единственный канал фактических адресов и ошибок доставки.
  • Замеры батчей — локально против брокера в Docker; песочница показывает аккумулятор на чистом Go.

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

Против брокера из третьего урока снимите три профиля одного потока: 1000 метрик Writer'ом с дефолтами, с BatchSize: 1000 и с Async: true. Для каждого запишите из Stats число Writes, средний размер батча и ошибки. Затем уроните брокер docker stop и повторите прогон на Async: nil из вызовов против ненулевого Errors в Stats — зафиксируйте это как обоснование запрета Async для платёжных потоков. Финал: поднимите BatchSize до 10000 на тихом потоке из 5 событий в секунду и объясните по графику WriteTime, почему это стало хуже. И последнее: добавьте Completion-колбэк в асинхронный профиль и убедитесь, что он получает упавшие пачки с err != nil и заполненными адресами партиций.

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

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

Какой BatchSize выбрать: больше всегда лучше?

Нет: размер батча выигрывает только при трафике, который наполняет его до таймаута. Ориентир — фактическая статистика из Stats: средний размер батча должен быть близок к цели на пике, а на провалах дожиматься таймаутом. Начните с дефолта 100, удваивайте, пока число Writes падает заметно, и остановитесь, когда выигрыш исчезнет, а задержка на тихом трафике станет заметной. И не двигайте BatchSize без BatchTimeout: поднятая цель на редком потоке обернётся задержкой в полный интервал.

Почему при Async WriteMessages возвращает nil на упавшем брокере?

Потому что при Async вызов лишь ставит сообщения в очередь аккумулятора: отправка и подтверждение происходят в фоне, и результат доставки вызывающему не передаётся. Документация предупреждает об этом прямо: ошибки игнорируются, потому что вызывающий не получает возвращаемое значение. Видимая цена — только счётчики Stats, поэтому асинхронный Writer без мониторинга Stats — чёрный ящик.

Что делать с MessageTooLargeError?

Ошибка означает, что одно сообщение превышает BatchBytes или лимит брокера, и WriteMessages возвращается, не отправив ничего. Проверьте размер сообщения до отправки: либо поднимите лимиты, либо дробите событие, либо переводите крупные payload-ы в ссылки на объектное хранилище. Повторная отправка той же пачки упрётся в ту же ошибку — виновника нужно исключить из пачки. Исключение делает за вас поле Remaining из самой ошибки: отправьте его повторно без пересборки.

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

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

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

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

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

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

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