Проблема: при увеличении кол-ва страниц на сайте, значительно разрастается кэш сайта. Это связано с тем, что MODx Revolution кэширует все варианты ЧПУ:
- ID-документа - ЧПУ
- ЧПУ - ID-документа
- Родители и дочерние записи и т.п.
В
результате на 20 000 страниц мы имеем почти 20 Мб кэш. Мало того, что
это просто извращение, подгружать 20 метров кэша для того, чтобы выдать
20 кило конечного HTML, так дело еще в том, что это почти что лимит для
MODx Revolution, и 50 000 страниц нам просто никогда не создать.
Но
это в базовой комплектации. Слава Богу MODx Revolution имеет очень
мощное API, и при желании платформу можно адаптировать практически под
любые нужды.
В нашем случае мы поставили для себя задачу - прокачать MODx Revolution так, чтобы он легко работал с 200 000 документами.
Хотя
я буду максимально подробно все расписывать, все же сразу оговорюсь о
необходимых минимальных знаниях, чтобы без труда все это сделать:
- PHP ООП
- PDO/xPDO
- Javascript + ExtJS/Sencha
- MySQL
- Синтаксис JSON
- Средства отладки Ajax-запросов, к примеру Firefox + Firebug
- Синтаксис XML
Краткий отчет о результатах краш-тестов в цифрах:
crashTest.jpg — изображение пока не получено из архива
Наши исходные данные:
- MODx Revolution 2.0.8
- Кол-во документов: 20793
- Размер кэш: 18 769 995 байт
- 1-ая загрузка страницы (с генерацией кэш-файла): 8,97 сек.
- 2-ая загрузка частично кэшированной страницы с уже сгенерированным кэшем: 0,5 - 1,32 сек.
При тюнинге MODx Revolution мы должны учесть следующие обязательные моменты:
- Обязательно ЧПУ для всех страниц
- Для вывода главного меню использовать стандартные модули Ditto, Wayfinder, Breadcrumbs и чтобы их не пришлось модифицировать для этого.
Для начала разберемся, что же нам необходимо сделать, и к чему это приведет.
У нас есть несколько вариантов:
- Немного
подправить код класса modCacheManager и отключить кэширование карты
ресурсов. Таким образом мы предотвратим разрастание кэша и на 20000
документов мы будем иметь кэш всего в 40 кило. Но:
- У нас перестанут работать все стандартные навигационные модули типа Ditto, Wayfinder и т.п.
- У нас в принципе не будет работать ЧПУ, потому что MODx Revolution всю работу с навигацией проводит через этот раздел кэша.
Какой же оптимальный вариант мы видим? Частичное кэширование
документов, только тех, которые нужны для модулей навигации, типа
Wayfinder и т.п.
Для этого нам надо:
- Добавить возможность указывать в настройках каждой отдельной страницы, включать ее в кэш или нет.
- Скорректировать класс modCacheManager таким образом, чтобы он включал в кэш только те страницы, которые отмечены как включаемые в кэш карты ресурсов
- Добавить плагин, который будет налету, в случае не нахождения страницы (в случае, если запрашиваемая страница не оказалась кэшированной карте ресурсов), дополнительно производить поиск ресурсов по запрашиваемому УРЛ, и в случае нахождения, вывод контента страницы.
- Добавить пользовательский кэш каждой отдельной страницы с кэшированием УРЛ-ов дочерних ресурсов до 3-го уровня, или только тех ресурсов, УРЛы которых присутствуют на запрашиваемой страницы
Итак, приступим по порядку:
1. Добавить возможность указывать в настройках каждой отдельной страницы, включать ее в кэш или нет.
1.1
Добавляем дополнительный чекбокс в панель редактирования документов.
Для этого открывает файл
/manager/assets/modext/widgets/resource/modx.panel.resource.js
На строке 285 начинается перечисление полей формы настроек
{
xtype: 'checkbox'
,fieldLabel: _('resource_searchable')
,description: _('resource_searchable_help')
,name: 'searchable'
,id: 'modx-resource-searchable'
,inputValue: 1
,checked: config.record.searchable
}
Здесь
xtype: 'checkbox' - это и есть тип элемента <input type=”checkbox” ,
нам подобный и нужен, по-этому где-нибудь сразу за блоком чекбокса
“кэшируемый” мы и вставляем через запятую свой блок.
{
xtype: 'checkbox'
,fieldLabel: 'Включать в кеш Карты ресурсов'
,description: 'Если отмечено, то документ будет включен '+
'в карту ресурсов в кеше контекста и будет доступен для '+
'стандартных средств навигации типа Wayfinder и Ditto'
,name: 'inResourceMapCache'
,id: 'modx-resource-inResourceMapCache'
,inputValue: 1
,checked: config.record.inResourceMapCache
}
Обратите внимание на строку типа
,fieldLabel: _('resource_searchable')
Это
перечисление различных заголовков и т.п. _(text) - это функция, для
работы со словорями MODx, то есть если хотите, чтобы ваши доработки еще
имели мультиязычную основу или возможность управления через редактор
словарей, то копайте глубже. Направление я показал, а самому лень, ибо
это не критично для нашей задачи.
Но
в любом случае пропишите свои названия полей, ID-шник и т.п. Можете
просто скопировать мой блок, что в данном случае будет правильней, чтобы
нигде не было в дальнейшем конфликтов имен
Всё.
Вставив этот код и сохранив файл на сервере, обновите страницу
редактора. Вы увидите, что во вкладке “Настройки страницы” появился
дополнительный чекбокс.
Самое
интересное, что для того, чтобы данные этого чекбокса отправлялись на
сервер, ничего делать не надо! Скрипты MODx-а написаны так, что они
автоматом подхватывают добавляемые поля, и обрабатывают их значения.
Только
обратите внимание, что параметр ‘includeInResourceMapCache’ будет передаваться
только в том случае, если чекбокс будет отмечен. Это важно, если на
стороне сервера вы вздумаете проверять
isset($_POST[‘includeInResourceMapCache’]). В данном случае isset будет
false, потому что этого элемента не будет в передаваемых данных.
newCheckbox.jpg — изображение пока не получено из архива
1.2
Забегая вперед, предлагаю сразу создать настраиваемый параметр, который
будет указывать, включать по умолчанию в карту ресурсов создаваемый
документ, если не отмечено данное поле, или нет. Ведь платформа у нас
мультисайтовая, и на одной и той же платформе может лежать и
высоконагруженные сайты, и нет. И поверьте мне на слово, что при
желании с MODx Revolution можно так извратиться, что файлы разных сайтов
и даже их таблицы будут лежать на разных физических серверах, а
платформа будет одна, и все будет летать.
Для этого откроем через админку страницу “Настройки системы” и добавим в core.Кэширование нашу пользовательскую настройку.
addNewOption_1.jpg — изображение пока не получено из архива
addNewOption_2.jpg — изображение пока не получено из архива
addNewOption_3.jpg — изображение пока не получено из архива
addNewOption_4.jpg — изображение пока не получено из архива
addNewOption_5.jpg — изображение пока не получено из архива
1.3 Итак, элемент управления мы добавили. Теперь надо это значение где-то хранить. Разумно добавить колонку в саму таблицу site_content (не забывайте про префикс таблиц. По умолчанию он modx_, но мог быть указан и другой префикс при установке. То есть скорее всего полное название таблицы modx_site_content).
Добавляем
в конец таблицы колонку inResourceMapCache с атрибутами: tinyint(1)
unsigned default 0 . Лучше для этих целей воспользоваться PHP-клиентом
phpMyAdmin.
1.4 Все, колонка у нас есть. Теперь надо добавить логику в обработку создаваемых и обновляемых документов.
Для этого подредактируем два документа:
1.4.1 /core/model/modx/processors/resource/create.php (отвечает за создание документов)
У меня это строка 89. После строки
$scriptProperties['deleted'] = empty($scriptProperties['deleted']) ? 0 : 1;
Добавляем
$scriptProperties['inResourceMapCache'] =
!isset($scriptProperties['inResourceMapCache']) ?
(integer) $workingContext->getOption('inResourceMapCache_default', 1) :
(empty($scriptProperties['inResourceMapCache']) ? 0 : 1);
В
данном случае мы указали, что если в Пост-данных нет элемента
inResourceMapCache, то в качестве значения используем значение настройки
системы inResourceMapCache_default, которую мы создали в пункте 1.2
данной мануалы, или, если нет такой переменной, то 1.
1.4.2 /core/model/modx/processors/resource/update.php (отвечает за обновление документов)
Где-то в районе 100-ой строки находим блок /* setup default values */
Там перечислено штук 10 параметров, типа
$scriptProperties['parent'] = empty($scriptProperties['parent']) ? 0 : intval($scriptProperties['parent']);
Вот после последнего параметра вставляем свою строку:
$scriptProperties['inResourceMapCache'] = empty($scriptProperties['inResourceMapCache']) ? 0 : 1;
В данном случае мы перехватываем передаваемые Пост-данные, и если указано 1, соответственно значение ставится 1
1.5
Всё, с редактированием исполняемых скриптом и оформлением интерфейса
закончено. Но для того, чтобы заработало обновление информации в базе
данных, необходимо отредактировать еще один важный файл (плюс один файл
на будущее).
Дело
в том, что MODx Revolution полностью основан на расширении PDO - xPDO.
Это сторонний класс для работы с различными базами данных, типа тонкого
клиента. Так вот, для работы с БД он использует файлы, в которых
описана структура базы данных и ее таблиц, и если не внести информацию о
новой колонке в таблице site_content, то он и не будет о ней знать, и
обновлять ее упорно не будет.
Итак, нам придется отредактировать два файла:
1.5.1 /core/model/schema/modx.mysql.schema.xml
В
этом файле в формате XML описана струтура базы данных. Именно на основе
таких XML-файлов генерируются PHP-файлы классов и карт для работы xPDO с
базой данных. Но нам легче будет отредактировать эти PHP-файлы вручную,
чем пересобрать эту базу, и хотя сейчас информация в этом файле не
будет напрямую влиять на работу наших скриптов, она понадобится в
будущем, если вы все таки решите пересобрать всю базу xPDO на основе
этого файла
Итак, открываем этот файл и находим строку
<object class="modResource" table="site_content" extends="modAccessibleSimpleObject">
Внутри этого блока перечислены все поля этой таблицы типа
<field key="type" dbtype="varchar" precision="20" phptype="string" null="false" default="document" />
Вот сразу после перечисления всех колонок, перед блоком индексов вставляем строку
<field key="inResourceMapCache" dbtype="tinyint" precision="1" attributes="unsigned" phptype="boolean" null="false" default="0" index="index" />
Для наглядности привожу последние 3 строки:
<field key="context_key" dbtype="varchar" precision="100" phptype="string" null="false" default="web" index="index" /> <field key="content_type" dbtype="int" precision="11" attributes="unsigned" phptype="integer" null="false" default="1" /> <field key="inResourceMapCache" dbtype="tinyint" precision="1" attributes="unsigned" phptype="boolean" null="false" default="0" index="index" />
Сохраняем
этот файл и закрываем. Напомню, что это не критичный файл, и если у вас
работать не будет, то на него не пеняйте, ищите проблему в другом
месте.
1.5.2 /core/model/modx/mysql/modresource.map.inc.php
Вот это уже очень важный файл! Если в нем не описать правильно информацию по таблице, обновление информации в БД не будет.
Открываем его для редактирования. В нем информация представлена в виде массива.
Находим самый первый блок
$xpdo_meta_map['modResource']= array ( 'package' => 'modx', 'version' => '1.1', 'table' => 'site_content', 'fields' => array ( 'type' => 'document', 'contentType' => 'text/html',
Тут идет перечисление колонок в таблице site_content type, contentType и т.п.
В конец блока
'fields' => array (
добавляем новый элемент
'inResourceMapCache' => 0,
Сразу за этим блоком идет следующий важный блок:
, 'fieldMeta' => array (
В нем представлена более развернутая информация по этим полям, включая
названия индексов и т.п. типа (Название колонки => ее атрибуты)
'type' =>
array (
'dbtype' => 'varchar',
'precision' => '20',
'phptype' => 'string',
'null' => false,
'default' => 'document',
),
Добавляем последним элементом наш:
'inResourceMapCache' =>
array (
'dbtype' => 'tinyint',
'precision' => '1',
'attributes' => 'unsigned',
'phptype' => 'boolean',
'null' => false,
'default' => 1
)
Все, сохраняем этот файл на сервере.
После всего этого на всякий случай обновляем кеш сайта и проверяем
работоспособность, создав новый документ и несколько раз обновить
значение Включать в кеш Карты ресурсов: в настройках страницы. При этом
проверьте изменения значения inResourceMapCache непосредственно в
таблице в Базе Данных.
Учтите,
что разные файлы работают в момент создания новой страницы и обновления
уже существующей, потому проверьте и сразу после создания страницы, и
отдельно после обновления страницы.
У меня все работает.
1.6 Теперь надо подредактировать файл /core/model/modx/modcachemanager.class.php
Именно этот класс генерирует кеш Контекста сайта, карту ресурсов и т.п.
1.6.1 Строка 79
$resourceFields= 'id,parent,alias,isfolder,content_type';
Это перечислены колонки, получаемые из таблицы site_content
Нам надо добавить и свою колонку, чтобы мы могли различить кэшируемые и некэшируемые документы.
Далее примерно на 110-ой строке после
while ($r = $collResources->fetch(PDO::FETCH_OBJ)) {
добавляем
if(!$r->inResourceMapCache) continue;
Получаем
while ($r = $collResources->fetch(PDO::FETCH_OBJ)) {
if(!$r->inResourceMapCache) continue;
Таким образом мы пропускаем все документы, не включаемые в кеш.
P.S. Оказывается, есть еще момент, который я сразу не заметил. Надо подправить этот запрос:
$criteria= new xPDOCriteria($this->modx,
"SELECT {$resourceCols} FROM {$tblResource} `r` LEFT JOIN {$tblContextResource} `cr`
ON `cr`.`context_key` = :context_key1 AND `r`.`id` = `cr`.`resource`
WHERE `r`.`id` != `r`.`parent`
AND (
`r`.`context_key` = :context_key2 OR `cr`.`context_key` IS NOT NULL)
AND `r`.`deleted` = 0 GROUP BY `r`.`id`
ORDER BY `r`.`parent` ASC, `r`.`menuindex` ASC", $bindings, false);
потому
что в данном случае он на уровне PHP пропускает все не кэшируемые
элементы, но все равно с Базы Данных запрашивает все строки. В
результате при >1 000 000 документов первый запрос выполняется более
трех секунд.
Да-да, вы правильно заметили))) 1 000 000 документов. И это еще не предел.
Подставляем в запрос еще один критерий
AND `r`.`inResourceMapCache` = 1
И получаем вот такой запрос:
$criteria= new xPDOCriteria($this->modx,
"SELECT {$resourceCols} FROM {$tblResource} `r` LEFT JOIN {$tblContextResource} `cr`
ON `cr`.`context_key` = :context_key1 AND `r`.`id` = `cr`.`resource`
WHERE `r`.`id` != `r`.`parent`
AND (
`r`.`context_key` = :context_key2 OR `cr`.`context_key` IS NOT NULL)
AND `r`.`deleted` = 0 AND `r`.`inResourceMapCache` = 1 GROUP BY `r`.`id`
ORDER BY `r`.`parent` ASC, `r`.`menuindex` ASC", $bindings, false);
И теперь при более 1 000 000 документов любой запрос к сайту - менее 0,35 секунд!)))
По результатам тестирования одновременно 50-ти посетителей, загрузка страницы не превышает 0,8 секунд.
Результаты тестирования: http://loadimpact.com/result/modxforum.ru-0620c6997a541f794293b84714671c3d
Итак,
подытожим: мы добились загрузки страниц менее чем за 0,4 сек. при 1 000
000 докунтов. Очень неплохой разгон для MODx Revolution)))
Это
еще при том, что страницы не были до конца закэшированы и выполняли для
своих нужд несколько запросов к таблице site_content, то есть если
полностью кешировать страницы, сайт будет грузиться менее чем за 0,2
сек., и я это обязательно докажу!
Есть
еще момент: мы частично отключили кеширование карты ресурсов. А это
значит, что при использовании ЧПУ, то есть запрашивая страницу не по ее
ID, кроме корневой страницы вы не получите ни одной страницы, не
включенной в эту карту сайта.
Но это лечится. Вот маленькое решение, которое вы должны уже будете само развить дальше:
1. Создаем плагин, выполняемый по событию OnWebPageInit.
2. Прописываем в нем:
if($modx->checkSiteStatus() && !is_object($modx->resource))
$modx->resource = $modx->request->getResource('id', $docID);
Все,
теперь можно в этот код в качестве параметра $docID передавать ID
нужного нам документа, и вы получите код нужной вам страницы.
Правда
вам самим нужно будет думать, как получить ID документа. Очевидно, что
надо будет раскладывать адрес запрашиваемой страницы и проходиться по
таблице site_content и получать ID конечного документа, но надо еще и
кэширование этого результата тоже сохранять, чтобы потом при повторном
обращении не дергать каждый раз базу.
Я обязательно это доделаю в следующий раз и выложу готовое решение.
| Кол-во ресурсов | Симптомы | Первая загрузка (с генерацией кеша). Секунд | Вторая загрузка (закешированный раздел). Секунд | Размер кеш-файла | Размер таблицы site_content | Размер индексов таблицы site_content |
| 2363 | Не получилось клонировать раздел, не хватило на исполнение 30 секунд лимита. | 2,18 | 0,453 0,328 |
4 087 000 | ||
| 7035 | 4,42 | 0,594 0,360 |
6 112 295 | |||
| 9371 | Не
получается в дереве документов перенести раздел в другой раздел. Помог выход из админки и опять вход, но все равно тормозит |
9,94 | ??? | ??? | ||
| 18715 | Сильно
тормозит клонирование раздела (2-4 документа в сек) Так и не смог проклонировать раздел. Только примерно 3000 документов создал |
1,03 1,24 0,438 |
16 769 994 | 43.5 Mb | 5.2 Mb | |
| 20793 | Невозможно произвести удаление ветки с более чем 1000 ресурсов | 7,09 | 1,09 1,33 |
18 769 995 | 50,5 Мб | 6,6 Мб |
| 220449 | Не смог создать полный файл-кэш. | 5,8 | Ошибка 502, то есть сайт умер))) | 71 598 896 (не полный. должен был быть минимум 200 Мб) | ||
| После тюнинга | ||||||
| 220449 | 2 | 0,28-0,39 | 47 473 байт | 520 Мб | 54,7 Мб | |
| 439608 | 3,91 - 5,31 | 0,25-0,3 | 46 782 байт | 1 Gb | 102.7 Mb | |
| 878632 | 55.14 | 2 Gb | 191.9 Mb | |||
| После тюнинга 2. Добавления еще одного параметра в запрос | ||||||
| ################################################################################################################################# | ||||||
| 878632 | 0,25-0,6 | 2 Gb | 200 Mb | |||
| 1008904 | ~0.3 | 0.25-0.29 | 2.3 Gb | 220.9 Mb | ||