Сценарий до боли знакомый. Redis работал как часы, пока однажды не упёрся в потолок памяти. Дальше возможны два неприятных исхода. Либо операционная система замечает прожорливый процесс и убивает его, унося с собой весь кэш. Либо Redis сам начинает выбрасывать ключи, и среди вылетевших оказываются те самые, что выбрасывать было нельзя: флаги конфигурации, активные сессии, важные счётчики. И то и другое происходит из-за двух настроек, которые по умолчанию выставлены совсем не так, как нужно боевому сервису. Разберём, как настроить предел памяти и политику вытеснения, чтобы Redis отдавал ненужное и берёг ценное.
Почему Redis без предела памяти это бомба замедленного действия
По умолчанию Redis не имеет лимита памяти. Соответствующая настройка равна нулю, а ноль означает безграничность. На практике это значит, что Redis будет расти, пока на сервере не закончится оперативная память. А когда у машины под управлением Linux заканчивается память, в дело вступает специальный механизм ядра, который выбирает процесс на уничтожение. Угадайте, кого он выберет на сервере, где Redis раздулся до пределов. Падение всего сервиса целиком, без предупреждения, с полной потерей содержимого кэша.
Первое правило поэтому звучит безоговорочно: на боевом сервере предел памяти обязан быть выставлен. Разумная отправная точка это около шестидесяти или семидесяти процентов доступной оперативной памяти на выделенном под Redis сервере. Остаток нужен под служебные накладные расходы, фрагментацию и пиковые всплески.
# Проверить текущий лимит (0 означает безлимит)
redis-cli CONFIG GET maxmemory
# Выставить лимит динамически, без перезапуска
redis-cli CONFIG SET maxmemory 6gb
Те же значения стоит прописать и в конфигурационном файле, чтобы они пережили перезапуск. В свежих версиях лимит можно задавать и в процентах от доступной памяти, что удобно при изменении размера машины.
Что вообще происходит в момент достижения лимита
Понимание механики снимает половину вопросов. Когда приходит команда записи, а Redis уже на пределе или выше него, он сначала пытается вытеснить ключи и только потом выполнить запись. Что именно вытеснять, решает выбранная политика. Если же политика запрещает вытеснение и освободить место нельзя, Redis возвращает ошибку о том, что команда не разрешена при превышении памяти. Важная деталь: команды, которые только читают существующие данные, продолжают работать как ни в чём не бывало даже в этом состоянии.
Ещё один нюанс, о котором забывают: вытеснение это фоновый процесс, и ключи удаляются не мгновенно по достижении лимита. При высокой скорости записи поток новых данных может обгонять вытеснение, и тогда даже при настроенной политике возникает состояние нехватки памяти. То есть политика вытеснения не отменяет необходимости держать запас по памяти.
Чем отличаются восемь политик и какую выбрать под свою задачу
Redis предлагает несколько политик вытеснения, и разница между ними принципиальна. Все они делятся по двум осям: на каких ключах работать и по какому критерию выбирать жертву.
По охвату ключей политики бывают двух типов. Семейство с приставкой allkeys рассматривает вообще все ключи как кандидатов на вылет. Семейство с приставкой volatile трогает только те ключи, которым явно задано время жизни. Это различие и есть ключ к защите важных данных, к нему вернёмся отдельно.
По критерию выбора жертвы вариантов больше. Политика на основе давности обращения выбрасывает ключи, к которым дольше всего не обращались. Политика на основе частоты обращения, доступная начиная с четвёртой версии, выбрасывает те, к которым обращаются реже всего за всё время. Есть политика случайного выбора, есть политика по ближайшему истечению времени жизни, выбрасывающая ключи с самым коротким остатком. И есть особая политика полного запрета вытеснения, при которой ничего не выбрасывается, а запись просто начинает возвращать ошибку.
Выбор политики напрямую вытекает из того, что хранит сервис. Для чистого кэша, где любые данные можно пересоздать из базы, подходят политики семейства allkeys на основе давности или частоты обращения. Для хранилища сессий, где смешаны постоянные и временные данные, разумнее политика семейства volatile. Для основного хранилища данных вроде очереди, где потеря недопустима, выбирают полный запрет вытеснения и навешивают тревогу на заполнение памяти.
Чем давность обращения отличается от частоты и когда это важно
Между двумя самыми популярными критериями, давностью и частотой обращения, есть тонкая, но важная разница. Политика на основе давности хороша там, где работает временная локальность: недавнее обычно и есть актуальное. Её слабость в уязвимости к операциям массового сканирования, которые проходят по множеству ключей разом и засоряют кэш, вытесняя реально горячие данные свежими, но одноразовыми.
Политика на основе частоты обращения устойчивее к такому засорению. Она отслеживает, как часто к ключу обращаются, и держит в памяти именно популярные элементы, а редкие выбрасывает. Это лучший выбор для стабильного рабочего набора, где важна именно частота, а не свежесть. Классический пример это каталог товаров: у разных позиций разная популярность, и политика на основе частоты сохранит в кэше ходовые товары, не давая редкому всплеску интереса к экзотике вытеснить бестселлеры.
# redis.conf для кэша каталога с горячими и холодными ключами
maxmemory 8gb
maxmemory-policy allkeys-lfu
maxmemory-samples 10
lfu-log-factor 10
lfu-decay-time 30
Тонкая настройка частотной политики идёт через два параметра. Фактор логарифмического счётчика определяет, сколько обращений нужно, чтобы счётчик частоты насытился, а время затухания задаёт, как быстро забывается накопленная частота. Для каталога, где популярность меняется медленно, затухание выставляют на десятки минут, чтобы вчерашний хит не вылетел из-за пары часов затишья.
Почему вытеснение в Redis приблизительное и зачем крутить размер выборки
Тут кроется деталь, которую многие не осознают. Redis не реализует настоящие давность и частоту обращения. Полноценный список давности потребовал бы отслеживать каждое обращение к каждому ключу в упорядоченной структуре, а это съело бы слишком много памяти и процессора при миллионах ключей. Вместо этого Redis использует приближение: он берёт небольшую случайную выборку ключей и среди них выбирает жертву по старости обращения или частоте.
Размер этой выборки настраивается и по умолчанию равен пяти. Чем больше выборка, тем точнее приближение к идеальному алгоритму, но тем больше расход процессора. Значение десять заметно приближает поведение к настоящему алгоритму давности при умеренных дополнительных затратах.
# Поднять точность вытеснения за счёт небольшого роста нагрузки на CPU
redis-cli CONFIG SET maxmemory-samples 10
Под капотом всё держится на хитрой экономии. Для давности обращения Redis хранит на каждый ключ всего двадцать четыре бита с отметкой времени, а для частоты переиспользует те же биты под логарифмический счётчик с затуханием. Посмотреть приблизительную частоту обращения к конкретному ключу можно специальной командой, но работает она только при активной частотной политике.
Как защитить критичные ключи от вылета
Вот теперь самое важное, ради чего вся статья. Если в Redis лежат вперемешку и пересоздаваемый кэш, и ключи, терять которые нельзя, то политика семейства allkeys опасна: она с равной готовностью выбросит и кэш страницы, и флаг конфигурации. Решение элегантно простое и опирается на различие охвата ключей.
Приём состоит в том, чтобы навешивать время жизни только на те ключи, которые можно вытеснять, и не навешивать его на критичные. Затем выбирается политика семейства volatile, которая трогает исключительно ключи со временем жизни.
# Постоянный ключ без срока жизни — его не тронут
SET config:app "production"
# Кэш-ключ со сроком жизни — только такие пойдут под нож
SET cache:page:home "<html>..." EX 3600
CONFIG SET maxmemory-policy volatile-lru
В этой схеме конфигурационный ключ без срока жизни физически не может стать кандидатом на вытеснение, потому что политикой охвачены только ключи с явным временем жизни. Но у схемы есть коварная оговорка, о которой нужно помнить. Политики семейства volatile ведут себя как полный запрет вытеснения, если ни у одного ключа нет заданного времени жизни. То есть если вся нагрузка вдруг окажется без сроков, Redis при заполнении памяти начнёт возвращать ошибки записи вместо вытеснения. Поэтому при выборе volatile-политики критично следить, чтобы вытесняемые ключи действительно получали время жизни.
За какими метриками следить, чтобы поймать проблему до аварии
Настроить политику мало, нужно проверять, что бюджет памяти вообще достаточен. Главная метрика это скорость вытеснения ключей, видимая в статистике. Если ключи вытесняются постоянно и помногу, это сигнал, что памяти не хватает и пора либо увеличивать лимит, либо разбираться, что распухло.
# Сколько ключей вытеснено всего
redis-cli INFO stats | grep evicted_keys
# Наблюдать за скоростью вытеснения в реальном времени
watch -n 1 'redis-cli INFO stats | grep evicted_keys'
# Общая картина по памяти и фрагментации
redis-cli INFO memory
Отдельно стоит присматривать за коэффициентом фрагментации памяти. Крупные значения ключей при вытеснении оставляют память фрагментированной, и сервер может упереться в нехватку памяти даже при формально невысоком отношении использованной памяти к лимиту. Падающий коэффициент попаданий в кэш при этом часто намекает на неверно выбранную политику вытеснения, когда из памяти вылетает то, что на самом деле нужно.
Хорошая профилактика проще лечения. Стоит проактивно навешивать время жизни на кэш-ключи, чтобы они истекали сами, не дожидаясь давления по памяти. Вытеснение под давлением создаёт дополнительную нагрузку на сервер, тогда как плановое истечение проходит мягче. Связка из выставленного лимита памяти, политики, подобранной под характер данных, аккуратного использования времени жизни для защиты критичных ключей и наблюдения за скоростью вытеснения превращает капризное хранилище в предсказуемый инструмент. Redis перестаёт быть рулеткой, где можно лишиться важного ключа в случайный момент, и становится кэшем, который сам знает, чем пожертвовать, а что сохранить любой ценой.