Event loop

Предпосылки: Что такое Redis, процессы и потоки, системные вызовы (read, write), файловые дескрипторы.

Что такое Redis | Pipelining

Один поток и тысячи клиентов

Redis обрабатывает все команды в одном потоке, последовательно. Тысяча клиентов отправила по команде — Redis выполняет их одну за другой, без параллелизма. Как один поток успевает обслуживать тысячи клиентов одновременно?

Наивный подход — читать из сокета каждого клиента по очереди — не работает: пока текущий клиент не пришлёт данные, поток заблокирован на системном вызове read, а остальные клиенты ждут. Мультиплексирование ввода-вывода решает эту проблему: вместо того чтобы читать из каждого сокета напрямую, поток просит ОС сообщить, на каких сокетах уже есть готовые данные, и обрабатывает только их.

Зачем один поток

Почему Redis не использует несколько потоков для параллельной обработки команд? Когда несколько потоков работают с одними и теми же данными, им нужна координация: замки на общие структуры, чтобы потоки не портили данные друг другу, и переключение процессора между потоками. Для in-memory хранилища, где сама операция занимает наносекунды, накладные расходы на координацию могут превысить стоимость самой операции.

Один поток означает: нет гонок, нет взаимных блокировок, каждая команда атомарна по определению. Однопоточность не означает «один поток на весь процесс» — Redis использует дополнительные потоки для фоновых задач (сброс данных на диск, закрытие соединений, освобождение памяти больших объектов). Но главный цикл, который читает команды клиентов и выполняет их, работает строго в одном потоке.

Цикл событий (event loop)

Мультиплексирование и однопоточное выполнение объединяются в цикл событий (event loop). Каждый оборот цикла выглядит так: Redis передаёт ОС список всех клиентских соединений и ждёт, пока хотя бы на одном из них не появятся данные для чтения или готовность к записи. Когда ОС возвращает список готовых соединений, Redis обходит их по очереди: читает команду, выполняет её, записывает ответ в выходной буфер клиента. После обработки всех готовых событий — снова ожидание.

ожидание готовых сокетов → чтение команд → выполнение → запись ответов → ожидание

За один оборот цикла Redis может обработать команды от сотен клиентов. Между оборотами выполняются периодические задачи: проверка TTL ключей, обновление статистики, фоновое сохранение.

Что блокирует event loop

Раз все команды выполняются в одном потоке, медленная команда задерживает всех остальных клиентов. Общий паттерн проблемных команд — одна команда, которая за один вызов обходит большой объём данных. Практические примеры на Rails — в заметке о блокирующих командах.

Самый известный пример — KEYS *: она обходит всю таблицу ключей и возвращает все имена. При миллионе ключей это занимает десятки миллисекунд, и все клиенты ждут. Безопасная альтернатива — SCAN, который возвращает ключи порциями и отдаёт управление event loop между итерациями.

Тот же принцип касается команд, читающих всё содержимое структуры за раз: HGETALL возвращает все поля хеша, SMEMBERS — все элементы множества, LRANGE 0 -1 — весь список. На маленьких структурах (сотни элементов) они безопасны, но при миллионе элементов сериализация результата блокирует цикл.

SORT с параметрами BY и GET загружает и сортирует данные из нескольких ключей — сложность зависит от объёма данных, но может быть неожиданно высокой.

Отдельная категория — Lua-скрипты (EVAL): Redis позволяет отправить серверу программу на встроенном языке Lua, которая выполнится атомарно. Пока скрипт работает, никакие другие команды не обрабатываются. Длинный Lua-скрипт — длинная блокировка для всех клиентов.

Наконец, удаление большого объекта. DEL списка из миллионов элементов в Redis до 4.0 блокировал event loop на время освобождения памяти. Начиная с Redis 4.0 команда UNLINK удаляет ключ из пространства имён в основном потоке (моментально), а само освобождение памяти передаёт фоновому потоку lazyfree. Это не нарушает однопоточную модель: lazyfree не читает и не модифицирует данные Redis, он только возвращает ОС освобождённые блоки памяти.

I/O threads (Redis 6.0+)

Однопоточная модель упирается в пропускную способность сети раньше, чем в CPU на выполнение команд. На серверах с быстрыми сетевыми картами (10/25/100 Gbit) узким местом становится не выполнение команд, а чтение данных из сокетов и запись ответов. Redis 6.0 добавил возможность переложить сетевой I/O на несколько потоков (io-threads в конфигурации). Выполнение команд по-прежнему происходит в основном потоке — это сохраняет однопоточную семантику и атомарность. I/O threads только читают данные из сокетов в буферы и записывают ответы из буферов в сокеты.

На типичных нагрузках до 100 000 ops/sec I/O threads не нужны — одного потока хватает. Они становятся полезны при высокой пропускной способности, когда разбор входящих команд и формирование ответов начинают занимать заметную долю CPU.

Батчинг команд через pipelining снижает нагрузку на event loop с другой стороны — уменьшая количество системных вызовов и roundtrip’ов.

Sources


Что такое Redis | Pipelining