Как ускорить WordPress в 4 раза: Опыт настройки Proxmox LXC и Nginx

автор: rss9020 3 мин. чтения

При запуске небольшого блога на собственном железе часто кажется, что мощного процессора серверного класса хватит для любых задач. Однако без правильной программной конфигурации даже минимальная нагрузка может полностью парализовать сайт.

Ниже описан практический кейс оптимизации WordPress-сайта на сервере, в результате которого среднее время ответа снизилось с 2.28 до 0.59 секунды под нагрузкой в 100 одновременных пользователей.


Исходная конфигурация и «синтетический» триумф

На старте проект имел следующие вводные:

  • Аппаратная часть: CPU Intel Xeon E5-2680 v4 (14 ядер / 28 потоков), 64 ГБ DDR4 RAM.
  • Виртуализация: Контейнер LXC под управлением Proxmox (выделено 2 ядра CPU и всего 512 МБ RAM).
  • Стек: Nginx + PHP-FPM 8.4 + MariaDB.

Первичные синтетические тесты в Google PageSpeed Insights показывали идеальную «зеленую» зону: First Contentful Paint (FCP) — 0.6 сек, Largest Contentful Paint (LCP) — 1.0 сек, а Total Blocking Time (TBT) — 50 мс. Сайт казался безупречно оптимизированным.


Проблема: Обрушение под стресс-тестом

Реальную производительность системы показывает только нагрузочное тестирование. При запуске стресс-теста на 100 одновременных пользователей сервер фактически захлебнулся:

  1. Критический рост времени ответа: Среднее время генерации страницы взлетело до 2.28 секунды, а пиковые задержки доходили до 5 секунд. Сайт стал некомфортным для использования.
  2. Перегрузка процессора: Утилизация выделенных ядер CPU достигла 90%. Очередь задач (Load Average) поднялась до 5.08 при физическом лимите в 2.0.
  3. Память и жесткий свопинг: Выделенные 512 МБ оперативной памяти мгновенно исчерпались. Система ушла в жесткий SWAP (занято около 230 МБ на диске). Процессор стал простаивать в ожидании медленных дисковых операций ввода-вывода (iowait).
В чем была проблема

При каждом визите виртуального пользователя Nginx заново дергал тяжелый интерпретатор PHP-FPM и базу данных MariaDB, порождая десятки требовательных процессов.


Решение: Переход на правильное файловое кэширование

Для исправления ситуации архитектура отдачи контента была перестроена на генерацию чистого статического HTML:

  1. Установка плагина кэшировнаия: Установлен WP Super Cache, ориентированный на дисковое кэширование под любыми веб-серверами.
  2. Настройка Simple-режима: В плагине был активирован режим Simple и включено Gzip-сжатие.
  3. Предзагрузка (Preload): Был запущен процесс предзагрузки кэша, чтобы плагин заранее создал HTML-копии для всех страниц блога, не дожидаясь визитов реальных пользователей.

Результат: Стабильность и четырехкратное ускорение

Повторный нагрузочный тест на те же 100 одновременных пользователей показал кардинальное изменение картины:

  • Скорость: Среднее время ответа упало до 0.59 секунды (ускорение более чем в 4 раза). График задержек стал ровным, без пугающих пиков.
  • Эффективность CPU: Нагрузка на процессор Xeon упала с 90% до скромных 8.7%. Очередь задач снизилась до идеальных 0.95.
  • Разгрузка памяти: Процессы php-fpm перестали множиться и забивать ОЗУ. Потребление памяти стабилизировалось, а использование медленного SWAP полностью прекратилось. Основную работу по отдаче готового HTML взял на себя легковесный Nginx.
До оптимизации (Nginx + php-fpm):     2.28 сек | CPU 90% | Высокий SWAP
После оптимизации (WP Super Cache):   0.59 сек | CPU 8.7% | Стабильная RAM, SWAP есть но он не используется

Промежуточные итоги

Высокие характеристики серверного процессора не спасут сайт, если конфигурация ПО вынуждает систему совершать лишнюю работу. Правильно настроенное файловое кэширование позволяет даже скромному контейнеру с 512 МБ RAM легко выдерживать серьезные всплески трафика, превращая динамический WordPress в сверхбыструю статическую систему.

До и после кэширования: при одинаково количестве отданного трафика, разная загрузка CPU

Продолжение банкета

В первой части мы остановились на том, что установка плагина кеширования WP Super Cache снизила время ответа сайта под нагрузкой. Однако при попытке провести глубокий стресс-тест через облачный инструмент k6 (Grafana) сервер столкнулся с каскадом новых скрытых проблем: от жестких лимитов памяти до ложных срабатываний систем безопасности.

Ниже — пошаговый разбор того, как оптимизировать стек PHP-FPM под увеличенные ресурсы и обойти подводные камни защитных плагинов для достижения эталонной стабильности.


Шаг 1. Расширение RAM и тюнинг пула PHP-FPM

Ограничение контейнера Proxmox LXC в 512 МБ оперативной памяти приводило к тому, что процессы PHP мгновенно вытесняли систему в SWAP [2].

Было принято решение выделить контейнеру честные 2 ГБ RAM, что позволило отказаться от жестких ограничений и перенастроить менеджер процессов PHP-FPM в конфигурационном файле /etc/php/8.4/fpm/pool.d/www.conf:

ini

pm = dynamic
pm.max_children = 25
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10
pm.max_requests = 500

Зачем нужны эти параметры?

  • pm.max_children = 25: Потолок одновременно работающих процессов. Один процесс WordPress потребляет около 40–60 МБ RAM. Лимит в 25 процессов гарантирует, что на пике нагрузки PHP заберет максимум ~1.5 ГБ, оставив безопасный резерв для Nginx и базы данных MariaDB.
  • pm.max_requests = 500: Эффективное лекарство от утечек памяти, свойственных WordPress и его плагинам. Обработав ровно 500 запросов, процесс полностью завершает свою работу, освобождая память, а вместо него автоматически создается «чистый» поток.

Шаг 2. Подготовьте Nginx к высокой нагрузке

По умолчанию Nginx имеет жесткие ограничения на количество соединений. Измените два параметра.

Откройте главный конфиг Nginx

nano /etc/nginx/nginx.conf

Увеличьте количество соединений на один воркер

nginxevents { worker_connections 2048; } 

Проверьте конфигурацию и перезапустите Nginx

nginx -t && systemctl restart nginx


Шаг 3. Неочевидный враг: Ложные срабатывания Wordfence

Даже после расширения памяти повторный стресс-тест k6 неожиданно показал лавину ошибок. Проверка почтовых уведомлений выявила неожиданного виновника — плагин безопасности Wordfence.

[Wordfence Alert] Блокировка IP 52.15.45.107
Причина: "Превышено максимальное количество ошибок 'страница не найдена' в минуту для краулера."

Облачный сервер Grafana k6, отправляя десятки запросов в секунду, мгновенно превысил встроенный лимит безопасности Wordfence (60 запросов с ошибкой 404 в минуту). Защитный плагин заблокировал IP-адрес самого теста на 1 час и начал принудительно отдавать ему заглушки.

Более того, являясь тяжелым PHP-скриптом, Wordfence тратил колоссальные ресурсы процессора Xeon на валидацию каждого атакующего чиха, искусственно перегружая сервер. На время проведения финального тестирования плагин был полностью деактивирован.


Шаг 3. Финальный стресс-тест: Абсолютная стабильность

После очистки заблокированных IP, временного отключения Wordfence и применения оптимизации PHP-FPM, утилизация ресурсов в утилите top приняла идеальный рабочий вид:

  • Память и SWAP: Из 2 ГБ RAM свободными остались около 824 МБ, а под кэш ушло 896 МБ. Нагрузка на SWAP упала до 0.0 MiB used. Диск полностью освободился от избыточных циклов перезаписи.
  • Процессор: Два выделенных ядра CPU Xeon E5-2680 v4 загрузились ровно на 100% (Load Average: 2.00), эффективно выполняя чистую обработку кода, без затыков и очередей.

Результаты графиков Grafana:

Финальный график k6 зафиксировал безоговорочную победу над задержками:

  1. Контроль задержки (P95 Response Time): На пике нагрузки в 100 одновременных пользователей (100 VUs) время ответа не превысило 721 мс, а при спаде графиков мгновенно вернулось к комфортным 400 мс. Никаких улетов к 5 секундам, как в начале пути.
  2. Высокая пропускная способность (Peak RPS): Сервер уверенно переваривал 39.67 запросов в секунду, отдавая страницы без задержек.
  3. Минимум ошибок: Розовая линия сбоев (Failure Rate) осталась лежать на самом дне графика. Единичные сетевые погрешности составили незначительный процент от общей массы в тысячи запросов.
+------------------------------------+------------------------+-----------------------+

| Этап оптимизации                   | Среднее время ответа   | Ошибки под нагрузкой  |
+------------------------------------+------------------------+-----------------------+

| Исходный (Без кэша, 512 МБ RAM)    | 2.28 сек (пики до 5с)  | Сервер падал в SWAP   |
| Промежуточный (Лимиты WAF)         | Искусственный бан      | Лавина ложных 503/404 |
| Финал (Simple кэш, 2ГБ RAM, FPM)   | 0.72 сек (стабильно)   | Стремится к 0%        |
+------------------------------------+------------------------+-----------------------+

Заключение

Оптимизация домашнего сервера под WordPress успешно завершена. Текущей конфигурации контейнера Proxmox LXC с запасом хватит, чтобы выдержать не просто 100 посетителей в день, а десятки тысяч визитов ежедневно.

Главный урок этого кейса: при настройке веб-сервера необходимо смотреть на систему комплексно. Увеличение оперативной памяти бессмысленно без правильной настройки лимитов PHP-FPM, а любой стресс-тест железа будет заблокирован встроенными плагинами безопасности, если вовремя не перевести их в режим обучения или не внести тестирующие IP-адреса в белые списки. Теперь сайт полностью готов к выходу в продакшн.


код Grafana k6 Test script для стресс тестирования сайта

import http from ‘k6/http’

import { check, sleep } from ‘k6’

const BASE_URL = ‘https://rss9020.ru’

const WORDPRESS_PATHS = [

  ‘/optimization-wordpress-wpsupercache/’,

  ‘/dmz-logs-lxc-proxmox-alloy-loki-grafana/’,

  ‘/backup-pve-pbs/’,

  ‘/proxmox-veeam-backup-non-root/’,

  ‘/pfssdfsdf/’,

]

function getRandomPath() {

  return WORDPRESS_PATHS[Math.floor(Math.random() * WORDPRESS_PATHS.length)]

}

function randomSleep(min, max) {

  const duration = Math.random() * (max — min) + min

  sleep(duration)

}

export const options = {

  stages: [

    { duration: ‘1m’, target: 100 },

    { duration: ‘1m’, target: 0 },

  ],

  thresholds: {

    http_req_failed: [‘rate<0.01’],

    http_req_duration: [‘p(95)<2000’],

  },

}

export default function () {

  // Step 1: Open the home page

  const homeResponse = http.get(BASE_URL)

  check(homeResponse, {

    ‘home page status is 200’: (r) => r.status === 200,

  })

  // Think Time: Random pause between 1 and 3 seconds

  randomSleep(1, 3)

  // Step 2: Navigate to a random internal page

  const randomPath = getRandomPath()

  const pageResponse = http.get(`${BASE_URL}${randomPath}`)

  check(pageResponse, {

    ‘internal page status is 200’: (r) => r.status === 200,

  })

  // Think Time: Random pause between 2 and 4 seconds

  randomSleep(2, 4)

  // Step 3: Simulate a form/comment submission

  const commentPostId = ‘1’

  const commentData = {

    author: `User_${Math.floor(Math.random() * 10000)}`,

    email: `user_${Math.floor(Math.random() * 10000)}@example.com`,

    comment: `This is a test comment from k6 performance test. Timestamp: ${new Date().toISOString()}`,

    comment_post_ID: commentPostId,

  }

  const commentResponse = http.post(

    `${BASE_URL}/wp-comments-post.php`,

    commentData,

    {

      headers: {

        ‘Content-Type’: ‘application/x-www-form-urlencoded’,

      },

    }

  )

  check(commentResponse, {

    ‘comment submission status is 200 or 302’: (r) =>

      r.status === 200 || r.status === 302,

  })

  sleep(1)

}

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *