Как стать автором
Обновить
Флант
DevOps-as-a-Service, Kubernetes, обслуживание 24×7

Наша новая удачная попытка бесшовной замены Redis на KeyDB

Время на прочтение 5 мин
Количество просмотров 13K

Мы уже как-то рассказывали о базе данных KeyDB — форке Redis, разработка которого началась в 2019 году. Проект распространяется под свободной лицензией BSD, и у него уже почти 6k звезд на GitHub. Авторы в свое время столкнулись с проблемами производительности оригинала и пошли хардкорным путём: взяли всё в свои руки и привнесли много нового как в части многопоточности, так и в других областях.

В статье делимся еще одним положительным опытом замены Redis на KeyDB.

В одном из клиентских проектов у нас довольно нагруженный Redis. Поначалу мы использовали spotahome/redis-operator для реализации Redis Failover в режиме master/slave. Но с ростом проекта стали банально упираться в гигабитную сеть на узле с master-Redis'ом: независимо от количества реплик, вся нагрузка всегда приходилась на мастер-узел, а реплики были «на подхвате».

Тогда мы решили переехать на Redis-кластер: ключи шардируются между несколькими master'ами, у каждого master'а есть реплики. Это избавило от проблемы с сетевой загрузкой, так как данные расползлись по нескольким шардам, и нагрузка, соответственно, тоже распределилась.

Казалось бы, вопрос с ресурсами был закрыт надолго (на такой схеме мы проработали около года). Но беда пришла, откуда не ждали.

Проблема с однопоточностью Redis

После переезда сервиса из одного дата-центра в другой, приложение на PHP вдруг стало работать медленно. Одно из подозрений упало на время ответа Redis, хотя, на первый взгляд, с ним все было неплохо.

Для проверки написали простой тест, эмулирующий работу PHP-приложения с Redis:

<?php
$start=microtime(true);
$redis = new RedisCluster(NULL, Array('redis-cluster:6379'));
$key='test'.rand(0,10000);
$redis->set($key,'test_data',10);
$redis->get($key);
echo (microtime(true)-$start)."\n";

Погоняли тест, и результат получился неожиданным — иногда Redis действительно отвечал медленно:

0.003787
0.144506
0.007667
0.005908
0.00354
0.003886
0.006331
0.193661
0.222443
0.00558
0.0029

Присмотревшись к проблеме внимательнее, мы обнаружили, что master одного из шардов потребляет почти 100% ресурсов одного ядра. Тут уже вспомнилось, что Redis однопоточный, а значит он просто не может обработать больше запросов. 

В новом дата-центре мы переехали на другое железо, в целом более мощное. Но производительность на одно ядро у новых процессоров ниже, чем у предыдущих — ранее были «железные» Intel(R) Xeon(R) CPU @ 3.40GHz, а теперь — vSphere с Intel(R) Xeon(R) Gold 6132 CPU @ 2.60GHz. В результате именно это и вылилось в задержки при обращении к некоторым ключам в Redis'е, а также в целом в работе приложения.

Так это выглядело на машинах в исходном кластере (здесь и далее по оси ординат — используемые процессом ядра CPU):

А так — в новом, когда пришла полноценная нагрузка:

Решение

Что делать? 

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

Пришла идея попробовать заменить его на KeyDB. Просто взять и поменять образ redis в контейнере Kubernetes на keydb — должно сработать, ведь разработчики KeyDB заявляют, что структура данных Redis поддерживается без изменений.

Для начала провели опыт в тестовом окружении: в кластер Redis'а записали сотню случайных ключей, заменили образ в контейнере на образ eqalpha/keydb, а команду запуска — с redis-server -c /etc/redis.conf на keydb-server -c /etc/redis.conf --server-threads 4. Затем перезапустили по одному Pod'ы (кластер запущен как набор StatefulSet c updateStrategy OnDelete).

Контейнеры перезапустились по одному, подключились к кластеру и синхронизировались. Все данные на месте: сотня тестовых ключей, которые мы записали в кластер Redis'а, прочиталась из кластера KeyDB.

Все прошло успешно, поэтому мы решились проделать то же самое и с рабочим кластером. Поменяли образ в конфигурации, подождали, пока обновятся Pod'ы и стали наблюдать за временем ответа от всех шардов — теперь оно стало одинаковое для всех. 

На графике видно, что новые «Redis'ы» потребляют более одного ядра:

Мониторинг задержек

Чтобы в дальнейшем мониторить скорость ответа Redis-кластера, мы написали небольшое приложение на Go. Раз в секунду оно обращается к кластеру и помещает в него ключ со случайным именем (можно сконфигурировать префикс имени ключа) и TTL 2 сек. Случайное имя использовано для того, чтобы попадать в разные шарды кластера и сделать результат более приближенным к работе реального приложения. Время, затраченное на операцию соединения и записи, сохраняется. Хранятся 60 последних измерений.

В случае, если операция записи не удалась, увеличивается счетчик неудачных попыток. Неудачные попытки не учитываются при расчете среднего времени ответа. 

Приложение экспортирует метрики в формате Prometheus: максимальное, минимальное и среднее время операции за последние 60 сек., а также количество ошибок:

# HELP redis_request_fail Counter redis key set fails
# TYPE redis_request_fail counter
redis_request_fail{redis="redis-cluster:6379"} 0
# HELP redis_request_time_avg Gauge redis average request time for last 60 sec
# TYPE redis_request_time_avg gauge
redis_request_time_avg{redis="redis-cluster.:6379"} 0.018229623
# HELP redis_request_time_max Gauge redis max request time for last 60 sec
# TYPE redis_request_time_max gauge
redis_request_time_max{redis="redis-cluster:6379"} 0.039543021
# HELP redis_request_time_min Gauge redis min request time for last 60 sec
# TYPE redis_request_time_min gauge
redis_request_time_min{redis="redis-cluster:6379"} 0.006561593

График с результатами:

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

Код приложения доступен в репозиториии. Также можно воспользоваться подготовленным Docker-образом.

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

CONFIG SET latency-monitor-threshold 100

Но это видение метрики со стороны Redis, а мы хотим наблюдать время ответа и со стороны приложения.

Многопоточность в Redis

В Redis 6 уже реализована многопоточность, впрочем, судя по описанию, не так эффективно, как в KeyDB или Thredis. Для активации этого режима нужно добавить параметр io-threads 4. Прием запросов, парсинг, обработка и отправка будут происходить в разных потоках. Это может быть полезно, когда размер ключей очень большой: в однопоточном режиме Redis не будет принимать и обрабатывать новые запросы, пока не будет отправлен ответ на предыдущий запрос.

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

Итог

Мы еще раз проверили и убедились, что в сложной ситуации Redis можно масштабировать вертикально, просто заменив его на KeyDB. У такого способа нет сложных подводных камней, поскольку KeyDB — это форк Redis’а, и он должен без проблем подхватывать данные от оригинального проекта.

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

P.S.

Читайте также в нашем блоге:

Теги:
Хабы:
+45
Комментарии 19
Комментарии Комментарии 19

Публикации

Информация

Сайт
flant.ru
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель
Тимур Тукаев