Очень шустрый блог на WordPress при помощи связки nginx + PHP-FPM + MariaDB + Varnish +26


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

CPU: 1 x 2GHz
HDD: 10Gb
RAM: 512Mb
OS: Debian 8 x64

Схема работы системы выглядит следующим образом:

image

Описание работы схемы


Для посетителей сайта происходит перенаправление на HTTPS, где nginx работает в качестве прокси для Varnish, при этом на выходе nginx помимо реализации HTTPS-соединения происходит gzip-сжатие данных, передаваемых пользователю. Следующим элементом в данной системе является HTTP-акселератор Varnish, ожидающий соединения на 6081 порту. Получая запрос от клиента он выполняет поиск запрашиваемого URL в кэше, и в случае его обнаружения мгновенно отдаёт его фронтенду. Таким образом, при наличии запрашиваемого файла в кэше скорость запроса к страницам сокращается до скорости запроса к статическим данным. Если же запрашиваемого файла в кэше не обнаруживается, Varnish передаёт запрос бэкенду. Так же в Varnish реализована оптимизация клиентской стороны — здесь статическим данным устанавливаются заголовки Cache-Control и Expires, указывающие браузеру на необходимость кэширования этих данных на стороне клиента. Таким образом сокращается время загрузки сайта и уменьшается нагрузка на сервер.

В роли бэкенда выступает опять же nginx, ожидающий соединений на 127.0.0.1:81. Интерпретация PHP реализована с помощью FPM. Версия PHP — 5.6 с включенным по умолчанию акселератором OPcache. В качестве СУБД — MariaDB 10, являющаяся одной из лучших по производительности и кушающих в меру оперативную память СУБД среди форков MySQL. В качестве движка таблиц — MyISAM, так как запись производится редко, в основном чтение, для которого данный движок больше оптимизирован. За счёт отключения движка InnoDB реализуется экономия оперативной памяти. Наконец, в качестве CMS функционирует WordPress с установленным плагином Varnish HTTP Purge, отправляющий PURGE-запросы на адреса страниц, на которых были произведены изменения, что приводит к очистке кэша Varnish для данных страниц. Таким образом, пользователь получает всегда актуальную версию сайта. Далее я детально расскажу об установке и настройке данных компонентов, а так же о проблемах, с которыми я столкнулся.

Установка и настройка nginx


Устанавливаем:

apt-get install nginx

Содержимое основного конфига /etc/nginx/nginx.conf:

# Пользователь и группа, от имени которых будет запущен процесс
user                    www-data www-data;

# Число воркеров в новых версиях рекомендовано устанавливать в auto
worker_processes        auto;

error_log               /var/log/nginx/error.log;
pid                     /var/run/nginx.pid;

events {
    # Максимальное количество соединений одного воркера
    worker_connections              1024;

    # Метод выбора соединений (для FreeBSD будет kqueue)
    use                             epoll;

    # Принимать максимально возможное количество соединений
    multi_accept                    on;
}

http {
    # Указываем файл с mime-типами и указываем тип данных по-умолчанию
    include                         /etc/nginx/mime.types;
    default_type                    application/octet-stream;

    # Отключить вывод версии nginx в ответе
    server_tokens off;

    # Метод отправки данных sendfile эффективнее чем read+write
    sendfile                        on;

    # Ограничивает объём данных, который может передан за один вызов sendfile(). Нужно для исключения ситуации когда одно соединение может целиком захватить воркер
    sendfile_max_chunk  128k;

    # Отправлять заголовки и и начало файла в одном пакете
    tcp_nopush                      on;
    tcp_nodelay                     on;

    # Сбрасывать соединение если клиент перестал читать ответ
    reset_timedout_connection       on;
    # Разрывать соединение по истечению таймаута при получении заголовка и тела запроса
    client_header_timeout           3;
    client_body_timeout             5;
    # Разрывать соединение, если клиент не отвечает в течение 3 секунд
    send_timeout                    3;

    # Задание буфера для заголовка и тела запроса
    client_header_buffer_size       2k;
    client_body_buffer_size         256k;
    # Ограничение на размер тела запроса
    client_max_body_size            12m;

    # Отключаем лог доступа
    access_log                      off;

    # Подключаем дополнительные конфиги
    include                         /etc/nginx/conf.d/*.conf;
}

Создадим файл настроек бэкенда /etc/nginx/conf.d/backend.conf:

server {
    # Ожидать локального соединения на 81 порту
    listen 127.0.0.1:81;

    # Корневая директория и индексовый файл
    root /var/www/site.ru/public_html;
    index index.php;

# Включить gzip-сжатие на выходе бэкенда. В кэш пойдут уже сжатые версии файлов. Здесь происходит сжатие на 9 уровне компрессии. Обратите внимание, среди типов отсутствует text/plain, его мы сжимаем во фронтенде на 1 уровне компрессии, чтобы избежать высокой степени загрузки CPU при отдаче динамических данных
    gzip                on;
    gzip_comp_level     9;
    gzip_min_length     512;
    gzip_buffers        8 64k;
    gzip_types text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript image/svg+xml;
    gzip_proxied        any;

    # Имя хоста
    server_name site.ru www.site.ru;

    # Запрет на доступ к скрытым файлам
    location ~ /\. {
        deny all;
    }

    # Запрет на доступ к загруженным скриптам
    location ~* /(?:uploads|files)/.*\.php$ {
        deny all;
    }

    # Поиск запрашиваемого URI по трем путям
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # Добавление слэша в конце для запросов */wp-admin
    rewrite /wp-admin$ $scheme://$host$uri/ permanent;

    location ~ \.php$ {
        # При ошибке 404 выдавать страницу, сформированную WordPress
        try_files $uri =404;

        # При обращении к php передавать его на интерпретацию FPM
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/var/run/php5-fpm.sock;
    }
}

На тему детального описания настройки HTTPS в nginx рекомендую к прочтению данную статью: habrahabr.ru/post/252821
Создаём файл настроек фронтэнда /etc/nginx/conf.d/frontend.conf:

server {
    # Редирект на HTTPS
    listen      REAL_IP:80;
    server_name site.ru www.site.ru;
    return 301 https://$server_name$request_uri;
}

server {
    listen      93.170.105.102:443 ssl;
    server_name site.ru www.site.ru;

    # Устанавливать Keep-Alive соединения с посетителями
    keepalive_timeout               60 60;

    # Сжатие данных перед отправкой клиенту. Обратите внимание, из типов здесь присутствует только text/plain, для других данных применяется сжатие на бэкенде на более высоком уровне компрессии, после чего эти данные отправляются в кэш. Сделано для того, чтобы избежать нагрузок на CPU при сжатии динамических документов.
    gzip                on;
    gzip_comp_level     1;
    gzip_min_length     512;
    gzip_buffers        8 64k;
    gzip_types text/plain;
    gzip_proxied        any;

    # Отдавать предпочтение шифрам, заданным на сервере
    ssl_prefer_server_ciphers on;
    # Установка длительности TLS сессии в 2 минуты
    ssl_session_cache shared:TLS:2m;
    ssl_session_timeout 2m;

    # Задание файла, содержащего сертификат сайта и сертификат УЦ
    ssl_certificate      /etc/ssl/combined.crt;
    # Указание закрытого ключа
    ssl_certificate_key  /etc/ssl/3_site.ru.key;

    # Файл с параметрами Диффи-Хеллмана
    ssl_dhparam /etc/ssl/dh2048.pem;

    # Поддерживаемые протоколы
    ssl_protocols TLSv1.2 TLSv1.1 TLSv1;

    # Наборы шифров, данный набор включает forward secrecy
    ssl_ciphers EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA512:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:ECDH+AESGCM:ECDH+AES256:DH+AESGCM:DH+AES256:RSA+AESGCM:!aNULL:!eNULL:!LOW:!RC4:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS;

    # Передача Strict-Transport-Secutiry заголовка
    add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains';

    location / {
        # Проксирование на Varnish
        proxy_pass      http://127.0.0.1:6081/;

        proxy_set_header    Host              $host;
        proxy_set_header    X-Real-IP         $remote_addr;
        proxy_set_header    X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header    X-Forwarded-Proto https;
        proxy_set_header    X-Forwarded-Port  443;
    }
}

Перечитаем конфиги nginx:

service nginx reload

Теперь при попытке зайти на сайт увидим ошибку 502. Это нормально, так как Varnish пока не запущен.

Установка и настройка Varnish


Устанавливаем Varnish:

apt-get install varnish

Файл параметров запуска располагается здесь — /etc/default/varnish. В DAEMON_OPTS задаём следующие параметры:

DAEMON_OPTS="-a :6081              -T 127.0.0.1:6082              -f /etc/varnish/default.vcl              -S /etc/varnish/secret              -s malloc,128m"

-a — задаёт порт, на котором Varnish будет принимать соединения, в нашем случае от фронтенда — nginx;
-T — здесь крутится админка, подробнее в описании к флагу -S;
-f — файл с конфигурацией VCL — специальном языке, предназначенном для определения правил обработки запросов и кэширования в Varnish;
-S — Varnish имеет панель администрирования. Для входа необходимо выполнить команду varnishadm, при этом пользователь должен иметь права на чтение файла /etc/varnish/secret для прохождения аутентификации;
-s указание места хранения кэша и его размер, в данном случае 128Mб в оперативной памяти.

Как вы уже, наверное, поняли, самое интересное нас ждёт в файле с правилами обработки запросов. Во время старта процесса Varnish’а данный файл компилируется. В VCL используется несколько подразделов-функций, в которых описываются эти правила. Кратко расскажу о них, полное описание рекомендую прочитать на официальном сайте.

sub vcl_recv — данная функция используется когда приходит запрос от клиента;
sub vcl_pass — выполняется, когда запрос клиента необходимо передать напрямую бэкенду, не кэшировать и не искать соответствия в кэше;
sub vcl_hash — определяет правила кэширования, можно использовать несколько хранилищ для одного и того же документа, в зависимости от разных условий, например, поддержки сжатия клиентом, или каких-либо других особенностей клиента. В нашем случае не будет использоваться, так как клиент у нас для Varnish’а один — nginx на фронтенде;
sub vcl_backend_response — данная функция используется когда приходит запрос от бэкенда (nginx);
sub vcl_deliver — используется непосредственно перед отправкой данных клиенту, например, для добавления/изменения заголовков.

Схема работы компонентов VCL может быть представлена следующим образом:

image

Если обращение к бэкенду происходит при этом из функции vcl_miss ответ бэкенда отправляется и в кэш. Сам язык очень похож на C. Приступим к настройке. Открываем файл /etc/varnish/default.vcl и начинаем кодить:

# Сообщаем компилятору о том, что используется новая версия VCL 4
vcl 4.0;

# Настройки бэкенда
backend default {
    .host = "127.0.0.1";
    .port = "81";
}

# Диапазон IP/Хостов, которым разрешено выполнять PURGE-запросы для очистки кэша
acl purge {
    "localhost";
    "127.0.0.1";
}

# Получение запроса от клиента
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-запросы а также страницы с Basic-авторизацией пропускать
        if (req.http.Authorization || req.method == "POST") {
                return (pass);
        }

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

        # Пропускать sitemap и файл robots, у меня sitemap генерируется плагином Google XML Sitemaps
        if (req.url ~ "sitemap" || req.url ~ "robots") {
                return (pass);
        }

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

        # Удаление префикса ";" в cookies, если вдруг будет обнаружен
        set req.http.Cookie = regsub(req.http.Cookie, "^;\s*", "");

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

        # Удаляем wp-settings-time-1 cookie
        set req.http.Cookie = regsuball(req.http.Cookie, "wp-settings-time-1=[^;]+(; )?", "");

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

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

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

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

        # Если cookie не найдено, удаляем данный параметр из пришедшего запроса как таковой
        if (!req.http.cookie) {
                unset req.http.cookie;
        }

        # Не кэшировать запросы с установленными cookies, это уже не касается WordPress
        if (req.http.Authorization || req.http.Cookie) {
                # Not cacheable by default
                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, сообщая браузеру о том, что эти файлы стоит сохранить в кэше клиента и не нагружать лишниий раз наш сервер
                unset beresp.http.Cache-Control;
                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-запрос или Basic авторизации
        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);
        }


        # Хранить в кэше всё прочее на протяжении одного дня
        set beresp.ttl = 1d;
        # Срок жизни кэша после истечения его TTL
        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);
}

После чего выполняем команду:

service varnish restart

Перейдя теперь в браузере на наш сайт, мы увидим index.php, который нужно предварительно создать.

Проблема Varnish и Debian 8

А что если вы захотите изменить порт, на котором Varnish будет принимать входящие соединения или изменить объём кэша. Судя по официальной документации нужно изменить файл с параметрами запуска Varnish, располагающийся по пути: /etc/default/varnish и перезапустить сервис. Но нет! Ничего не изменится, и если мы зайдём в top и нажмем на клавишу ‘c’, то увидим, что сервис запущен с прежними настройками. А всё дело в том, что в новой версии Debian используется systemd вместо init.d в качестве системы инициализации, и поэтому нужно зайти в файл /lib/systemd/system/varnish.service и прописать там в директиве ExecStart те же параметры запуска:

[Unit]
Description=Varnish HTTP accelerator

[Service]
Type=forking
LimitNOFILE=131072
LimitMEMLOCK=82000
ExecStartPre=/usr/sbin/varnishd -C -f /etc/varnish/default.vcl
ExecStart=/usr/sbin/varnishd -a :6081 -T 127.0.0.1:6082 -f /etc/varnish/default.vcl -S /etc/varnish/secret -s malloc,128m
ExecReload=/usr/share/varnish/reload-vcl

[Install]
WantedBy=multi-user.target

После сохранения выполнить следующие команды для вступления изменений в силу:

systemctl daemon-reload
service varnish restart

В данный момент данная проблема отписана разработчикам, когда и как они её решат — неизвестно, поэтому на всякий случай производите одинаковые изменения в обоих файлах, чтобы однажды после апдейта всё не упало.

Установка и настройка PHP-FPM


Устанавливаем FPM и библиотеку PHP для работы с СУБД:

apt-get install php5-fpm php5-mysqlnd

Заходим в файл конфигурации /etc/php5/fpm/pool.d/www.conf и меняем директиву:

listen = 127.0.0.1:9000

На следующее:

listen = /var/run/php5-fpm.sock

В этом же файле задаём настройки воркеров:

; Динамическое изменение количества воркеров
pm = dynamic
; Максимальное число воркеров, создаются под нагрузкой, не может быть меньше pm.max_spare_servers.
pm.max_children = 10 
; Сколько воркеров запускать при старте FPM
pm.start_servers = 1
; Минимальное количество запасных воркеров (остаются в памяти при отсутствии нагрузки)
pm.min_spare_servers = 1
; Максимальное количество запасных воркеров (при простое, остальные неиспользуемые будут завершаться)
pm.max_spare_servers = 3
; Максимальное количество запросов, которые выполняет один воркер, прежде чем перезапуститься
pm.max_requests = 500

Меняем несколько директив в /etc/php5/fpm/php.ini
upload_max_filesize = 10M
post_max_size = 12M
allow_url_fopen = Off

post_max_size задаём чуть больше, чем upload_max_filesize, так как помимо файла в запросе идут другие данные.
Здесь же директивой allow_url_fopen запрещаем выполнять скрипты, расположенные удаленно (убирая возможность эксплуатации уязвимости удалённого инклуда).

И говорим FPM перечитать конфиг:

service php5-fpm reload

Теперь создайте файлик, выводящий phpinfo() и обратитесь к нему в браузере, всё должно работать. Не забывайте, что он уже закэшировался в Varnish и если вы будете изменять конфигурацию PHP, то она не будет обновляться в вашем браузере. Можете написать правило на пропуск данного файла в Varnish, либо же на время тестов проксировать не Varnish, а напрямую бэкенд на 81 порту.

Установка и настройка MariaDB


Эту СУБД я выбрал по причине её лучшей производительности и способности выдерживать большие нагрузки, при этом затрачивая меньшее количество оперативной памяти по сравнению с MySQL, а так же её полной совместимостью с WordPress. Установка очень проста, будет запрошен пароль для пользователя root.

apt-get install mariadb-server

В качестве движка для таблиц я использую MyISAM, по причине того, что запись в таблицу выполняется редко, а на чтении MyISAM показывает лучшие характеристики. Я полностью отключил поддержку InnoDB для освобождения оперативной памяти. Настройки хранятся в файле /etc/mysql/my.cnf. Опишу только те директивы, которые я изменил:

# Кэш для работы с ключами и индексами
key_buffer = 64M

# Кэш запросов
query_cache_size = 32M

# Установка MyISAM в качестве стандартного движка
default-storage-engine=MyISAM

# Отключение движка InnoDB
skip-innodb

После сохранения изменений перезапускаем сервис:

service mysql restart

Настройка WordPress — плагин «Varnish HTTP Purge»


Устанавливаем в панели администрирования WP плагин «Varnish HTTP Purge». Теперь при обновлении данных на измененные страницы будет отправлен PURGE-запрос, очищающий кэш в Varnish, и для посетителей данные всегда будут обновлёнными.

Дополнительная оптимизация


Для оптимизации клиентской стороны с помощью Varnish мы указываем браузеру на необходимость хранения статических данных в локальном кэше клиента. Но если вы жаждете ещё большей оптимизации, перейдите на страничку developers.google.com/speed/pagespeed/insights и введите URL вашего сайта или даже конкретной страницы. Вам предоставится список рекомендаций, а так же предложат архив со сжатыми версиями ваших css и js стилей. Замените их на своём сайте и получите ещё большую скорость загрузки за счёт уменьшенного объема передаваемых данных, так же уменьшится нагрузка на сервер и место, занимаемое данными файлами в кэше.

Как поступить с документами, запрашиваемыми со сторонних серверов, например, шрифтами или библиотекой jquery? Можно перенести их к себе, и тут за счёт установки соединения только с одним сервером возрастёт скорость загрузки страниц, однако, в то же время, возрастёт список обращений и общая нагрузка. Какой вариант выбрать — решайте сами, в зависимости от загруженности вашего сервера и вашей лени.

Итог


По большей части наибольший эффект дали сжатие gzip и кэширование в Varnish. В комментариях уже написали много дополнительных методов оптимизации, которые я непременно изучу и по мере необходимости внедрю. Пока же результаты оптимизации следующие:
До
image
После
image
Полноценные стресс-тесты проведу чуть позже.




К сожалению, не доступен сервер mySQL