Чуть более года назад мне посчастливилось погрузиться в углублённое изучение Redis. Всё, что я знал про него на тот момент, это две команды — get и set. Примерно в это же время у нас начался плавный переход со Standalone Redis на Redis Cluster.

Почитать про переход на кластер можно тут, а сегодня я хочу рассказать о том, что я узнал, о проблемах, которые могут возникнуть, как их отлавливать и что со всем этим делать.

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

  • Невозможность подключиться из-за скопившихся клиентов.

  • Неоправданно большое количество соединений по причине некорректной настройки таймаутов или непредвиденной конфигурации клиентов.

  • Потеря данных до истечения их реального срока жизни в связи с вытеснением.

  • Ненормированное распределение соединений, команд или сетевой нагрузки между нодами кластера.

  • Слишком большой коэффициент кеш-промахов.

Проблемы, возможности и искусственные ограничения

Контроль за использованием команд

Неосторожные действия клиентов могут серьёзно навредить работе приложения. Мы можем попытаться уменьшить вероятность вреда, и в этом нам поможет встроенный механизм ACL, который разделяет пользователей и их возможности.

ACL поставляется с 6 версией Redis. Если у вас нет возможности обновить приложение, то создатели рекомендуют использовать rename-command

Если у вас есть необходимость и возможность создавать инструменты для разработчиков, то следует закладывать ограничения на уровне библиотеки, не реализуя опасные команды: flushdb, flushall, keys и т.д.

ACL

ACL (Access Command List) поддерживает не только выборочную блокировку команд, но и заготовленные группы. Если вы уверены что нет никого, кто бы использовал нежелательные команды, то стоит прибегнуть к разделению прав на уровне пользователей.

Рассмотрим пример, в нём показано, как может выглядеть файл с заготовленным пользователем cache. У него не будет доступа к ключам, которые не сопоставляются с образцом cache:*, а также отсутствует возможность пользоваться командами, которые могут помешать другим пользователям:

# Пользователь **cache** не должен выполнять долгие запросы и может работать только
# с ключами в пространстве имён **cache:** такая схема поможет избежать чтения
# данных непредназначенных для данного пользователя
user cache -@dangerous -@admin -@slow -@blocking ~cache:* on >password_1

Пользователь admin должен обладать всеми возможностями
Не следует задавать пароль в открытом виде: >password_1
Пользуйтесь sha256 хешем #<hash>
user admin +@all on #cbd908627c922e9c4589ba7ae04d332861462c8300868c3e9f6f5da628cedcb

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

Главная боль — сканирование и поиск по паттерну

Во время переезда на Redis Cluster одной из основных проблем стал отказ от поиска по шаблону (KEYS) и не заканчивающегося сканирования (SCAN).

Поиск по паттерну выполняется при помощи команды KEYS, которая является блокирующей операцией и выполняется за О(n), где n — количество ключей в базе. Использовать эту команду в production-окружении крайне не рекомендуется, но если воспользовались, то следует отказаться в пользу сканирования.

Сканирование (команда SCAN) также выполняется за O(n), где n — переданное клиентом количество строк для чтения за одну итерацию. SCAN не панацея и может оказаться бесполезной альтернативой, ведь если вы не ограничиваете строго выборку максимального значения за одну итерацию, то ничто не мешает появлению внезапных длительных блокирующих запросов (если верить документации, то сканирование 1 миллиона ключей на локальном ноутбуке может занимать 40 мс).

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

Решить проблему блокирующего поиска по шаблону можно несколькими способами:

  • Если у вас standalone-конфигурация, то поможет разбивка ключей по разным базам. Также можно развернуть отдельную версию Redis, и тогда при использовании SCAN ключи будут перебираться только внутри конкретной базы и только среди необходимого набора ключей.

  • В случае с кластерами:

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

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

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

Конфигурация

Упомянутые параметры статистики можно посмотреть, выполнив команду INFO.

maxclients

Определяет максимальное количество одновременно подключенных клиентов. Слишком маленькое значение может привести к недоступности Redis'а, клиенты будут получать сообщение об ошибке: max number of clients reached. Если такое поведение и предполагалось, то стоит обратите внимание, какими именно клиентами заполнено пространство. Возможно, это простаивающие соединения. Увидеть количество клиентов, которым было отказано в установлении соединения, можно через параметр rejected_connections.

timeout

По умолчанию timeout установлен в 0, это означает, что сервер не будет закрывать соединения даже после долгого простоя. Такая ситуация может возникнуть из-за неправильной конфигурации клиента, который не закрывает соединения. Привести это может к большому количеству открытых соединений и невозможности подключиться к серверу (при достижении maxclients).

Как может выглядеть список клиентов при плохо настроенном клиенте:

id=26490 addr=192.168.0.1:12840 fd=6811 name= age=72920 idle=72635 flags=N db=135 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
id=2430 addr=192.168.0.2:58482 fd=675 name= age=73514 idle=73514 flags=N db=135 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=set
id=2431 addr=192.168.0.3:33384 fd=676 name= age=73514 idle=73513 flags=N db=135 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=mget

Установка этого параметра не избавит вас от некорректно настроенного клиента, но может помочь увеличить время жизни приложения за счёт принудительного обрыва простаивающего соединения.

Текущее количество клиентов можно посмотреть в статистике через параметр connected_clients. Следует настроить уведомление, когда значение приближается к максимально допустимому. Также стоит обратить особое внимание на idle-соединения, их наличие поможет выявить клиенты с дефектами. Отдельного параметра, показывающего количество таких клиентов, нет, но посчитать их можно, выполнив команду:

redis-cli client list | grep -v "idle=0" | wc -l

tcp-keepalive

По умолчанию имеет значение 300, полезен для обнаружения мёртвых клиентов.

Если timeout и tcp-keepalive установлены в 0, то соединения могут находиться в состоянии idle до перезапуска сервера.

maxmemory

Задаёт максимально возможное количество потребляемой памяти. Указывать можно как абсолютные значения, так и проценты. Рекомендуется установить ограничение, если на сервере с Redis есть и другие приложения. Тогда Redis не сможет захватить всё свободное пространство. При достижении указанного значения система попытается высвободить память, руководствуясь алгоритмом, указанным в maxmemory-policy:

  • volatile-lru — вытесняются значения, которые дольше всего не запрашивались, у которых задан срок действия;

  • allkeys-lru — вытесняются значения, которые дольше всего не запрашивались;

  • volatile-lfu — вытесняются значения, которые наименее часто используется, у которых задан срок действия;

  • allkeys-lfu — вытесняются значения, которые наименее часто используется;

  • volatile-random — вытеснение случайных ключей у которых задан срок действия;

  • allkeys-random — вытеснение случайных ключей;

  • volatile-ttl — вытеснение ключей, у которых истекает срок действия;

  • noeviction — отключает вытеснение, будет просто возвращаться ошибка.

Следить за количеством вытесненных ключей можно в статистике через параметр evicted_keys. Чтобы заметить момент, когда память начинает заканчиваться, стоит настроить уведомления на perc_memory. Этот параметр можно посчитать по формуле:

perc_memory = used_memory / maxmemory * 100

Далее используйте perc_memory в качестве порогового значения, рекомендуется сигнализировать о проблеме, если значение приближается к 85.

requirepass

Установка пароля не является обязательной процедурой, и по умолчанию параметр остаётся пустым. Стоит заметить, что отсутствие пароля может привести к потере, компрометации и порче данных, а также к несанкционированному доступу на сервер (https://book.hacktricks.xyz/pentesting/6379-pentesting-redis). Не стоит забывать, что Redis может находиться не только в защищённом от посторонних глаз production-окружении, но и на тестовом стенде, откуда можно получить исходный код вашего приложения.

unixsocket

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

Эффективность использования

В этом разделе мы не будем рассматривать системные метрики вроде CPU и RAM, а сосредоточимся на внутренней статистике приложения.

Здесь представлены не все параметры, часть советов по отслеживанию эффективности уже дана в разделе «Конфигурация».

Представленные ниже параметры полезно выводить на графиках чтобы следить за их изменениями.

Ключи

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

?  ~ redis-cli info keyspace
# Keyspace
db0:keys=4234823493,expires=72334549,avg_ttl=42892

Кэш-промахи

Эта метрика покажет, сколько раз запрашивали данные, которых уже нет в хранилище. Вычисляется по формуле:

hit_rate = keyspace_hits / (keyspace_hits + keyspace_misses)

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

?  ~ redis-cli info stats | grep keyspace
keyspace_hits:480683
keyspace_misses:65

Клиенты

Эта метрика показывает количество клиентов в единицу времени. В случае с кластером и проблемой неравномерного распределения будет заметно, что на какой-то из нод клиентов в разы больше.

Клиенты, выполняющие блокирующие операции

Операции над списками (BLPOP, BRPOP, BRPOPLPUSH, BLMOVE, BZPOPMIN, BZPOPMAX) могут приводить к длительным блокировкам. Увидеть количество таких клиентов можно через параметр blocked_clients. Эти команды будут блокировать до тех пор, пока список, с которым они работают, не перестанет быть пустым или пока не отвалятся по таймауту. Настройте мониторинг увеличения этого параметра, если он растёт, то это звоночек, что ваше приложение внезапно может стать медленным.

?  ~ redis-cli info clients | grep blocked
blocked_clients:5

Большой разрыв между количеством клиентов на нодах кластера

Количество подключений на нодах может отличаться на два порядка и более. Причины разные: клиент, который не закрывает соединение и при первом подключении обращается к «первой» ноде за картой слотов, или неравномерное распределение ключей. Следить за этим поможет комплексный набор метрик ключей и соединений по всем нодам или профилирование кластера и составление отчёта.

Пропускная способность

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

?  ~ redis-cli info stats | grep ops
instantaneous_ops_per_sec:73

Обработка грядки инсектицидами

Для упрощения профилирования конфигурационных файлов была написана утилита insecticide, она поможет упростить и ускорить анализ конфигурационных файлов, найдёт возможные проблемы или даст советы по настройке вашего Redis'а.

Подробную инструкцию по установке и использованию вы можете найти в репозитории: https://github.com/city-mobil/insecticide

Пример использования

Результатом работы будет отчёт с рекомендацией, причиной и критичностью. Для получения отчёта достаточно передать путь до конфигурационного файла redis.conf:

?  ~ insecticide --redis-version 6 --config=/etc/redis/redis.conf
Parameter: timeout
[CRITICAL]
Advice: Set timeout
Reason: If timeout equal 0, clients connections won't be closed. They will be in idle status until server will be restart.

Parameter: tcp-keepalive
[CRITICAL]
Advice: For better experience you should use default value: 300 if you don't have any reason for change it.
Reason: This option is useful in order to detect dead peers (clients that cannot be reached even if they look connected). Moreover, if there is network equipment between clients and servers that need to see some traffic in order to take the connection open, the option will prevent unexpected connection closed events.
Parameter: maxmemory
[CRITICAL]
Advice: Parameter maxmemory is not set. Index: 0
Reason: Set variable for Param maxmemory and Index 0
Parameter: appendonly
[WARNING]
Advice: Read this page and make decision: <https://redis.io/topics/persistence>
Reason: Persistence disabled. Your data just exists as long as the server is running
Parameter: loglevel
[WARNING]
Advice: In production use a less aggressive logging policy (notice or warning)
Reason: Many rarely useful info, but not a mess like the debug level
Parameter: requirepass
[CRITICAL]
Advice: Parameter requirepass is not set. Index: 0
Reason: Set variable for Param requirepass and Index 0

Планы

В ближайших планах — сделать инструмент, который позволит подробно анализировать Redis (в том числе кластер), чтобы выяснить:

  • равномерность распределения ключей;

  • разницу между количеством клиентов между нодами;

  • долгие idle-соединения;

  • разницу в размере данных между нодами кластера;

  • долю кеш-промахов;

  • недавний slowlog.

Подводя итог

Хочется отметить, что всё индивидуально и эффективность использования зависит от того, какие данные вы храните и насколько они вам нужны. Добиться максимального результата для кеш-сервера, шины данных или постоянного хранилища — это абсолютно разные задачи, и приведённые в статье советы подходят не для каждого случая. Из общих рекомендаций я бы выделил:

  1. Следите за количеством подключённых клиентов и за библиотеками, которыми пользуются разработчики, проводите аудит сервисов.

  2. Настройте мониторинг состояния системы в реальном времени: количество команд, кеш-промахов, ключей, нагрузка на сеть, CPU, RAM.

  3. По возможности ограничивайте использование команд из групп admin, dangerous, slow и blocking.

  4. Заведите чек-лист и проверяйте redis.conf перед вводом сервера в эксплуатацию.