Мы в Jooble активно используем Redis как кеш и быструю базу данных. У нас два master-slave-кластера, которые выполняют в среднем 12 000 операций в секунду. А дальше — история о том, как наш кластер стал падать по «out of memory», и о сложном поиске причины.
Предыстория
На тот момент в Redis кешировались в основном такие данные:
- Пользовательские результаты поиска. Каждая запись занимает много памяти, с недолгим временем жизни (20 минут).
- Части HTML. Каждая запись занимает много памяти, короткое время жизни (до 10 минут).
- Тестовые группы пользователя. Мы используем A/B тесты (UI, тесты релевантности и т.д.) и в Redis храним розданные группы для пользователя. Записей очень много, и храним мы их долго (до 14 дней). Каждая запись имеет очень маленький размер.
В какой-то момент мы заметили, что наш кластер становится нестабильным. Redis часто падал по «out of memory».
Мы рассчитали, что на каждом из серверов должно быть занято около 20 Гб RAM. Вот только info memory
показывала совсем другое. В пиковые моменты Redis занимал 80 Гб оперативной памяти:
used_memory:59681435584 used_memory_human:55.58G used_memory_rss:65407717376 used_memory_rss_human:60.92G used_memory_peak:85893162568 used_memory_peak_human:79.99G total_system_memory:134991085568 total_system_memory_human:125.72G used_memory_lua:71680 used_memory_lua_human:70.00K maxmemory:0 maxmemory_human:0B maxmemory_policy:allkeys-lru mem_fragmentation_ratio:1.10 mem_allocator:jemalloc-3.6.0
Поиск проблемы
Вначале мы решили разобраться, из-за чего падал кластер. Redis занимал максимум 80 Гб RAM, но на сервере доступно 126 Гб. В чем тогда проблема?
Ответ нашли довольно быстро – это RDB Snapshots. В момент создания снепшота Redis делает fork()
текущего процесса, а это может потребовать выделения дочернему процессу такого же количества памяти, как и у родителя.
Пока проблема с большим потреблением ОЗУ не была решена — мы просто выключили сохранение RDB-снепшотов.
Далее мы выдвинули первую гипотезу увеличения потребления ОЗУ: в Redis попадает много больших по размеру записей. Решили проанализировать данные, которые хранили в Redis, найти самые тяжеловесные ключи, сгруппировать и определить виновника.
К счастью, свою утилиту для анализа писать не пришлось — мы воспользовались Redis Memory Analyzer и командой DEBUG OBJECT
для этих целей.
Результат получился неутешительный.Больше всего памяти занимали кеши результатов поиска, причем они занимали столько места на 1 элемент, сколько мы и ожидали. Наша первая гипотеза провалилась.
Корень зла
Используя Redis Memory Analyzer, мы наткнулись на странное явление. Каждый раз когда запускали анализ, уменьшалось потребление памяти.
Из документации мы знали, что Redis удаляет просроченные объекты двумя способами:
- Если вы запрашиваете просроченный объект, Redis его удаляет в этот же момент (пассивный механизм).
- Активный механизм очистки “устаревших” данных в бекграунде. Он работает постоянно.
Все указывало на то, что Redis Memory Analyzer активирует первый способ. Мы провели простой тест: просканировали все ключи в Redis и снизили объем потребляемой ОЗУ до ожидаемого размера (20 Гб).
Тогда выдвинули вторую гипотезу: что-то повлияло на активный механизм очистки данных, и Redis просто не успевает удалять весь мусор.
Документация к EXPIRE описывает активный механизм очистки ключей:
Redis 10 раз в секунду проделывает следующее: 1. Тестирует 20 случайных ключей из всего набора ключей в БД с проставленным TTL. 2. Удаляет все ключи с просроченным сроком жизни. 3. Если более 25% ключей удалены, возвращаемся к пункту 1.
Значит, большое количество долгоживущих ключей могло влиять на качество очистки ключей в целом.
Мы просмотрели ключи с самым большим сроком жизни и обнаружили, что не так давно начали кешировать легковесную запись для каждого пользователя на 14 дней. В количественном отношении это была самая большая группа ключей в нашем кластере.
В этот раз мы попали в точку:
- В Redis появилось очень много долгоживущих ключей (14 дней).
- Redis брал 20 случайных ключей, большая часть из которых относилась к «долгожителям». Алгоритм стал часто недобирать 25% удаленных ключей для запуска следующей итерации.
- В памяти стало хранится гораздо больше просроченных результатов поиска, которые зря занимают память.
- Мы получили большее потребление ОЗУ и нестабильный кластер.
Итоги
В результате для решения проблемы с очисткой ключей мы сделали следующее:
- Уменьшили время жизни этих записей.
- Перенесли долгоживущие записи в отдельную логическую DB в рамках того же Redis кластера. Таким образом, Redis проводит активную очистку в рамках каждой из логических баз, что в нашем случае агрессивнее очищает просроченые результаты поиска.
- Просканировали все ключи в Redis, чтобы очистить все просроченные (пассивный механизм очистки).
Теперь один экземпляр Redis занимает в 2-3 раза меньше места, как мы и ожидали изначально:
used_memory:23734192088 used_memory_human:22.10G used_memory_rss:27195826176 used_memory_rss_human:25.33G used_memory_peak:45282896632 used_memory_peak_human:42.17G total_system_memory:134991085568 total_system_memory_human:125.72G used_memory_lua:59392 used_memory_lua_human:58.00K maxmemory:0 maxmemory_human:0B maxmemory_policy:allkeys-lru mem_fragmentation_ratio:1.15 mem_allocator:jemalloc-3.6.0
Этот материал – не редакционный, это – личное мнение его автора. Редакция может не разделять это мнение.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: