сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Базы и брокеры в проде

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, остальные разбираются в тренажёре.

  1. #dvo_cache_ops1 / 5
    Redis используют как кэш. Что настраивают, чтобы он не упёрся в память?
    A)Предел памяти и политику вытеснения плюс сроки жизни ключей
    B)Сохранение на диск: вытесненные ключи уедут в файл
    C)Ограничение числа клиентов и размера пакета
    D)Репликацию: реплика заберёт часть данных на себя
    показать ответ и разбор
    +A)Предел памяти и политику вытеснения плюс сроки жизни ключей

    // разбор: Без ограничения памяти кэш растёт до предела контейнера и получает убийство по нехватке памяти. Штатно задают maxmemory и политику вытеснения — обычно выбрасывать давно не используемые ключи среди тех, у кого задан срок жизни, — и следят, чтобы сроки действительно проставлялись. Иначе кэш незаметно превращается в хранилище: ключи копятся, вытеснять нечего, и всё кончается отказом.

  2. #dvo_cache_ops2 / 5
    После перезапуска кэша база легла под шквалом запросов. Как этого избежать в следующий раз?
    A)Поднять таймауты в приложении, чтобы запросы дождались базы
    B)Прогревать кэш и не давать всем промахам идти в базу разом
    C)Отключить кэш на время нагрузки и включить после стабилизации
    D)Уменьшить срок жизни ключей, чтобы данные обновлялись чаще
    показать ответ и разбор
    +B)Прогревать кэш и не давать всем промахам идти в базу разом

    // разбор: Пустой кэш означает, что каждый запрос идёт в базу, и она получает нагрузку, на которую не рассчитана. Помогают три вещи: прогрев горячих ключей до включения трафика, блокировка на пересчёт (первый промах считает, остальные ждут результат) и разброс сроков жизни, чтобы ключи не протухали одновременно. Отдельно проверяют, что база переживёт полный отказ кэша хотя бы в деградации.

  3. #dvo_cache_ops3 / 5
    В Redis держат не кэш, а очередь задач. Что нужно знать про сохранность данных?
    A)Каждая операция очереди сразу пишется на диск
    B)Репликация заменяет сохранение на диск и защищает от потери
    C)Снимок теряет минуты, журнал — около секунды
    D)Задачи в очереди хранятся отдельно от общей политики сохранения
    показать ответ и разбор
    +C)Снимок теряет минуты, журнал — около секунды

    // разбор: Периодический снимок пишется раз в несколько минут: аварийное падение стирает всё, что случилось после последнего. Журнал команд ближе к надёжности базы, но при типовой настройке сбрасывается на диск раз в секунду — то есть теряется последняя секунда, а файл требует места и периодического уплотнения. Репликация тут не спасает: она передаёт то же самое и с тем же отставанием. Если терять задачи нельзя, берут брокер с подтверждениями.

  4. #dvo_cache_ops4 / 5
    Как устроен базовый паттерн cache-aside при чтении данных?
    A)Смотрим кэш; промах — читаем БД и кладём в кэш с TTL
    B)Пишем сразу в кэш, а БД синхронизируется сама
    C)Читаем из БД, а кэш обновляем ночью пакетно
    D)Кэш заполняется заранее полностью, БД не читаем
    показать ответ и разбор
    +A)Смотрим кэш; промах — читаем БД и кладём в кэш с TTL

    // разбор: Cache-aside (lazy loading): при чтении приложение сначала смотрит в кэш; если попадание — отдаёт из кэша; если промах — читает из БД, кладёт значение в кэш (обычно с TTL) и возвращает. Кэш наполняется «по требованию», горячие данные оседают в нём, а TTL ограничивает, насколько данные могут устареть. Это самый распространённый паттерн; его пара — вопрос инвалидации при записи, чтобы в кэше не залежалось старое.

  5. #dvo_cache_ops5 / 5
    После обновления записи в БД пользователи какое-то время видят старое значение. Почему и как правильно?
    A)База отдаёт устаревшие строки, поможет вакуум
    B)Реплика отстаёт, дело в лаге репликации
    C)Старое значение в кэше — сбросить при записи
    D)Не хватает памяти под кэш, надо её увеличить
    показать ответ и разбор
    +C)Старое значение в кэше — сбросить при записи

    // разбор: Классическая проблема согласованности кэша: обновили данные в БД, но в кэше по этому ключу лежит старое значение, и до истечения TTL пользователи видят его. Правильно — инвалидировать (удалить) или обновить кэш этого ключа в момент записи, чтобы следующее чтение пошло в БД и переположило свежее. Альтернатива — сознательно принять ограниченную по TTL устарелость там, где она допустима. Выбор зависит от того, насколько критична свежесть для этих данных.

дальше

Теорию прочитали. Навык ставится повторением

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