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

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

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

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

Первый продюсер: Writer

Первый kafka producer на Go собирается из двух частей: kafka.NewWriter с конфигом брокера и вызова WriteMessages, который отдаёт сообщение в топик и ждёт подтверждения. В этом уроке пишем продюсера целиком: WriterConfig построчно, механика записи между Go-кодом и диском брокера, обработка ошибок и таймаутов, роль Close и типовые грабли первого дня.

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

Брокер из прошлого урока крутится на localhost:9092, топик orders с тремя партициями создан — самое время отправить в него первое событие из Go. Продюсер в kafka-go называется Writer, и это не случайное имя: клиент ведёт себя как надёжный писатель — копит сообщения, дописывает их в лог брокера и возвращает управление только после подтверждения записи. На первый взгляд всё просто: конструктор, вызов, готово. Но именно здесь живут самые дорогие ошибки новичка: кто-то создаёт Writer на каждое сообщение и тонет в соединениях, кто-то игнорирует возвращённую ошибку и теряет события молча, кто-то не ставит таймаут и зависает на деплое против упавшего брокера. Разберём каждую часть честно: от WriterConfig до судьбы ошибки при недоступном брокере, чтобы продюсер выглядел одинаково уверенно и в учебном примере, и в боевом сервисе. К концу у вас будет каркас, который можно копировать в сервис и не краснеть на code review.

kafka.NewWriter: WriterConfig и подключение к брокеру#

Writer создаётся функцией kafka.NewWriter, которой передаётся kafka.WriterConfig — и вот первый сюрприз: NewWriter ничего не подключает. Конструктор лишь запоминает настройки; соединения с брокерами появятся при первой записи, а Writer будет поддерживать их пул дальше. Сердце конфига — Brokers, список адресов брокеров, и Topic — топик по умолчанию, куда уйдут все сообщения этого Writer. Поле Balancer решает, как сообщения распределяются по партициям топика; если его не задать, работает RoundRobin — по кругу. Батчевые настройки BatchSize и BatchTimeout влияют на производительность и подробно разобраны в десятом уроке, здесь оставим их по умолчанию. Важно правило проектирования: Writer — долгоживущий объект уровня приложения. Он потокобезопасен, держит внутреннюю очередь и пул соединений, и создавать его на каждое сообщение — самый известный антипаттерн kafka producer на golang: каждый конструктор — это новые соединения и новое ожидание, а выигрыша ноль. Собрали один Writer в main, передали в обработчики — и живёте с ним весь процесс. Список адресов в Brokers нужен не для красоты: Writer узнаёт у первого живого брокера метаданные кластера — где какой лидер партиций — и дальше общается уже напрямую с ними. Если захочется альтернативного синтаксиса — есть литерал kafka.Writer с полем Addr: библиотека поддерживает оба пути, курс держится классического NewWriter c WriterConfig.

Продюсер целиком: от NewWriter до Close

GO
package main

import (
        "context"
        "fmt"
        "log"
        "time"

        "github.com/segmentio/kafka-go"
)

func main() {
        writer := kafka.NewWriter(kafka.WriterConfig{
                Brokers: []string{"localhost:9092"},
                Topic:   "orders",
        })
        defer writer.Close()

        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()

        err := writer.WriteMessages(ctx, kafka.Message{
                Key:   []byte("order-42"),
                Value: []byte("created"),
        })
        if err != nil {
                log.Fatal("send:", err)
        }
        fmt.Println("сообщение записано")
}

Каркас боевого продюсера: NewWriter с адресом брокера и топиком, defer Close на выходе, контекст с таймаутом десять секунд — страховка против зависания на недоступном брокере. WriteMessages принимает kafka.Message: Value — полезная нагрузка в байтах, Key — байтовый ключ маршрутизации, который выбирает партицию и которому посвящён следующий урок. Ошибка после WriteMessages — единственный сигнал о судьбе сообщения, поэтому проверяется сразу, а не по желанию.

Что происходит между WriteMessages и диском брокера#

Внутри WriteMessages скрыта конвейерная работа, и о ней полезно знать до первого инцидента. Вызов складывает сообщение во внутреннюю очередь Writer, откуда отдельные горутины собирают их в батчи по адресату — топик и партиция определяются балансировщиком. Батч уезжает по сети на лидера партиции, тот дописывает данные в свой лог и отвечает подтверждением; уровень ожидания задаётся RequiredAcks: конструктор NewWriter при нулевом значении подставляет kafka.RequireAll — запись считается состоявшейся, когда её подтвердили все реплики из ISR; на локальном брокере с одной репликой это просто подтверждение лидера, а сниженный порог kafka.RequireOne и его цена — тема урока про durability. Только после ответа брокера WriteMessages возвращает управление — поэтому «отправил» в этом коде означает «брокер подтвердил запись», а не «байты ушли в сокет». Для вызывающего кода это удобный контракт: при успехе сообщение уже в логе, и о нём можно забыть до чтения. Обратная сторона — запись синхронная по умолчанию: редкие события это устраивает, а для высоких скоростей есть асинхронный режим и батчи — оба разберём в десятом уроке. Кстати, именно поэтому продюсер здесь называется Writer: библиотека подчёркивает метафору дописывания в журнал — вы не шлёте письмо в никуда, а вносите запись в лог, который переживёт и ваш процесс.

WriteMessages: как kafka producer отправляет сообщения#

Сигнатура WriteMessages принимает контекст и переменный список сообщений: WriteMessages(ctx, msg) для одиночной отправки и WriteMessages(ctx, msgs...) для пачки, собранной в срез. Сообщение — это kafka.Message с полями Key и Value типа []byte: Kafka не знает про JSON, protobuf и ваши структуры, она хранит байты, и сериализация — тема одиннадцатого урока. Key внешне похож на опцию, но на самом деле это маршрутизатор: одинаковый ключ всегда попадает в одну партицию, что и создаёт порядок событий по сущности — заказу, аккаунту, пользователю. Если WriterConfig.Topic пуст, топик можно указать в каждом сообщении через поле Message.Topic — так один Writer обслуживает несколько каналов. Пачка за один вызов экономит не только строки кода, но и сетевые поездки: одно сообщение — один produce-запрос, тысяча — один-два в зависимости от размера. Наконец, WriteMessages безопасно звать из нескольких горутин одновременно: очередь Writer сериализует работу, и мьютексы в вызывающем коде не нужны. Отдельно про размер: Value не обязан быть маленьким, но у брокера есть потолок на сообщение — по умолчанию около мегабайта, поэтому большие файлы в топик не таскают: их место в объектном хранилище, а в сообщении — ссылка.

Меню: пачка сообщений одним вызовом

GO
messages := make([]kafka.Message, 0, len(orders))
for _, o := range orders {
        messages = append(messages, kafka.Message{
                Key:   []byte(o.ID),
                Value: payload(o),
        })
}

err := writer.WriteMessages(ctx, messages...)

var writeErr kafka.WriteError
if errors.As(err, &writeErr) {
        log.Printf("не улетело %s: %v",
                writeErr.Message.Key, writeErr.Err)
}

Пачка собирается как обычный срез kafka.Message, после чего WriteMessages получает его через многоточие — один вызов вместо сотни. Если при отправке пачки что-то не записалось, ошибка приходит обёрнутой в kafka.WriteError, в поле Message которой лежит проблемное сообщение: удобно логировать именно его ключ, а не гадать по индексу. Про пачки, пороги и BatchTimeout — подробно в десятом уроке.

Ошибки и таймауты: что возвращает WriteMessages#

Честная обработка ошибок начинается с понимания, откуда они берутся. Первая группа — недоступный брокер: Writer не сдаётся после первой попытки, а ретраит запись с нарастающим интервалом, пока жив контекст; если брокер так и не поднялся, вызов завершается ошибкой контекста — context.DeadlineExceeded при таймауте или context.Canceled при отмене. Отсюда практическое правило: контекст на запись — не формальность, а ваш дедлайн; без него продюсер может крутиться в ретраях неограниченно долго. Вторая группа — ошибки брокера на корректной связи: запись в несуществующий топик без AllowAutoTopicCreation прилетает ошибкой Unknown Topic Or Partition, отказ в доступе к каналу — своим кодом. Все они приходят обёрнутыми, и вытащить код умеет errors.As с целью kafka.Error: значение этого типа само и есть код, его сравнивают с константами вроде kafka.UnknownTopicOrPartition. Третий источник — само сообщение: превышение лимита размера (BatchBytes в WriterConfig и потолок брокера) возвращает MessageTooLargeError с индексом проблемного сообщения — его выбрасывают из пачки и отправляют отдельно, а не глушат всю пачку целиком. Каждой группе — свой ретрай и свой алерт: это тема двенадцатого урока, здесь важно видеть карту. Хорошая новость: все три группы ловятся в одном месте — ошибкой, возвращённой из WriteMessages, и различаются уже по кодам, поэтому единый обработчик с логированием и метрикой покрывает весь спектр.

Close и жизнь Writer в сервисе#

writer.Close() в продюсере — не церемония, а часть контракта. Close дожидается отправки накопленных батчей, завершает внутренние горутины и закрывает пул соединений: без него последние сообщения могут остаться в памяти процесса при завершении, а «молчаливая потеря» — худший из видов отказа для шины событий. Канонический порядок такой: Writer создаётся один на процесс, defer Close ставится рядом с конструктором, а при остановке сервиса закрывается явно после того, как обработчики закончили работу — приёмы выключения без потерь собраны в уроке про грабли продакшена. Обратная сторона долгожизни — конфигурация: Writer настраивается один раз и не пересобирается на ходу, поэтому всё, что может меняться, — топики в сообщениях, ключи и значения; сам Writer и его конфиг остаются неизменными. Для тестов Writer инжектится интерфейсом, как любая зависимость, — приём из урока про тесты Kafka. Правило на память: один Writer, длинная жизнь, аккуратный Close. И держите конфиг рядом с кодом запуска: полдня дебага уходит на то, чтобы найти, где именно продюсер настроен иначе, чем вы думали.

Анатомия записи одним столом#

ЭлементРоль в записи
WriterConfig.BrokersАдреса брокеров для начального подключения и метаданных
WriterConfig.TopicТопик по умолчанию; пустое значение — топик в каждом сообщении
WriterConfig.BalancerПравило выбора партиции; по умолчанию RoundRobin
Message.KeyКлюч маршрутизации: одинаковый ключ — одна партиция
Message.ValueПолезная нагрузка в байтах
WriteMessages(ctx, ...)Запись с ожиданием подтверждения брокера до смерти контекста
writer.Close()Флеш накопленных батчей и закрытие соединений

Чек-лист продюсера одним списком#

  • Один Writer на процесс: конструктор — в main, соединения — по мере необходимости.
  • Контекст с таймаутом на каждую WriteMessages: дедлайн — страховка от вечных ретраев.
  • Ошибка после записи проверяется всегда: молчаливое подчёркивание — согласие на потерю.
  • Ключ сообщения выбирается в домене: он определяет партицию и порядок — тема следующего урока.
  • Close — один раз при завершении: флеш батчей и закрытие соединений.
  • AllowAutoTopicCreation — только в dev: в проде опечатка должна быть ошибкой, а не новым топиком.

Проверка: когда NewWriter подключается

После kafka.NewWriter(...) соединений с брокером ещё нет. Когда Writer впервые позвонит брокеру?

Проверка: запись в несуществующий топик

Топика orders нет, AllowAutoTopicCreation не задан. Что вернёт WriteMessages?

Что напечатает программа?

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

writer := kafka.NewWriter(kafka.WriterConfig{
        Brokers: []string{"localhost:9092"},
        Topic:   "orders",
})
defer writer.Close()

// брокер остановлен: docker compose stop kafka
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

err := writer.WriteMessages(ctx, kafka.Message{
        Key:   []byte("order-1"),
        Value: []byte("created"),
})
fmt.Println(err)

Соберите первый продюсер

Допишите топик, конструктор сообщения и вызов записи.

writer := kafka.NewWriter(kafka.WriterConfig{ Brokers: []string{"localhost:9092"}, Topic: "", }) defer writer.() msg := kafka.{ Key: []byte("order-42"), Value: []byte("created"), } err := writer.(ctx, msg)

Соберите продюсера по строкам

Расставьте строки в порядке работы программы: Writer, его закрытие, контекст, сообщение, отправка, обработка ошибки, вывод.

Строки

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

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

Карточки: первый продюсер

Термин

kafka.NewWriter

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

Конструктор Writer по WriterConfig. Ленив: не подключается, соединения появляются при первой записи. Создаётся один раз на процесс.

1 / 6

Продюсер готов: один Writer на процесс, запись через WriteMessages с контекстом, ошибки обрабатываются, Close на месте. Дальше по курсу: в уроке про ключи и партиционирование разберём, как Hash-балансировщик собирает порядок по сущности и что ломается при увеличении партиций; в уроке про батчи выжмем из Writer скорость; про консьюмера — всё с шестого урока. Код урока запускается против вашего локального брокера из третьего урока: скопируйте main.go, запустите и загляните в топик консольным консьюмером — первое сообщение в логе поставит жирную точку над теорией. Песочница сеть не эмулирует, зато интерактивы ниже тренируют сам каркас записи. После урока вернитесь к домашнему заданию: отправить пачку, сломать брокера и вернуть его — лучший способ выучить поведение Writer до первого продакшен-инцидента.

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

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

Вывод

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

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

  • Writer — долгоживущий объект приложения: NewWriter ленив, соединения появляются при первой записи.
  • WriteMessages блокируется до подтверждения брокера; контекст задаёт дедлайн всей операции.
  • Message — это Key и Value в байтах: Key маршрутизирует по партициям, Value — полезная нагрузка.
  • Недоступный брокер — ретраи до смерти контекста и context.DeadlineExceeded; несуществующий топик — Unknown Topic Or Partition.
  • Пачка сообщений одним вызовом экономит сетевые поездки; ошибка приходит в kafka.WriteError с проблемным сообщением.
  • Close флешит накопленные батчи: без него хвост событий тихо умирает вместе с процессом.
  • Контекст — не формальность: он ограничивает ретраи Writer и возвращает DeadlineExceeded, когда брокер так и не ответил.
  • AllowAutoTopicCreation — инструмент dev-окружения: в проде опечатка вместо ошибки создаёт пустой топик.

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

Расширь продюсера из урока: отправьте в orders десять сообщений с ключами order-1..order-10 одной пачкой через WriteMessages(ctx, msgs...). Затем остановите брокер командой docker compose stop kafka и запустите отправку с таймаутом три секунды — убедитесь, что получаете context deadline exceeded, а не панику. Верните брокера через docker compose start kafka, перечитайте топик консольным консьюмером и ответьте себе: какие сообщения доехали, а какие надо отправить повторно.

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

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

Нужно ли закрывать Writer после каждой отправки?

Нет — наоборот: Writer создаётся один на процесс и живёт вместе с сервисом. Close вызывается один раз при завершении работы, чтобы доехали накопленные батчи. Конструктор на каждое сообщение — антипаттерн: он плодит соединения и не даёт Writer'у собрать пачку. Если сервисов много, выносите создание Writer в конструктор приложения и передавайте его зависимостью.

WriteMessages блокирующий — не повредит ли это моему HTTP-обработчику?

По умолчанию да, вызов ждёт подтверждения брокера, поэтому в обработчиках его вызывают с контекстом с таймаутом, а при высоких скоростях уходят в асинхронный режим Writer. Для типовых нагрузок синхронная запись со сдержанным таймаутом — самый безопасный старт; ускорение — отдельная тема десятого урока.

Как отправлять сообщения сразу в несколько топиков?

Варианта два: поле Topic внутри каждого kafka.Message при пустом WriterConfig.Topic, либо отдельный Writer на топик. Первый экономит объекты и соединения, второй изолирует настройки вроде балансировщика и батчей. В обоих случаях топик обязан существовать — либо с включённым AllowAutoTopicCreation в dev. Для пачки в несколько топиков первый вариант и есть штатный путь.

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

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

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

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

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

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

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