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 Documentation: Redis event library. https://redis.io/docs/reference/internals/internals-rediseventlib/
- Antirez, «Lazy Redis is better Redis», 2015. http://antirez.com/news/93
- Redis source:
src/ae.c,src/networking.c
← Что такое Redis | Pipelining →