Топ-5 узких мест производительности Redis и способы их устранения

Раскройте максимальную производительность ваших развертываний Redis с помощью этого важного руководства по распространенным узким местам. Научитесь выявлять и устранять такие проблемы, как медленные O(N) команды, чрезмерное количество сетевых往返ов, давление памяти и неэффективные политики вытеснения, накладные расходы на сохранение и операции, связанные с загрузкой ЦП. В статье представлены практические шаги, примеры и лучшие практики: от использования конвейеризации и `SCAN` до оптимизации структур данных и сохранения, что гарантирует быструю и надежную работу вашего экземпляра Redis для всех задач кэширования, обмена сообщениями и хранения данных.

Топ-5 узких мест производительности Redis и способы их устранения

Проблемы производительности Redis обычно кажутся загадочными, пока вы не вспомните одну вещь: Redis быстр, но он не освобожден от работы. Команда, которая обходит миллион ключей, все равно обходит миллион ключей. Клиент, который отправляет одну команду за сетевой往返, все равно платит за каждый往返. Сервер, у которого заканчивается память, все равно должен вытеснять, подкачивать, отклонять записи или выходить из строя в зависимости от конфигурации.

Когда Redis замедляется, не начинайте с изменения случайных настроек. Начните с доказательств:

redis-cli INFO
redis-cli SLOWLOG GET 20
redis-cli LATENCY DOCTOR
redis-cli INFO commandstats
redis-cli INFO memory

Эти команды обычно указывают на одно из пяти узких мест: медленные команды, сетевые往返ы, давление памяти, накладные расходы на сохранение или насыщение ЦП.

1. Медленные команды на больших данных

В Redis много крошечных операций с постоянным временем, но не каждая команда крошечная. Такие команды, как KEYS, большие LRANGE, SMEMBERS, HGETALL, ZRANGE по огромным диапазонам, SORT и длинные Lua-скрипты могут блокировать других клиентов во время выполнения.

Классический инцидент начинается с команды очистки или отладки:

KEYS *

На небольшом экземпляре для разработки она возвращается мгновенно. В рабочем пространстве ключей с миллионами ключей она может остановить сервер на время, достаточное для накопления запросов приложений. Та же ситуация происходит с хешем, который начинался как "несколько полей на пользователя" и незаметно превратился в гигантский объект.

Найдите доказательства:

redis-cli SLOWLOG GET 20
redis-cli INFO commandstats
redis-cli LATENCY LATEST

SLOWLOG записывает команды, превысившие настроенный порог. INFO commandstats показывает количество вызовов каждой команды и общее время. Если одна команда доминирует по времени, начинайте с нее.

Исправьте шаблон доступа:

redis-cli --scan --pattern 'user:*'

Используйте SCAN вместо KEYS для итерации по пространству ключей. Используйте HSCAN, SSCAN и ZSCAN для больших хешей, множеств и отсортированных множеств. Извлекайте страницы или диапазоны вместо целых структур:

LRANGE feed:user:42 0 49
ZRANGE leaderboard 0 99 WITHSCORES

Если объект стал слишком большим, разделите его в соответствии с тем, как приложение его читает. Один хеш user:42 с тысячами несвязанных полей может быть удобен для записи, но болезнен для чтения, которому нужны только настройки профиля. Отдельные ключи, такие как user:42:profile, user:42:prefs и user:42:counters, могут уменьшить объем данных, обрабатываемых за запрос.

Для удаления предпочитайте UNLINK, когда значения могут быть большими:

UNLINK old:large:set

UNLINK удаляет ключ из пространства ключей и освобождает память асинхронно. Он безопаснее, чем DEL, для больших значений, хотя массовая очистка все равно требует регулирования.

2. Слишком много сетевых往返ов

Redis может обработать команду за микросекунды, в то время как ваше приложение тратит миллисекунды на ожидание сети. Если путь запроса отправляет 50 последовательных команд Redis, сеть может доминировать в общем времени, даже если сам Redis здоров.

Это часто встречается в таком коде:

for user_id in user_ids:
    profile = redis.get(f"user:{user_id}:profile")

Каждый GET ждет собственного ответа, прежде чем начнется следующий. По сети это дорого.

Используйте конвейеризацию:

pipe = redis.pipeline(transaction=False)
for user_id in user_ids:
    pipe.get(f"user:{user_id}:profile")
profiles = pipe.execute()

Конвейеризация отправляет несколько команд, не дожидаясь каждого ответа по отдельности. Redis все еще выполняет команды по порядку, но клиент избегает платить за往返 для каждой команды.

Используйте многоключевые команды, где это уместно:

MGET user:1:profile user:2:profile user:3:profile

Не превращайте каждый запрос в один огромный конвейер. Большие конвейеры могут увеличить использование памяти и создать всплески ответов. Пакетируйте достаточно, чтобы устранить очевидные потери на往返ы, затем измеряйте.

Для путей с интенсивным чтением также проверьте, не запрашивает ли ваше приложение повторно значения из Redis, которые можно получить один раз и повторно использовать в течение запроса. Небольшой локальный кэш запросов может устранить случайные дублирующиеся чтения без изменения Redis.

3. Давление памяти и циклы вытеснения

Redis ориентирован на память. Как только память становится ограниченной, производительность ухудшается несколькими способами: вытеснение потребляет ЦП, записи могут завершаться ошибкой при noeviction, форки сохранения могут стать сложнее, реплики могут отставать, а операционная система может начать подкачку, если хост неправильно настроен или перегружен.

Проверьте память:

redis-cli INFO memory
redis-cli INFO stats | grep evicted_keys
redis-cli CONFIG GET maxmemory
redis-cli CONFIG GET maxmemory-policy

Важные признаки:

  • used_memory близко к maxmemory.
  • evicted_keys быстро увеличивается.
  • Хост выполняет подкачку.
  • Большие ключи потребляют больше памяти, чем ожидалось.
  • Истекающие ключи кэша на самом деле не имеют TTL.

Найдите большие ключи осторожно. Не выполняйте широкие дорогие команды во время пикового трафика. Выборка с помощью --bigkeys может помочь:

redis-cli --bigkeys

Для кэшей установите лимит памяти и политику вытеснения, соответствующую данным:

maxmemory 4gb
maxmemory-policy allkeys-lru

allkeys-lru или allkeys-lfu могут иметь смысл, когда все ключи являются записями кэша. volatile-lru вытесняет только ключи с TTL, что полезно, когда постоянные ключи находятся на одном экземпляре с ключами кэша. noeviction часто подходит для Redis, используемого в качестве основного хранилища данных, потому что молчаливое вытеснение данных, похожих на постоянные, было бы хуже, чем возврат ошибки.

Устанавливайте TTL при записи:

SET cache:product:123 "$json" EX 300

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

Также уменьшайте накладные расходы на ключи и значения, где это важно. Тысячи крошечных ключей могут стоить больше метаданных, чем ожидалось. Иногда компактный хеш лучше, чем много отдельных ключей; иногда верно обратное, потому что чтениям нужно только одно поле. Измеряйте с реальными шаблонами доступа, а не предполагайте, что одна форма всегда лучшая.

4. Сохранение и задержки дискового ввода-вывода

Сохранение защищает данные, но оно вводит поведение диска и форка, которое вам нужно понимать. RDB-снимки и AOF-перезаписи обычно являются фоновыми операциями, но они все равно могут вызывать задержки из-за времени форка, давления памяти при копировании при записи и дискового ввода-вывода.

Проверьте состояние сохранения:

redis-cli INFO persistence
redis-cli LATENCY LATEST
iostat -xz 1

Ищите неудачные фоновые сохранения, длительное время форка, активность AOF-перезаписи и насыщение диска. Если всплески задержек совпадают с BGSAVE или BGREWRITEAOF, настройка сохранения должна быть в списке приоритетов.

Для AOF основная настройка долговечности/производительности:

appendfsync everysec

everysec — обычный сбалансированный выбор. always синхронизирует каждую запись и может быть очень медленным. no оставляет синхронизацию операционной системе и принимает больший риск потери данных при сбое.

Для RDB избегайте правил снимков, которые срабатывают постоянно при интенсивной рабочей нагрузке записи, если это не намеренно:

save 900 1
save 300 10
save 60 10000

Эти примеры по умолчанию не автоматически подходят для каждой рабочей нагрузки. Высоконагруженный Redis, используемый как одноразовый кэш, может вообще не нуждаться в сохранении. Экземпляр Redis, используемый как очередь заданий или хранилище сессий, вероятно, нуждается, но допустимое окно потерь должно быть четким.

Если сохранение конкурирует с трафиком приложения, рассмотрите:

  • Более быстрое локальное SSD-хранилище.
  • Отделение сохранения Redis от других дисковых служб.
  • Запуск сохранения на реплике, когда первичный может выдержать такую конструкцию.
  • Поддержание размера набора данных ниже того, который хост может комфортно форкнуть.
  • Установка Linux vm.overcommit_memory=1, где Redis рекомендует это для фоновых сохранений.

Не отключайте сохранение вслепую, чтобы "исправить производительность", если данные действительно не являются одноразовыми. Это может улучшить график, но превратить перезапуск в потерю данных.

5. Насыщение ЦП и однопоточное выполнение команд

Выполнение команд Redis в основном однопоточное, даже если современный Redis использует дополнительные потоки для некоторого ввода-вывода и фоновой работы. Если одно ядро занято Redis, добавление большего количества простаивающих ядер на том же экземпляре может не помочь горячему пути команд.

Проверьте хост и смесь команд Redis:

top -H -p $(pgrep redis-server)
redis-cli INFO commandstats
redis-cli SLOWLOG GET 20
redis-cli INFO clients

Распространенные причины загрузки ЦП:

  • Большие операции с множествами, отсортированными множествами, списками или хешами.
  • Тяжелые Lua-скрипты.
  • Накладные расходы на сжатие или сериализацию в приложении, приводящие к большим значениям, чем ожидалось.
  • Очень высокий разброс Pub/Sub.
  • Дорогое вытеснение при давлении памяти.
  • Слишком много подключений, постоянно переподключающихся или отправляющих маленькие команды.

Исправьте ЦП, уменьшая работу, разделяя работу или распределяя работу.

Уменьшите работу, изменив команды и формы данных. Если вам нужно только 50 элементов, не извлекайте 5000. Если каждый запрос разбирает JSON-блоб размером 500 КБ для чтения одного флага, разделите этот флаг на меньший ключ или поле.

Разделите работу, переместив длинные циклы на клиенты с помощью инкрементальных сканирований:

HSCAN big:hash 0 COUNT 100

Распределите работу с помощью реплик для чтения или Redis Cluster для шардирования. Реплики помогают при трафике с интенсивным чтением, но они не делают записи дешевле на первичном. Redis Cluster распределяет ключи по первичным, что может увеличить общую емкость ЦП и памяти, но также добавляет операционную сложность и ограничения по слотам ключей.

Для Pub/Sub следите за выходными буферами и разбросом:

redis-cli PUBSUB NUMSUB events:updates
redis-cli CLIENT LIST

Медленный подписчик может превратиться в давление памяти. Тысячи подписчиков могут превратить одну публикацию в большой объем сетевого вывода. Если Pub/Sub тяжелый, рассмотрите изоляцию его на отдельном экземпляре Redis.

Быстрый процесс триажа

Когда задержка Redis возрастает, выполните эти проверки по порядку:

redis-cli --latency
redis-cli SLOWLOG GET 10
redis-cli LATENCY DOCTOR
redis-cli INFO memory
redis-cli INFO clients
redis-cli INFO persistence
redis-cli INFO commandstats

Затем спросите:

  • Появилась ли медленная команда?
  • Достигла ли память maxmemory или началось вытеснение?
  • Начало ли сохранение сохранение или перезапись?
  • Увеличилось ли количество подключенных клиентов или заблокированных клиентов?
  • Взорвалось ли количество вызовов или время одной команды?
  • Изменили ли развертывания приложения шаблоны доступа к Redis?

Большинство узких мест Redis не решаются одной волшебной настройкой. Они решаются путем уменьшения рабочей нагрузки, ее инкрементализации, пакетирования или лучшей изоляции. Лучшие развертывания Redis скучны: ключи истекают, когда должны, большие операции разбиты на страницы, клиенты разумно используют конвейеры, настройки сохранения соответствуют ценности данных, а мониторинг улавливает тенденцию до того, как ее почувствуют пользователи.

Что измерять до и после исправления

Исправление производительности реально только в том случае, если график меняется для рабочей нагрузки, которая имеет значение. Перед изменением кода или конфигурации захватите небольшой базовый уровень:

redis-cli INFO stats
redis-cli INFO commandstats
redis-cli INFO memory
redis-cli INFO persistence
redis-cli SLOWLOG GET 20

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

После изменения сравните:

  • p50, p95 и p99 задержки приложения для запросов, поддерживаемых Redis.
  • Задержку команд Redis, а не только задержку запросов.
  • Записи Slowlog по командам.
  • Скорость вытеснения и свободное место в памяти.
  • Подключенных клиентов и отклоненные подключения.
  • Время форка сохранения и статус AOF/RDB.
  • Отставание реплики, если реплики обслуживают чтения или защищают долговечность.

Будьте подозрительны к исправлениям, которые только перемещают боль. Например, большой конвейер может уменьшить задержку запросов, но увеличить всплески памяти. Отключение AOF может устранить задержку диска, но ослабить восстановление. Увеличение maxmemory может отсрочить вытеснение, но истощить хост, если машина уже была общей.

Одна полезная практика — написать небольшой нагрузочный тест вокруг точного шаблона Redis, который вы изменили. Если старый код делал 40 последовательных GET, протестируйте последовательные GET против MGET или конвейеризации с реалистичными размерами полезной нагрузки. Если старый код использовал HGETALL, протестируйте HGET для полей, которые действительно нужны запросу. Настройка Redis намного проще, когда вы тестируете форму, которую фактически запускаете, а не общее число "операций Redis в секунду".

Наконец, держите откат простым. Изменение производительности Redis часто одновременно находится в коде приложения, настройках клиента и конфигурации сервера. Меняйте одну вещь, когда можете. Если вы должны изменить несколько вещей, запишите, какой симптом должно улучшить каждое изменение. Это предотвратит наследование следующему инженеру кучи загадочных настроек, которые никто не хочет удалять.