Конвейеризация: убираем задержки сети
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. - Очень большие пачки разбивайте на порции, чтобы не раздувать буферы.