Pipelining

Предпосылки: Event loop, RTT (round-trip time — время от отправки запроса до получения ответа).

Event loop | Логические базы

Проблема: ожидание ответа на каждую команду

Стандартный обмен между клиентом и Redis выглядит так: клиент отправляет команду, ждёт ответа, отправляет следующую. При RTT 0.1 мс внутри одного дата-центра и 100 командах на одну страницу — 10 мс уходит только на ожидание сети, хотя Redis выполняет каждую команду за микросекунды. При RTT 1 мс между зонами доступности потери вырастают на порядок.

Pipelining: отправка без ожидания ответа

Pipelining — это отправка нескольких команд подряд, не дожидаясь ответа на каждую. Клиент записывает N команд в TCP-соединение, затем читает N ответов. Redis получает команды и выполняет их последовательно — в зависимости от объёма данных это может занять один или несколько оборотов event loop.

Без pipelining:              С pipelining:
 
клиент → SET a 1             клиент → SET a 1
клиент ← OK                           SET b 2
клиент → SET b 2                       SET c 3
клиент ← OK                 клиент ← OK
клиент → SET c 3                       OK
клиент ← OK                           OK
 
3 RTT                        1 RTT

При 100 командах pipelining сокращает общее время со 100 RTT до 1 RTT. На уровне Redis ничего не меняется — команды выполняются последовательно, как обычно. Выигрыш целиком за счёт сети.

Pipelining не даёт атомарности

Команды в pipeline выполняются последовательно, но между ними могут вклиниться команды от других клиентов. Если клиент A отправил pipeline из SET x 1; SET y 2, а клиент B одновременно отправил GET x, клиент B может увидеть новое значение x, но старое значение y.

Если нужна гарантия, что набор команд выполнится без вклинивания, используется EXEC — все команды между MULTI и EXEC выполняются как единое целое. MULTI/EXEC можно комбинировать с pipelining: отправить MULTI, команды и EXEC одним пакетом, получив и атомарность, и экономию на сетевых задержках.

Ограничение размера pipeline

Если pipelining так выгоден, почему не отправить все команды одним пакетом? Потому что Redis накапливает ответы в выходном буфере клиента, пока клиент их не прочитает. При pipeline из десятков тысяч команд объём накопленных ответов может вырасти настолько, что Redis превысит лимит client-output-buffer-limit (ограничение на размер выходного буфера одного соединения) и принудительно закроет соединение, либо исчерпает память сервера.

На практике pipeline разбивают на серии по 100–1000 команд: отправить пачку, прочитать ответы, отправить следующую. Это сохраняет выигрыш от pipelining (один RTT на пачку вместо одного RTT на команду) и не перегружает память Redis.

Когда pipelining не помогает

Pipelining работает, когда команды независимы — их можно отправить все сразу, не глядя на результаты. Если каждая следующая команда зависит от результата предыдущей (прочитать значение, посчитать что-то на его основе, записать результат), отправить их одним пакетом невозможно. В таких случаях нужен либо Lua-скрипт, который выполнит всю логику на стороне Redis, либо несколько последовательных round-trip’ов.

Sources


Event loop | Логические базы