Возможно ли «выжать» из сайта на WordPress высокую производительность? Наш ответ — да! В этой статье мы покажем, как добиться устойчивой работы сайта на WordPress при высоких нагрузках, доходящих до 10,000 соединений в секунду, что равно 800 миллионам посещений в сутки.
Прежде всего, нам нужен собственный виртуальный сервер (VPS). Для тестов использовался VPS, арендованный у DigitalOcean за 20 USD в месяц со следующими параметрами: 2GB памяти, 2 процессора, 40GB дискового пространства на SSD. В качестве операционной системы была выбрана CentOS Linux release 7.3.
Дальнейший текст можно рассматривать почти как пошаговую инструкцию для опытных администраторов. Будут приведены только те параметры, которые отличаются от стандартных и увеличивают производительность сервера. Итак, вперед!
Замените “OS” на “rhel” или “centos”, в зависимости от используемого дистрибутива, и “OSRELEASE” на “5”, “6”, or “7”, для версий 5.x, 6.x, или 7.x, соответственно.
Запускаем процесс установки:
yum -y install nginx
Правим nginx.conf
# Автоматически установить число рабочих процессов, равное числу процессоров
worker_processes auto;
# В секции events
# epoll — эффективный метод обработки соединений
use epoll;
# Рабочему процессу принимать сразу все новые соединения (иначе только одно за другим)
multi_accept on;
# В секции http
# Выключаем логи
error_log /var/log/nginx/error.log crit; # только сообщения о критических ошибках
access_log off;
log_not_found off;
# Отключаем возможность указания порта при редиректе
port_in_redirect off;
# Разрешить больше запросов для keep-alive соединения
keepalive_requests 100;
# Уменьшаем буферы до разумных пределов
# Это позволяет экономить память при большом числе запросов
client_body_buffer_size 10K;
client_header_buffer_size 4k; # на WordPress 2k может не хватать
client_max_body_size 50m;
large_client_header_buffers 2 4k;
# Уменьшаем таймауты
client_body_timeout 10;
client_header_timeout 10;
send_timeout 2;
# Разрешаем сброс соединений по таймауту
reset_timedout_connection on;
# Ускорение работы tcp
tcp_nodelay on;
tcp_nopush on;
# Разрешение использование системной функции Linux - sendfile() для ускорения файловых операций
sendfile on;
# Включаем сжатие данных
gzip on;
gzip_http_version 1.0;
gzip_proxied any;
gzip_min_length 1100;
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_disable msie6;
gzip_vary on;
# Включаем кэширование открытых файлов
open_file_cache max=10000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# Устанавливаем Таймаут php
fastcgi_read_timeout 300;
# Подключаем php-fpm по сокету - работает быстрее, чем по tcp
upstream php-fpm {
# Это должно соответствовать директиве "listen" в пуле php-fpm
server unix:/run/php-fpm/www.sock;
}
# Включаем дополнительные файлы конфигураций
include /etc/nginx/conf.d/*.conf;
Создаем файл /etc/nginx/conf.d/servers.conf и записываем туда секции server для сайтов. Пример:
Ниже группа настроек для сервера с памятью 2ГБ. Для сервера с 1ГБ памяти размеры буферов следует урезать вдвое, но не стоит трогать те параметры, которые имеют значения 64M и ниже. Все параметры тут важны, но важнейшим является «query_cache_type = ON». По умолчанию, кэширование запросов в MySQL выключено! Причина непонятна, возможно, экономия памяти. Но с включением этого параметра работа с базой сильно ускоряется, что сразу же чувствуется на административных страницах сайта WordPress.
max_connections = 64
key_buffer_size = 32M
# innodb_buffer_pool_chunk_size по умолчанию 128M
innodb_buffer_pool_chunk_size=128M
# Когда innodb_buffer_pool_size меньше чем 1024M,
# innodb_buffer_pool_instances будет установлен MySQL в 1
innodb_buffer_pool_instances = 1
# innodb_buffer_pool_size должен быть равен ил быть кратен
# innodb_buffer_pool_chunk_size * innodb_buffer_pool_instances
innodb_buffer_pool_size = 512M
innodb_flush_method = O_DIRECT
innodb_log_file_size = 512M
innodb_flush_log_at_trx_commit = 2
thread_cache_size = 16
query_cache_type = ON
query_cache_size = 128M
query_cache_limit = 256K
query_cache_min_res_unit = 2k
max_heap_table_size = 64M
tmp_table_size = 64M
Запускаем:
systemctl start mysqld
systemctl enable mysqld
Общий принцип построения высокопроизводительного сайта на WordPresss таков: все должно помещаться примерно в 75% оперативной памяти: nginx, mysql, php-fpm. В консоли в команде top должно быть видно, что свободно 20-25% оперативки и swap не используется. Эта память еще очень пригодится при запуске множества процессов php-fpm во время обработки динамических страниц сайта.
Устанавливаем php
Конечно, нас интересует только 7 версия, которая примерно вдвое быстрее предыдущей — php5. Отличная сборка есть в репозитории webtatic. Кроме того, нужен еще и репозиторий epel.
Обратите внимание на пакет php72w-pecl-apcu, который подключает кэширование данных (APCu), дополняющее кэширование объектов (OpCache), а также на php72w-pecl-redis, который подключает сервер объектного кеширования Redis. WordPress поддерживает APCu. Поддержка Redis доступна с помощью плагина Redis Object Cache.
Правим /etc/php.ini
; Это значение надо увеличить в системах, где PHP открывает много файлов
realpath_cache_size = 64M
; Не надо увеличивать стандартный размер памяти для процесса без необходимости
; Большинство WordPress сайтов потребляют порядка 80 МБ
memory_limit = 128M
; А вот это стоит уменьшить, для экономии памяти под буферы
; Размер поста редко превышает 64 МБ
post_max_size = 64M
; Обычно размер загружаемых файлов не превышает 50 МБ
upload_max_filesize = 50M
; Должно выполняться правило:
; upload_max_filesize < post_max_size < memory_limit
; И не забываем о настройках OpCache
; Немедленно убивать процессы, блокирующие кэш
opcache.force_restart_timeout = 0
; Экономить память, сохраняя одинаковые строки только 1 раз для всех процессов php-fpm
; На 2ГБ сервере не получилось поставить больше, чем 8
opcache.interned_strings_buffer = 8
; Сколько файлов PHP можно удерживать в памяти
; На 2ГБ сервере не получилось поставить больше, чем 4000
opcache.max_accelerated_files = 4000
; Сколько памяти потребляет OpCache
; На 2ГБ сервере не получилось поставить больше, чем 128
opcache.memory_consumption = 128
; Битовая маска, в которой каждый поднятый бит соответствует определённому проходу оптимизации
; Полная оптимизация
opcache.optimization_level = 0xFFFFFFFF
Правим /etc/php-fpm.d/www.conf
; Уменьшаем число дочерних процессов
;pm.max_children = 50
pm.max_children = 20
;pm.start_servers = 5
pm.start_servers = 8
;pm.max_spare_servers = 35
pm.max_spare_servers = 10
; Убиваем процессы через 10 секунд неактивности
pm.process_idle_timeout = 10s;
; Число запросов дочернего процесса, после которого процесс будет перезапущен
; Это полезно для избежания утечек памяти
pm.max_requests = 500
На WordPress в обязательном порядке нужно установить плагин WP Super Cache. Можно со стандартными настройками. Что делает этот плагин? При первом обращении к странице или записи WordPress, плагин создаёт файл .html с тем же именем и сохраняет его. При последующих обращениях — перехватывает работу WordPress и вместо повторной генерации страницы выдаёт готовый .html из кэша. Очевидно, это радикальным образом увеличивает скорость доступа к сайту.
Тесты
Поехали?
Собственно, да — можно начинать тестирование сайта. В качестве подопытного кролика выступал сайт, который вы читаете, и его копия по адресу test2.kagg.eu — все то же самое, только без https.
В качестве инструмента проведения нагрузочных тестов использовался loader.io. Веб-сервис предоставляет на бесплатном аккаунте возможность тестирования одного сайта со скоростью посещений до 10,000 в секунду. Но нам больше и не надо, как мы впоследствии убедимся.
Первые результаты впечатлили — без https сайт выдержал 1,000 посещений главной страницы в секунду. Размер страницы вместе с картинками — 1.6 МБ.
Микрокэширование
1,000 хитов в секунду неплохо, но мы хотим большего! Что делать? Есть в nginx такая очень полезная вещь, как микрокэширование. Это кэширование файлов и прочих ресурсов на короткий промежуток времени — от секунд до минут. Когда идут быстрые запросы, следующий пользователь получает данные из микрокэша, что существенно уменьшает время отклика.
Правим /etc/nginx/nginx.conf — вставляем одну строку перед завершающим include в коде выше:
Результат впечатляет — производительность улучшилась в 3 раза. Сайт отдает по http главную страницу весом в 1.6 МБ 3,000 раз в секунду! Среднее время отклика — 108 мс.
Проверяем в другой модели: линейное нарастание числа запросов от 0 до 3,000 в секунду. Работает. Среднее время отклика — 210 мс.
Поток данных при постоянном числе запросов 3,000 в секунду достигает 1 Гбит в секунду, даже с использованием сжатия. Что мы и видим на экране системы мониторинга Zabbix.
Zabbix, кстати, крутится на том же тестовом сервере.
А что с https?
А с https, как и ожидалось, существенно медленнее. Слишком много операций по согласованию ключей, хешированию, шифрованию и т.д. Забавно наблюдать в команде top в консоли, как два процесса nginx пожирают каждый 🙂 по 99% процессора. Вот результат — 1,000 соединений в секунду:
Результаты совсем неплохие, но мы можем больше!
Rewrite в nginx
Давайте задумаемся — что сейчас происходит при обращении к странице?
nginx принимает запрос, видит, что надо обратиться к index.php
nginx запускает php-fpm (что не быстро совсем)
WordPress начинает работу
В самом начале загрузки встревает плагин WP Super Cache и подсовывает готовый .html вместо бесконечно долгой (в этих временных терминах) генерации страницы
Все это неплохо, но: процесс php-fpm стартовать надо, а потом еще какой-никакой, но php код выполнить, чтобы отдать .html.
Есть решение, как обойтись вообще без php. Делать rewrite сразу на nginx.
Снова правим /etc/nginx/conf.d/servers.conf — и после строки «index index.php;» вставляем:
include snippets/wp-supercache.conf;
Создаем папку /etc/nginx/snippets и в ней создаём файл wp-supercache.conf с таким содержимым:
# WP Super Cache rules.
set $cache true;
# POST requests and urls with a query string should always go to PHP
if ($request_method = POST) {
set $cache false;
}
if ($query_string != "") {
set $cache false;
}
# Don't cache uris containing the following segments
if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|/wp-(app|cron|login|register|mail).php
|wp-.*.php|/feed/|index.php|wp-comments-popup.php
|wp-links-opml.php|wp-locations.php|sitemap(_index)?.xml
|[a-z0-9_-]+-sitemap([0-9]+)?.xml)") {
set $cache false;
}
# Don't use the cache for logged-in users or recent commenters
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+
|wp-postpass|wordpress_logged_in") {
set $cache false;
}
# Set the cache file
set $cachefile "/wp-content/cache/supercache/$http_host${request_uri}index.html";
set $gzipcachefile "/wp-content/cache/supercache/$http_host${request_uri}index.html.gz";
if ($https ~* "on") {
set $cachefile "/wp-content/cache/supercache/$http_host${request_uri}index-https.html";
set $gzipcachefile "/wp-content/cache/supercache/$http_host${request_uri}index-https.html.gz";
}
set $exists 'not exists';
if (-f $document_root$cachefile) {
set $exists 'exists';
}
set $gzipexists 'not exists';
if (-f $document_root$gzipcachefile) {
set $gzipexists 'exists';
}
if ($cache = false) {
set $cachefile "";
set $gzipcachefile "";
}
# Add cache file debug info as header
#add_header X-HTTP-Host $http_host;
add_header X-Cache-File $cachefile;
add_header X-Cache-File-Exists $exists;
add_header X-GZip-Cache-File $gzipcachefile;
add_header X-GZip-Cache-File-Exists $gzipexists;
#add_header X-Allow $allow;
#add_header X-HTTP-X-Forwarded-For $http_x_forwarded_for;
#add_header X-Real-IP $real_ip;
# Try in the following order: (1) gzipped cachefile, (2) cachefile, (3) normal url, (4) php
location / {
try_files @gzipcachefile @cachefile $uri $uri/ /index.php?$args;
}
# Set expiration for gzipcachefile
location @gzipcachefile {
expires 43200;
add_header Cache-Control "max-age=43200, public";
try_files $gzipcachefile =404;
}
# Set expiration for cachefile
location @cachefile {
expires 43200;
add_header Cache-Control "max-age=43200, public";
try_files $cachefile =404;
}
Выполняя эти инструкции, nginx сам смотрит, есть ли .html (или сжатый .html.gz) в папках плагина WP Super Cache и если есть, берет его, не запуская никакой php-fpm.
Перезапускаем nginx, стартуем тест, смотрим в консоли в top. И, о чудо! — ни одного процесса php-fpm не стартует, а раньше их запускалось пара десятков. Работает один nginx, двумя своими рабочими процессами.
Что имеем в результате? Вместо 3,000 раз, сервер способен отдавать главную сайта без https 7,500 раз в секунду! Напомню, страница весит 1.6 МБ. Поток данных — 2 Гигабит в секунду, и, похоже, мы упёрлись в ограничение DigitalOcean по полосе пропускания.
А что, если протестировать скорость отдачи небольшой страницы, скажем, /contacts на этом сайте?
И вот тут мы, видимо, подошли к пределу сервера. Но получили результат из заголовка статьи: 10,000 посещений в секунду! Напомню — 10,000 посещений в секунду абсолютно реального сайта на WordPress — с установленными плагинами, красивой (и не очень лёгкой темой) и т.д.
Сценарий с возрастанием нагрузки тоже работает.
Тест в данной конфигурации с https показал незначительное увеличение производительности — до 1,100 посещений в секунду. Весь процессор, похоже, потрачен на шифрование…
Резюме
Мы показали, что вполне стандартный сайт на WordPress, при правильной конфигурации сервера и организации кэширования, способен отдавать не менее 10,000 страниц в секунду при работе через http. В течение суток такой сайт выдержит запредельные 800 миллионов посещений.
При включении шифрования тот же сайт может отдавать не менее 1,100 страниц в секунду.
Применённые для достижения результата средства и методики:
Виртуальный сервер (VPS) на DigitalOcean: 2 ГБ памяти, 2 процессора, 40 ГБ SSD, 20 USD в месяц
CentOS 7.3 64-bit
Последние версии nginx, php7, mysql
Плагин WP Super Cache
Микрокэширование в nginx
Полное исключение запуска php для показа кэшированных страниц с помощью rewrite в nginx
Единственный обладатель значков золотой WordPress и бронзовый WooCommerce на StackOverflow RU. WordPress Core contributor. Работал ведущим девелопером в команде WPML.