Николай Ланец
27 авг. 2013 г., 20:54

Ускорение MODX-а за счет облегчения ядра

(Еще не законченное).
Я не раз уже пытался ускорить работу MODX-а (как правило за счет снижения нагрузки на парсер), но старался делать это так, чтобы максимально не трогать самого ядра MODX-а. Но в этом эксперименте я попробую облегчить ядро MODX, при этом сохранить его максимальную работоспособность. То есть это уже будет хак ядра, но интересно насколько быстро может работать MODX, при этом не потеряв функциональности самого API.
Все тесты я буду выполнять на digitalocean.com, что должно обеспечить боле менее стабильные показатели (во всяком случае предыдущие тесты показывали примерно стабильные результаты).
Для начала я установил абсолютно чистый MODX Revolution-2.2.8, и сделаю на нем несколько замеров. При этом на главной странице уберу галочку Кэшируемый, чтобы получить именно результаты «на горячую». Плагин для замера времени и памяти не буду создавать в систему, а пропишу функцией, чтобы во-первых, не было погрешности на вызове плагины, а во-вторых одна из целей эксперимента — полное исключение запросов к БД тогда, когда в этом нет необходимости.

Первичные замеры.

1. Сразу в начале index.php (как раз объявил функцию для замеров).
$mtime= microtime(); $mtime= explode(" ", $mtime); $mtime= $mtime[1] + $mtime[0]; $tstart= $mtime; function usage(){ global $tstart, $modx; $memory = round(memory_get_usage(true)/1024/1024, 4).' Mb'; print "<div>Memory: {$memory}</div>"; $totalTime= (microtime(true) - $tstart); $totalTime= sprintf("%2.4f s", $totalTime); if(!empty($modx)){ $queryTime= $modx->queryTime; $queryTime= sprintf("%2.4f s", $queryTime); $queries= isset ($modx->executedQueries) ? $modx->executedQueries : 0; $phpTime= $totalTime - $queryTime; $phpTime= sprintf("%2.4f s", $phpTime); print "<div>queries: {$queries}</div>"; print "<div>queryTime: {$queryTime}</div>"; print "<div>phpTime: {$phpTime}</div>"; } print "<div>TotalTime: {$totalTime}</div>"; } usage(); exit;
Результат:
Memory: 0.5 Mb TotalTime: 0.0001 s
Вот от этого и оттолкнемся :-))) А дальше нас интересует сразу несколько первичных этапов (скорость на подгрузке основных файлов движка, скорость на инициализации $modx-а, скорость на инициализации контекста, скорость на полной обработке страницы).
2. Подгрузка конфигов MODX-а, классов MODx, xPDO и т.п.
@include(dirname(__FILE__) . '/config.core.php'); if (!defined('MODX_CORE_PATH')) define('MODX_CORE_PATH', dirname(__FILE__) . '/core/'); /* include the modX class */ if (!@include_once (MODX_CORE_PATH . "model/modx/modx.class.php")) { $errorMessage = 'Site temporarily unavailable'; @include(MODX_CORE_PATH . 'error/unavailable.include.php'); header('HTTP/1.1 503 Service Unavailable'); echo "<html><title>Error 503: Site temporarily unavailable</title><body><h1>Error 503</h1><p>{$errorMessage}</p></body></html>"; exit(); } usage(); exit;
Memory: 1.5 Mb TotalTime: 0.0094 - 0.0150 s
Хочу сказать, что по большому счету у нас уже есть практически все API MODX-а и xPDO. Дальше это уже окончательная инициализация объекта $modx, подгрузка дополнительных файлов/классов через xPDO::loadClass() и т.п., то есть это уже то, что и составляет основной рост потребления памяти и времени. Посмотрим, что мы получим на выходе…
3. Инициализация объекта $modx. А вот здесь мы сразу что говорится «с места в карьер».
$modx= new modX(); if (!is_object($modx) || !($modx instanceof modX)) { @ob_end_flush(); $errorMessage = '<a href="setup/">MODX not installed. Install now?</a>'; @include(MODX_CORE_PATH . 'error/unavailable.include.php'); header('HTTP/1.1 503 Service Unavailable'); echo "<html><title>Error 503: Site temporarily unavailable</title><body><h1>Error 503</h1><p>{$errorMessage}</p></body></html>"; exit(); } usage(); exit;
С ходу потребление времени почти в 5 раз увеличилось, всего лишь на $modx= new modX(); С чего же так жестко? Дело в том, что это не просто инициализация объекта, но еще и выполнение автоматического метода __construct(). Посмотрим, что там в этой функции:
public function __construct($configPath= '', $options = null, $driverOptions = null) { try { $options = $this->loadConfig($configPath, $options, $driverOptions); parent :: __construct( null, null, null, $options, null ); $this->setLogLevel($this->getOption('log_level', null, xPDO::LOG_LEVEL_ERROR)); $this->setLogTarget($this->getOption('log_target', null, 'FILE')); $debug = $this->getOption('debug'); if (!is_null($debug) && $debug !== '') { $this->setDebug($debug); } $this->setPackage('modx', MODX_CORE_PATH . 'model/'); $this->loadClass('modAccess'); $this->loadClass('modAccessibleObject'); $this->loadClass('modAccessibleSimpleObject'); $this->loadClass('modResource'); $this->loadClass('modElement'); $this->loadClass('modScript'); $this->loadClass('modPrincipal'); $this->loadClass('modUser'); $this->loadClass('sources.modMediaSource'); } catch (xPDOException $xe) { $this->sendError('unavailable', array('error_message' => $xe->getMessage())); } catch (Exception $e) { $this->sendError('unavailable', array('error_message' => $e->getMessage())); } }
Как мы видим, у нас здесь, во-первых, вызов родительского xPDO::__construct() (то есть нам еще и его предстоит посмотреть), а во-вторых, подгрузка еще нескольких классов чарез $this->loadClass().
Посмотрим куда основные затраты идут. Во-первых, сразу выполним возврат функции.
public function __construct($configPath= '', $options = null, $driverOptions = null) { return; try {
Результат:
Memory: 1.5 Mb queries: 0 queryTime: 0.0000 s phpTime: 0.0188 s TotalTime: 0.0188 s
То есть, сама инициализация объекта $modx у нас не съедает ресурсы (во всяком случае это не ощущается). Проблема где-то дальше. Копаем дальше (переводим return ниже по коду, выполняя все большую часть функции).
public function __construct($configPath= '', $options = null, $driverOptions = null) { try { $options = $this->loadConfig($configPath, $options, $driverOptions); return;
Memory: 1.5 Mb queries: 0 queryTime: 0.0000 s phpTime: 0.0215 s TotalTime: 0.0188 - 0.0215 s
Нет, не то…
public function __construct($configPath= '', $options = null, $driverOptions = null) { try { $options = $this->loadConfig($configPath, $options, $driverOptions); parent :: __construct( null, null, null, $options, null ); return;
Memory: 2 Mb queries: 0 queryTime: 0.0000 s phpTime: 0.0300 s TotalTime: 0.0300 s
А вот это уже больше похоже на правду. Сразу + 0,5 метров памяти и 0.01 сек. Идем дальше.
$this->setPackage('modx', MODX_CORE_PATH . 'model/'); $this->loadClass('modAccess'); $this->loadClass('modAccessibleObject'); $this->loadClass('modAccessibleSimpleObject'); return;
Memory: 2.25 Mb queries: 0 queryTime: 0.0000 s phpTime: 0.0183 s TotalTime: 0.02 - 0.3 s
Еще дальше:
$this->setPackage('modx', MODX_CORE_PATH . 'model/'); $this->loadClass('modAccess'); $this->loadClass('modAccessibleObject'); $this->loadClass('modAccessibleSimpleObject'); $this->loadClass('modResource'); return;
Memory: 3 Mb queries: 0 queryTime: 0.0000 s phpTime: 0.0426 s TotalTime: 0.0426 s
Уже 3 метра, плюс почти стабильно 0.04+ сек.
Еще дальше.
$this->loadClass('modElement'); $this->loadClass('modScript'); $this->loadClass('modPrincipal'); $this->loadClass('modUser'); return;
Memory: 3.5 Mb queries: 0 queryTime: 0.0000 s phpTime: 0.0489 s TotalTime: 0.0489 s
Дальше уже без особых изменений. То есть, просто на инициализации ядра с подгрузкой основных классов, мы получили прирост в потреблении памяти больше, чем в два раза, и увеличение времени выполнения почти в 5 раз. При этом, это еще далеко не все! Ведь мы еще не инициализировали обязательный для работы MODX-сайта контекст (именно для работы публичной части) (то есть сейчас еще у нас как бы и нету еще сайта, пока что только ядро). А ведь там и сессия, и права, и запросы к БД и т.п.
Двигаемся дальше в index.php
$modx->initialize('web'); usage(); exit;
Memory: 4.75 Mb queries: 0 queryTime: 0.0000 s phpTime: 0.0476 s TotalTime: 0.0476 - 0.07 s
Вот, еще раз значительный подъем. Посмотрим метод MODx::initialize()
public function initialize($contextKey= 'web', $options = null) { if (!$this->_initialized) { if (!$this->startTime) { $this->startTime= $this->getMicroTime(); } $this->getCacheManager(); $this->getConfig(); $this->_initContext($contextKey, false, $options); $this->_loadExtensionPackages($options); $this->_initSession($options); $this->_initErrorHandler($options); $this->_initCulture($options); $this->getService('registry', 'registry.modRegistry'); if (is_array ($this->config)) { $this->setPlaceholders($this->config, '+'); } $this->_initialized= true; } return $this->_initialized; }
Здесь мы опять видим кучу методов. Но это опять-таки еще не все. Контекст мы инициализировали, но MODX еще даже не начал обрабатывать запрошенную страницу. Для этого освобождаем последний участок кода в index.php
if (!MODX_API_MODE) { $modx->handleRequest(); }
Для вывода статистики уже вызов нашей функции прописываем в modResponse
echo $this->modx->resource->_output; usage(); while (@ob_end_flush()) {} flush(); exit();
Результат:
Memory: 7 Mb queries: 0 queryTime: 0.0000 s phpTime: 0.0659 s TotalTime: 0.0659 s
Вот это уже полностью отработана страница (хоть и пустая). И вот как раз этот результат я и хочу значительно улучшить.
Про сбор статистики по запросам.
Обратите внимание на вывод статистики. Страница у нас отработана полностью, а что нам статистика показывает? queries: 0, queryTime: 0.0000 s. Ни одного запроса к БД? Но у нас же даже кеширование страницы отключено… А вот на этот счет я уже писал очень давно. У нас в Рево печалька с этим делом. Руководствуясь своим же топиком включил в отладку хотя бы часть запросов. Получаем:
Memory: 7 Mb queries: 6 queryTime: 0.0010 s phpTime: 0.1222 s TotalTime: 0.1232 - 13 s
.
Так вот даже это не полная информация. Во-первых, сбора статистики не учитываются запросы на save(), а ведь это тоже запросы. Во-вторых, даже после того, как я прописал туда сбор статистики, вы выводе все равно ничего не поменялось, хотя реально в статистику добавилось еще как минимум 2 запроса. С чем это связано? С тем, что у нас по умолчанию MODX хранит сессии в базе данных, то есть для сохранения сессии ему надо сделать запрос к этой БД (и делает он это уже после того, как выполнение скрипта выполнено, и в вывод уже ничего не добавляется). При этом хотя я и заметил уже очень давно, что он делает два сохранения сессии за одно посещение страницы, я еще не копал с чем это связано. В общем, а метод modSessionHandler::write дописываем:
$this->modx->log(xPDO::LOG_LEVEL_ERROR, $this->modx->executedQueries);
Я специально вывожу на уровне ERROR, чтобы не менять уровень логирования MODX-а.
Результат в логах:
[2013-08-14 03:40:50] (ERROR @ /index.php) 7 [2013-08-14 03:40:50] (ERROR @ /index.php) 7 [2013-08-14 03:40:51] (ERROR @ /index.php) 7 [2013-08-14 03:41:12] (ERROR @ /index.php) 8
И вот как здесь я разобрался, когда и почему он делает два запроса: если сессия еще не записана в БД, то уже сразу при инициализации сессии, он ее пишет в БД. И по окончании выполнения скрипта он опять записывает в БД. А если при заходе сессия уже есть, то он пишет в БД только после выполнения скрипта. Вот и получается то 7, то 8 запросов.
В общем, сейчас почти стабильные вот такие цифры:
Memory: 7.5 Mb queries: 6 queryTime: 0.0010 s phpTime: 0.1281 s TotalTime: 0.1291 s
И теперь мы попробуем это все дело оптимизировать, при чем так, чтобы не потерять особо в функционале. То есть цель — минимальное время на пустой странице. И вот здесь я сразу хочу определить свою принципиальную позицию: в то время, как в большинстве систем даже при наличии большого функционала на борту, по умолчанию запускают только минимум, в MODX-е из коробке сразу включено многое, при чем не всегда обязательно нужное. Поясню: элементарно сессии в базе данных. Вот зачем они? По опыту они нужны только для одного — чтобы можно было сбросить через админку все сессии. Но многие ли этим пользуются? Тем более на каком-нибудь сайте-визитке, где во фронте вообще не политик ничего нет. ИМХО — это можно иметь на борту, но это не нужно включать из коробки. Надо — включи. Не надо — пусть будет минимум. Почему такая политика в MODX-е? Мое мнение — просто потому что изначально расчет был на широкие массы. Априори считалось, что сайт может делать тот, кто программировать не умеет вообще. И ему надо дать сразу все, чтобы ни в чем себя не ограничивал. А специалист уже уберет лишнее. Я с этим в корне не согласен. Надо включать минимум, а навороты — это уже опционально.
Итак, для начала отключим сессии на основе БД. Для этого заходим в системные настройки и очищаем значение настройки session_handler_class. Все, теперь у нас стандартные php-сессии. Результат — 2-3 запроса минус (чтение и сохранение сессии), а так же небольшая надежда на то, что в итоге мы сможем полностью исключить запросы к БД (включая соединение с сервером MySQL), когда у нас документ будет полностью из кеша и в запросе не будет никакой необходимости.

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