MAATRIX / Блог / Redis упёрся не в память: как найти предел операций в секунду на своём сервере

Redis упёрся не в память: как найти предел операций в секунду на своём сервере

MAATRIX

Redis тормозит, вы добавляете памяти, а латентность не меняется. Это частый симптом: планирование Redis почти всегда сводится к расчёту RAM под датасет, а реальный потолок нередко лежит в другом — в том, сколько команд успевает выполнить один поток на одном ядре за секунду. Разберём, почему у Redis именно такая природа предела, как её диагностировать штатными средствами и когда шардирование решает проблему, а когда нет.

Почему Redis упирается не в память, а в CPU одного ядра

Redis хранит данные в оперативной памяти, и это создаёт понятную привычку: если сервис не справляется, первым делом смотрят на free -h и докупают гигабайты. Но у памяти и у скорости обработки команд разная природа ограничения. Память — это объём, который либо есть, либо кончился: превысили maxmemory — сработала политика вытеснения или пришёл отказ на запись. А скорость обработки команд — это пропускная способность, и здесь у Redis есть архитектурная особенность, которая не лечится добавлением RAM.

Сама модель выполнения команд в Redis однопоточная: один процесс, один event loop, который читает команду, исполняет её над структурой данных и переходит к следующей — строго последовательно, без параллельного выполнения двух команд над данными одновременно. Это осознанное решение: оно избавляет от блокировок и гонок при доступе к данным и делает операции атомарными «бесплатно», без явных локов на стороне приложения. Цена — сколько бы ядер ни было на сервере, реальную работу с данными выполняет одно из них, и предел пропускной способности — это предел одного ядра, а не сумма всех ядер хоста.

Начиная с Redis 6 у сервера появились io-threads — несколько потоков, которые параллельно читают и пишут в сетевые сокеты, разбирают протокол RESP. Это реально снижает накладные расходы при большом числе одновременных подключений. Но само выполнение команды — обращение к хэш-таблице, вставка в отсортированное множество, сортировка списка — как выполнялось в одном потоке, так и выполняется: io-threads ускоряют доставку команды до исполнителя, а не саму работу с данными. Поэтому для CPU-тяжёлых команд потолок ops/sec принципиально не сдвигается, даже если сетевая часть стала быстрее.

Практическое следствие: у вас может быть сервер с 32 ядрами и 128 ГБ RAM, из которых Redis реально нагружает одно ядро на 100%, а остальные тридцать одно простаивают. В htop это выглядит как «нагрузка низкая» (агрегированный процент по всем ядрам маленький), а по факту сервис уже в потолке. Разница между потреблением памяти и упором в операции в секунду подробно разобрана в статье Redis: высокое потребление памяти — причины и решение; здесь речь о другом узком месте — не о том, сколько данных вмещается, а о том, сколько операций над ними можно сделать за секунду.

Диагностика: redis-cli --latency и загрузка конкретного ядра

Первый вопрос — не «сколько операций делает Redis», а «растёт ли задержка ответа с ростом нагрузки». Штатный инструмент — встроенный флаг redis-cli:

redis-cli --latency -h 127.0.0.1 -p 6379

Команда шлёт PING в цикле и печатает текущую, минимальную, максимальную и среднюю задержку в миллисекундах. На незагруженном инстансе задержка PING держится на уровне долей миллисекунды и почти не колеблется. Если под нагрузкой она регулярно подскакивает и не возвращается к базовому уровню — это сигнал, что очередь команд в event loop растёт быстрее, чем сервер её разгребает.

Для разбора по времени удобнее вариант с историей — он выводит статистику интервалами, а не одной непрерывной строкой:

redis-cli --latency-history -i 5 -h 127.0.0.1 -p 6379

Флаг -i 5 задаёт длину интервала в секундах — так видно, растёт ли задержка волнами (например, синхронно с пиками трафика приложения) или держится ровно высокой постоянно.

Дальше — самое важное для проверки однопоточной гипотезы: смотреть не на суммарную загрузку CPU, а на загрузку конкретного ядра, где работает процесс Redis. Суммарный процент по top вводит в заблуждение на многоядерном сервере: 100% занятости одного ядра из 16 покажет в верхней строке какие-то жалкие 6%, и это выглядит как «процессор свободен». Правильный способ — смотреть на загрузку по каждому ядру отдельно:

mpstat -P ALL 1

Или в top/htop включить построчный вывод по ядрам (в top — клавиша 1). Если PID процесса redis-server через top -p $(pgrep redis-server) стабильно показывает загрузку около 100% в колонке %CPU (для однопоточного процесса это значит «одно ядро занято полностью»), а остальные ядра простаивают — это классическая картина упора Redis в CPU одного ядра, а не в память, диск или сеть.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

INFO commandstats: кто именно ест бюджет одного ядра

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

redis-cli INFO commandstats

Вывод — построчная статистика по каждой команде, с которой хоть раз обращались к серверу. Формат строки такой (цифры ниже условные, для иллюстрации формата — ваши будут другими):

cmdstat_get:calls=1204853,usec=98123456,usec_per_call=81.44
cmdstat_set:calls=302011,usec=41208233,usec_per_call=136.45
cmdstat_hgetall:calls=8821,usec=15904221,usec_per_call=1803.10
cmdstat_zrange:calls=4102,usec=9982001,usec_per_call=2434.19

Три поля важны в связке: calls — сколько раз команда вызывалась, usec — суммарное время на неё с последнего сброса статистики, usec_per_call — среднее время одного вызова. Именно usec_per_call показывает, кто по-настоящему дорогой: команда с небольшим числом вызовов, но огромным временем на каждый (как hgetall и zrange в примере выше), съедает заметную долю бюджета потока, оставаясь незаметной по счётчику обращений.

Статистику полезно сбрасывать перед контрольным замером, чтобы видеть картину именно за нужный период, а не с момента последнего рестарта:

redis-cli CONFIG RESETSTAT

Рядом стоит смотреть INFO stats — там есть instantaneous_ops_per_sec (мгновенная скорость обработки команд по данным последнего опроса сервера) и total_commands_processed (счётчик с момента старта). Первое поле — практический ориентир текущей нагрузки; если оно упирается в одно и то же значение независимо от того, сколько клиентов пытаются писать больше, — вы нашли фактический потолок пропускной способности этого инстанса.

Ещё один источник, который стоит проверить в этой же связке — SLOWLOG, встроенный журнал медленных команд:

redis-cli CONFIG SET slowlog-log-slower-than 10000
redis-cli SLOWLOG GET 20

Порог задаётся в микросекундах (10000 = 10 мс). Всё, что попало в лог, — команды, которые заметно дольше обычного держали единственный поток занятым и на это время блокировали всех остальных клиентов. Из-за однопоточной модели это не конкуренция за CPU, а прямая очередь: пока выполняется одна команда, остальные клиенты ждут буквально, а не делят такты процессора.

Дорогие команды: KEYS, большие SORT/SCAN и объёмные структуры

Не все команды стоят одинаково, и разница на порядки. GET и SET по одному ключу — операции O(1), доли микросекунды на современном ядре. А есть команды, чья стоимость растёт с размером данных, и именно они чаще всего оказываются виновником упора в CPU.

**KEYS *** — классический антипаттерн. Команда сканирует всё пространство ключей за один вызов и блокирует event loop до завершения: пока KEYS работает, ни один другой клиент не получит ответа. На небольшом датасете это незаметно, на миллионах ключей — секунды полного простоя. Замена — SCAN с курсором:

redis-cli --scan --pattern 'session:*' --count 100

SCAN возвращает данные порциями и не блокирует сервер одним вызовом, но по совокупной стоимости не дешевле KEYS — чтобы пройти всё пространство ключей, нужна та же суммарная работа, просто нарезанная на кусочки между вызовами других команд, а не одним блокирующим куском.

SORT без LIMIT на большой коллекции — операция O(N log N), а с BY/GET по внешним ключам (паттерн «джойна» в Redis) каждый элемент добавляет ещё одно обращение внутри той же блокирующей операции.

Команды над большими структурамиSMEMBERS, HGETALL, LRANGE 0 -1, ZRANGE 0 -1 на коллекции с десятками и сотнями тысяч элементов — каждая стоит O(N) и целиком выполняется одним куском, блокируя event loop на всё время сериализации ответа. Регулярный HGETALL по хэшу на 200 000 полей — это не «одна операция», а тяжёлый CPU-цикл, повторяющийся с частотой запроса.

Lua-скрипты и транзакции (MULTI/EXEC) выполняются атомарно — в однопоточной модели это значит «весь скрипт целиком без прерываний», то есть сложный скрипт с циклами держит поток занятым на всё время выполнения. Удобство атомарности здесь прямо конкурирует с отзывчивостью.

Практический вывод: при разборе commandstats ищите не команды с максимальным числом вызовов, а с максимальным произведением calls × usec_per_call — это и есть реальная доля бюджета единственного ядра, которую съедает конкретная команда.

Redis Cluster и шардирование: когда помогает, а когда нет

Раз потолок — это потолок одного потока на одном ядре, естественная идея — взять несколько инстансов Redis и раскидать нагрузку между ними. Это и делает Redis Cluster: пространство ключей делится на 16384 хэш-слота, слоты распределяются между мастер-узлами, и каждый узел — это отдельный процесс со своим собственным однопоточным исполнителем команд. Подробно про механику хэш-слотов, топологию и разворачивание — в статье Redis-кластер: настройка; здесь — именно вопрос, решает ли это проблему предела ops/sec.

Когда шардирование действительно помогает. Если суммарная нагрузка приложения — это множество независимых ключей (сессии тысяч пользователей, кэш карточек товаров, счётчики по разным сущностям), и трафик более-менее равномерно распределён по ключам, кластер из N мастеров даёт совокупный потолок, примерно кратный N: каждый узел независимо упирается в свой однопоточный предел, но узлов несколько, и они работают параллельно на разных ядрах и, как правило, разных серверах. Это то же соображение, что при расчёте плотности виртуалок по вводу-выводу — предел одного ресурса преодолевается не увеличением его размера, а горизонтальным умножением ресурсов; общий подход к таким расчётам — в статье предел IOPS: как отличить упор в диск от упора во всё остальное.

Когда шардирование не помогает вообще. Если нагрузка сконцентрирована на одном логическом объекте — глобальный счётчик, единый Sorted Set под общий лидерборд, одна очередь задач с высокой частотой LPUSH/BRPOP — этот ключ физически лежит в одном хэш-слоте на одном конкретном узле, независимо от числа узлов в кластере. Десять мастеров не ускорят обработку одного горячего ключа ни на микросекунду: девять из десяти узлов для этого ключа попросту не участвуют в работе, потому что кластер размазывает *пространство ключей*, а не нагрузку на конкретный ключ. Единственный выход в таком случае — разрезать логическую сущность на несколько физических ключей самостоятельно (счётчик — на N шардированных с суммированием на стороне приложения, лидерборд — на несколько частичных с последующим слиянием) и уже эти шарды раскладывать по узлам.

Есть и операционная цена: многие команды над несколькими ключами требуют, чтобы все ключи лежали в одном слоте, иначе сервер вернёт CROSSSLOT; клиенту нужна логика обработки редиректов MOVED/ASK, а типичная топология требует минимум трёх мастеров плюс реплики. Кластер решает проблему совокупной пропускной способности ценой усложнения архитектуры, и браться за него стоит только когда диагностика действительно показала упор в CPU по всему пространству ключей, а не в один горячий узел.

Практическая методика замера предела на своём железе

Паспортных цифр «Redis держит столько-то тысяч операций в секунду» в природе не существует в применимом к вашему серверу виде — предел зависит от частоты и микроархитектуры конкретного ядра, от смеси команд, от размера значений, от того, включён ли TLS и персистентность. Единственный надёжный способ узнать свой потолок — измерить его на своём железе с нагрузкой, похожей на реальную.

Шаг 1. Снимите профиль реальной нагрузки. Возьмите INFO commandstats с прод-инстанса за представительный период и посчитайте пропорции: сколько вызовов — GET, сколько SET, сколько тяжёлых команд. Тестировать одними PING бессмысленно — это покажет теоретический максимум простейшей команды, а не потолок вашего реального паттерна.

Шаг 2. Соберите нагрузку той же формы через redis-benchmark. Инструмент идёт в комплекте с Redis:

redis-benchmark -h 127.0.0.1 -p 6379 -t get,set -c 50 -n 1000000 -d 100 -q

Здесь -c 50 — параллельные соединения, -n 1000000 — запросов на команду, -d 100 — размер значения в байтах, -q — сжатый вывод с итоговым числом операций в секунду. Подберите -d и набор команд под вашу реальную смесь — иначе результат покажет потолок синтетического сценария, а не ваш.

Шаг 3. Запускайте redis-benchmark с отдельной машины, не с самого сервера Redis. На том же хосте утилита сама потребляет CPU и конкурирует с redis-server за то самое ядро, предел которого вы измеряете. Клиент должен быть в той же сети, но на отдельном железе.

Шаг 4. Одновременно снимайте mpstat -P ALL 1 на сервере и instantaneous_ops_per_sec из INFO stats. Наращивайте нагрузку, пока не увидите оба признака сразу: ядро, на котором сидит redis-server, устойчиво держится у 100%, и instantaneous_ops_per_sec перестаёт расти при дальнейшем увеличении клиентов — выходит на плато. Это плато и есть фактический предел вашего инстанса на данной смеси команд, а не цифра из документации.

Шаг 5. Заложите запас. Работать на самой границе потолка — плохая идея: у event loop, близкого к насыщению, задержка растёт не линейно, а резко, и прод почти никогда не бывает идеально ровным. Практический ориентир — планировать рабочую нагрузку на уровне 60–75% от измеренного плато, а саму методику замера повторять при заметном изменении профиля команд или размера датасета — предел привязан к конкретной нагрузке, а не к железу самому по себе.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Поможет ли переезд на сервер с бóльшим числом ядер, если Redis упёрся в CPU?

Само по себе число ядер не поможет — лишние ядра простаивают, пока команды выполняются в одном потоке. Реально сдвигает потолок одного инстанса более высокая тактовая частота и лучшая микроархитектура того ядра, на котором работает процесс. Дополнительные ядра пригодятся, только если вы запускаете несколько независимых инстансов Redis — тогда каждый займёт своё ядро.

Правда ли, что Redis 6+ многопоточный и снимает эту проблему?

Только частично. Io-threads распараллеливают чтение и запись сетевых сокетов, снижая накладные расходы при большом числе подключений. Но выполнение команд над структурами данных остаётся строго однопоточным, поэтому предел ops/sec для CPU-тяжёлых команд io-threads не сдвигают.

Как отличить упор в CPU от упора в сеть или диск?

Смотрите на загрузку конкретного ядра redis-server (не суммарную по хосту) и сверяйте с instantaneous_ops_per_sec — если ядро на 100%, а ops/sec не растёт при увеличении нагрузки, это CPU. Если ядро свободно, а задержка всё равно растёт, ищите насыщение сети или диска, если включена персистентность с частым fsync.

Есть ли смысл в Redis Cluster, если вся нагрузка идёт в один Sorted Set (лидерборд, счётчик)?

Сам по себе кластер не поможет: горячий ключ лежит в одном слоте на одном узле независимо от общего числа узлов. Такую структуру нужно логически разрезать на несколько ключей на уровне приложения и агрегировать частичные результаты.

Обязательно ли переходить на Redis Cluster, если предел найден?

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

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →