Суть Читай реальные конфиги продьюсера, окно buffered-writer в Go, цикл split-and-retry и строку метрик батчера; предскажи поведение и выбери фикс с максимальным рычагом.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Баги батчинга прячутся в дефолтах конфига, в отсутствующем таймере и в слишком ретивом ретрае. Читай код и метрики, затем выбери фикс, который senior делает первым.
Цель
Отрепетируй цикл, который ты гоняешь на каждом пути батчинга: прочитай конфиг или горячий цикл, предскажи, какой триггер срабатывает и что ломается, и тянись к фиксу с максимальным рычагом раньше всего прочего.
Продьюсер на этом конфиге выдаёт сильно ниже ёмкости кластера, пока брокеры скучают. compression.type уже zstd. Какова доминирующая проблема и первый фикс?
Heads-up acks=all добавляет latency репликации на запрос, но при linger.ms=0 реальный cap — крошечные батчи: ты шлёшь куда больше запросов, чем нужно. Сначала чини батчинг; ослабляй acks, только если durability позволяет и всё ещё медленно.
Heads-up Выбор кодека — твик второго порядка. При linger.ms=0 кодек едва работает на почти пустых батчах — проблема в пустых батчах, а не в том, какой кодек их сжимает.
Heads-up 16 КБ — дефолт и редко бывает слишком велик, а меньший cap делает батчи ещё мельче, ухудшая throughput. Продьюсер всё равно не наполняет 16 КБ, потому что linger.ms=0 сбрасывает раньше.
Этот батчер отлично работает под нагрузкой, но его tail latency взрывается, когда ночью трафик падает. Чего не хватает и почему симптом появляется только при низкой нагрузке?
Heads-up Это реальная проблема корректности (bufio.Writer не goroutine-safe), но не то, что взрывает tail latency при низкой нагрузке. Причина описанного stall — отсутствующий таймер max-wait.
Heads-up Больший буфер делает low-load stall хуже — его дольше наполнять. Фикс — таймер, сбрасывающий частичный буфер, а не больший size-cap.
Heads-up Микрооптимизация, не связанная с обрывом latency. Структурный дефект — отсутствие сброса по max-wait, что и кусается, когда триггер size не может сработать.
Сниппет 3 — цикл ретрая консьюмера
func process(batch []Record) error { for _, r := range batch { if err := handle(r); err != nil { return err // прервать весь батч, будет повторён } } return commitOffsets(batch)}
Викторина
Completed
Одна запись в батче необратимо повреждена (handle всегда на ней ошибается). Фреймворк ретраит process(batch) на любую возвращённую ошибку. Что произойдёт и какова правильная структура?
Heads-up commitOffsets выполняется только после успешного завершения цикла; ранний return не коммитит ничего. Каждый ретрай переобрабатывает весь батч и снова бьёт poison-запись.
Heads-up Скип всего батча (если бы он вообще был) выбросил бы каждую хорошую запись ради сброса одной плохой — потеря данных. Правильный механизм — per-item-изоляция через split-and-retry плюс DLQ, а не сплошной скип.
Heads-up Idempotency делает повторы безопасными, но запись всё равно ошибается вечно, так что offset всё равно не продвигается. Idempotency не ломает poison-message stall; это делает failure isolation.
Читая одну эту строку метрик батчера, какое утверждение верно?
Heads-up Наоборот: drops=0 значит данные не теряются, а 12% depth значит буфер почти пуст — оба сигнала здоровы. Единственная лёгкая забота — недозаполненный батч по таймеру.
Heads-up 120/4096 — использовано записей из cap: батч держит 120 из возможных 4096, т.е. заполнен на 3%, противоположность переполнению.
Heads-up 20 мс — настроенный max-wait, и это latency-налог, а не цена CPU. Урезать ли его — зависит от SLO и от того, наполняются ли батчи; здесь они недозаполнены, так что урезание wait лишь сделает батчи ещё мельче.
Итог
Batching читается в конфиге и коде: linger.ms=0 морит батчи голодом и кастрирует компрессию при любом кодеке; size-only-буфер нуждается в таймере max-wait, иначе встаёт при низкой нагрузке; abort-whole-batch-ретрай на необратимой ошибке — poison-message stall, который решают split-and-retry плюс DLQ; а строка метрик батчера сразу говорит reason сброса, долю заполнения, wait, depth и drops — depth и drops ведут, throughput отстаёт. Диагностируй по сигналу, чини причину с максимальным рычагом, затем переизмеряй.