10,000 соединений в секунду на WordPress — возможно!

Возможно ли «выжать» из сайта на WordPress высокую производительность? Наш ответ — да! В этой статье мы покажем, как добиться устойчивой работы сайта на WordPress при высоких нагрузках, доходящих до 10,000 соединений в секунду, что равно 800 миллионам посещений в сутки.

Прежде всего, нам нужен собственный виртуальный сервер (VPS). Для тестов использовался VPS, арендованный у DigitalOcean за 20 USD в месяц со следующими параметрами: 2GB памяти, 2 процессора, 40GB дискового пространства на SSD. В качестве операционной системы была выбрана CentOS Linux release 7.3.

Дальнейший текст можно рассматривать почти как пошаговую инструкцию для опытных администраторов. Будут приведены только те параметры, которые отличаются от стандартных и увеличивают производительность сервера. Итак, вперед!

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

Сначала создаем репозиторий для yum:

touch /etc/yum.repos.d/nginx.repo

Вставляем в файл следующий текст:

[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/OS/OSRELEASE/$basearch/
gpgcheck=0
enabled=1

Замените “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 для сайтов. Пример:

# Rewrite с http
server {
    listen 80;
    server_name domain.com www.domain.com;
    rewrite ^(.*) https://$host$1 permanent;
}

# Обработка https
server {
# включаем http2 - ускоряет обработку (бинарность, мультиплексирование и т.д.)
    listen 443 ssl http2;
    ssl_certificate /etc/nginx/ssl/domain.pem;
    ssl_certificate_key /etc/nginx/ssl/domain.key;

    server_name domain.com www.domain.com;

    root /var/www/domain;
    index index.php;

# Обрабатываем php файлы
    location ~ .php$ {
        fastcgi_buffers 8 256k;
        fastcgi_buffer_size 128k;
        fastcgi_intercept_errors on;
        include fastcgi_params;

        try_files $uri = 404;
    	fastcgi_split_path_info ^(.+.php)(/.+)$;

        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/var/run/php-fpm.sock;
        fastcgi_index index.php;
    }

# Кэшируем всю статику
    location ~* ^.+.(ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|atom|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
        expires max;
	add_header Cache-Control "public";
    }

# Кэшируем js и css
    location ~* ^.+.(css|js)$ {
	expires     1y;
	add_header Cache-Control "max-age=31600000, public";
    }
}

Запускаем nginx:

systemctl start nginx
systemctl enable nginx

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

yum install -y https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-community-server-5.7.18-1.el7.x86_64.rpm
yum -y install mysql-community-server

Правим /etc/my.cnf

Ниже группа настроек для сервера с памятью 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.

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

rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
rpm -Uvh https://mirror.webtatic.com/yum/el7/webtatic-release.rpm
yum -y install php72w
yum -y install php72w-fpm
yum -y install php72w-gd
yum -y install php72w-mbstring
yum -y install php72w-mysqlnd
yum -y install php72w-opcache
yum -y install php72w-pear.noarch
yum -y install php72w-pecl-imagick
yum -y install php72w-pecl-apcu
yum -y install php72w-pecl-redis

Обратите внимание на пакет 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

Запускаем:

systemctl restart php-fpm
systemctl enable php-fpm

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

Тут все просто, никаких настроек.

yum -y install redis
systemctl start redis
systemctl enable redis

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

Ну, этот раздел мы опускаем, как очевидный.

На 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 в коде выше:

    fastcgi_cache_path /var/cache/nginx/fcgi levels=1:2 keys_zone=microcache:10m max_size=1024m inactive=1h;

Правим /etc/nginx/conf.d/servers.conf — в секцию «location ~ .php$ {» вставляем:

        fastcgi_cache microcache;
        fastcgi_cache_key $scheme$host$request_uri$request_method;
        fastcgi_cache_valid 200 301 302 30s;
        fastcgi_cache_use_stale updating error timeout invalid_header http_500;
        fastcgi_pass_header Set-Cookie;
        fastcgi_pass_header Cookie;
        fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

Ну, само собой, рестартуем nginx.

Результат впечатляет — производительность улучшилась в 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

Давайте задумаемся — что сейчас происходит при обращении к странице?

  1. nginx принимает запрос, видит, что надо обратиться к index.php
  2. nginx запускает php-fpm (что не быстро совсем)
  3. WordPress начинает работу
  4. В самом начале загрузки встревает плагин 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

И никакого Varnish! 🙂

Источник: KAGG Design

Игорь Гергель

Единственный обладатель значков золотой WordPress и бронзовый WooCommerce на StackOverflow RU. WordPress Core contributor. Работал ведущим девелопером в команде WPML.

%d такие блоггеры, как: