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

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

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

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

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

На момент написания статьи характеристики 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://php.zone/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 может быть представлена следующим образом:

Если обращение к бэкенду происходит при этом из функции 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.

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

До

После

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

loader
Комментарии
К этому посту больше нельзя оставлять новые комментарии
Логические задачи с собеседований