В настоящий момент занимаюсь переделкой одного новостного портала на MODX Revolution. Так как посещаемость на сайте бывает до 100 000 человек в сутки, вопрос производительности здесь один из самых важных. С учетом того, что на текущий момент в базе более 75 000 статей, при неправильном (и даже при традиционном подходе к разработке на MODX) тормоза сайта практически гарантированы, а если частота посещений превысит время выполнения запроса, то сервер вообще ляжет. Вот часть приемов задействованных здесь для решения этих проблем я и опишу в этой статье.
1. Долгая генерация кеша.
Наверняка многие знают, что при обновлении кеша MODX проходится по всем документам и набивает карту ресурсов в кеш контекста. Если кто не в курсе, подробно я писал про это здесь. И хотя в MODX начиная с версии 2.2.7 (или в районе той) можно в настройках отключать кеширование карты ресурсов (системная настройка cache_alias_map) проблема эта решается только частично — MODX не кеширует УРЛы документов, но структуру с ID-шниками фигачит все равно, перебирая все документы из базы данных. Это приводит к тому, что во-первых, кеш-файл контекста разрастается, а во-вторых, скрипт может просто не выполниться за 30 секунд и кеш-файл побьется, что может вообще привести к фатальным ошибкам и сделать сайт нерабочим.
Но даже если сервер все-таки в состоянии дернуть все документы и набить все в кеш, давайте посмотрим на сравнительные цифры на один запрос при разных настройках. Цифры эти будут весьма относительные ибо многое зависит от настройки сервера и на разных серверах потребление памяти у одного и того же сайта будет разное, но в сравнении эти цифры дадут представление о разнице состояний. Для оценки потребления памяти буду вызывать getdata-процессор на получение 10-ти статей.
Итак, вариант первый: Полное кеширование карты ресурсов включено.
Размер кеш-файла контекста: 5 792 604 байт.
Потребление памяти при запросе: 28,25 Mb
Время: 0,06-0,1 сек.
Вариант второй: Полное кеширование карты ресурсов отключено (системная настройка cache_alias_map == false).
Размер кеш-файла контекста: 1 684 342 байт.
Потребление памяти при запросе: 15,5 Mb
Время: 0,03-0,06 сек.
Вариант третий: Полностью отключено кеширование карты ресурсов патчем cacheOptimizer.
Размер кеш-файла контекста: 54 945 байт.
Потребление памяти при запросе: 4,5 Mb
Время: 0,02-0,03 сек.
И это всего лишь на 75 000 ресурсов. На сотнях тысяч разница будет гораздо ощутимей.
Есть конечно тут и минусы. Например не будет работать Wayfinder, который строит менюшку на основе данных карты алиасов. Здесь придется самому менюшку собирать. Я чаще всего использую menu-процессор, про который писал здесь (см. раздел 2. Замена Wayfinder).
2. Низкая производительность из-за TV-параметров документов.
А вот это основная и наиболее интересная причина написания данного топика. Наверно нет ни одного MODX-разработчика, который бы не использовал телевизоры TV-поля. Они решают сразу две проблемы: 1. добавляют пользовательские поля документам, 2. дают различные интерфейсы для их редактирования в зависимости от типа поля.
Но есть у них и серьезный минус — все они хранятся в одной таблице. Это добавляет сразу несколько проблем:
1. Нельзя управлять уникальностью значений на уровне базы данных.
2. Нельзя использовать различные типы данных для различных TV-полей. Все данные TV-полей содержатся в единой колонке value с типом данных mediumtext. То есть мы и большего объема данные не можем использовать, и числовые значения у нас будут храниться как строчные (что накладывает дополнительные требования к формированию запроса с сортировкой), и сравнение данных из различных колонок у нас не по фэншую, и вторичные ключи не настроить и много-много еще всего неприятного из-за этого.
3. Низкая производительность при выборке из нескольких таблиц. К примеру, у нас для одного документа есть несколько TV-полей, из которых хотя бы 2-3 поля практически всегда заполнены. Хотим мы получить в запросе сразу данные и документов и полей к ним. У нас есть два основных варианта формирования запроса на это:
1. Просто приджоинить таблицу TV-шек.
$q = $modx->newQuery("modResource"); $alias = $q->getAlias(); $q->leftJoin("modTemplateVarResource", "tv", "tv.contentid = {$alias}.id"); $c->select(array( "tv.*", "{$alias}.*", ));
Но здесь есть серьезный минус: в результирующую таблицу мы получим C*TV число записей, где C — кол-во записей в site_content, а TV — количество записей в таблице site_tmplvar_contentvalues для каждого документа в отдельности. То есть, если у нас, к примеру, 100 записей документов и по 3 записи TV на каждый документ (в среднем), то мы получим в итоге 100*3 = 300 записей.
Так как по этой причине в результате на один документ приходилось более одной результирующей записи, то на уровне PHP приходится дополнительно обрабатывать полученные данные чтобы сформировать уникальные данные. Это у нас и в getdata-процессоре выполняется. А это так же увеличивает нагрузку и увеличивает время выполнения.
Вот у меня в этом новостном портале как раз и было в среднем по 3 основных записи на документ. В итоге ~225 000 записей ТВ. Даже с оптимизацией запросов выполнение с условиями занимало 1-4 секунды, что очень долго.
2. Джоинить каждое TV-поле по отдельности.
Примерный запрос:
$q = $modx->newQuery("modResource"); $alias = $q->getAlias(); $q->leftJoin("modTemplateVarResource", "tv1", "tv1.tmplvarid = 1 AND tv1.contentid = {$alias}.id"); $q->leftJoin("modTemplateVarResource", "tv2", "tv2.tmplvarid = 2 AND tv2.contentid = {$alias}.id"); // ......... $c->select(array( "tv1.value as tv1_value", "tv2.value as tv2_value", "{$alias}.*", ));
Такой запрос отработается быстрее, так как в результирующей таблице будет столько же записей сколько и записей документов, но все равно нагрузка будет не маленькая когда счет записей пойдет на десятки и сотни тысяч, а а количество ТВ-шек перевалит за десяток (ведь каждая ТВ-шка — это плюс еще один джоининг таблицы).
Безусловно самый лучший вариант в данном случае — это хранение ТВ-значений в самой системной таблице site_content, то есть каждое значение хранится в отдельной колонке этой таблицы.
Если кто думает, что это очередной урок по изъезженной теме CRC, то это не совсем так. Традиционно нас учили расширять имеющиеся классы своими и там дописывать нужные нам колонки (а то и вовсе таблицу собственную прописывать). Но этот путь не оптимальный. Главная проблема здесь — это то, что мы расширяем как-то то класс, но не меняем его самого. Расширения касаются только расширяющего (а не расширяемого) класса, а так же тех расширяющих классов, которые будут расширять наш класс. Запутанно, но сложно проще сказать. Объясню. У нас есть базовые класс modResource. Его расширяют классы modDocument, modWebLink, modSimLink и т.п. Все они наследуют от modResource мапу таблицы. Если мы расширим нашим классом класс modResource, то в нашем классе будут новые колонки которые мы допишем, но их не будет в классе modDocument, так как он не расширяет наш класс. Для того, чтобы информация о новых колонках появилась во всех расширяющих modResource классах, информация эта должна быть в самом классе modResource. Но как это сделать не трогая самих системных файлов?.. На самом деле частично об этом я писал еще более двух лет назад (статью перенес сюда), но только сейчас это реализовал в боевом режиме. Делаем так:
1. Создаем новый компонент, который будет подгружаться как extensionPackage (подробно об этом писал здесь).
2. Создаем новые колонки в таблице site_content через phpMyAdmin или типа того.
3. С помощью CMPGenerator-а генерируем отдельный пакет с мапой таблицы site_content. В этой мапе будет и описание ваших новых колонок и таблиц.
4. Прописываем в вашем пакете в файле metadata.mysql.php данные ваших колонок и индексов (пример такого файла можно увидеть и в нашей сборке ShopModxBox).
<?php $custom_fields = array( "modResource" => array( "fields" => array( "article_type" => array( "defaultValue" => NULL, "metaData" => array ( 'dbtype' => 'tinyint', 'precision' => '3', 'attributes' => 'unsigned', 'phptype' => 'integer', 'null' => true, 'index' => 'index', ), ), "image" => array( "defaultValue" => NULL, "metaData" => array ( 'dbtype' => 'varchar', 'precision' => '512', 'phptype' => 'string', 'null' => false, ), ), ), "indexes" => array( 'article_type' => array ( 'alias' => 'article_type', 'primary' => false, 'unique' => false, 'type' => 'BTREE', 'columns' => array ( 'article_type' => array ( 'length' => '', 'collation' => 'A', 'null' => true, ), ), ), ), ), ); foreach($custom_fields as $class => $class_data){ foreach($class_data['fields'] as $field => $data){ $this->map[$class]['fields'][$field] = $data['defaultValue']; $this->map[$class]['fieldMeta'][$field] = $data['metaData']; } if(!empty($class_data['indexes'])){ foreach($class_data['indexes'] as $index => $data){ $this->map[$class]['indexes'][$index] = $data; } } }
Внимательно его изучите. Он добавляет информацию о двух колонках и одном индексе в таблицу site_content.
Давайте убедимся, что колонки действительно были добавлены. Выполним в консоли этот код:
$o = $modx->newObject('modDocument'); print_r($o->toArray());
Увидим вот такой результат:
Array ( [id] => [type] => document [contentType] => text/html [pagetitle] => [longtitle] => // Тут еще куча колонок перечислено // и в конце наши две колонки [article_type] => [image] => )
Вот теперь мы можем работать с системной таблицей с нашими кастомными полями. К примеру, так можно писать:
$resource = $modx->getObject('modResource', $id); $resource->article_type = $article_type; $resource->save();
В таблицу для этого документа будет записано наше значение.
Создание своих колонок и индексов на чистом MODX.
Понятное дело что при таком подходе у нас возникает проблема миграции с такого кастомного сайта на чистый MODX, ведь там в таблицах нет наших кастомных полей и индектов. Но на самом деле это как бы и не проблема совсем. Дело в том, что как мы генерируем мапу из таблиц, так и таблицы, колонки и индексы мы можем создавать из мап-описаний классов. Создать колонку или индекс очень просто:
// Получаем менеджер работы с базой данных $manager = $modx->getManager(); // Создаем колонку $manager->addField($className, $fieldName); // Создаем индекс $manager->addIndex($className, $fieldName);
При этом не надо никакие данные колонок и индексов указывать кроме как их названия. Эти данные xPDO получит из нашей мапы и использует при создании описанной колонки или индекса.
Если вы свой компонент соберете в нормальный установочный пакет, то там можете прям прописать скрипт чтобы при установке пакета сразу были созданы в таблицах ваши кастомные колонки и индексы.
Рендеринг ваших кастомных данных в TV-полях при редактировании документов.
Как я и говорил выше, удобство TV-шек заключается в том, что для них созданы различные управляющие элементы (текстовые поля, выпадающие списка, чекбоксы, радиобоксы и т.п.). Плюс к этому в родном редакторе форм можно разграничить права на те или иные ТВ-поля, чтобы кому не покладено не мог видеть/редактировать приватные поля. На самом деле можно, если очень хочется, но все же приватные поля не будут мозолить глаза кому не поподя. И вот как раз эти механизмы и не хотелось бы терять, ибо иначе придется фигачить свои собственные интерфейсы на управление этими данными, а это весьма трудозатратно. Хотелось бы все-таки для редактирования таких данных использовать родной редактор ресурсов. Идеального механизма здесь нет, но боле менее пригодный вариант я отработал. Смысл его заключается в том, чтобы на уровне плагина в момент рендеринга формы редактирования документа подставить TV-поле со своим кастомным значением, а при сохранении документа перехватить данные TV-шки и эти данные сохранить в наши кастомные поля. К сожалению, не получается здесь вклиниться как положено (просто потому что API не позволяет), так что мы не можем повлиять на передаваемые процессору документа данные, из-за чего данные ТВшки все равно будут записаны в таблицу ТВшек, но это не проблема — просто после сохранения документа автоматом подчистим эту табличку и все. Вот пример плагина, срабатывающего на три события (1. рендеринг формы редактирования документа с подстановкой TV-поля и кастомными данными, 2. получение данных и изменение объекта документа перед его сохранением, 3. чистка ненужных данных).
<?php /* OnBeforeDocFormSave OnDocFormSave OnResourceTVFormRender */ switch($modx->event->name){ /* Рендеринг ТВшек */ case 'OnResourceTVFormRender': $categories = & $scriptProperties['categories']; foreach($categories as $c_id => & $category){ foreach($category['tvs'] as & $tv){ /* Рендеринг тэгов */ if($tv->id == '1'){ if($document = $modx->getObject('modResource', $resource)){ $q = $modx->newQuery('modResourceTag'); $q->select(array( "GROUP_CONCAT(distinct tag_id) as tags", )); $q->where(array( "resource_id" => $document->id, )); $tags = $modx->getValue($q->prepare()); $value = str_replace(",", "||", $tags); $tv->value = $value; $tv->relativeValue = $value; $inputForm = $tv->renderInput($document, array('value'=> $tv->value)); $tv->set('formElement',$inputForm); } } /* Рендеринг картинок */ else if($tv->id == 2){ if($document = $modx->getObject('modResource', $resource)){ $tv->value = $document->image; $tv->relativeValue = $document->image; $inputForm = $tv->renderInput($document, array('value'=> $tv->value)); $tv->set('formElement',$inputForm); } } /* Рендеринг статусов */ else if($tv->id == 12){ if($document = $modx->getObject('modResource', $resource)){ $tv->value = $document->article_status; $tv->relativeValue = $document->article_status; $inputForm = $tv->renderInput($document, array('value'=> $tv->value)); $tv->set('formElement',$inputForm); } } } } break; // Перед сохранением документа case 'OnBeforeDocFormSave': $resource = & $scriptProperties['resource']; /* Тэги. Перед сохранением документа мы получим все старые теги и установим им active = 0. Всем актуальным тегам будет установлено active = 1. После сохранения документа в событии OnDocFormSave мы удалим все не активные теги */ if(isset($resource->tv1)){ $tags = array(); foreach((array)$resource->Tags as $tag){ $tag->active = 0; $tags[$tag->tag_id] = $tag; } // $tags = array(); if(!empty($resource->tv1)){ foreach((array)$resource->tv1 as $tv_value){ if($tv_value){ if(!empty($tags[$tv_value])){ $tags[$tv_value]->active = 1; } else{ $tags[$tv_value] = $modx->newObject('modResourceTag', array( "tag_id" => $tv_value, )); } } } } $resource->Tags = $tags; $tags_ids = array(); foreach($resource->Tags as $tag){ if($tag->active){ $tags_ids[] = $tag->tag_id; } } $resource->tags = ($tags_ids ? implode(",", $tags_ids) : NULL); } /* Обрабатываем изображение */ if(isset($resource->tv2)){ $resource->image = $resource->tv2; } /* Обрабатываем статусы */ if(isset($resource->tv12)){ $resource->article_status = $resource->tv12; } break; /* Сохранение документа */ case 'OnDocFormSave': $resource =& $scriptProperties['resource']; /* Удаляем все не активные теги */ $modx->removeCollection('modResourceTag',array( 'active' => 0, 'resource_id' => $resource->id, )); /* Удаляем TV-картинки, так как они сохраняются в системную таблицу Удаляем TV-статусы, так как они сохраняются в системную таблицу */ $modx->removeCollection('modTemplateVarResource',array( 'tmplvarid:in' => array( 1, // Тэги 2, // Картинки 12, // Статусы ), 'contentid' => $resource->id, )); break; }
Благодаря этому плагину кастомные данные рендерятся в форму редактирования документа и обрабатываются при его сохранении.
Итог
Из 225+ тысяч записей в таблице дополнительных полей осталось только 78. Конечно не все ТВшки будут фигачиться в системную таблицу (а только те, что используются для поиска и сортировки), и какие-то данные конечно будут в таблице ТВ-полей, но нагрузка все же серьезно снизилась, а запросы стали попроще.
ссылка на оригинал статьи http://habrahabr.ru/post/253737/
Добавить комментарий