Redis как кэш: память, прогрев, сохранность
Поставил Redis с пределом памяти 8 мегабайт и залил в него 200 тысяч ключей. При политике «ничего не вытеснять» в базу поместилось 37 052 ключа, а дальше каждая запись стала возвращать ошибку «OOM command not allowed» - out of memory, память кончилась. Поменял политику на вытеснение давно неиспользуемых и повторил: записались все 200 тысяч, в базе осталось 34 923, вытеснено 165 205, ошибок нет.
Стержень: кэшу нужны предел памяти, политика вытеснения и сроки жизни; пустой кэш опаснее отсутствующего; а сохранность у кэша ограниченная по своей природе.
// Формулировки: «что будет, когда кончится память?», «чем опасен сброс кэша?», «переживёт ли Redis перезапуск?»
Предел памяти и политика вытеснения
Кэш - это быстрое хранилище готовых ответов, которое стоит перед медленным источником и снимает с него нагрузку. Без предела памяти он растёт, пока его не убьёт операционная система, - и убьёт целиком, вместе со всем содержимым. Поэтому предел ставят всегда, а вместе с ним выбирают политику: что делать, когда предел достигнут.
Три поведения, все три я проверил. «Ничего не вытеснять» - запись начинает возвращать ошибку, а чтение продолжает работать: у меня после 37 тысяч ключей все новые записи получали отказ. Такое годится, когда Redis используется как хранилище, а не как кэш. «Вытеснять давно неиспользуемые из всех ключей» (это и есть LRU, least recently used) - запись всегда проходит, старое уходит: у меня выжило 34,9 тысячи ключей из 200 тысяч, а первый по счёту ключ был вытеснен.
// Третье поведение - ловушка. «Вытеснять только ключи со сроком жизни» на первый взгляд аккуратнее. Но если срока жизни у ключей нет, вытеснять нечего: мой замер дал 35 690 ключей, ноль вытеснений и ту же ошибку нехватки памяти на запись. То есть эта политика молча превращается в «ничего не вытеснять», если про сроки забыли. Проверять надо не настройку, а счётчик вытеснений: он честно показывает, работает ли механизм.
- предел памяти
- потолок, после которого включается политика вытеснения
- LRU
- вытеснение давно неиспользуемых ключей (least recently used)
- счётчик вытеснений
- показывает, вытесняется ли что-то на самом деле
Сроки жизни и холодный кэш
Срок жизни ключа - TTL (time to live) - это и способ не хранить лишнее, и способ не отдавать устаревшее. Проверил: ключ со сроком в две секунды через три секунды перестал существовать, ключ без срока остался. У ключа без срока значение времени жизни равно минус единице, и это буквально «никогда».
А теперь про то, чем опасен пустой кэш. Посчитаем на живых числах. Сервис держит 2000 запросов в секунду, доля попаданий в прогретый кэш (то есть запросов, ответ на которые в нём нашёлся) - 98%, значит, до базы доходит 40 запросов в секунду. Кэш сбросили - до базы доходят все 2000, то есть в пятьдесят раз больше. База, которая спокойно жила годами, ложится за секунды, и поднять её обратно нельзя, пока кэш не прогреется, а прогреться он не может, потому что база лежит.
// Хуже того, промахи идут одновременно по одному и тому же горячему ключу: сто параллельных запросов дадут сто одинаковых запросов в базу вместо одного. Лечится это тремя приёмами. Разбросать сроки жизни случайной добавкой, чтобы ключи не истекали пачкой. Пускать в базу только один запрос на ключ, а остальные заставить ждать его результата. И прогревать кэш заранее, до того как пустить трафик.
- TTL
- срок жизни ключа (time to live); минус единица означает «бессрочно»
- холодный кэш
- пустой кэш; вся нагрузка уходит в базу разом
- разброс сроков
- случайная добавка к сроку жизни, чтобы ключи не истекали одновременно
Что переживёт перезапуск
Redis держит данные в памяти, и сохранность у него настраиваемая. Проверил три режима. Без снимков и журнала: записал ключ, перезапустил - ключа нет. С журналом дозаписи, AOF (append-only file), каждая команда записи дописывается в файл: тот же опыт, и после перезапуска ключ на месте.
Важна частота сброса журнала на диск. По умолчанию это раз в секунду - я проверил настройку, там стоит именно такое значение. То есть при внезапной смерти процесса теряется последняя секунда записей, а не всё. Есть режим сброса на каждую команду, он надёжнее и заметно медленнее.
// Третий режим - снимки: раз в столько-то времени вся память выгружается в файл. Быстро восстанавливается, но между снимками теряется всё. На практике часто включают и снимки, и журнал сразу. И главный вывод: если данные критичны, их место не в кэше. Кэш - это ускоритель, из которого допустимо потерять всё; всё, что нельзя потерять, должно уметь восстановиться из основного хранилища.
- AOF
- журнал дозаписи (append-only file): каждая команда записи дописывается в файл
- снимок
- периодическая выгрузка всей памяти в файл; между снимками данные теряются
Как отвечать: «Сбросили кэш на проде, и база легла. Почему и как не повторить?»
Потому что кэш снимал с базы почти всю нагрузку, и после сброса она пришла к ней целиком. Считается это в одну строку: при двух тысячах запросов в секунду и попадании в кэш 98% до базы доходит сорок запросов в секунду, а после сброса - все две тысячи, то есть в пятьдесят раз больше. Добавьте, что промахи идут одновременно по одним и тем же горячим ключам, и получите сотню одинаковых запросов в базу вместо одного. Чтобы не повторилось: прогревать кэш до того, как пускать трафик, разбрасывать сроки жизни случайной добавкой, чтобы ключи не истекали пачкой, и пускать в базу только один запрос на ключ, заставляя остальных ждать его результата. И отдельно проверяю политику вытеснения, потому что «вытеснять только ключи со сроком жизни» при ключах без срока молча превращается в отказ на запись - я это ловил замером, вытеснений ноль и ошибка нехватки памяти.
Ответ считает нагрузку числами, называет три конкретных приёма и добавляет настройку, которая ломается молча. Это разговор человека, который такую аварию разбирал.
На чём валятся
- −Запускают кэш без предела памяти: однажды его убивает система целиком.
- −Ставят политику «вытеснять только со сроком жизни», а сроки не проставляют - запись начинает отказывать.
- −Не смотрят счётчик вытеснений и считают, что механизм работает.
- −Сбрасывают кэш на проде и кладут базу в пятьдесят раз выросшей нагрузкой.
- −Хранят в кэше то, что нельзя потерять, и полагаются на его сохранность.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 6, остальные разбираются в тренажёре.
- Redis используют как кэш. Что настраивают, чтобы он не упёрся в память?A)Предел памяти и политику вытеснения плюс сроки жизни ключейB)Сохранение на диск: вытесненные ключи уедут в файлC)Ограничение числа клиентов и размера пакетаD)Репликацию: реплика заберёт часть данных на себя
показать ответ и разбор
+A)Предел памяти и политику вытеснения плюс сроки жизни ключей// разбор: Без ограничения памяти кэш растёт до предела контейнера и получает убийство по нехватке памяти. Штатно задают maxmemory и политику вытеснения — обычно выбрасывать давно не используемые ключи среди тех, у кого задан срок жизни, — и следят, чтобы сроки действительно проставлялись. Иначе кэш незаметно превращается в хранилище: ключи копятся, вытеснять нечего, и всё кончается отказом.
- После перезапуска кэша база легла под шквалом запросов. Как этого избежать в следующий раз?A)Поднять таймауты в приложении, чтобы запросы дождались базыB)Прогревать кэш и не давать всем промахам идти в базу разомC)Отключить кэш на время нагрузки и включить после стабилизацииD)Уменьшить срок жизни ключей, чтобы данные обновлялись чаще
показать ответ и разбор
+B)Прогревать кэш и не давать всем промахам идти в базу разом// разбор: Пустой кэш означает, что каждый запрос идёт в базу, и она получает нагрузку, на которую не рассчитана. Помогают три вещи: прогрев горячих ключей до включения трафика, блокировка на пересчёт (первый промах считает, остальные ждут результат) и разброс сроков жизни, чтобы ключи не протухали одновременно. Отдельно проверяют, что база переживёт полный отказ кэша хотя бы в деградации.
- В Redis держат не кэш, а очередь задач. Что нужно знать про сохранность данных?A)Каждая операция очереди сразу пишется на дискB)Репликация заменяет сохранение на диск и защищает от потериC)Снимок теряет минуты, журнал — около секундыD)Задачи в очереди хранятся отдельно от общей политики сохранения
показать ответ и разбор
+C)Снимок теряет минуты, журнал — около секунды// разбор: Периодический снимок пишется раз в несколько минут: аварийное падение стирает всё, что случилось после последнего. Журнал команд ближе к надёжности базы, но при типовой настройке сбрасывается на диск раз в секунду — то есть теряется последняя секунда, а файл требует места и периодического уплотнения. Репликация тут не спасает: она передаёт то же самое и с тем же отставанием. Если терять задачи нельзя, берут брокер с подтверждениями.
- Как устроен базовый паттерн cache-aside при чтении данных?A)Смотрим кэш; промах — читаем БД и кладём в кэш с TTLB)Пишем сразу в кэш, а БД синхронизируется самаC)Читаем из БД, а кэш обновляем ночью пакетноD)Кэш заполняется заранее полностью, БД не читаем
показать ответ и разбор
+A)Смотрим кэш; промах — читаем БД и кладём в кэш с TTL// разбор: Cache-aside (lazy loading): при чтении приложение сначала смотрит в кэш; если попадание — отдаёт из кэша; если промах — читает из БД, кладёт значение в кэш (обычно с TTL) и возвращает. Кэш наполняется «по требованию», горячие данные оседают в нём, а TTL ограничивает, насколько данные могут устареть. Это самый распространённый паттерн; его пара — вопрос инвалидации при записи, чтобы в кэше не залежалось старое.
- После обновления записи в БД пользователи какое-то время видят старое значение. Почему и как правильно?A)База отдаёт устаревшие строки, поможет вакуумB)Реплика отстаёт, дело в лаге репликацииC)Старое значение в кэше — сбросить при записиD)Не хватает памяти под кэш, надо её увеличить
показать ответ и разбор
+C)Старое значение в кэше — сбросить при записи// разбор: Классическая проблема согласованности кэша: обновили данные в БД, но в кэше по этому ключу лежит старое значение, и до истечения TTL пользователи видят его. Правильно — инвалидировать (удалить) или обновить кэш этого ключа в момент записи, чтобы следующее чтение пошло в БД и переположило свежее. Альтернатива — сознательно принять ограниченную по TTL устарелость там, где она допустима. Выбор зависит от того, насколько критична свежесть для этих данных.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.