awesome-everything EN
↑ Обратно к восхождению

Производительность

Batching в Kafka и Postgres

Суть Пакеты в Kafka (batch.size, linger.ms) и COPY vs INSERT в Postgres для эффективной массовой записи; почему компрессии сначала нужен пакет.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 15 min

Команда переносит логи на Kafka. Пропускная способность упирается в 40 МБ/с, а брокеры скучают — диск и сеть ниже 20%. Добавляют продюсеров, добавляют партиции, ничего не меняется. Причина — одна настройка по умолчанию, на которую никто не посмотрел: linger.ms=0. Каждое сообщение уходило отдельным запросом в момент возврата send(), поэтому «пакетирование» так и не происходило. Одна строка — linger.ms=20, batch.size=131072 — и тот же кластер выдал 300 МБ/с. Узким местом никогда не были брокеры. Им был продюсер, который отказывался ждать.

У каждой записи есть фиксированная стоимость (сетевой round-trip, системный вызов, ACK, fsync) и переменная (сами байты). Когда фиксированная стоимость доминирует — а для мелких сообщений она доминирует всегда — пакетирование амортизирует ее по N записям. Двумерное окно из прошлого урока (max-size ИЛИ max-wait) — это ровно то, как и Kafka, и Postgres решают, когда отправить пакет. Системы различаются только механизмом, не идеей.

Пакеты в Kafka producer

Kafka producer не отправляет один запрос на каждый send(). Он накапливает записи в буфере в памяти, один пакет на (topic, partition), и отправляет пакет, когда первым срабатывает один из триггеров:

  • достигнут batch.size — лимит байт на партицию, по умолчанию 16 КБ (16384 байта).
  • истек linger.ms — максимальное время ожидания новых записей для заполнения пакета.

Вот ловушка, поймавшая команду выше. В Kafka 3.x linger.ms по умолчанию был 0, то есть «отправляй как можно скорее». Это не отключает пакетирование совсем — записи, накопившиеся пока запрос в полете, все равно склеиваются — но при ровной, размеренной нагрузке он фактически отправляет крошечные пакеты, ограничивая пропускную способность далеко ниже железа. Kafka 4.0 сменила умолчание на linger.ms=5 именно потому, что слишком многие развертывания недобирали пропускную способность. Лечение продюсера, упёртого в пропускную способность, — намеренно подождать: linger.ms в диапазоне 5–100 мс, batch.size поднят до 64–512 КБ. Вы меняете несколько мс задержки на стороне продюсера на меньшее число более жирных запросов.

Старший компромисс острый: linger.ms добавляет задержку только когда пакет еще не заполнен. Под высокой нагрузкой batch.size заполняется раньше, чем истечет таймер, поэтому таймер ничего не стоит — большие пакеты достаются бесплатно. Под низкой нагрузкой linger.ms — небольшой ограниченный налог, покупающий эффективность. Поэтому ненулевой linger почти всегда выигрывает: он может помочь только тогда, когда у вас все равно есть свободное время.

НастройкаУмолчаниеТюнинг под throughputЧто контролирует
linger.ms0 (3.x) → 5 (4.0)5–100 мсМакс. ожидание заполнения пакета (триггер по времени)
batch.size16 КБ64–512 КБЛимит байт на партицию (триггер по размеру)
compression.typenonelz4 / zstdКомпрессия на пакет — в сети и на диске
buffer.memory32 МБподнять, если send блокируетсяОбщий буфер продюсера по всем партициям

Массовые вставки в Postgres: INSERT vs COPY

Та же форма проявляется на стороне БД — три ступени амортизации:

  • Построковый INSERT — каждый оператор парсится, планируется, выполняется и (при autocommit) коммитится отдельно. Коммит вызывает flush WAL. Пропускная способность около 1–5к строк/с. В горячем пути здесь приём данных умирает.
  • Многострочный INSERT ... VALUES (...), (...), ... — один parse, один план, один коммит на весь оператор. Теперь фиксированная стоимость амортизируется по строкам оператора: примерно 5–50к строк/с. Загвоздка — практический предел числа строк на оператор (лимиты параметров, размер оператора), поэтому режут на пакеты по несколько сотен — тысяч.
  • COPY — массовый путь Postgres. Он потоково передает строки по выделенному протоколу, полностью обходя построковый SQL-парсер и планировщик, и коммитит один раз. Документация прямо говорит, что COPY «значительно оптимизированнее» серии INSERT, и что отключать autocommit не нужно — это уже одна команда. Пропускная способность достигает 50–500к строк/с.

ORM открывают среднюю и верхнюю ступени, так что писать их вручную почти не приходится: bulk_create в Django, executemany и помощники COPY в SQLAlchemy, insert_all в Rails. Старший приём — распознать, на какой ступени тихо сидит код: цикл ORM, вызывающий save() на каждый объект, — это построковый INSERT в красивой обертке, и он молча ограничит массовый импорт парой тысяч строк в секунду.

МетодParse / planКоммитыСтрок/с (порядок)
Построковый INSERTНа строкуНа строку (autocommit)1–5к
Многострочный INSERT VALUESРаз на операторРаз на оператор5–50к
COPYНет (потоковый протокол)Один раз50–500к

Почему компрессии сначала нужен пакет

Компрессия и пакетирование — не две отдельные победы; компрессия зависит от пакетирования. Словарные кодеки (lz4, snappy, zstd) ищут повторы внутри своего входного окна. У одной 200-байтной строки лога почти нет внутренних повторов, поэтому сжимать ее отдельно — мало пользы, иногда даже накладные расходы. Соберите несколько сотен таких похожих строк в пакет — и общая структура (те же имена полей, те же хосты, те же ключи JSON) становится отлично сжимаемой. В Kafka кодек работает на пакет, на всем наборе записей, поэтому compression.type делает так мало при linger.ms=0 и так много на жирных пакетах.

Числа подавляюще в пользу этого. Пакет 128 КБ повторяющихся событий стабильно сжимается в 2–4 раза с lz4 или zstd (zstd обычно на 20–30% лучше lz4 ценой большего CPU). CPU на компрессию — около 50–100 МБ/с на ядро — дешево против сетевых байт и fsync на брокере, которых вы избегаете: сжатый пакет — это и то, что ложится на диск, и то, что копируют реплики. Сначала пакет, потом компрессия: порядок не опционален.

Почему это работает

Два классических отказа продюсера Kafka — зеркальны. linger.ms=0 (старое умолчание) тихо ограничивает пропускную способность, потому что пакеты не заполняются. Обратное — огромные batch.size/buffer.memory перед медленным или отстающим брокером — позволяют продюсеру буферизовать, пока buffer.memory не исчерпается, после чего send() блокируется (или бросает исключение по max.block.ms), и backpressure прорывается сквозь приложение. Большие пакеты помогают ровно до тех пор, пока брокер не перестает успевать; тогда буфер — просто очередь, прячущая проблему емкости.

Викторина

Producer Kafka упирается в пропускную способность: брокеры простаивают, но протолкнуть удается лишь долю емкости кластера. Producer на умолчаниях Kafka 3.x. Первое исправление?

Викторина

Вы загружаете 50 миллионов строк в Postgres разовым импортом. Какой путь быстрее?

Расставь шаги по порядку

Расположите пути записи от наименьшей к наибольшей пропускной способности:

  1. 1 Построковый INSERT с autocommit (~1–5к строк/с)
  2. 2 Многострочный INSERT VALUES, пакетами (~5–50к строк/с)
  3. 3 COPY, потоковый протокол, один коммит (~50–500к строк/с)
Выбери лучший вариант

Producer Kafka шлёт события кликстрима с мягким бюджетом задержки (пара сотен мс — норм) и жёстким бюджетом на сетевой egress. Выберите конфиг, который защитит старший.

Вспомните перед уходом
  1. 01
    Какие два триггера заставляют producer Kafka отправить пакет, и почему ненулевой linger.ms обычно выигрывает по throughput, почти не вредя задержке?
  2. 02
    Почему Postgres COPY эффективнее INSERT VALUES, и почему компрессии сначала нужен пакет?
Итог

Kafka и Postgres пакетируют одним двумерным окном. Producer Kafka буферизует записи на (topic, partition) и отправляет пакет, когда достигнут batch.size (по умолчанию 16 КБ) или истёк linger.ms; старое умолчание linger.ms=0 тихо ограничивает throughput, поэтому 4.0 подняла его до 5 — ставьте 5–100 мс с batch.size 64–512 КБ, меняя пару мс задержки на куда более жирные запросы. Postgres поднимается на три ступени: построковый INSERT (1–5к строк/с) платит parse, plan и commit на каждую строку; многострочный INSERT VALUES (5–50к) амортизирует их на оператор; COPY (50–500к) идёт по выделенному протоколу, пропуская построковый parse/plan и коммитя один раз. Компрессия зависит от пакетирования — lz4 и zstd находят повторы только по всему пакету, давая 2–4x за дешёвый CPU, так что сначала пакет, потом компрессия. Отказы зеркальны: linger.ms=0 морит пакеты голодом, а раздутый буфер перед медленным брокером лишь прячет проблему емкости, пока send() не заблокируется.

Связанные уроки
встречается в260
Продолжить восхождение ↑io_uring и наблюдаемость пакетирования
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources5
expand
  1. 01
  2. 02
  3. 03
  4. 04
  5. 05

Trademarks belong to their respective owners. Editorial reference only.