Содержание урока · 10 разделов
- Батчи kafka batch producer: как Writer копит сообщения
- BatchSize и BatchTimeout: два рычага одной цены
- WriteMessages пачкой: один вызов, много партиций
- RequiredAcks и цена надёжности на пути батча
- Правила тюнинга батчей одним списком
- Рычаги производительности Writer одним столом
- Локальный замер батчей: как увидеть выигрыш
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Нагрузочный тест, который сводит с ума: сервис телеметрии шлёт тысячу метрик в секунду, 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 с явными порогами батчей
GOwriter := 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: true ради CPU, через месяц аналитика заметила дыры в графиках. Код был безупречен по логике — err игнорировать было даже нечего: при Async WriteMessages никогда не блокируется и не возвращает ошибок отправки, документация предупреждает об этом прямо. Ошибки при этом не исчезают: они учитываются в статистике writer.Stats(), поле Errors. Лечение: горутина, раз в секунду читающая Stats и поднимающая счётчик, — а лучше интерфейс с фоновым подтверждением через Completion для критичных потоков. Правило из истории: Async — не бесплатное ускорение, а обмен гарантией на задержку; брать его стоит только там, где потеря события легальна, — метрики, телеметрия, кэш-инвалидация. Для платежей и заказов асинхронный Writer — враньё самому себе: события будут уходить в пустоту ровно в день, когда брокер мигнёт.
Меню: Async и ловля ошибок через Stats
GOasyncWriter := 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.
Второй способ узнать судьбу сообщений — поле Completion в WriterConfig: функция вызывается после попытки доставки каждой пачки и получает срез сообщений с ошибкой или без. В синхронном режиме Completion вызывается до возврата WriteMessages, в асинхронном — из фоновых горутин Writer'а, и это единственное место, где асинхронная отправка отчитывается о результате. Сообщения из колбэка приходят с заполненными Topic, Partition и Offset — фактическими адресами записи у брокера, — причём все сообщения одного вызова уехали в одну партицию. Две оговорки из документации: Close блокируется до отработки колбэков, а паника внутри Completion гасит весь процесс — обёртка с recover обязательна.
Проверка: куда уходят ошибки при 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: falseRequiredAcks и цена надёжности на пути батча#
Третий рычаг — сколько подтверждений ждёт продюсер. Поле 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 одним столом#
| Настройка | Дефолт | Что меняет |
|---|---|---|
| BatchSize | 100 | Цель по сообщениям в батче: меньше запросов при большем размере |
| BatchTimeout | 1 секунда | Срок дожития неполного батча: потолок задержки на тихом трафике |
| BatchBytes | 1048576 | Потолок батча в байтах; крупнее — MessageTooLargeError |
| Async | false | Мгновенный возврат без ошибок; судьба сообщений — только в Stats |
| RequiredAcks | RequireAll у NewWriter | Число подтверждений: весь ISR против одного лидера |
Локальный замер батчей: как увидеть выигрыш#
Про формат честно: песочница сеть не поднимает, поэтому замеры делаются локально против брокера из Docker — третий урок ещё пригодится. Против брокера прогоните одинаковый поток метрик через Writer с дефолтами и с вашими порогами и сравните Stats: счётчик Writes покажет число produce-запросов, BatchSize-статистика — фактические размеры батчей, WriteTime — время записи. Разница между тысячей запросов и пятью видна невооружённым глазом и в цифрах, и в нагрузке CPU. Интерактивы ниже тренируют механику накопления на программах в чистом Go: тот же аккумулятор, те же пороги, тот же таймаут — без сети, зато мгновенно и с предсказуемым выводом, который совпадает с поведением брокера на тех же числах.
Соберите батч-продюсер
Допишите поля конфига, задающие размер и таймаут батча, и метод отправки пачки.
Соберите асинхронный Writer с мониторингом
Расставьте строки: конфиг с Async, мгновенная отправка, вывод nil-ошибки, горутина со Stats, лог счётчиков.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: батчи и производительность
Продюсер теперь масштабируется вместе с потоком: батчи собираются сами, пороги стоят по измерениям, а асинхронность — осознанный выбор с мониторингом ошибок, а не копипаста. Из урока в код уезжают два решения: пороги под измеренный профиль трафика и честный ответ на вопрос, можно ли этому потоку терять события, — первое числом, второе словом 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 и заполненными адресами партиций.
Личные заметкиПусто
Вопросы и ответы по уроку
Какой BatchSize выбрать: больше всегда лучше?
Нет: размер батча выигрывает только при трафике, который наполняет его до таймаута. Ориентир — фактическая статистика из Stats: средний размер батча должен быть близок к цели на пике, а на провалах дожиматься таймаутом. Начните с дефолта 100, удваивайте, пока число Writes падает заметно, и остановитесь, когда выигрыш исчезнет, а задержка на тихом трафике станет заметной. И не двигайте BatchSize без BatchTimeout: поднятая цель на редком потоке обернётся задержкой в полный интервал.
Почему при Async WriteMessages возвращает nil на упавшем брокере?
Потому что при Async вызов лишь ставит сообщения в очередь аккумулятора: отправка и подтверждение происходят в фоне, и результат доставки вызывающему не передаётся. Документация предупреждает об этом прямо: ошибки игнорируются, потому что вызывающий не получает возвращаемое значение. Видимая цена — только счётчики Stats, поэтому асинхронный Writer без мониторинга Stats — чёрный ящик.
Что делать с MessageTooLargeError?
Ошибка означает, что одно сообщение превышает BatchBytes или лимит брокера, и WriteMessages возвращается, не отправив ничего. Проверьте размер сообщения до отправки: либо поднимите лимиты, либо дробите событие, либо переводите крупные payload-ы в ссылки на объектное хранилище. Повторная отправка той же пачки упрётся в ту же ошибку — виновника нужно исключить из пачки. Исключение делает за вас поле Remaining из самой ошибки: отправьте его повторно без пересборки.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Ebitengine: 2D-игры на Go
Курс Ebitengine для Go: игровой цикл Update/Draw, спрайты и анимации, ввод, коллизии, тайлмапы, камера, звук, сцены и сборка на платформы. 25 уроков.
- Понимать игровой цикл Update/Draw и Layout
- Рисовать спрайты, атласы и примитивы
- Обрабатывать ввод: клавиатура, мышь, тач
- Строить движение, коллизии и анимации