WordPress. Начало

История началась пять часов назад.
Ко мне обратился владелец одного тематического новостного сайта. Тематика — спортивные соревнования. У сайта есть две проблемы.

  • Во-первых, в моменты крупных и сильно ожидаемых состязаний количество посетителей на сайте увеличивается на порядок.
  • Вторая проблема — он сделан на WordPress, причем довольно небрежно.

Думаю, что изначально это был обычный WP-сайт. Но потом он многократно «дорабатывался»: куда ни попадя втыкались разные рекламные блоки, вводились новые «решения», ставились всякие плагины для «оптимизации» и расширения возможностей. Кроме того, каждый день, на протяжении нескольких лет, появлялось около десятка постов. Размер БД — несколько гигабайт, ‘upload’ идет на десятки гигабайт. Со временем сайт превратился во что-то похожее на это:

Мусор на рабочем столе

И вот, где-то в 0.00 по Москве сайт начинает падать под наплывом любителей спорта. Главная страница то отдает код 500, то загружается дольше минуты. Подозреваю, что периоды таких «набегов» посетителей — самое хлебное время для подобных ресурсов: клики, ставки и все такое. Владелец нервничает, что, в общем-то, понятно. И так получается, что из тех, кто не спит в эту субботнюю ночь и что-то понимает в веб-разработке у владельца есть только я.

И тут следующая проблема. Я, мягко говоря, не очень люблю WordPress. И, что самое плохое в этой ситуации, не очень в нем разбираюсь. Возможно, потому и не люблю, что не умею его правильно готовить. Но факт остается фактом — до этого момента я никогда не имел дела со сколько-нибудь крупными проектами, которые сделаны на основе WordPress. Я, конечно, встречался с личными блогами и сайтами небольших контор на WP, где несколько сотен посетителей в сутки — счастье. Там он вполне справлялся, никаких проблем. Но и особых причин выяснять как он работает с ресурсами и как обеспечить его нормальную производительность там не было. Работает и работает.

Но в сложившейся ситуации нужно решить проблему его производительности. Причем очень быстро. Времени на чтение мануалов нет, особых знаний и умений тоже нет. Что делать?

Варианты решения

Разумеется, первая мысль — пойти по пути наименьшего сопротивления. Т.е. просто перейти на старший, более «мощный» тариф. Сайт размещен на VDS. И тут выясняется, что этот способ не пройдет: переход на большие ресурсы — только по заявке в тех. поддержку хостера. Срок исполнения — от нескольких часов. Хостер объясняет это виртуализацией KVM. Ok, этот вариант не подходит. Одновременный звонок в поддержку и чтение сайта хостера занимают около пяти минут.

Следующий вариант, который проносится в моей голове: «WordPress славен своими плагинами. Я даже помню названия двух плагинов для кэширования: WP Super Cache и WP Fastest Cache». Как я узнаю позже, оба входят во всякие списки вроде «Топ-10 плагинов кэширования для WP» и т.п. Я подозреваю, что устанавливать плагины на постоянно падающем сайте — очень неприятное занятие. Поэтому сайт около пяти минут работает только для меня. Я быстро ставлю первый плагин, пробегаю глазами настройки — вроде все ok. Включаю сайт для всех посетителей. Результат… Ноль. Возможно, что-то улучшилось, но это как мертвому припарки — улучшение незаметно невооруженным глазом. Сайт упорно продолжает падать. Неудачный выбор плагина? Еще быстрее жму на кнопки и ставлю второй плагин. Результат… Да такой же. Сношу и этот плагин. На все эти манипуляции у меня уходит почти пятнадцать минут.

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

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

Искра

Руки начинают искать и скачивать чистый дистрибутив WP, а голова — думать. Перед глазами стоит образ дядюшки, который любил часто (и весьма ехидно) говорить мне во времена моей юности: «Знание немногих принципов часто заменяет знание многих фактов». И вот о чем я подумал:

  • Все запросы на выдачу страниц сайта, скорее всего, проходят через некоторую единую «точку входа», роутер. Там определяется (или начинает определяться), что и в каком порядке будет дальше подключаться/считаться/выводиться.
  • Дальше, собственно, все подключается, считается и, к сожалению, выводится. Именно тут, скорее всего, происходит пожирание ресурсов. На бесконечные запросы к БД и прочие ужасы.
  • Ужасы заканчиваются, а результаты в некоторой «точке выхода» отдаются пользователю.

И если это все хотя бы примерно так, то можно применить систему кэширования, которую я называю «Топор». Кроме того, на этом сайте всем посетителям отдаются одинаковые по виду страницы. Т.е. конкретному URL соответствует страница, которая будет выглядеть одинаково для любого пользователя. Ну, кроме тех, что авторизовались в админ. панели WP, но они не в счет. Сейчас это только я.

Теперь нужно найти упомянутые выше точки «входа» и «выхода». «Вход» обнаруживается в файле /index.php. В котором честно написано: «This file doesn't do anything, but loads...». Чудненько. С «точкой выхода» сложнее, но я вспоминаю, что что-то слышал про событие «shutdown» в WP, которое выполняется после всего. Быстрым поиском по чистому дистрибутиву нахожу функцию «shutdown_action_hook» в /wp-includes/load.php. Еще быстрее, написав внутри нее «echo» проверяю в каком месте страницы она выведет «GoldenAxe!». В конце. Отлично! Искомые точки найдены, на все ушло около 10 минут. Теперь нужно как-то реализовать очень простую логику:

  • При первом обращении к странице она генерируется как обычно. После того, как весь вывод сгенерируется, его нужно будет записать в отдельный файл. Произойти это должно в «точке выхода». Файл нужно будет записать в папку /_cache/. Имя файла будет выглядеть следующим образом: md5-хэш URL страницы (без параметров), которая запрашивается. Т.к. это единственный параметр от которого зависит вид выводимой страницы, то этого вполне достаточно.
  • При втором обращении к странице в «точке входа» мы будем проверять есть ли в папке с файлами кэша нужный (с соответствующим запрашиваемому URL md5-именем). Если есть — сразу читаем из него и выводим результат.

Написание кода

Все. Вся система. Остается только придумать как собрать вывод. Как многие знают, шаблоны WP — это адская смесь HTML и PHP. Т.е. в них постоянно встречаются конструкции вроде:

    <?php 
        if($Some==1) echo '<div class="novost">';
        else echo '<div class="statya">';
    ?>
        <div class="item"></div>
    </div>

Таким образом, нет некоторой переменной $Content в которую бы собирался весь контент страницы перед выводом и уже затем выводился один раз с помощью того же echo. У нас есть сотни этих «echo» в шаблонах. И их нужно как-то собрать. Когда-то давно, когда я читал мануал к PHP я встречал там функцию ob_get_contents. Я никогда ей не пользовался, но тут она пригодилась. Она возвращает содержимое буфера вывода и идет в компании с функциями ob_start и ob_end_clean. Первая включает буферизацию, вторая — выключает и очищает буфер. Все это я мгновенно загуглил. Прошло еще пять минут. Вот какой код я в результате поместил в «точку входа» и «точку выхода»:

    <?php

    // «Точка входа»: 
    
    $URL_WO_QS = strtok($_SERVER["REQUEST_URI"], '?');
    
    $CacheFile = $_SERVER["DOCUMENT_ROOT"].'/_cache/'.md5($URL_WO_QS).'.cache';
    
    if(file_exists($CacheFile) && intval($_REQUEST['test'])!=1){
        
        echo file_get_contents($CacheFile);
        exit();
        
    }
    
    ob_start();
    // «Точка выхода»: 
    
    $Result = ob_get_contents();
    
    ob_end_clean();
    
    $URL_WO_QS = strtok($_SERVER["REQUEST_URI"], '?');
    
    $CacheFile = $_SERVER["DOCUMENT_ROOT"].'/_cache/'.md5($URL_WO_QS).'.cache';
    
    file_put_contents($CacheFile, $Result);
    
    echo $Result;	
    
    ?>

Тестирование

Ну а дальше оставалось только проверить результат. Я специально оставили себе возможность запросить сгенерированную стандартным способом страницу (передав в URL параметр «test» для $_REQUEST в «точке входа»). Получив выдачу страницы после кэширования «Топором» я сравнил ее со «стандартной» выдачей. Нашел небольшое отличие — в кэшированную версию не попал один js-скрипт. Тот, который отвечал за загрузку дополнительных постов в список при нажатии на кнопку «показать еще посты». Выяснять почему он не попадает в вывод было некогда, поэтому я просто добавил вызов скрипта в шаблон. И все заработало.

Конечно, у решения есть масса недостатков: если добавить через админ. панель новый пост, то он не будет отображаться в некоторых списках. В тех, которые на этом сайте не подгружаются с помощью AJAX. По какой-то причине неправильно стал срабатывать подсчет просмотров постов. Но все это мелочи, которые можно пережить несколько часов.

А плюс один, зато большой: сайт перестал отдавать код 500 или ждать ответа и выдачи страницы по полторы минуты. Теперь сам вывод (ответ страницы) занимает около 40 ms вместо 1.5 m и все пользователи без исключения могут нажимать на рекламу. Конечно, полная загрузка страницы занимает больше времени: многочисленные неоптимизированные изображения, скрипты рекламы и прочие шрифты делают свое дело. Но глазу это незаметно. Сайт работает быстро.

На все внедрение моего «Топора» ушло не более получаса. Сейчас я пойду снимать его и возвращать сайт в исходное состояние. Спортивное соревнование окончено, фанаты, а с ними и я, идем спать.

От автора

Не судите мое решение слишком строго. Помните, что я принимал его в условиях жестких временных ограничений и без серьезных знаний WordPress. Возможно, существовало более изящное и правильное решение. Если вы его знаете — напишите в комментариях. Я же могу сказать, что чувствовал себя ветеринаром, которому пришлось оперировать человека. Ну или наоборот. Хорошей вам погоды, друзья!

Эта статья не о поиске действительно правильного решения при общих условиях. Она о поиске оптимального решения в условиях ограничений: по времени, по результату и по знаниям. Под знаниями я имею в виду не только свои личные знания в области WP, которые, признаю, весьма поверхностны, но и предоставленную в мое распоряжение информацию. Под «информацией» я имею в виду, например, различные доступы. Я имел доступ к файлам самого сайта и к личному кабинету на сайте хостера. В нем, в основном, решаются вопросы оплаты и тарифов. При этом доступа к панели управления самим сервером я не имел. Со всеми вытекающими. С самим этим хостером я ранее не сталкивался, поэтому порядок и особенности его работы для меня тоже были загадкой.

Несомненно, правильное решение проблемы этого сайта в общем выглядит так: «ищем причины низкой производительности с помощью разных инструментов и методик. Препарируем локальную копию сайта. За чашечкой чая обдумываем решения, проверяем их. Обновляем рабочую копию. Анализируем ее работу и эффективность обновлений. При необходимости вносим правки. Все!».

В реальности у меня был час времени, сайт теряющий посетителей в самый ненужный момент и нервничающий владелец. Времени на создание бэкапа и локальной копии (да и, по сути, самой возможности этого) у меня не было. Отсюда еще один момент — я отчаянно не хотел навредить и сломать. Это, как вы понимаете, была бы катастрофа. Поэтому еще одним требованием к решению была его безопасность. Такие решения, как удаление уже интегрированных плагинов, на мой взгляд, этому критерию не соответствуют. Даже установка новых, в общем-то, не вызывала у меня восторга.

Мое решение — это костыль. Я полностью отдаю себе в этом отчет, равно как и в том, что данное решение не является чем-то новым или прорывным и имеет определенные недостатки. В то же время, оно было простым, безопасным и обеспечило необходимый результат: доступность сайта, его контента и функционала для пользователей в момент пиковых нагрузок. Кроме того, костыль (в силу простоты) был легко и бесследно удаляем. Также я признаю, что имея возможность и время на размышления и некоторую дополнительную информацию я, возможно, принял бы другое решение.