В данной статье я расскажу о том, как я заставил свой блог на WordPress летать за счёт грамотного кэширования, сжатия и другой оптимизации серверной и клиентской сторон. На момент написания статьи характеристики
CPU: 1 x 2GHz
HDD: 10Gb
RAM: 512Mb
OS: 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;
# Метод отправки данных sendfile эффективнее чем read+write
sendfile on;
# Отправлять заголовки и и начало файла в одном пакете
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;
# Имя хоста
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
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
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;
# Включить gzip-сжатие на выходе
gzip on;
gzip_comp_level 9;
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;
# Отдавать предпочтение шифрам, заданным на сервере
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 и начинаем кодить:
# Сообщаем компилятору о том, что используется новая версия 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-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? Можно перенести их к себе, и тут за счёт установки соединения только с одним сервером возрастёт скорость загрузки страниц, однако, в то же время, возрастёт список обращений и общая нагрузка. Какой вариант выбрать — решайте сами, в зависимости от загруженности вашего сервера и вашей лени.
Автор: ivashkevitch