Шустрый WordPress с NGINX и умным кэшем на Varnish

В статье расскажу, как собрать шустрый стэк для Вордпресса с помощью Varnish, NGINX и балансировщика нагрузки. 

Резюме

Водпресс сделан довольно гибким, чтобы как можно больше разработчиков могло его поддерживать и писать к нему всяческие расширения. Однако это негативно сказывается на производительности: чтобы отрендерить страницу, Водпрессу приходится пробираться через тонну строчек кода и сделать кучу SQL запросов к базе.

Обычно для Водпресса используют такой стек: Apache+PHP+MySQL+какой-нибудь кэширующий плагин внутри CMS. Это популярное решение, но не сильно быстрое и хорошо работает только пока трафик маленький. Для хайлод проектов на Вордпрессе такой стек не подойдет — будет есть много ресурсов и, как следствие, увеличивать затраты на оборудование.

Хорошая новость в том, что Вордпресс чаще используют для создания статичного контента. В этом смысле, нет никакого смысла тратить ресурсы на перерендеривание страницы при каждом клиентском запросе. Чтобы этого не делать, я предлагаю отдавать статику для всех анонимных пользователей, а для авторизованных пользователей или динамики направлять запросы к балансировщику нагрузки.

В качестве кэширующего сервера я взял Варниш 4.0. Он быстрый, гибкий и прекрасно справится с моей задачей.

Архитектура

На фронтенд я поставил NGINX, Varnish как кэширующий сервер «позади» NGINX, поднял несколько нод с NGINX+php-fpm и повесил на них Haproxy в качестве балансировщика нагрузки. Я ожидал получить такую картину: 

Артём Зайцев

Злой маркетолог

WordPress+Varnish+Cluster

Как работает стэк

Nginx принимает запросы с клиента и проксирует их к Варнишу. Потом Варниш проверяет: если у него есть закэшированная страница, он отдает её, если нет — отправляет запрос к Хапрокси, который, в свою очередь, распределяет нагрузку между несколькими нодами.

В нодах стоит стэк из NGINX в FastCGI mode в кластерном варианте конфигурации, что позволяет ему отправлять запросы к ближайшему PHP серверу. В качестве базы я взял MariaDB.

Чтобы сделать такой же стек с помощью D2C понадобятся следующие сервисы:

Сервисы

  • Nginx
  • Custom Docker service для Varnish
  • Haproxy
  • NginxCluster
  • Php-FPM
  • MariaDB

Плагины Вордпресса

  • Varnish HTTP Purge plugin, чтобы обновлять кэш Варниша при добавлении постов.
  • Fake press plugin. Я хотел протестировать стек не на пустом Водпрессе, а с данными. Этот плагин помог мне сгенерировать несколько сотен постов, тегов и категорий.

Итак, собираю стэк

1. Создаю хосты. Для этой статьи, я создал 3 демо-хоста на AWS. Вы можете использовать собственные.

2. Выбираю SQL базу. Я предлагаю взять MariaDB. В этом примере Мария была в StandAlone конфигурации для чистоты эксперимента и с дефолтными настройками. 

Не забудьте сгенерировать пароль для root и создать новую базу для проекта.

3. Разворачиваю PHP-FPM. На этом этапе понадобится Вордпресс. Его можно развернуть тремя способами: из Гита, загрузить по ссылке или прикрепить .zip/.tar архив. Я использовал официальный git репозиторий WordPress. 

4. Ставлю NginxCluster. Для стэка понадобится Nginx Cluster в FastCGI mode. В D2C он уже настроен на работу с PHP-FPM, так что особо ничего не придется делать.

5. Добавляю балансировщик нагрузки для нескольких инстансов NGINX. В D2C это будет Haproxy с алгоритмом round robin.

6. Разворачиваю Varnish cache из Докер-образа. Пока в D2C еще не добавили готового сервиса для Варниша, так что приходится его разворачивать из Докер-образа.

В настройках сервиса указываю в полях Docker Image «debian«, version «jessie» и прописываю на выполнение следующею команду:

apt-get install wget
wget -qO- https://packagecloud.io/install/repositories/varnishcache/varnish51/script.deb.sh | bash
apt-get install varnish 

Эта команда скачивает образ и устанавливает его.

Потом прописываю в поле «Start Command«:

varnishd -j unix,user=vcache -F -f /etc/varnish/default.vcl -s malloc,100m -a 0.0.0.0:80

Команда запускает Варниш от пользователя «vcache», выделяет 100MB оперативной памяти и определяет путь к конфигурационному файлу и порт, который должен слушать Варниш.

Потом делаю записи в конфигурационный файл, который создаю по следующему адресу: «/etc/varnish/default.vcl«. Пока что он будет содержать немного настроек:

vcl 4.0;
backend default { 
    .host = "web alias";  
    .port = "80";
}
 

«Web alias» это имя веб-сервера, на который Варниш будет отправлять запросы. В нашем стеке мы пропишем алиас Хапрокси.

Пока что это базовый конфиг. После того, как я добавлю все необходимые сервисы, я вернусь к нему для тонкой настройки. 

6. Ставлю простой Nginx в качестве фронтенда. Он будет обслуживать SSL. Здесь достаточно добавить сервис и выбрать Варнишь в зависимостях, а затем сгенерировать https конфигурацию для SSL.

Пока что это базовая конфигурация. После того как я добавлю все необходимые сервисы, я вернусь к нему для тонкой настройки. 

Теперь пора настроить инфраструктуру. 

Настраиваю Varnish

Конфигурация выше мне не подходит. Надо её доработать. Посмотрим, что можно сделать.

Для начала, надо указать хосты, с которых Варниш будет получать запросы на очистку:

acl purge {
"host-alias1";
"host2-alias2";
"host3-alias3";
...
}

Вместо хостов надо прописать алиасы ближайших к Варнишу сервисов. В этом стеке я указал алиасы Nginx на фронтенде и Хапрокси.

Затем нужно слегка доработать алгоритм, по которому будет работать с кэшем Варниш:

# Получаем запросы с клиента
sub vcl_recv {
# Разрешаем очистку кэша с указанных выше хостов
if (req.method == "PURGE") {
# Если запрос не с хоста выше
if (!client.ip ~ purge) {
return(synth(405, "This IP is not allowed to send PURGE requests."));
}
return (purge);
}

# Пропускаем POST запросы и запросы со страницы с авторизацией
if (req.http.Authorization || req.method == "POST") {
return (pass);
}

# Пропускаем запросы из админки
if (req.url ~ "wp-(login|admin)" || req.url ~ "preview=true") {
return (pass);
}

if (req.url ~ "sitemap" || req.url ~ "robots") {
return (pass);
}

# Удаляем куки с "has_js" and "__*", добавляемые CloudFlare и Google Analytics, т.к. Варниш не кэширует запросы с кукисами
set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(_[_a-z]+|has_js)=[^;]*", "");

# Убираем префикс ";" из кукисов
set req.http.Cookie = regsub(req.http.Cookie, "^;\s*", "");

# Убираем Quant Capital cookies (некоторые лагины добавляют)
set req.http.Cookie = regsuball(req.http.Cookie, "__qc.=[^;]+(; )?", "");

# Убираем wp-settings-1 куки
set req.http.Cookie = regsuball(req.http.Cookie, "wp-settings-1=[^;]+(; )?", "");

# wp-settings-time-1 куки
set req.http.Cookie = regsuball(req.http.Cookie, "wp-settings-time-1=[^;]+(; )?", "");

# Удаляе wp test куки
set req.http.Cookie = regsuball(req.http.Cookie, "wordpress_test_cookie=[^;]+(; )?", "");

# Удаляем куки, состоящие из одних пробелов или пустые
if (req.http.cookie ~ "^ *$") {
unset req.http.cookie;
}

# Для этих статичных файлов удаляем вообще все куки, чтобы они кэшировались
if (req.url ~ "\.(css|js|png|gif|jp(e)?g|swf|ico|woff|svg|htm|html)") {
unset req.http.cookie;
}

# Если стоят кукисы "wordpress_" или "comment_" пропускать запросы на бэкенд
if (req.http.Cookie ~ "wordpress_" || req.http.Cookie ~ "comment_") {
return (pass);
}

# Если кукисы отсутствуют, удалять этот параметр из входящих запросов
if (!req.http.cookie) {
unset req.http.cookie;
}

# Не кэшировать запросы с кукисами, не связанными с вордпрессом
if (req.http.Authorization || req.http.Cookie) {

# Не кэшируемым по дефолту
return (pass);
}

# Кэшировать все остальное
return (hash);
}

sub vcl_pass {
return (fetch);
}

sub vcl_hash {
hash_data(req.url);

return (lookup);
}

# Получаем запросы с бэкенда
sub vcl_backend_response {
# Удаляем ненужные заголовки
unset beresp.http.Server;
unset beresp.http.X-Powered-By;

# Не храним в кэше robots и sitemap
if (bereq.url ~ "sitemap" || bereq.url ~ "robots") {
set beresp.uncacheable = true;
set beresp.ttl = 30s;
return (deliver);
}

# Для файлов с бэкенда.
if (bereq.url ~ "\.(css|js|png|gif|jp(e?)g)|swf|ico|woff|svg|htm|html") {
# удаляем куки
unset beresp.http.cookie;
# Устанавляваем время жизни кэша в неделю
set beresp.ttl = 7d;
# Ставим заголовки Cache-Control и Expires
set beresp.http.Cache-Control = "public, max-age=604800";
set beresp.http.Expires = now + beresp.ttl;
}

# Не кэшируем админку
if (bereq.url ~ "wp-(login|admin)" || bereq.url ~ "preview=true") {
set beresp.uncacheable = true;
set beresp.ttl = 30s;
return (deliver);
}

# Разрешам устанавливать куки при доступе только с этих адресов
if (!(bereq.url ~ "(wp-login|wp-admin|preview=true)")) {
unset beresp.http.set-cookie;
}

# Не кэшируем результат ответа на POST запросы
if ( bereq.method == "POST" || bereq.http.Authorization ) {
set beresp.uncacheable = true;
set beresp.ttl = 120s;
return (deliver);
}

# Не кэшируем результаты поиска
if ( bereq.url ~ "\?s=" ){
set beresp.uncacheable = true;
set beresp.ttl = 120s;
return (deliver);
}

# Не кешируем страницы с ошибками долго
if ( beresp.status != 200 ) {
set beresp.uncacheable = true;
set beresp.ttl = 120s;
return (deliver);
}


# Все остальное держим в кэше 1 день
set beresp.ttl = 1d;
# The lifetime of the cache after TTL expires
set beresp.grace = 30s;

return (deliver);
}

# Действим при ответе клиенту
sub vcl_deliver {
# Удаляет заголовки
unset resp.http.X-Powered-By;
unset resp.http.Server;
unset resp.http.Via;
unset resp.http.X-Varnish;

return (deliver);
}

Почти сделано. Осталось поставить Varnish HTTP Purge plugin, чтобы посылать запросы Варнишу на очистку кэша при добавлении и изменении материалов и наш стек готов.

Результаты тестов

Напоследок несколько ab тестов двух наших t2.micro Amazon EC2 инстансов. Я поставил oncurrency level 100 и 1000 запросов всего. 

Без кэширования

С кэшированием

Как видно, новый стек дал такой не слабый прирост производительности 🙂

Полезные ссылки