Назад в блог

Cache-warming через Cron: дёшево, скучно, работает

Каждую ночь в 02:00 у нас сбрасывается кеш.

В 02:01 на сайт приходят боты Google. В 02:03 начинают заходить пользователи из Азии — время там утреннее.

Раньше первые 20 минут TTFB составлял 4 секунды. Сейчас — 0.7. Разница: один bash-скрипт и запись в crontab. Без Redis Cluster, без Varnish.

Почему холодный кеш — это потеря денег

Тут не про «немного медленнее». Тут про конверсию.

Реальная картина: после ночного сброса кеша или деплоя PHP генерирует каждую страницу с нуля. Запросы в базу, компиляция шаблонов, выборки инфоблоков — всё по новой. На проекте с 28k SKU и 200–300 одновременных пользователей в пике это означает TTFB 3–5 секунд на первых запросах к популярным страницам.

По нашим измерениям на одном из ecom-проектов: в первые 15 минут после сброса кеша отказ (bounce rate) рос на 40% по сравнению с нормальным ночным трафиком. Часть пользователей просто уходила, не дождавшись загрузки.

Это заказы, которых нет в отчёте за ноябрь.

Решение казалось слишком простым. Прогреть кеш до того, как придут пользователи.

Как работает прогрев кеша

Скрипт обходит список URL до того, как придут реальные пользователи. Делает HTTP-запросы, PHP генерирует страницы и кладёт результат в кеш. Когда приходят люди — кеш уже тёплый, TTFB нормальный.

Три варианта реализации. На большинстве проектов хватает первого.

Вариант 1 — wget + sitemap.xml

Пятнадцать строк bash, работает на любом PHP-проекте.

#!/bin/bash
SITEMAP_URL="https://example.com/sitemap.xml"
DELAY=0.5  # секунд между запросами

URLS=$(wget -qO- "$SITEMAP_URL" | grep -oP '(?<=<loc>)[^<]+')

COUNT=0
for URL in $URLS; do
    wget -q --spider "$URL" &>/dev/null
    COUNT=$((COUNT + 1))
    sleep $DELAY
done

echo "$(date): warmed $COUNT URLs" >> /var/log/cache-warm.log

Запускаем через cron за час до пика трафика или сразу после деплоя:

0 1 * * * /usr/local/bin/warm-cache.sh

Что здесь важно — --spider означает, что wget делает запрос, но не скачивает контент. Страница генерируется, кеш заполняется, лишнего трафика нет. Параметр DELAY=0.5 — принципиальный: без throttling можно самому себе устроить DoS.

На проекте с 2000 URL и задержкой 0.5 секунды прогрев занимает около 17 минут. Это нормально, если запускать за час до пика.

Вариант 2 — PHP/Bitrix CLI

Когда нужно больше контроля: прогревать только категории с высоким приоритетом, пропускать служебные страницы, логировать статусы ответа.

Bitrix поддерживает запуск PHP из командной строки через агентов. Создаём скрипт warm_cache.php:

<?php
define('STOP_STATISTICS', true);
define('NO_KEEP_STATISTIC', true);
define('NO_AGENT_CHECK', true);
define('NOT_CHECK_PERMISSIONS', true);

$_SERVER['DOCUMENT_ROOT'] = '/var/www/html';
require('/var/www/html/bitrix/modules/main/include/prolog_before.php');

$urls = [
    'https://example.com/',
    'https://example.com/catalog/',
    // добавляем приоритетные страницы вручную или из БД
];

foreach ($urls as $url) {
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_NOBODY, true);
    curl_setopt($ch, CURLOPT_HEADER, false);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    file_put_contents('/var/log/cache-warm.log', 
        date('Y-m-d H:i:s') . " $url HTTP/$code\n", 
        FILE_APPEND
    );
    usleep(500000); // 0.5 секунды
}

Запускаем через cron:

0 1 * * * php /var/www/html/local/scripts/warm_cache.php

Плюс этого подхода: можно приоритизировать. Сначала главная, каталог, топ-10 категорий — за 5 минут. Потом остальное.

Вариант 3 — headless прогрев через Next.js ISR

Если фронт на Next.js, а Bitrix — источник данных, то прогрев кеша имеет два слоя: PHP-кеш на бэкенде и ISR-кеш на фронте. После деплоя Next.js оба могут быть холодными.

Next.js поддерживает revalidation endpoint — можно дёргать его принудительно:

# Принудительная ревалидация конкретного пути
curl -X POST "https://example.com/api/revalidate?path=/catalog&secret=YOUR_SECRET"

В реальном проекте мы запускаем это сразу после деплоя через CI/CD-хук. Bitrix-прогрев через wget идёт параллельно. К моменту первых пользователей оба кеша заполнены.

Детальнее механику ISR и связку двух кешей разберём в отдельной статье.

Как не убить сервер прогревом

Первая ошибка при внедрении — убрать sleep. Кажется логичным: хочется прогреть 2000 URL за 10 минут, а не за 17. Результат: сервер под нагрузкой, медленные ответы, в худшем случае падение PHP-FPM.

Во-первых, throttling обязателен. Минимум 300мс между запросами на нагруженных проектах, на слабых серверах — секунда. Конкретное значение подбираем по top во время тестового прогона в staging.

Во-вторых, одновременно должен работать только один прогрев. Используем lock-файл:

LOCKFILE="/tmp/cache-warm.lock"
if [ -f "$LOCKFILE" ]; then
    echo "Already running" >> /var/log/cache-warm.log
    exit 0
fi
touch "$LOCKFILE"
trap "rm -f $LOCKFILE" EXIT

В-третьих, логировать обязательно. >> /var/log/cache-warm.log — обязательная строка. Когда через месяц возникнет вопрос «а работал ли прогрев в ту ночь», ответ будет в файле, а не в воспоминаниях.

Похожую философию «скучная автоматизация надёжнее умной» мы применяли при построении cron-инфраструктуры из 13 задач — подробнее в этой статье.

Когда cache-warming не поможет

Прогрев кеша решает одну конкретную проблему: страницы медленно генерируются после сброса кеша. Если проблема другая — прогрев не поможет.

Симптомы других проблем, которые часто путают с cold cache:

  • TTFB стабильно высокий в течение дня, а не только после сброса — это N+1 запросы или отсутствие индексов, а не cold cache. Подробнее — в статье «Bitrix не тормозит. Тормозит то, как с ним работают».
  • TTFB нормальный, но страница долго рендерится в браузере — это LCP, JS-бандл, CSS blocking. Не серверная проблема.
  • Прогрев работает, но кеш сбрасывается слишком часто — проблема в TTL или в слишком агрессивных инвалидациях. Тут нужно смотреть на стратегию кеширования, а не на прогрев.

Итог

Cache-warming через cron — одно из редких решений, где соотношение «сложность внедрения / эффект» максимальное.

Два часа работы, 15 строк bash и одна запись в crontab. TTFB после сброса кеша упал с 4 секунд до 0.7. Bounce rate в ночные часы вернулся в норму.

Это не масштабируется до уровня крупного маркетплейса с миллионом SKU. Там нужны другие инструменты. Но на типичном ecom-проекте на Bitrix с 5k–50k SKU — работает, и больше ничего не нужно.

Скучно. Дёшево. Работает.