Кэш перед приложением — самый дешёвый способ ускорить сайт: PHP и база данных перестают выполняться на каждый запрос, а готовый HTML отдаётся за миллисекунды. Связка Nginx и Varnish закрывает две разные задачи: Varnish хранит ответы приложения и умеет точечно их сбрасывать, а Nginx обслуживает статику, завершает TLS и страхует динамику микрокэшем. Менять код приложения не нужно — достаточно расставить заголовки и правила обхода кэша.
Схема и роли компонентов
Varnish не умеет работать с TLS, поэтому на входе ставим Nginx с сертификатом, а Varnish слушает внутренний порт. Типовая цепочка выглядит так: клиент → Nginx :443 → Varnish :6081 → Nginx :8080 → PHP-FPM. Статику первый Nginx может отдавать сам, не пропуская запрос через кэш. Если сайт работает только по HTTP внутри доверенной сети, Varnish можно поставить и на восьмидесятый порт.
- Varnish отвечает за полные HTML-страницы и инвалидацию по URL.
- Nginx завершает TLS, отдаёт файлы и ограничивает методы.
- PHP-FPM и база данных получают только промахи кэша.
- Заголовки
Cache-ControlиSet-Cookieстановятся частью контракта между приложением и кэшем.
sudo apt install varnish
sudo systemctl status varnish
varnishd -V
ss -ltnp | grep -E ':(80|443|6081|8080)'
Базовая конфигурация Varnish
Файл /etc/varnish/default.vcl описывает бэкенд, проверку доступности и правила кэширования. Проверка probe важна: если приложение перезапускается, Varnish временно исключит узел из ротации, а не будет отдавать пятисотые из кэша.
vcl 4.1;
acl purge {
"localhost";
"10.0.0.0"/8;
}
backend default {
.host = "127.0.0.1";
.port = "8080";
.probe = {
.url = "/healthz";
.timeout = 2s;
.interval = 5s;
.window = 5;
.threshold = 3;
}
}
В vcl_recv мы запрещаем кэшировать личные разделы, запросы с авторизацией и сессионными cookie, а также разрешаем сброс только из доверенных сетей. В vcl_backend_response задаём время жизни: статике — сутки, обычным страницам — несколько минут, ответам с Set-Cookie — ноль.
sub vcl_recv {
if (req.method == "PURGE") {
if (!client.ip ~ purge) {
return (synth(405, "Not allowed"));
}
return (purge);
}
if (req.url ~ "^/(admin|cart|checkout|account)") {
return (pass);
}
if (req.http.Authorization) {
return (pass);
}
if (req.http.Cookie ~ "sessionid|csrftoken") {
return (pass);
}
unset req.http.Cookie;
}
sub vcl_backend_response {
if (bereq.url ~ "^/static/") {
set beresp.ttl = 1d;
set beresp.grace = 1h;
unset beresp.http.Set-Cookie;
} else if (beresp.status == 200 && !beresp.http.Set-Cookie) {
set beresp.ttl = 5m;
set beresp.grace = 30m;
}
if (beresp.http.Cache-Control ~ "no-store|private") {
set beresp.uncacheable = true;
set beresp.ttl = 0s;
}
}
sub vcl_deliver {
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
} else {
set resp.http.X-Cache = "MISS";
}
}
После правки проверяем синтаксис и перезапускаем службу. Ошибка в VCL не роняет работающий процесс до перезапуска, поэтому старую конфигурацию можно спокойно откатить.
sudo varnishd -C -f /etc/varnish/default.vcl > /dev/null
sudo systemctl reload varnish
sudo systemctl status varnish --no-pager
Микрокэш в Nginx на источнике
Второй уровень нужен не для дублирования, а для защиты от всплесков: когда Varnish пропускает запрос из-за cookie или кэш только что очищен, Nginx может отдать копию возрастом в несколько секунд. Для анонимных страниц это снижает нагрузку на PHP в десятки раз. Включаем отдельную зону и запрещаем кэшировать ответы для авторизованных пользователей. Зону и map объявляем в контексте http, а location — внутри server.
fastcgi_cache_path /var/cache/nginx/fcgi levels=1:2 keys_zone=fcgi:32m inactive=15m max_size=4g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_lock on;
fastcgi_cache_valid 200 10s;
add_header X-FastCGI-Cache $upstream_cache_status;
map $http_cookie $skip_cache {
default 0;
"~*sessionid|csrftoken|wordpress_logged_in" 1;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_cache fcgi;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
include fastcgi_params;
}
Заголовок X-FastCGI-Cache показывает HIT, MISS или BYPASS. Если на странице появляется имя пользователя, значит, правило обхода не поймало cookie и кэш отдаёт чужие данные — такую страницу нужно срочно исключить.
Инвалидация и диагностика
Сброс по URL через ban асинхронен и не требует перезапуска. Команда purge удаляет конкретный объект, ban — все совпадения по регулярному выражению. После публикации новой статьи или изменения цены достаточно отправить ban на раздел.
varnishadm "ban req.url ~ ^/catalog/"
curl -sI https://example.com/catalog/ | grep -iE 'age|x-cache|via'
varnishstat -1 -f MAIN.cache_hit -f MAIN.cache_miss
varnishlog -g request -q 'ReqURL ~ "^/catalog/"'
Заголовок Age больше нуля означает попадание в кэш. varnishstat показывает счётчики хитов и промахов, по ним считают hit rate: для контентного сайта нормально 80–95 %, для личного кабинета кэш почти не работает. В varnishlog ищем причины обхода: VCL_call PASS, Fetch, Hit.
Типичные ошибки
- Кэширование ответа с
Set-Cookie— пользователи получают чужие сессии. - Забытый
Vary: Cookieили, наоборот, лишнийVary: User-Agent, который дробит кэш. - Сохранение ответов 404 и 500 на часы — после исправления ошибка живёт в кэше.
- Отсутствие обхода для
Authorizationи служебных путей вроде/healthz. - Слишком маленькое хранилище: по умолчанию Varnish может использовать немного памяти, размер задаётся в
/etc/systemd/system/varnish.service.d/override.conf.
Чек-лист перед включением
- Проверить, что все динамические страницы отдают корректный
Cache-Control. - Убедиться, что форма входа и корзина исключены из кэша.
- Сравнить HTML из кэша и с источника: возраст, cookie, CSRF-токены.
- Настроить мониторинг hit rate и алерт на падение ниже порога.
- Описать процедуру сброса кэша при деплое.
Итог
Nginx и Varnish не конкурируют, а дополняют друг друга: первый отвечает за TLS, статику и защиту источника, второй — за агрессивное кэширование HTML с управляемой инвалидацией. Начните с малого TTL и узкого списка исключений, измерьте hit rate, и только потом увеличивайте время жизни. Главное правило — кэшировать можно только то, что не зависит от конкретного пользователя, а всё остальное должно уходить в pass.