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
- Сразу после rsync - открыть тестовую страницу через curl с заголовками
no-cache. Видите старую версию - это OPcache, идёте в.user.ini. Видите новую - отлично, дальше проверяйте через браузер. - Проверить
phpinfo()один раз для конкретного хостинга - узнать значенияopcache.validate_timestampsиopcache.revalidate_freq. Они отличаются у Beget, Reg.ru, Timeweb. - Положить
.user.iniсразу после первого деплоя. Подождать 5 минут до подхвата. Дальше все правки видны сразу. - CDN - отдельный слой кэша. Если используете Beget CDN или Cloudflare, после крупных правок ходите в их панель и сбрасывайте кэш руками. Или ставите
Cache-Control: max-age=300в заголовках для быстрой инвалидации.
Целиком кейс по запуску самописного сайта на shared с PHP 8.4 - в статье «50 дней SEO в B2B-клининге». OPcache - одна из восьми граблей shared-хостинга, на которые я наступил в первые недели.
Если у вас shared-хостинг и сайт ведёт себя странно - пишите в Telegram @dimik90. По услугам:
- SEO для услуг - полный цикл с упором на технику и performance
- Независимый аудит подрядчика - проверка текущего стека, в том числе хостинга
Близкое по теме: CLS 0.377 → 0.002 за день - ещё одна история про быстрые правки с большим эффектом.
Вопрос-ответ