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

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

Batching: амортизируй фиксированную цену каждой операции

Суть Когда цена операции в основном фиксирована (syscall, round-trip, ACK), группировка амортизирует её. Окно — размер плюс max-wait — решает, где throughput встречается с tail-latency.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 8 min

Команда две недели гоняется за медленным ingest-пайплайном. CPU простаивает, сеть простаивает, диск простаивает — а 1000 строк вставляются 9 секунд. Ничто не «занято». Профайлер наконец указывает на очевидное: 1000 отдельных INSERT, 1000 отдельных round-trip к Postgres, и каждый ждёт ACK перед следующим. Заменили цикл на один COPY. То же железо, те же строки: 14 миллисекунд. Они ничего не ускорили — просто перестали платить пошлину тысячу раз.

Модель фиксированной vs переменной цены

У каждой операции две цены. Переменная масштабируется с данными: сериализовать 4 КБ дороже, чем 40 байт. Фиксированная платится за вызов независимо от размера — и обычно это дорогая половина. Syscall платит переход user→kernel и обратно. Сетевой вызов платит полный round-trip плюс ACK. Запись в БД платит планирование запроса, commit транзакции и WAL flush. Строка лога платит fsync.

Запишем общую цену N операций как total = N * (F + V), где F — фиксированная, V — переменная на элемент. Когда F доминирует над V, а N велик, почти всё, что ты платишь — это F, повторённый N раз, и сами данные — погрешность округления. Batching меняет форму на total = F + N * V: ты платишь F один раз и V на элемент. Фиксированная цена амортизирована по всему батчу. Это единственное алгебраическое движение — вынести F за пределы цикла — и есть вся идея.

Вот почему медленный пайплайн был медленным. CPU, диск и сеть выглядели простаивающими, потому что узкое место — это latency, а не utilization: каждая операция жила в ожидании завершения предыдущего round-trip. Фиксированная цена никогда не была временем CPU, видимым во flame graph — это было мёртвое время на проводе и в ядре.

Фиксированная цена живёт на каждом слое

Причина, по которой batching встречается всюду — TCP Nagle, Kafka linger.ms, Postgres COPY, Redis pipelining, io_uring submission queues, syslog buffering — в том, что у каждого слоя стека своя пошлина за операцию. Знание, какую именно фикс. цену ты амортизируешь, говорит, насколько большим будет выигрыш.

СлойФикс. цена на операциюКак батчитсяЗаявленный выигрыш
Syscallпереход user↔kernel (~сотни нс каждый)io_uring / writev / батч-submissionмиллионы IOPS без syscall на операцию
Сеть (Redis)полный RTT + ACK, на каждую командуpipelining (шлёшь много, читаешь ответы разом)10k PING: 1.19s → 0.25s (~5x)
Брокер (Kafka)produce request + ACK репликацииbatch.size + linger.ms~8k → ~150k msg/s с включённым batching
База (Postgres)parse + plan + commit + WAL flushCOPY / multi-row INSERT10M строк: 9000s одиночных INSERT → 14s COPY

Случай Redis — самая чистая иллюстрация модели. Через линию с 250 мс RTT сервер, способный обслуживать 100k запросов/сек, ограничен 4 запросами/сек, если клиент ждёт каждый ответ — потому что узкое место это RTT, платимый на каждую команду. Запайплайнь команды — и платишь один RTT за весь батч: throughput прыгает обратно к реальному потолку сервера. Железо не менялось; фиксированная цена просто перестала повторяться.

Окно: размер и max-wait

Батч не собирается бесплатно — элементы должны накопиться перед отправкой. Это накопление управляется окном с двумя ручками, и что сработает первым — закрывает батч:

  • Размер — лимит по числу или байтам. batch.size Kafka по умолчанию 16 КБ; заполнил — батч сбрасывается немедленно.
  • Max-wait — лимит по времени. linger.ms Kafka (по умолчанию 5 мс в современных версиях) — это максимум, сколько producer держит недозаполненный батч в надежде, что придёт ещё.

Под высокой нагрузкой батчи заполняются раньше таймера, так что ты едешь на лимите размера и получаешь почти максимальную амортизацию бесплатно. Под лёгкой нагрузкой батч закрывает таймер — и вот тут прячется цена. Элемент, пришедший в пустое окно, платит полный linger.ms мёртвого времени, хотя система простаивает. Бо́льшие окна покупают больше throughput на единицу фикс. цены, но списывают это с tail-latency: элементы в начале окна ждут дольше всех. Поздние уроки уйдут глубоко в тюнинг этого; пока держи форму — окно это диал между throughput и tail-latency, и сеньорный вопрос никогда не «батчить или нет», а «какое окно держит p99 под SLO?».

Где НЕ батчить

Batching не бесплатен, и сеньор знает случаи, где это чистый убыток:

  1. Редкие операции. Нет queue depth — нет элементов для амортизации, ты просто добавляешь linger.ms чистой latency к одному вызову. Батч из одного медленнее, чем без батча.
  2. Жёсткий sub-миллисекундный SLO. Если контракт p99 < 1ms, любое окно ожидания его ломает. Математика амортизации выигрывает по throughput, но latency тебе тратить нечем.
  3. Causal зависимость между операциями. Если вход операции N+1 зависит от подтверждённого результата операции N, ты не можешь запустить их группой — они последовательны по определению.
  4. Нельзя терпеть partial-batch loss. Батч часто подтверждается или теряется как единица. Если падение одной записи не должно откатывать её 999 соседей, или крах в середине батча не должен терять забуференные-но-неподтверждённые элементы — твоя failure model дерётся с границей батча.
Выбери лучший вариант

Платёжный сервис пишет одну строку леджера на транзакцию. Объём ~30 записей/сек, SLA — «строка durable до того, как мы вернём пользователю success». Коллега предлагает буферизовать записи в 50 мс COPY-батчи, чтобы снизить нагрузку на БД. Выбери решение, которое защитит сеньор.

Викторина

Пайплайн делает 1000 одиночных INSERT, и CPU, диск и сеть все около простоя, но это занимает 9 секунд. В чём узкое место?

Викторина

Что отдаёт увеличение batching-окна (больший размер, дольше max-wait)?

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

Упорядочи рассуждение сеньора перед решением батчить операцию:

  1. 1 Цена операции в основном фиксирована (syscall, RTT, commit), а не переменный payload?
  2. 2 Rate операций достаточно высок, чтобы создать queue depth для амортизации?
  3. 3 Producer терпит добавленное ожидание (нет жёсткого sub-мс SLO, нет causal зависимости)?
  4. 4 Failure model переживёт потерю/откат на гранулярности батча?
  5. 5 Только тогда: выбери окно (размер + max-wait), держащее p99 под SLO
Почему это работает

Причина, по которой простаивающая на вид система всё ещё медленна, в том, что batching атакует цену latency, а не цену CPU. Flame graph показывает, куда уходит время CPU; он слеп к потоку, припаркованному в ожидании round-trip. Когда utilization низкий, а throughput плохой — подозревай последовательные фикс. цены и тянись за батчем раньше, чем за железом покрупнее.

Вспомните перед уходом
  1. 01
    В одном абзаце: объясни, почему batching существует и где его использовать, а где нет.
  2. 02
    Какие два измерения имеет batching-окно и что закрывает окно?
  3. 03
    Почему система может выглядеть полностью простаивающей (простой CPU, диск, сеть) и всё ещё быть медленной, и почему batching это чинит?
Итог

Batching существует, чтобы амортизировать фиксированную цену операции — переход syscall, network round-trip, ACK, commit транзакции или запись в лог — по многим элементам, превращая N*(F+V) в F+N*V. Он окупается, когда фикс. цена доминирует над переменной, когда rate достаточно высок для queue depth, и когда есть запас latency, чтобы его потратить. У окна две ручки, размер и max-wait, и что сработает первым — закрывает батч: под нагрузкой едешь на лимите размера, под лёгкой нагрузкой таймер закрывает окно и списывает ожидание с tail-latency. Не батчь редкие операции, жёсткие sub-мс SLO, causally зависимые операции или системы, не переживающие partial-batch loss. Повторяющаяся сеньорная ловушка — оптимизировать throughput, за который никто не платит, ломая контракт latency или durability — так что настраивай окно под SLO, а не под максимальный throughput.

Связанные уроки
встречается в260
Продолжить восхождение ↑Окно батчинга: размер и время ожидания
хоткеи развернуть
поиск
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.