Чат Telegram
Группа ВКонтакте
Ускорение блога на WordPress: Nginx, MariaDB, Varnish

Ускорение блога на WordPress: Nginx, MariaDB, Varnish

… или отчего летает WebShake.RU на VDS за $2.99 в месяц.

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

К слову сказать, wordpress здесь как один из вариантов. На практике же данный мануал можно применить в общем-то к любому сайту, так что будет полезна многим, как мне кажется.

Онлайн обучение PHP
Путь с полного нуля до джуниора!
Начать бесплатно

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

На момент написания статьи характеристики VDS следующие:

CPU RAM HDD Virtualization Type OS
1 x 2 GHz 512Mb 10Gb KVM Debian 8 x64

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

Для посетителей сайта происходит перенаправление на 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-сжатие на выходе бэкенда. В кэш пойдут уже сжатые версии файлов
    gzip                on;
    gzip_comp_level     1;
    gzip_min_length     512;
    gzip_buffers        8 64k;
    gzip_types text/plain 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 и получении сертификатов здесь: https://webshake.ru/post/175

И создаём файл /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;

    # Отдавать предпочтение шифрам, заданным на сервере
    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 может быть представлена следующим образом:

Онлайн-курсы PHP и MySQL
Обучение с полного нуля до уровня джуниора!
Начать бесплатно

Если обращение к бэкенду происходит при этом из функции vcl_miss ответ бэкенда отправляется и в кэш.

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

# Настройки бэкенда
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 = 11M

post_max_size задаём чуть больше, чем upload_max_filesize, так как помимо файла в запросе идут другие данные.

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

service php5-fmp 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.

Результаты получились следующими:

До

После

Продолжение читайте в этой статье. Спасибо за прочтение, всего Вам наилучшего.

Присоединяйтесь к нам в ВКонтакте и в Facebook, чтобы не пропустить новые уроки. А также вступайте в наш чат PHP-разработчиков в Telegram.
Об авторе
Артём Ивашкевич
Артём Ивашкевич
Занимаюсь программированием более трех лет. В свободное время обучаю программированию на PHP других людей, потому что мне это нравится. Если вы интересуетесь темой IT и хотели бы стать разработчиком, рекомендую прочитать статью о том, как я стал программистом.
Онлайн-курсы PHP и MySQL
Обучение с полного нуля до уровня джуниора!
Начать бесплатно
Курс программирования на PHP
Подготовка до уровня устройства на работу!
Начать бесплатно
Комментарии (18)
Курс программирования на PHP
Подготовка до уровня устройства на работу!
Начать бесплатно
loader
Есть вопрос по программированию?
Здесь быстро ответят!
Задать вопрос
Онлайн обучение PHP
Путь с полного нуля до джуниора!
Начать бесплатно
loader
Форум веб-разработчиков
Здесь все!
Перейти
loader
Логические задачи с собеседований