Главная > Coding, Блог > Разгоняем ZendFramework и WordPress

Разгоняем ZendFramework и WordPress

Недавно в очередной раз пришла в голову мысль, что неплохо бы заняться продвижением сайта. Решил начать с анализа существующего положения дел. Скопировал с сервера access-логи, загрузил в свой любимый анализатор (WebLog Expert Lite), и был приятно удивлен: средняя посещаемость — 174 уникальных пользователя в день. Причем, это вполне живые юзеры, а не какие-то там поисковые боты. Посмотрел статистику переходов и поисковых запросов — все разумно, в основном по тем вопросам, которые я освещал в статьях. В общем, тематический блог — реально работающая штука. Однако, в данной статье я не буду писать о SEO. В последнее время заметил, что сайт стал подтормаживать. Причины очевидны. Во-первых, ограничение ресурсов виртуального хостинга (нагрузка на CPU, использование базы данных), ну и во-вторых, увеличение количества пользователей. В связи с этим было принято решение о переезде на отдельный сервер. Для начала я выбрал услугу VPS (virtual private server). При данной услуге нам выделяется виртуальная машина со статическим IP адресом, что по сравнению с виртуальным хостингом гораздо полезнее для сайта, чем соседство со множеством других сайтов (на моем старом сервере хостилось еще 14 тысяч (!) сайтов). Помимо ресурсов, такое соседство еще и не безопасно: взломав один их этих четырнадцати тысяч, и получив root права, все другие сайты становятся так же доступны.

Переезд был недолгим, но все же немного заставил помучаться. На веделенном сервере оказалась моя любимая nix-система FreeBSD. Нет, не то, чтобы я это обнаружил только когда приобрел VPS, конечно это было известно изначально. Что же нового довелось мне узнать в процессе переезда? Во-первых, знакомство с ISPManager’ом. Это веб-интерфейс для управления виртуальным сервером. Если нет серьезных амбиций, то настроить с помощью этого инструмента основные элементы сервера (веб-сервер, пхп, базы данных) задача не сложная и интуитивно понятная. В общем, не буду заострять на нем внимания, он хорошо документирован, причем даже с видеороликами. На следующем этапе я начал заниматься разгоном сайта. В связи с этим советую неплохой сборник рецептов Реактивные веб-сайты. Правда, из нее я почерпнул только сведения о том, как можно ускорить движок WordPress, к сожалению про оптимизацию Zend Framework там повествования не ведется. Поизучав мануалы о производительности Zend Framework (наподобие http://framework.zend.com/manual/ru/performance.html), решил реализовать два подхода. Первый — отказ от стандартной зендовской автозагрузки классов. Второй — подключение кэширования байт-кода. Теперь по порядку.

Чем плоха автозагрузка и что это вообще такое (если кто не в курсе)? Предположим, есть у нас файлы myclass.php и index.php.

myclass.php:

  class MyClass {
    public function foo() {
       return 'bar';
    }
  }

index.php:

<html><body>
<?php
  require_once('MyClass.php');
  $myclass = new MyClass();
  echo($myclass->foo());
?>
</body></html>

Так выглядит вариант без автозагрузки.

Если убрать вызов require_once, то скрипт index.php будет завершен с ошибкой Class MyClass is undefined

Zend Framework — это структурированный набор классов наподобие MyClass, только их значительно больше чем один (в моей сборке — свыше 1600 файлов объемом 36Mb), так сказать, на все случаи жизни. Чтобы быть уверенным, что все необходимые классы доступны в любой точке программы, нужно их всех подключить, например, через require_once или просто require.

Понятное дело, что все эти классы вряд ли когда-нибудь понадобятся, поэтому в таком огромном фрэймворке используется автозагрузка, которая позволяет загружать только те классы, потребность в которых возникает в runtime. Как этого можно добиться? В php есть для этого дела набор функций. В частности, функция spl_autoload_register. В качестве параметра ей указывается название функции, которая будет вызываться, когда интерпретатор php встретит команду, в которой есть обращение к какому-либо классу (например, создание нового экземпляра, как в примере выше). Через эту функцию можно зарегистрировать несколько callback-функций, которые будут вызываться по очереди по принципу стека. Внутри такой функции мы можем проверить, загружен ли запрашиваемый класс, и, если не загружен, загрузить. Таким образом приведенный мной пример можно переписать следующим образом:

index.php:

<html><body>
<?php
  class ClassAutoloader {
    public function __construct() {
      spl_autoload_register(array($this, '__autoload'));
    }
    private function __autoload($className) {
      require_once $className . '.php';
    }
  }
  $autoloader = new ClassAutoloader();
  $myclass = new MyClass();
  echo($myclass->foo());
?>
</body></html>

Ровно таким образом действует Zend Framework. Чем может быть плох данный подход? Много мнений я слышал на этот счет, в частности, считается, что современный php-код, обеспечивающий автозагрузку, не влечет за собой снижение производительности. Но мне лично здесь видится проигрыш в производительности как минимум за счет использования require_once — данная операция тратит ресурсы на проверку, не загружен ли уже такой класс. Если заменить require_once на просто require, которая такую проверку не производит, это значительно ускорит работу, но неизбежно приведет к ошибкам вида «Can not redeclare class». Кроме того, некоторые энтузиасты проводили различные bencnmark тестирования, в частности, на предмет того, что работает быстрее парсинг множества маленьких php-файлов или одного большого. Один большой парсится быстрее. Поэтому существует способ склеивания файлов классов Zend Framework в один большой, за счет чего устраняется проблема множественного require_once и увеличивается скорость загрузки. Долго искал в интернете способ, но конкретики не нашел, поэтому стал экспериментировать, и вот как в итоге добился нужного эффекта. Для начала нам нужно собрать список всех используемых Zend-классов нашего приложения. Я это сделал следующим образом. В конце файла index.php (после appication->run()) добавляем код:

  $cont = file_get_contents("classes_included.txt");
  $cont = explode("\n", $cont);
  $c = get_included_files();
  foreach($c as $cls)
    if (!in_array($cls, $cont)) $cont[] = $cls;
  file_put_contents("classes_included.txt", implode("\n", $cont));

Затем открываем в браузере наш сайт и начинаем ходить по всем разделам, тыкать все кнопочки, в общем использовать всю функциональность. После всех этих махинаций открываем в текстовом редакторе файл classes_included.txt (он будет лежать рядом с index.php) и видим кучу строчек с именами файлов. Выкидываем из этого файла все строчки, не относящиеся к библиотеке Zend. Теперь нужно запустить склеивалку. Я написал вот такую.

<?php
  $cont= file_get_contents('classes_included.txt');
  $cont=explode("\n", $cont);
  $res = '';
  foreach($cont as $c) {
    $cont2 = file_get_contents($c);
    echo('getting '.$c."... ");
    if ($cont2) {
      echo("Success \n");
      $res .= str_replace(array("<?php", "require_once"), array("", "//require_once"), $cont2);
    } else
      echo("Failed \n")
  }
  file_put_contents('zfclasses.php', "<?php\n".$res);

Помимо склеивания она выкидывает из склеиваемых файлов теги <?php (иначе будет syntax error), а также оборачивает в комментарии вызовы require_once (это больше не нужно). В итоге получается один файл со всеми необходимыми Zend-классами. У меня этот файл получился в районе 1Мб. Если повыкидывать из него все ненужное (комментарии, переносы строк), думаю, ужмется еще на треть. Далее мы сталкиваемся с одним неприятным моментом. Если попытаться скормить получившийся файл интерпретатору php (php zfclasses.php), он будет ругаться в стиле «Class Zend_Controller_Router_Route_Interface is undefined».

Связано это с тем, что объявление класса Zend_Controller_Router_Route_Interface в нашем большом файле идет позже, чем этот класс используется. Например:

abstract class Zend_Controller_Router_Route_Abstract implements Zend_Controller_Router_Route_Interface

Не знаю, если честно, почему php так себя ведет, причем не всегда. Как известно, в php нет такого понятия, как forward declaration, поэтому код

<?php
  foo();
  function foo() {
    echo('foo');
    $c = new MyClass();
    echo ($c->bar());
  }
 
  class MyClass {
    function bar() {return 'bar';}
  }

является валидным, в то время как большинство компилируемых языков не допускают подобных вольностей. Тем не менее и php не всегда на такое согласен. Поэтому в нашем случае он тоже ругается на несколько классов. Естественно, подключать наш сборный файл с классами к проекту пока эти ошибки будут не устранены, не имеет смысла. Устраняются они легко — нужно всего лишь перенести в файле объявление того класса выше, чем он используется. Конечно, в идеале, можно сделать алгоритм склеивалки более интеллектуальным, чтобы он сам отслеживал эти зависимости. В общем, времени на это совсем не хотелось тратить, поэтому решил, что лучше за 2 минуты сделаю вручную. Открываем файлик classes_included.txt ищем в нем сточку с именем файла класса, на который ругается php, переносим эту строчку чуть выше того класса, который его использует, и снова запускаем склеивалку, после чего пытаемся обработать zfclasses.php. Если интерпретатор продолжает ругаться, повторяем танцы с бубном до тех пор, пока файл не обработается без ошибок. Далее дело техники — кладем zfclasses.php в какое-либо место на свое усмотрение (я положил в папку library), модифицируем index.php

<?php
  ob_start();
  session_start();
  error_reporting(E_ALL ^ (E_NOTICE | E_WARNING));
  defined('APPLICATION_PATH')
    || define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../application'));
 
  defined('APPLICATION_ENV')
    || define('APPLICATION_ENV', (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : 'development'));
  defined('SITE_URL')
    || define('SITE_URL', 'http://'.$_SERVER['HTTP_HOST'].'/');
 
  require_once APPLICATION_PATH.'/../library/zfclasses.php';
  spl_autoload_unregister(array('Zend_Loader_Autoloader','autoload'));
  require_once APPLICATION_PATH.'/Bootstrap.php';
  $application = new Zend_Application(
    APPLICATION_ENV,
    APPLICATION_PATH . '/configs/application.ini'
  );
  $application->bootstrap();
  $application->run();

и все должно работать.

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

Что это такое? Сущность кэширования байт-кода заключается в следующем. Исходный php-код компилируется интерпретатором в байт-код (набор виртуальных машинных кодов). Если кэширование отсутствует, то php каждый раз будет это делать. Собственно, идея кэширования заключается в том, чтобы один раз скомпилировать php-код, и сохранить этот байт-код в памяти, чтоб при следующем обращении к скрипту не производить повторно компиляцию. По некоторым бенчмаркам это увеличивает производительность в 22 раза (!)

Решений для php существует не мало, я остановил свой выбор на APC. APC устанавливается как расширение apache. Для его установки и работы APC понадобится доустановить библиотеки классов PEAR и PECL. Во FreeBSD большинство подобных операций делается через репозиторий ports. Все необходимые исходники находятся в папке /usr/ports. Попробуем установить PEAR:

# cd /usr/ports/devel/pear
# make all install clean

Если шелл ругается на манер

This port requires the CLI version of PHP, but you have already installed a PHP port without CLI.
***Error code 1

значит в системе не установлен PHP CLI (command line interface). Устанавливаем его:

# cd /usr/ports/lang/php5
# make config

В появившемся окне ставим галочки Build CLI Version и Build Apache Module (можно поставить и CGI version, если есть такая необходимость) нажимаем ОК, и вводим

# make install clean

Далее ставим порт pecl-APC

# cd /usr/ports/www/pecl-APC && make config

Важно: в окне конфигурации не надо ставить никаких галок, особенно

[ ] IPC         Enable IPC shm memory support (default: mmap)

Запускаем исталляцию

# make install

После всех этих махинаций нужно перезапустить апач. Во FreeBSD это делается так:

# apachectl restart

Скажу еще пару слов о разгоне WordPress. Эта тема меня почему-то не очень волнует, видимо потому, что в отличие от ZendFramework он работал вполне сносно даже на моем тормозном хостинге. Тем не менее, я последовал нескольким рекомендациям из книги «Реактивные веб-сайты».

1. Настройка многоуровнего кэширования запросов. Делается это через my.ini (my.cnf в *nix). Добавляем параметры

query-cache-type=1
query-cache-size=20M

20Мб — этот параметр назначается исходя доступного объема оперативной памяти.

2. Кэширование статических страниц. Смысл в том, чтобы не генерировать каждый раз страницы с постами, а хранить их в статических страницах, пока не случится какого-либо изменения на данной странице. В этом случае страница перегенерируется и снова сохраняется в кэше. Включается это просто. Нужно раскомментировать в wp-config.php строки

define('ENABLE_CACHE', true );
define('CACHE_EXPIRATION_TIME', 900);

3. Кэширование php-скриптов. Это я уже сделал при разгоне Zend Framework.

После этих манипуляций нужно перезапустить апач и мускуль. Апач я уже написал, как перезапускать, а мусукль рестартится так:

# /usr/local/etc/rc.d/mysql-server stop
# /usr/local/etc/rc.d/mysql-server start

Пожалуй буду закругляться.
Теперь после разгона можно опять вернуться к попыткам продвижения))

  1. Боб
    28 сентября 2011 в 13:51 | #1

    Спасибо!
    Очень помогло!

    А как реализовать, чтобы после этого автоподгружались классы только из папки

    application/models ?

    хотелось бы, чтобы они были отдельно от файла zfclasses.php

  2. 29 сентября 2011 в 10:26 | #2

    See no problem here, cap (:
    Если бы я ставил перед собой такую задачу, то в процедуре сбора списка автоподгружаемых классов (которая формирует файл zfclasses.php), дополнительно анализировал, принадлежит ли файл папке models. Если принадлежит, то кладем его не в zfclasses, а, например, в modelclasses.php.
    Потом, соответственно инклудим уже отдельно modelclasses.php, вероятно, внутри Bootstrap.php

  3. Dimas
    30 декабря 2012 в 09:29 | #3

    Может, Вы напишите статью о том как оптимизировать работу службы mysql на VPS сервере.

Подписаться на комментарии по RSS