Программа курса

Модуль 01 / Урок 1 из 27

Event loop и однопоточность

Теория и практика

Redis — это сервер в памяти с одним потоком исполнения команд. Отсюда следуют и его скорость, и все его «сюрпризы» на проде. В этом уроке разберём, что именно происходит между вашим GET и ответом сервера.

Что такое Redis на самом деле

Redis хранит весь основной датасет в оперативной памяти; диск используется только для персистентности (модуль 4). Обращение к RAM занимает ~100 наносекунд, обращение к SSD — ~100 микросекунд, к HDD — миллисекунды. Разница на 3–4 порядка и объясняет, почему Redis такой быстрый: операция GET — это, по сути, поиск по словарю в памяти плюс сетевая отправка ответа.

Второй источник скорости — протокол RESP: он текстовый и предельно простой, парсится тривиально и не требует тяжёлых библиотек. Это можно увидеть даже вручную, без клиента:

# «сырой» разговор с Redis через openssl/nc
printf 'PING\r\n' | nc localhost 6379
+PONG

Однопоточная модель исполнения

Ключевой факт: команды исполняются в одном потоке. Поток крутит событийный цикл (event loop) на системном мультиплексоре ввода-вывода — epoll в Linux, kqueue в BSD. Схема цикла:

  1. epoll сообщает: «на этих сокетах есть данные»;
  2. Redis читает команды из буферов этих сокетов;
  3. исполняет их последовательно, одну за другой;
  4. пишет ответы обратно;
  5. обрабатывает «фоновые» дела: истечение TTL, события персистентности.

Один поток спокойно обслуживает десятки тысяч соединений, потому что каждая операция занимает микросекунды — узкое место не в конкуренции соединений, а в суммарном времени исполнения команд.

Следствие №1: атомарность каждой команды

Пока Redis исполняет INCR counter, никакая другая команда не может вклиниться. Поэтому:

# Два клиента одновременно делают INCR на счётчике = 5
# Оба получат ответ: один 6, другой 7. Итог: 7. Ничего не потеряно.

Это фундамент популярных применений Redis: счётчики, rate-limiter'ы, глобальные ID (INCR seq), блокировки. Но атомарна только одна команда. Последовательность GET → вычисление → SET — уже гонка: между вашими командами чужой клиент может что-то изменить. Решения этой проблемы — транзакции и Lua (модуль 5).

Следствие №2: медленная команда тормозит всех

Команда, исполняющаяся 200 мс, на эти 200 мс «замораживает» весь сервер: ни один другой клиент не получает ответа. Это главная эксплуатационная особенность Redis. Типичные убийцы латентности:

  • KEYS * на большой БД — O(всей БД);
  • HGETALL / SMEMBERS / LRANGE 0 -1 по коллекции с сотнями тысяч элементов;
  • DEL гигантского ключа;
  • долгий Lua-скрипт;
  • fork процесса для BGSAVE на огромном датасете.
⚠️ Правило продакшена: ни одна команда не должна исполняться дольше единиц миллисекунд. О(N) команды — только по маленьким коллекциям или через итераторы (SCAN, HSCAN — урок 1.4).

IO-threads (Redis 6+): многопоточность, которая не ломает модель

С ростом скоростей сетей (25–100 Гбит) узким местом стало не исполнение команд, а чтение/запись сокетов и парсинг пакетов. Redis 6 добавил IO-потоки: несколько потоков читают запросы и пишут ответы, но исполнение команд по-прежнему в одном потоке. Это даёт до 2–3× throughput на больших инстансах без изменения семантики.

# redis.conf
io-threads 4               # разумно при ≥4 ядрах и трафике от ~100k RPS
io-threads-do-reads yes    # обычно эффект заметнее на записях; включает и чтение

Что работает вне главного потока

Модель «один поток» относится к исполнению команд. Некоторые тяжёлые задачи Redis намеренно выносит в фоновые потоки/процессы, чтобы не стопорить клиентов:

ЗадачаГде выполняетсяКоманда/настройка
Снапшот RDBдочерний процесс (fork)BGSAVE, save-правила
AOF rewriteдочерний процессBGREWRITEAOF
Освобождение больших объектовфоновый потокUNLINK, lazyfree-lazy-*
fsync AOFфоновый потокappendfsync everysec
Закрытие/вытеснение больших ключейфоновый потокlazyfree-lazy-eviction yes

Практика

  1. Запустите Redis в Docker: docker run -d --name redis-lab -p 6379:6379 redis:7.
  2. Проверьте атомарность: в двух терминалах одновременно выполните watch -n0.01 redis-cli INCR race и убедитесь, что итоговый счётчик равен числу выполненных инкрементов.
  3. Испытайте блокировку: залейте миллион ключей (redis-benchmark -n 1000000 -t set -d 8 -q), затем в одном терминале выполните redis-cli KEYS 'user:*', а во втором — сразу redis-cli PING. Почувствуйте, что PING замер на время работы KEYS.
  4. Посмотрите метрики потока: redis-cli INFO stats | grep instantaneous — текущий QPS вашего инстанса.
Мини-квиз

1. Почему INCR не требует блокировок, а последовательность GET→SET — требует? — INCR — одна команда, исполнение однопоточное и атомарное. GET и SET — две команды, между ними вклинится чужой клиент.

2. Включение io-threads делает Redis многопоточным?

— Нет: параллелится только I/O. Исполнение команд остаётся однопоточным, семантика не меняется.

3. Клиент сообщил, что раз в минуту все запросы к Redis «замирают» на ~100 мс. Первая гипотеза? — Периодическая тяжёлая работа: BGSAVE-форк по save-правилам, AOF rewrite или O(N)-команда из кода. Смотрим slowlog и INFO persistence.