Прогретый кеш к часу X: как подготовить его заранее
Старт продаж в 12:00, открытие регистрации в объявленную минуту, публикация результатов экзамена в заранее известный час — у таких пиков, как в статье «Предзаказ открыт», есть точное время начала, и это большое преимущество перед стихийным всплеском трафика. Но преимущество работает только если им воспользоваться: пустой кеш встречает первую волну пользователей точно так же, будто вы вообще не готовились. Разберём, как заполнить кеш до того, как придёт нагрузка — по слоям, с рабочими скриптами и без иллюзий насчёт того, что прогретые данные останутся актуальными вечно.
Содержание
- Почему пустой кеш в час X опаснее, чем кажется
- Слои кеша и порядок прогрева: от статики к динамике
- Составляем список URL: карта горячих путей
- Скрипт прогрева: проход по ключевым URL до старта
- Прогрев CDN и объектного кеша отдельно от HTML
- Риск: пик сдвинулся по времени, и прогретые данные устарели
- Проверка hit-rate перед стартом: как понять, что прогрев сработал
Почему пустой кеш в час X опаснее, чем кажется
В обычный день кеш наполняется естественно: первый пользователь получает страницу из базы или бэкенда, кладёт результат в кеш, следующие тысячи читают уже готовое. Промахи размазаны по времени, база успевает обработать каждый из них по отдельности.
На объявленном старте это предположение ломается. Тысячи пользователей открывают одну и ту же страницу — карточку акции, форму регистрации, стартовую страницу распродажи — в первые секунды после часа X. Если кеш для неё пуст, все эти запросы одновременно идут мимо кеша к бэкенду и в базу. Получается ровно тот cache stampede, который подробно разобран в статье «Кеш прогрелся и уронил базу: эффект стада» — только там причина в случайном совпадении: TTL истёк в неудачный момент при обычной нагрузке. Здесь причина полностью предсказуема и потому устранима: вы заранее знаете время пика, значит, можете заполнить кеш до того, как он понадобится, а не полагаться на то, что первый же запрос успеет пересчитать значение раньше, чем накопится очередь.
Прогрев — не альтернатива механизмам защиты кеша (mutex на пересчёт, stale-while-revalidate, джиттер TTL), а дополнение к ним. Даже с идеальной защитой от стада холодный кеш означает, что первые реальные запросы всё равно упадут на бэкенд и добавят задержку именно тем пользователям, которые пришли первыми. Прогрев убирает эту задержку целиком: к моменту старта нужные ответы уже лежат в памяти на всех уровнях.
Слои кеша и порядок прогрева: от статики к динамике
Прогревать нужно не «кеш» как единую сущность, а каждый слой отдельно, и порядок имеет значение — от самого дешёвого и стабильного к самому дорогому и переменчивому.
- Статика на CDN — картинки, CSS, JS, шрифты. Меняется редко, кешируется на CDN подолгу, прогревается первой и заранее (за часы, а не минуты до пика), потому что это не создаёт нагрузки на origin и можно делать без спешки.
- HTML-кеш страниц (Nginx
proxy_cache/fastcgi_cache, Varnish, кеш CDN для HTML). Это уже динамический по содержанию слой, но для анонимных пользователей одна и та же страница отдаётся всем одинаково — типичный кандидат на прогрев. - Объектный кеш приложения (Redis, Memcached) — результаты дорогих вычислений, агрегаций, сериализованные объекты, ответы API. Здесь прогрев наиболее ценен: именно эти запросы обычно самые тяжёлые для базы.
- Кеш на уровне базы (query cache, материализованные представления, подготовленные агрегаты) — если такой слой есть, его тоже стоит наполнить, но обычно это происходит как побочный эффект прогрева объектного кеша: чтобы положить значение в Redis, приложение всё равно один раз сходит в базу.
Обратный порядок — сначала пытаться прогреть базу, потом объектный кеш, потом HTML — не работает: верхние слои кеша всё равно перекроют нижние при реальном трафике, а если прогревать снизу вверх, вы тратите время на прогрев базы для запросов, которые в итоге всё равно уйдут не дальше HTML-кеша.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверСоставляем список URL: карта горячих путей
Прогрев без списка «что греть» превращается в стрельбу по площадям. Список URL для прогрева собирается из трёх источников:
- Логи трафика с прошлого похожего пика. Если акция или регистрация уже проводилась раньше, логи Nginx или CDN за первые 5–10 минут прошлого раза — лучший источник: именно эти URL реально запрашивали пользователи.
awk '{print $7}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head -100 > hot-urls.txt
- Карта сайта и навигация приложения. Для новых страниц (акция запускается впервые) список собирается вручную или скриптом, обходящим sitemap.xml и известные шаблоны URL: карточки категорий, топ-N товаров по продажам, страницы landing для акции.
- API-эндпоинты, которые фронтенд вызывает на этих страницах. Часто самая тяжёлая нагрузка приходится не на HTML, а на
/api/products?category=...,/api/cart,/api/user/recommendations— их тоже нужно включить в список отдельно, они прогреваются как обычные URL, но с учётом заголовков (Accept, авторизация для «усреднённого» неавторизованного варианта).
Список стоит держать в репозитории рядом с кодом прогрева, а не у одного инженера — тогда он обновляется вместе с изменениями каталога.
Скрипт прогрева: проход по ключевым URL до старта
Сам прогрев — это контролируемая волна запросов к нужным URL, которая должна успеть отработать и не должна сама создать нагрузку, похожую на DDoS для собственного бэкенда.
Простой вариант на curl с ограничением параллелизма через xargs:
#!/usr/bin/env bash
# warmup.sh — прогрев HTML-кеша по списку URL
BASE_URL="https://example.com"
CONCURRENCY=8
cat hot-urls.txt | xargs -P "$CONCURRENCY" -I{} \
curl -s -o /dev/null -w "%{http_code} %{time_total}s {}\n" \
-H "User-Agent: warmup-bot/1.0" "$BASE_URL{}"
Для больших списков (тысячи URL) удобнее hey или wrk в режиме одного прохода без нагрузочного профиля, либо GNU parallel, который умеет ограничивать не только параллелизм, но и общую скорость (--delay, -j):
parallel -j 8 --delay 0.05 'curl -s -o /dev/null "https://example.com{}"' :::: hot-urls.txt
Важные детали, которые часто упускают:
- Прогревать нужно с тем же набором заголовков, что и у реальных пользователей — иначе можно прогреть не тот вариант кеша. Если Nginx учитывает
Vary: Accept-Encoding, прогревающий запрос должен идти соgzip, иначе в кеше окажется несжатая версия, которую реальные браузеры не заберут. - User-Agent прогревающего бота стоит выделить отдельно, чтобы его строки не путались с реальным трафиком при анализе конверсии.
- Скорость прогрева должна расти постепенно, а не сразу бить максимальным параллелизмом — иначе сам прогрев станет причиной перегрузки, которую он должен был предотвратить. Начните с
CONCURRENCY=2, поднимайте до целевого значения за несколько минут. - Этапы идут по очереди: сначала статика, затем HTML, затем API — каждый следующий стартует только после того, как предыдущий прошёл без роста времени ответа и без всплеска 5xx.
Запуск планируется через cron или systemd timer с точным временем, отсчитанным назад от часа X — например, за 20 минут до старта для HTML-слоя и за 5 минут для самого дорогого объектного кеша, чтобы данные были максимально свежими к моменту реального трафика:
# /etc/systemd/system/cache-warmup.timer
[Unit]
Description=Прогрев кеша перед пиком
[Timer]
OnCalendar=*-*-* 11:40:00
Persistent=true
[Install]
WantedBy=timers.target
Прогрев CDN и объектного кеша отдельно от HTML
Прогрев HTML-страниц через curl неявно прогревает и CDN, если CDN стоит перед сервером и кеширует ответы по тем же ключам — достаточно, чтобы прогревающие запросы шли через тот же домен, что и пользователи, а не напрямую на origin. Но у части CDN-провайдеров есть нюанс: прогрев с одного IP или региона может заполнить кеш только на ближайшей edge-ноде, а пользователи из другого региона всё равно попадут на холодную ноду того же CDN. Если аудитория пика географически распределена, прогревающие запросы стоит запускать из нескольких регионов — либо через собственные серверы в разных локациях, либо через встроенный в CDN-панель инструмент предварительного прогрева (pre-warming / pre-fetch), если провайдер его предоставляет. Про то, какой контент вообще безопасно отдавать с границы сети CDN, а какой обязан идти к origin, подробно разобрано в статье «Кеширование на границе» — тот же принцип персонализации важен и здесь: прогревать нужно только те варианты страниц, что одинаковы для всех, иначе можно случайно закешировать чужую сессию.
Объектный кеш (Redis, Memcached) прогревается иначе — не через HTTP-запросы, а прямой записью нужных ключей. Практический способ: написать отдельный скрипт (Python, Node — то, на чём написан бэкенд), который вызывает те же функции пересчёта, что использует приложение при обычном промахе кеша, и явно раскладывает результат по ключам:
# warm_cache.py — прогрев объектного кеша перед пиком
import redis
from app.pricing import calculate_price
from app.catalog import get_hot_product_ids
r = redis.Redis(host="localhost", port=6379, db=0)
for product_id in get_hot_product_ids(limit=500):
price = calculate_price(product_id) # тот же путь, что и в реальном промахе
r.set(f"product:{product_id}:price", price, ex=600) # TTL с запасом на длительность пика
Ключевой момент: TTL прогретых ключей должен быть заметно больше ожидаемой длительности самого пика. Если акция длится 30 минут, а TTL прогретого ключа — 60 секунд, прогрев обнулится сам собой уже на второй минуте, и вы вернётесь к обычному режиму с промахами прямо в разгар нагрузки. Прогрев без увеличения TTL на время пика — частая причина, почему «мы же прогревали, а всё равно упало».
Риск: пик сдвинулся по времени, и прогретые данные устарели
Прогрев рассчитан на конкретный момент времени, а реальность регулярно сдвигает этот момент: старт продаж откладывают на 40 минут из-за проблем на стороне маркетинга, регистрацию переносят из-за технической паузы у смежной команды, а прогретый в 11:40 кеш ждёт трафик, который придёт только в 13:00.
Здесь возможны два сценария, и оба требуют разных решений:
- Цены, остатки, статусы устарели, но пик всё ещё впереди. Опасность не в производительности, а в корректности данных: пользователь увидит цену или наличие товара по состоянию на момент прогрева, а не на момент реального запроса. Решение — держать TTL с запасом, но не бесконечным, и добавить механизм повторного прогрева «по требованию»: если старт откладывается больше чем на исходный запас TTL, скрипт прогрева перезапускается автоматически по сигналу (webhook от системы объявления акции, ручной триггер, повторный запуск таймера с новым временем), а не оставляет кеш протухать молча.
- Пик так и не наступил в ожидаемое окно, а TTL истёк. Кеш опустошается сам, и первая реальная волна снова попадает в холодный кеш — прогрев де-факто теряет смысл, если никто не следит за тем, что час X сдвинулся. Практическое решение: не завязываться на единственный прогрев в фиксированное время, а держать скользящее окно — повторный прогрев каждые N минут вплоть до момента, когда реальный трафик начнётся (обнаруживается по резкому росту RPS) или до жёсткого дедлайна, после которого прогрев теряет смысл и его можно отменить вручную.
Отдельно стоит вопрос инвалидации при сдвиге: если между прогревом и реальным стартом что-то в данных изменилось не по времени, а по существу — поменяли цену, отключили промокод, — прогретый кеш нужно не просто оставить жить по TTL, а сбросить и прогреть заново по факту события, а не по расписанию. Разница между «протух по времени» и «устарел по содержанию» определяет, каким инструментом решать проблему: TTL с запасом закрывает первое, точечная инвалидация нужного ключа — второе.
Проверка hit-rate перед стартом: как понять, что прогрев сработал
Прогрев, который ничего не проверяет, не даёт уверенности — нужно убедиться, что после запуска скрипта данные действительно легли в кеш, а не были отброшены (например, из-за Cache-Control: no-store где-то в цепочке или неправильного ключа).
Для Nginx proxy_cache статус кеша виден в заголовке ответа при добавленной директиве:
add_header X-Cache-Status $upstream_cache_status;
После прогрева стоит пройтись по тем же URL ещё раз и убедиться, что статус HIT, а не MISS или EXPIRED:
curl -s -o /dev/null -D - "https://example.com/promo" | grep -i x-cache-status
Для Redis общий hit-rate смотрится через INFO stats:
redis-cli INFO stats | grep -E "keyspace_hits|keyspace_misses"
Соотношение keyspace_hits / (keyspace_hits + keyspace_misses) перед стартом пика должно быть близко к 100% по ключам из списка прогрева — если это не так, часть ключей не прогрелась (ошибка в скрипте, не тот TTL, не та база Redis) и стоит разобраться до, а не после часа X.
Для CDN большинства провайдеров есть панель со статистикой cache hit ratio по зоне — её стоит проверить за 5–10 минут до старта, а не полагаться на то, что скрипт прогрева отработал без ошибок молча. Простое правило: если прогрев не проверен измерением hit-rate, считайте, что прогрева не было — слишком много мест, где он может тихо не сработать: неверный домен, отсутствующие заголовки, региональная edge-нода, до которой прогревающие запросы не дошли.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Сколько времени нужно на прогрев перед пиком?
Зависит от объёма списка URL и веса каждого ответа, но как ориентир: статику и CDN стоит начинать греть за несколько часов, HTML-кеш — за 15–30 минут, самый дорогой объектный кеш — за 5–10 минут до старта, с проверкой hit-rate сразу после. Точные цифры для вашего случая получаются только замером на реальном списке URL и реальном бэкенде.
Прогрев кеша — это то же самое, что нагрузочное тестирование?
Нет, хотя инструменты пересекаются (curl, wrk, hey). Нагрузочное тестирование ищет предел системы и намеренно создаёт стресс; прогрев, наоборот, старается заполнить кеш максимально аккуратно, без лишней нагрузки на бэкенд. Не стоит запускать их одновременно и не стоит путать скрипт прогрева со скриптом нагрузочного теста — у них разные цели по параллелизму.
Что если часть страниц персонализирована и их нельзя прогреть заранее?
Прогревается только общий, неперсонализированный вариант — то, что видит анонимный пользователь или пользователь «по умолчанию». Персонализированные блоки (корзина, рекомендации по истории, баланс лояльности) грузятся отдельными запросами уже после отдачи прогретой оболочки страницы — это стандартный паттерн разделения статического кеша и динамических фрагментов.
Нужно ли прогревать кеш, если у нас и так низкая нагрузка?
Если пик даже в разы не подходит к пределам вашего сервера, прогрев не даст заметного эффекта и им можно пренебречь. Прогрев оправдан, когда прогнозируемый пик заметно (в разы) превышает обычную нагрузку и есть точный час старта — то есть именно ситуация, о которой эта статья.
Как автоматизировать повторный прогрев, если время старта объявляют не заранее, а по ходу события?
Свяжите триггер прогрева не только с расписанием, но и с событием — webhook от админки, публикующей акцию, или API-вызов, который дёргает скрипт прогрева в момент, когда время окончательно подтверждено. Расписание (cron/systemd timer) хорошо работает как страховочный повтор, событие — как основной триггер.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →