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

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

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

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

Грабли продакшена Kafka

Kafka в продакшене ломается не экзотикой, а шестью повторяющимися граблями: перегруженная партиция, коммиты не в ту сторону, ребаланс посреди обработки, retention без границ, Writer без батчей и отсутствие мониторинга лага. Каждый симптом узнаётся по первым строкам отчёта об инциденте, поэтому курс заканчивается чек-листом: symptom, причина, лечение. Две самые дорогие грабли — горячая партиция и арифметика коммитов — разберём с кодом, остальные сведём в таблицу и сверим с уроками, где их механика разобрана целиком. Грабли взяты из типовых инцидентов Go-команд, и каждая проверяется руками: половина — на локальном Docker-стенде, остальное — на кластере из трёх нод.

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

Инцидент на Kafka почти никогда не выглядит как сбой брокера: брокер честно делает то, что ему сконфигурировали, а урон приносит разрыв между ожиданием и настройкой. Продюсер скашивает ключи — и партиция выгорает; консьюмер коммитит в неправильный момент — и появляются дубли или пропажи; хендлер держит пачку дольше, чем живёт членство в группе, — и стартует ребаланс, который всё перечитывает заново. Каждая из шести граблей этого урока — symptom, который видно снаружи; причина, которая живёт в конфиге или коде; лечение, которое не требует миграции на другой брокер. Порядок разбора — от самой частой к самым дорогим: горячая партиция и коммиты получают полный разбор с кодом, остальные — краткие, со ссылками на уроки курса.

Горячая партиция: ключ клал всё в один отсек#

Symptom: одна партиция топика горит под стопроцентной загрузкой, соседние стоят на нуле, а пропускная способность топика равна мощности одной партиции вместо двенадцати. Причина — в балансере: Hash считает хеш ключа и всегда отправляет одинаковый ключ в одну партицию; крупный тенант, системный сервис-источник или любой другой перекос в данных наполняет именно его отсек, а RoundRobin, который размазывал бы равномерно, в этой конфигурации не участвует. Диагностика быстрее всего по разрезу лага и загрузки на партиции: перекос виден в первые минуты, тогда как общий график топика остаётся благополучным, пока горит хотя бы один отсек. Лечения три, по ситуации. Сменить ключ на более гранулярный, если порядок по старому ключу не нужен, — самое дешёвое. Просолить ключ детерминированной добавкой и получить партиций на ключ несколько — работоспособно, но ломает порядок внутри ключа, поэтому годится только там, где порядок не требовался. Добавить партиций — так ли это лечение: CreatePartitions умеет только увеличивать число, и после увеличения маппинг ключа в партицию пересчитается, то есть порядок по ключу сломается для всех существующих отправителей; партиции расширяют заранее, под рост, а не после инцидента.

Проверка: диагностика перекоса

Writer пишет события с ключом tenantID через kafka.Hash{}. У одного тенанта 80% всего трафика. Что покажет мониторинг?

Salting: разводим горячего тенанта по отсекам

GO
// было: все события тенанта 42 летят в партицию 3
// лечение: солим ключ — хеш размазывает по 12 корзинам
salt := seq % 12
msg := kafka.Message{
        Key:   []byte(fmt.Sprintf("42-%d", salt)),
        Value: payload,
}
err := writer.WriteMessages(ctx, msg)

Соль превращает один горячий ключ в двенадцать равномерных: Hash разложит корзины по всем партициям, и пропускная способность вернётся. Цена — порядок по исходному ключу: события тенанта теперь читаются вперемешку, поэтому солят только потоки без требования последовательности. Смена ключа и CreatePartitions дают тот же эффект слома порядка — выбор между скоростью и последовательностью делается один раз и осознанно.

Забытые коммиты: цена места, где стоит строка#

Symptom: после падения сервиса часть событий обработана дважды — или, хуже, исчезла. Причина одна: строка CommitMessages стоит не на своём месте, а гарантия доставки целиком задаётся этим местом, о чём девятый урок говорил в терминах at-least-once и at-most-once. Автокоммит с CommitInterval больше нуля коммитит фоново по расписанию: упал процесс — недокоммиченный хвост перечитается, это at-least-once с возможными дублями. Синхронная схема с CommitInterval равным нулю честнее: коммит — это явный вызов, и его позиция определяет гарантию. Коммит после успешной обработки — at-least-once: падение между обработкой и коммитом означает перечитывание и дубликат, который гасится идемпотентностью по бизнес-ключу. Коммит до обработки — at-most-once: падение после коммита теряет событие навсегда, потому что группа уже двинулась дальше. Дешёвого варианта нет — есть выбор между дубликатами и потерями, и осознанный выбор делают до инцидента, а не во время. Проверьте свой код прямо сейчас: одна строка CommitMessages, её позиция относительно process и знание, что произойдёт при падении между ними, — это вся дисциплина коммитов, остальное — следствия.

Что станет с сообщением после рестарта группы?

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

// CommitInterval: 0 — коммиты синхронные
m, err := reader.FetchMessage(ctx)
if err != nil {
        break
}
process(ctx, m)
// процесс убит OOM-killer-ом на этой строке
_ = reader.CommitMessages(ctx, m)

Ребаланс-шторм: долгий хендлер против членства в группе#

Symptom: консьюмеры циклятся — группа собирается, начинает обрабатывать, расцепляется и собирается снова, дубли множатся на каждом витке. Причина — арифметика членства: координатор считает консьюмера живым, пока тот шлёт heartbeats, и инициирует ребаланс, когда heartbeat замолкает на SessionTimeout. В kafka-go heartbeat идёт из фонового механизма группы с настраиваемым HeartbeatInterval — три секунды по умолчанию, — поэтому долгий хендлер сам по себе не выкидывает читателя из группы, как это случалось в клиентах, где пул совмещён с опросом. Но эффект рядом: пока хендлер держит пачку, коммиты стоят на месте, лаг растёт, а любая причина ребаланса — рестарт соседа, изменения в топике — отдаёт недоделанную работу другому консьюмеру, и тот перечитывает её с последнего закоммиченного офсета. Лечение структурное: хендлеры короче — батчи меньше и чаще; SessionTimeout и HeartbeatInterval задаются явно с запасом под реальный профиль нагрузки, а не остаются дефолтными; механика ребаланса — в тринадцатом уроке. Практический замер перед настройкой: возьмите время самой длинной пачки из логов, умножьте на запас и только потом выбирайте SessionTimeout — конфиг, снятый с чужого примера, здесь обходится дороже дефолта.

Проверка: выкинет ли координатор?

Хендлер держит пачку четыре минуты. SessionTimeout: 30s, HeartbeatInterval: 3s. Выкинет ли координатор консьюмера из группы?

Консьюмер с явным членством и коммитом после работы

GO
reader := kafka.NewReader(kafka.ReaderConfig{
        Brokers:           []string{"broker-1:9092"},
        GroupID:           "payments",
        Topic:             "payments",
        HeartbeatInterval: 3 * time.Second,
        SessionTimeout:    30 * time.Second,
        CommitInterval:    0, // коммиты синхронные
})
for {
        m, err := reader.FetchMessage(ctx)
        if err != nil {
                break
        }
        if err := process(ctx, m); err != nil {
                log.Printf("msg %s: %v", m.Key, err)
        }
        if err := reader.CommitMessages(ctx, m); err != nil {
                log.Fatal(err)
        }
}

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

Retention без границ и Writer по одному сообщению#

Ещё две грабли дешевле по ущербу и обнаруживаются диском и латентностью. Symptom первый: диск брокера кончился, запись остановилась. Причина: топик создавался без ConfigEntries, retention остался дефолтным или бесконечным, лог рос без границ — семнадцатый урок разбирает retention.bytes и retention.ms и компакцию как управление этим ростом. Лечение: лимиты на каждый топик при создании, а не после тревоги диска; тут же вспоминаем, что расширять число партиций задним числом опаснее, чем задать лимит заранее. Symptom второй: продюсер выдаёт миллисекунды латентности и перегружает брокера тысячами мелких запросов. Причина: Writer шлёт по одному сообщению на запрос — BatchSize и BatchTimeout остались дефолтными или перекрыты, и десятый урок показывал цену этой разницы: целевой батч — сто сообщений, флаш не реже секунды, плюс BatchBytes как потолок запроса. Лечение: пачка на стороне клиента собирается сама, если дать Writer время и объём, а не рвать WriteMessages на одиночные вызовы там, где можно передать срез. Проверка на стенде: Writer с одним сообщением и Writer со срезом на сотню на одном брокере дают разницу в десятки раз по числу запросов — её достаточно, чтобы почувствовать масштаб.

Нет мониторинга лага: узнали от пользователей#

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

Проверка: первые минуты инцидента

Пользователи жалуются на задержки обработки, мониторинга лага нет. Что поднять первым?

Шесть граблей: symptom, причина, лечение#

SymptomПричинаЛечение
Одна партиция пылает, соседние на нулеВсе сообщения с одним ключом через HashСменить или просолить ключ; партиции — заранее
Дубли после рестарта группыКоммит после обработки: упало до CommitMessagesAt-least-once осознанно + дедуп по бизнес-ключу
События исчезают без следаКоммит до обработки: падение после коммитаКоммит только после успешной обработки
Группа циклится ребалансамиДолгие пачки, таймауты членства без запасаХендлеры короче, явные SessionTimeout и HeartbeatInterval
Диск брокера кончилсяRetention не задан: лог растёт без границretention.bytes и retention.ms на каждый топик
Задержки узнали от пользователейЛаг не мониторитсяReader.Lag и алерт по росту отставания

Чек-лист, который вешают рядом с графиками#

  • Ключ продюсера гранулярный и без перекоса; Hash только там, где нужен порядок по ключу.
  • Число партиций взято с запасом под рост: CreatePartitions увеличивает, но пересчитывает маппинг ключей.
  • CommitMessages стоит после успешной обработки; дедуп по бизнес-ключу закрывает дубликаты at-least-once.
  • SessionTimeout и HeartbeatInterval заданы явно; хендлеры короче, чем терпение группы.
  • Каждый топик создан с retention.bytes или retention.ms, компакция — для ключевых логов.
  • Writer шлёт пачками: BatchSize, BatchTimeout и BatchBytes не перекрыты без причины.
  • Лаг в мониторинге с алертом: инцидент начинается с графика, а не с письма поддержки.

Задайте границу лога при создании топика

Допишите два имени конфигурационных записей: лимит байт на партицию и лимит срока хранения в миллисекундах.

client.CreateTopics(ctx, &kafka.CreateTopicsRequest{ Topics: []kafka.TopicConfig{{ Topic: "events", NumPartitions: 12, ReplicationFactor: 3, ConfigEntries: []kafka.ConfigEntry{ {ConfigName: "", ConfigValue: "1073741824"}, {ConfigName: "", ConfigValue: "604800000"}, }, }}, })

Соберите Writer, который не душит брокера

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

Строки

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

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

Карточки: шесть граблей в памяти

Термин

Горячая партиция

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

Все сообщения с одним ключом через Hash ложатся в один отсек: партиция пылает, соседние простаивают. Лечение — сменить или просолить ключ.

1 / 6

Чек-лист закрыт, и за каждой строкой стоит урок курса, где механика разобрана до конца: гарантии доставки и цена места коммита — в уроке про гарантии, устройство ребаланса и таймауты группы — в уроке про ребалансировку, лимиты лога и компакция — в уроке про retention, метрики лага с алертами — в уроке про мониторинг. Локально стоит воспроизвести хотя бы топ-две грабли на Docker-стенде: перекос по ключу и убийство процесса между обработкой и коммитом видны без кластера. На прогон уйдёт вечер, а взамен вы получите личный, проверенный руками ответ на вопрос «что делать», вместо сверки с чужими чек-листами в разгар инцидента. Дальше — финальный проект курса, где эти шесть граблей предлагается обойти ещё на этапе проектирования, а не разбирать по отчётам об инцидентах.

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

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

Вывод

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

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

  • Горячая партиция: ключ через Hash кладёт всё в один отсек; лечение — смена или salting ключа, партиции добавляются заранее.
  • CreatePartitions только увеличивает число партиций и пересчитывает маппинг ключей: порядок по ключу ломается.
  • Коммит после обработки — at-least-once с дубликатами под дедуп; коммит до обработки — at-most-once с молчаливыми потерями.
  • Rebalance-шторм: heartbeat идёт фоново, но долгие хендлеры замораживают коммиты; лечение — короткие пачки и явные SessionTimeout с HeartbeatInterval.
  • Retention без границ кончается диском: retention.bytes и retention.ms задаются на каждый топик при создании.
  • Writer по одному сообщению на запрос душит брокера: BatchSize, BatchTimeout и BatchBytes собирают пачку на клиенте.
  • Лаг — индикатор всех граблей сразу: Reader.Lag и алерт делают инцидент видимым раньше, чем его увидят пользователи.

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

Прогоните две главные грабли на Docker-стенде из третьего урока. Первая: создайте топик на три партиции, пишите с ключом одного «тенанта» через kafka.Hash{} и убедитесь, что все сообщения легли в одну партицию; затем просолите ключ и проверьте распределение, зафиксировав, что произошло с порядком. Вторая: соберите консьюмер с CommitInterval равным нулю и коммитом после обработки, убейте процесс между process и CommitMessages — после рестарта группы объясните перечитанное сообщение арифметикой at-least-once; затем переставьте коммит до обработки и опишите, как потеря выглядела бы в мониторинге. Финал: возьмите свой текущий проект и пройдите чек-лист из семи пунктов, отметив, какие грабли в нём уже закрыты, а какие ждут своего инцидента.

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

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

Можно ли уменьшить число партиций топика после инцидента с перекосом?

Нельзя: kafka-протокол умеет только увеличивать, и CreatePartitions в kafka-go отражает именно это. Пересоздание топика с меньшим числом партиций означает миграцию с перечитыванием и потерей офсетов групп — редкий случай оправдывает такую цену. Правильный порядок действий другой: сначала лечение ключа — смена или salting, — потому что перекос лечится балансировкой данных, а не геометрией партиций. Число партиций поднимают заранее, под рост потока, и помнят, что каждое увеличение пересчитывает маппинг ключей и ломает порядок по ключу для всех существующих отправителей.

Какой вариант честнее для платежей: дубликаты или потери?

Дубликаты: at-least-once с коммитом после обработки. Дубликат гасится идемпотентностью — обработчик сверяет бизнес-ключ и повтор не создаёт вторую сущность, а потерянный платёж не восстанавливается ничем, потому что брокер считает позицию закрытой и не подскажет. At-most-once оправдан на потоках, где событие дешевле потерять, чем обработать дважды, — телеметрия, счётчики, деградирующие уведомления. Для денег схема одна: синхронные коммиты, коммит после успешной обработки, дедупликация по идентификатору события и алерт на повторные ключи в DLT-топике.

С какой частоты лага ставить алерт, чтобы не утонуть в шумихе?

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

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

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

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

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

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

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

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