OPcache на shared-хостинге: почему правки не видны 5 минут после деплоя

После rsync PHP-файлы на сервере новые, а в браузере отдаётся старая версия. Причина - OPcache на shared. Фикс через .user.ini за 5 минут.

Это история одного дня дебага, который мог бы занять 5 минут - если бы я раньше знал про .user.ini и opcache.validate_timestamps. Сидел два часа, не понимал, почему правки в PHP не отображаются на сайте. Файл по SSH - новый. Браузер - старый. Перезалил по rsync второй раз - то же самое. Очистил Beget CDN - не помогло.

Причина - OPcache на shared-хостинге. Описываю как ловить и как чинить, чтобы вы не теряли часы.

Если коротко

  • На shared OPcache по умолчанию не проверяет mtime файлов - отдаёт старый бытекод 5-10 минут после деплоя.
  • Симптом: rsync прошёл, файл на сервере свежий по ls -la, но в браузере код старый.
  • Лечится файлом .user.ini в корне сайта с opcache.validate_timestamps=1 и opcache.revalidate_freq=0.
  • Особенность: первый .user.ini подхватится через 300 секунд (TTL user_ini). Дальше всё мгновенно.

Как ловится баг

Я работал над поправкой шаблона страниц метро. Дописал PHP-функцию render_metro_features(), прогнал по rsync на shared-хостинг Beget. Открыл https://example.ru/uborka-metro-pushkinskaya/ - вижу старую версию. Думаю - браузерный кэш. Hard reload (Ctrl+Shift+R), incognito-вкладка. То же самое.

Захожу по SSH, делаю cat ~/example.ru/public_html/metro.php | grep render_metro_features. Функция на месте, новая версия. То есть на диске файл свежий.

Делаю php -l metro.php - синтаксических ошибок нет.

Делаю прямой запрос к серверу через curl, без браузера и без CDN:

curl -H "Cache-Control: no-cache" -H "Pragma: no-cache" https://example.ru/uborka-metro-pushkinskaya/

Возвращается старая версия HTML. То есть это не браузер и не CDN. Беспокоить службу поддержки Beget «у вас что-то сломалось» - не вариант, потому что у других сайтов на их хостинге всё работает.

Зашёл в phpinfo() через служебную страницу. И увидел: opcache.enable On, opcache.validate_timestamps Off, opcache.revalidate_freq 300. Вот оно.

Что такое OPcache в одном абзаце

Когда вы открываете PHP-страницу, сервер делает три вещи: читает исходник с диска, парсит его в опкоды (внутреннее представление), исполняет опкоды. Парсинг - самый дорогой шаг, до 50-70% времени запроса. OPcache хранит уже распарсенные опкоды в shared memory сервера. Следующий запрос к тому же файлу пропускает парсинг - берёт опкоды из памяти и сразу исполняет.

На shared-хостингах OPcache включён всегда. Это позволяет провайдеру держать в три-пять раз больше клиентов на одном сервере. Минус - невидимый кэш на стороне сервера, который ещё нужно учитывать после каждого деплоя.

Почему shared не сбрасывает кэш сам

Есть два режима. Первый - opcache.validate_timestamps=1. PHP на каждом запросе делает stat() на исходник, сравнивает mtime с закэшированным. Если файл изменился - перечитывает и обновляет кэш. Минус - лишняя проверка диска на каждый запрос (100 микросекунд на средний сайт).

Второй - opcache.validate_timestamps=0. Никаких проверок. Кэш живёт до явного opcache_reset() или до перезапуска PHP-FPM. Это режим максимальной производительности, на крупных продакшен-сервисах его и ставят. Минус - после деплоя нужно вручную сбрасывать кэш.

На shared-хостинге opcache_reset() через PHP-CLI вам недоступен (часто это вообще запрещённая функция). К перезапуску PHP-FPM у вас нет доступа. Поэтому Beget и другие shared-провайдеры идут на компромисс: validate_timestamps=0, но revalidate_freq=60-300. Это значит, кэш проверяет mtime не каждый запрос, а раз в минуту-пять. Свежий код подхватывается через эту минуту-пять.

Если вы в это время разворачиваете и тестируете правку - она «не видна». Через 5 минут - внезапно становится видна. Через 5 минут вы уже накатили вторую правку. И тут начинается путаница, что от чего пришло.

Фикс через .user.ini

В корне сайта (там же, где index.php) кладёте файл .user.ini с двумя строками:

opcache.validate_timestamps=1
opcache.revalidate_freq=0

Что это даёт. validate_timestamps=1 - OPcache на каждом запросе проверяет mtime файла. revalidate_freq=0 - проверяет действительно каждый запрос, не реже. Эти две настройки в .user.ini поддерживаются (имеют PHP_INI_PERDIR scope в документации). Глобально на shared их менять нельзя - но локально для своей директории можно.

Подвох в TTL самого .user.ini. PHP перечитывает этот файл не на каждый запрос, а раз в 300 секунд по умолчанию. Это контролируется глобальной настройкой user_ini.cache_ttl, которую вы как раз изменить не можете. То есть после того, как вы кладёте .user.ini на сервер впервые, нужно подождать до 5 минут, пока PHP его подхватит. После этого момента - всё в реальном времени, никаких задержек.

Проверка, что подхватилось - открываете тестовый PHP-скрипт с phpinfo(). Должны увидеть opcache.validate_timestamps Local Value: On. Если ещё Off - ждёте, или принудительно ходите на сайт раз в 30 секунд, чтобы PHP перечитал ini-файлы.

Что писать в .user.ini ещё

Раз вы кладёте этот файл - туда же добавляются другие полезные настройки. Минимум для бэкенда сайта:

; OPcache видит правки сразу
opcache.validate_timestamps=1
opcache.revalidate_freq=0

; Лимиты загрузки файлов (если есть формы с фото)
upload_max_filesize=20M
post_max_size=22M
max_input_vars=3000

; Часовой пояс
date.timezone="Europe/Moscow"

; Сессии - куда писать
session.cookie_httponly=1
session.cookie_samesite="Lax"

Только настройки с областью PHP_INI_PERDIR или PHP_INI_USER сработают. Например, memory_limit в .user.ini Beget игнорирует - это PHP_INI_ALL в общем случае, но в shared-конфиге провайдер заблокировал. Если нужно поднять - обращаетесь в поддержку.

Когда .user.ini не помогает

Бывает, что вы поставили validate_timestamps=1, прошло 10 минут, всё равно код старый. Тогда возможные причины:

  • Beget CDN держит копию HTML на edge. Если страница не уникальная по куки/параметру, она кэшируется. Очистить - через панель Beget, раздел CDN, кнопка «Очистить кэш». Или временно отключить CDN на полчаса, посмотреть.
  • Браузер закэшировал HTML. Открывайте в incognito или через curl без заголовков.
  • Вы редактируете не тот файл. Бывает, что на shared есть alias-домен - папка ~/example.ru/ физически есть, но Nginx обслуживает домен из другой папки. Проверяется командой ls -la /var/www/<user>/data/www/, обычно там символическая ссылка. У меня была такая история - потерял сутки.
  • opcache_invalidate() через PHP-CLI заблокирован. Если вы пытаетесь сбросить кэш скриптом из крон-задачи - он молча не работает. На shared эту функцию обычно отключают в disable_functions. Лечится только настройкой через ini.

Альтернатива - versioned PHP-файлы

В крайнем случае, если .user.ini не помогает по политике провайдера, есть workaround. Деплоить новые версии PHP-файлов с версией в имени:

metro.php → metro_v2.php

И в .htaccess переписать роутинг:

RewriteRule ^uborka-metro-(.+)/?$ metro_v2.php?slug=$1 [L,QSA]

Старый metro.php остаётся в OPcache, но никто к нему не обращается. Новые запросы идут на metro_v2.php, который ещё не в кэше - PHP его парсит свежим. Через 5 минут OPcache подхватит и его, но к тому моменту вы уже видите свежий код.

Это грубо и не масштабируется, но иногда выручает в первый момент, пока разбираетесь с настройками.

Чек-лист после деплоя на shared

  1. Сразу после rsync - открыть тестовую страницу через curl с заголовками no-cache. Видите старую версию - это OPcache, идёте в .user.ini. Видите новую - отлично, дальше проверяйте через браузер.
  2. Проверить phpinfo() один раз для конкретного хостинга - узнать значения opcache.validate_timestamps и opcache.revalidate_freq. Они отличаются у Beget, Reg.ru, Timeweb.
  3. Положить .user.ini сразу после первого деплоя. Подождать 5 минут до подхвата. Дальше все правки видны сразу.
  4. CDN - отдельный слой кэша. Если используете Beget CDN или Cloudflare, после крупных правок ходите в их панель и сбрасывайте кэш руками. Или ставите Cache-Control: max-age=300 в заголовках для быстрой инвалидации.

Целиком кейс по запуску самописного сайта на shared с PHP 8.4 - в статье «50 дней SEO в B2B-клининге». OPcache - одна из восьми граблей shared-хостинга, на которые я наступил в первые недели.

Если у вас shared-хостинг и сайт ведёт себя странно - пишите в Telegram @dimik90. По услугам:

Близкое по теме: CLS 0.377 → 0.002 за день - ещё одна история про быстрые правки с большим эффектом.

Вопрос-ответ

Частые вопросы

Что такое OPcache и зачем он на shared?
OPcache - встроенный кэш скомпилированного бытекода PHP. Когда вы открываете страницу, PHP парсит исходник в опкоды (внутреннее представление), потом исполняет. OPcache хранит уже распарсенные опкоды в shared memory сервера: следующий запрос пропускает шаг парсинга. Это экономит 30-70% времени CPU на каждом запросе. На shared-хостингах OPcache обычно включён по умолчанию, чтобы провайдер мог упаковать больше клиентов на один сервер.
Почему shared-хостинг не подхватывает мои правки сразу?
По умолчанию OPcache на shared настроен с `validate_timestamps=0` - то есть он не проверяет, изменился ли файл на диске. Это даёт максимум скорости, но требует ручного reset после деплоя. На shared у вас нет доступа к команде `opcache_reset()` через CLI или к перезапуску PHP-FPM. Обычно настроен `revalidate_freq=60` или `300` - кэш сам сбрасывается раз в минуту-пять. До этого момента сервер отдаёт старый код.
Чем `.user.ini` отличается от `php.ini`?
`php.ini` - глобальный конфиг сервера, на shared его править нельзя. `.user.ini` - локальный конфиг для конкретной директории, его можно положить в корень своего сайта. PHP перечитывает его автоматически с TTL по умолчанию 300 секунд (контролируется `user_ini.cache_ttl` в php.ini). В `.user.ini` поддерживается подмножество настроек - те, у которых `PHP_INI_PERDIR` или `PHP_INI_USER` в документации. Для OPcache подходят `opcache.validate_timestamps`, `opcache.revalidate_freq`, `opcache.enable`.
Не упадёт ли скорость сайта после `validate_timestamps=1`?
Падает, но мало. OPcache на каждом запросе делает `stat()` на исходный файл и сравнивает mtime с закэшированным. На SSD это пара микросекунд на файл. На сайте из 50 PHP-файлов overhead - 100-200 микросекунд. На фоне SQL-запроса (5-20 мс) и сетевой задержки (50-200 мс) это незаметно. Outweigh-аргумент против `validate_timestamps=0` на shared: вы теряете часы на дебаг «почему правки не работают».
Где ещё кэш кроме OPcache мешает увидеть свежий код?
Несколько слоёв. Браузерный кэш по `Cache-Control` и `Expires` - обычно несколько часов на HTML, до года на CSS/JS с версиями. CDN-кэш - Beget CDN или Cloudflare держат копию на edge-серверах от 5 минут до 7 дней. Кэш Nginx на самом shared - обычно до 5 минут. Cookie-сессии - если страница персональная, не путайте кэш с правильно засохшей сессией. На правки CSS/JS отвечает versioned URL вида `/css/main.css?v=20260511`, на правки PHP - OPcache fix из этой статьи.

Оцените статью

Дальше

← Все записи