Конвейеризация: убираем задержки сети

Redis сам по себе очень быстр, но сеть — нет. Конвейеризация отправляет пачку команд сразу, не дожидаясь ответа на каждую, и превращает тысячи медленных «туда-обратно» в один.

Конвейеризация (pipelining) — приём, при котором клиент отправляет несколько команд подряд, не дожидаясь ответа на предыдущую, а потом читает все ответы разом. Это убирает из общего времени самую дорогую часть — сетевые задержки между командами.

Зачем на практике

Когда вы шлёте Redis команду, время складывается из двух частей: микроскопического времени самой операции в памяти (десятки наносекунд) и времени, пока пакет дойдёт до сервера и ответ вернётся обратно. Вторая часть — RTT (round-trip time) — на порядки больше. В локальной сети это доли миллисекунды, но если вы делаете тысячу команд по очереди, тысяча RTT складываются в ощутимые секунды, при том что сам Redis почти ничем не занят.

RTT (round-trip time) — время полного оборота: пакет с командой долетел до сервера, тот выполнил её и отправил ответ обратно клиенту. Именно RTT, а не скорость Redis, обычно ограничивает пропускную способность при множестве мелких команд.

Проблема: задержка на каждой команде

Без конвейеризации клиент ведёт диалог «вопрос — ответ»: послал команду, ждёт ответ, только потом шлёт следующую. На каждую команду тратится один RTT.

# последовательно: на каждую команду — отдельный round-trip
SET k1 v1   -> ждём ответ (1 RTT)
SET k2 v2   -> ждём ответ (1 RTT)
SET k3 v3   -> ждём ответ (1 RTT)
... 1000 команд = 1000 RTT

Если RTT равен 1 мс, тысяча команд займёт около секунды — и почти всё это время уйдёт на ожидание сети, а не на работу Redis.

Решение: отправить пачку без ожидания

При конвейеризации клиент записывает все команды в сокет подряд, не читая ответы, а затем считывает все ответы за один проход. Сервер выполняет команды по мере поступления и складывает ответы в выходной буфер. Итог — один общий round-trip на всю пачку.

# конвейером: отправили все команды разом, ответы читаем потом
SET k1 v1
SET k2 v2
SET k3 v3
...        # всё уходит без ожидания
<-- читаем 1000 ответов одним блоком (≈ 1 RTT)

Через redis-cli это удобно прогнать, скормив команды из файла: флаг --pipe отправляет их максимально плотно.

cat commands.txt | redis-cli --pipe

Посчитаем выигрыш на модели: тысяча команд, RTT = 1 мс.

rtt_ms = 1.0      # один round-trip — 1 мс
n = 1000          # тысяча команд

sequential = n * rtt_ms   # каждая команда ждёт свой ответ
pipelined  = rtt_ms       # один round-trip на всю пачку

print(f"Последовательно: {sequential:.0f} мс")
print(f"Конвейером:      {pipelined:.0f} мс")
print(f"Ускорение:       {sequential / pipelined:.0f}x")

Вывод:

Последовательно: 1000 мс
Конвейером:      1 мс
Ускорение:       1000x

Разница не в проценты, а в разы (а на тысячах команд — в сотни и тысячи раз). Это самый дешёвый способ ускорить массовую загрузку или пакетное чтение из Redis: код почти не меняется, а время падает кратно.

Чем это НЕ является: pipelining ≠ транзакция

Главная путаница новичков. Конвейеризация — это про сеть: «отправить много команд за один заход». Транзакция (MULTI/EXEC) — про атомарность: «выполнить команды неделимым блоком». Это независимые вещи.

PipeliningТранзакция (MULTI/EXEC)
Что решаетзадержку сети (RTT)атомарность/изоляцию
Атомарностьнет — между командами пачки могут вклиниться чужиеда — никто не вклинится
Чужие командымогут выполниться вперемешкуне выполнятся внутри блока
Зачемскорость массовых операцийсогласованность связанных правок

Внутри конвейера команды разных клиентов могут перемешаться: pipelining лишь экономит round-trip'ы, но не изолирует. Если нужна и скорость, и атомарность — отправляйте MULTI/EXEC внутри конвейера (это допустимо и часто делается), но именно MULTI/EXEC отвечает за неделимость, а не сам факт конвейеризации.

Как это работает под капотом

Протокол Redis (RESP) устроен так, что ответы возвращаются строго в порядке поступления команд. Это и делает конвейеризацию безопасной: клиент знает, что N-й прочитанный ответ соответствует N-й отправленной команде, даже если он отправил их все, не читая. Сервер по мере чтения команд из сокета выполняет их и пишет ответы в выходной буфер соединения; клиент опустошает этот буфер потом. Узкое место смещается с задержки сети на пропускную способность: за один RTT можно прогнать тысячи команд. Чтобы не раздуть память, очень большие массивы команд разбивают на разумные порции (например, по несколько тысяч), читая ответы между порциями.

Частые ошибки

  • Путать с транзакцией. Конвейер не делает команды атомарными; для неделимости нужен MULTI/EXEC.
  • Бесконечная пачка. Миллион команд без чтения ответов раздувает буферы клиента и сервера; разбивайте на порции.
  • Зависимые команды в одной пачке. Внутри конвейера вы не видите результат предыдущей команды (ответы читаются потом), поэтому «прочитать и решить» так не сделать — это к Lua.
  • Ожидать ускорения на одной команде. Pipelining помогает на множестве команд; для единичной он бессмыслен.
  • Игнорировать обработку ошибок в пачке. Среди ответов могут быть ошибки на отдельные команды — их нужно проверять, читая результаты.

Итоги

  • Основная цена мелкой команды — это RTT (оборот по сети), а не работа самого Redis.
  • Конвейеризация отправляет команды пачкой без ожидания и читает ответы разом — один RTT на всю пачку.
  • Выигрыш кратный: тысячи команд по одной превращаются в один сетевой оборот.
  • Pipelining ≠ транзакция: он ускоряет сеть, но не изолирует; за атомарность отвечает MULTI/EXEC.
  • Очень большие пачки разбивайте на порции, чтобы не раздувать буферы.
Проверьте себя
1. Что именно убирает конвейеризация (pipelining) из общего времени работы с Redis?
AВремя выполнения команд внутри Redis
BСетевые задержки (RTT) между командами — вместо одного round-trip на каждую команду получается один на всю пачку
CНеобходимость аутентификации на каждый запрос
DРасход памяти на сервере
2. Чем pipelining отличается от транзакции MULTI/EXEC?
APipelining делает команды атомарными, а MULTI/EXEC — нет
BЭто одно и то же, просто разные названия
CPipelining экономит сетевые round-trip'ы, но не изолирует команды (чужие могут вклиниться); MULTI/EXEC обеспечивает атомарность, но не ускоряет сеть
DPipelining работает только с одной командой за раз