Оптимизация DLE
Периодически у меня появлялось желание как-то оптимизировать DLE в плане программной части. Но все никак руки не доходили или желания не было.
Хочу немного поделиться своими решениями в плане оптимизации запросов к БД в DLE.
UPD [2020-12-01]: Добавлен пункт 6
UPD [2021-02-13]: Добавлен пункт 7
Не так давно проводил ряд работ по оптимизации сайта. Монитор нагрузки показывал загрузку процессора на 70-90%. Тут и top не нужно смотреть, чтобы понять что проблема в mysql.
Специально для этой задачи был написан относительно простенький модуль, который вел лог всех запросов в бд и записывал статистику. На скриншотах ниже - результат сбора статистики за один вечер, время 20:40 - 22:20.
Вот по результатам этих данных (и не только) проведем небольшой анализ и постараемся оптимизировать по максимуму.
Внимание: Если у вас DLE 13.x, то вместо ручного внесения изменений вам нужно использовать утилиту управления плагинами, как ею пользоваться описано в инструкции.
1. Общие рекомендации
В самую первую очередь строго обязательно рекомендуется отключить функцию "Поддержка публикации новостей на еще не наступившую дату". Этот параметр очень много дает.
Если вы не используете и не планируете использовать функционал "фиксации новостей", то я так же настоятельно рекомендую его отключить. В некоторых ситуациях это даст ощутимый прирост производительности.
Так же рекомендую отключить все другие не используемые модули, типа topnews, календарь, RSS информер и т.п.
Отдельно хочу обратить внимание на RSS информер. По умолчанию он включен и так же по умолчанию уже есть созданный и включенный информер яндекс новостей. Это следует знать и лучше его отключать или удалять.
UPD: Решил просто привести наглядный пример. Чистая DLE 14.0. В БД 50к записей.
[query] => SELECT ... FROM dle_post p LEFT JOIN dle_post_extras e ON (p.id=e.news_id) WHERE approve=1 AND allow_main=1 AND date < '2020-05-07 12:08:02' ORDER BY fixed desc, date DESC LIMIT 0,10
[time] => 0,52297616004944
[num] => 3
[query] => SELECT COUNT(*) as count FROM dle_post WHERE approve=1 AND allow_main=1 AND date < '2020-05-07 12:08:02'
[time] => 0,65341997146606
[num] => 4
А теперь отключаем "Поддержка публикации новостей на еще не наступившую дату"
[query] => SELECT ... FROM dle_post p LEFT JOIN dle_post_extras e ON (p.id=e.news_id) WHERE approve=1 AND allow_main=1 ORDER BY fixed desc, date DESC LIMIT 0,10
[time] => 0,0053451061248779
[num] => 3
[query] => SELECT COUNT(*) as count FROM dle_post WHERE approve=1 AND allow_main=1
[time] => 0,00032711029052734
[num] => 4
Думаю не стоит говорить на сколько существенна разница во времени.
2. not detected
Не актуально для DLE 13.3 и старше
Думаю тут все и так ясно, за исключением последней колонки GC (group count). Это количество - сколько раз выполнялся этот запрос.
Данные запросы появляются при обращении к не существующим категориям. В принципе не критично, но блин, зачем выполнять 2 запроса если априори известно, что в результате ничего не будет и быть не должно.
Решение:
Открыть файл engine/modules/show.short.php
Найти строку:
if( $allow_active_news ) {
Выше нее вставить:
if ($do == 'cat' && !(int)$category_id) {
$allow_active_news = false;
}
3. SELECT COUNT(*) as count FROM dle_post
Не актуально для DLE 15.0 и старше
Просто посмотрите на эти числа, а главное количество этих запросов. Разумеется кеширование включено, но...
Вот еще мне человек писал, цитата от хостера:
При проверке, мы видим, что в момент обновления указанной Вами ссылки, в базу данных поступает запрос вида:
При ручном выполнении данного запроса, мы и вправду наблюдаем затрату времени 2.70 sec
SELECT COUNT(*) as count FROM dle_post WHERE category regexp '[[:<:]](702|703|704|706|707|709|710|711|712|713|714|715|716|717|718|719|720|721|723|724|725|726|729|730|731|732|733|734|735|737|738|739|740|741)[[:>:]]' AND approve=1;
При ручном выполнении данного запроса, мы и вправду наблюдаем затрату времени 2.70 sec
Решение:
Если у вас DLE 11.1 или младше, то потребуется установить хак Количество новостей в категории
Для более старших версий - в настройках необходимо включить параметр "Осуществлять подсчет количества новостей в категориях"
Открыть файл engine/modules/show.short.php
Найти строку (первую):
$count_all = $db->super_query( $sql_count );
Выше нее вставить:
if ($do == 'cat' && isset($cat_info[(int)$category_id]['newscount'])) {
$count_all = array(
'count' => (int)$cat_info[$category_id]['newscount']
);
} else
Данный хак полностью исключит вышеуказанные проблемные запросы. Но именно только для категорий. Хотя по сути в категориях-то как раз и выполняется наибольшее количество подобных запросов.
4. Мысли по п.3
Запросы мы убрали, все хорошо. Но вот вопрос, почему этих запросов так много при включенном кешировании.
Тут немножко дело зависит от версии DLE. К примеру в версии 10.6 и младше кешировались только первые 5 страниц навигации, остальные вообще без кеша. Затем это число было увеличено до 10 страниц, так продолжалось вплоть до 11.2. И уже начиная с 11.3 и до актуальной версии это число можно вручную указывать в настройках - "Количество страниц сайта, которое необходимо кешировать при показе кратких публикаций".
Это правильный шаг, но пользоваться этим параметром нужно тоже с умом. Если у вас, к примеру около 30 категорий, то можно кешировать и 20-30 страниц, получится 600-900 файлов кеша, что вполне приемлемо.
Но если у вас много категорий, то количество кешируемых страниц оптимально как раз около 10-20.
Немного отвлекся от основной темы данного раздела, а именно почему этих запросов так много? И ответ достаточно прост - кеширование. А именно очистка кеша. Добавление нового комментария, оценка в рейтинге - оба эти действия полностью сносят кеш всех страниц с отображением короткой новости и блоков custom. Не слабо так, один лайк и кеш обнулен.
Многое зависит от сайта и желаний владельца сайта - нужно ли вам, чтобы каждое действие тут же было видно в короткой новости или может в короткой новости вообще не отображается ни рейтинг ни количество комментариев. Так что я просто приведу инструкцию, а вносить эти правки или нет - уже решать вам.
Открыть файл engine/modules/addcomments.php
Найти строку:
clear_cache( array( 'news_', 'comm_'.$post_id, $cprefix ) );
или
clear_cache( array( 'news_', 'rss', 'comm_'.$post_id, $cprefix ) );
Заменить на:
clear_cache( array( 'comm_'.$post_id, $cprefix ) );
Таким образом, при добавлении комментария будет очищен только кеш текущей новости и сами комментарии.
Открыть файл engine/ajax/rating.php
Найти строку (2шт):
clear_cache( array( 'news_', $cprefix ) );
или
clear_cache( array( 'news_', 'rss', $cprefix ) );
Заменить на:
clear_cache( array( $cprefix ) );
Теперь при выставлении оценки в рейтинге новости будет очищен только кеш самой новости.
5. order="RAND()"
RAND() - зло.
Но если вам очень уж хочется использовать эту сортировку, но я приведу простую аналогию.
К примеру новости - это стальные шарики весом 10г каждый. У вас есть 100 шариков и вы хотите выбрать из них 10 случайных.
Засыпаете их в ведро и колотите пока не выпадут 10шт. Это не сложно, всего-то 1кг шариков + вес ведра.
Теперь у вас 2000 шариков (новостей), вы хотите так же выбрать из них 10 случайных. Повторяем процедуру, только теперь уже нужно активно и быстро месить 20кг, что не так-то и просто, но пока вполне реально.
И совсем уж для понимания, когда в среднем на том же киносайте порядка 30тыс новостей, получается что месить уже нужно 300 кг. Думаю суть ясна.
Но чтобы закрепить понимание, добавим 100 посетителей, когда каждый заходит раз в 1 секунду и просит из 10кг шариков выбрать один любой. И причем каждый посетитель хочет свой уникальный шарик (типа без кеша). В один прекрасный момент наш работяга метусильник (mysql) пошлет все в ж**пу и пойдет на перекур (Server has gone away). И возможно сам не захочет возвращаться.
Если уж хотите использовать rand - делайте это с умом. Либо не заставляйте его каждую секунду метусить 300кг (кеширование результатов выборки, тут кстати может помочь Ручной кеш), либо пусть месит до 1кг (ограничить изначальную выборку, по дате или другим параметрам, чтобы итоговое максимальное количество новостей было небольшим). А еще лучше комбинировать оба метода.
6. Оптимизация запросов SELECT count(*) на страницах новостей
Не актуально для DLE 15.0 и старше
Этот раздел можно считать продолжением п.3. Но это решение немного более продвинутое и функциональное.
Полагаю, что многие сталкивались с такой проблемой, что запрос count(*) обрабатывается довольно таки продолжительное время.
[query] => SELECT COUNT(*) as count FROM dle_post p WHERE category IN ('1') AND approve=1
[time] => 0,28818893432617
Выше я уже предлагал одно из решений этой проблемы. Но у него есть свои минусы:
- работает только на страницах категорий;
- необходимо чтобы был включен счетчик новостей в категориях.
Оба эти недостатка решены в этой реализации.
Суть проблемы в том, что на каждой странице /page/1/ - page/N/ повторно выполняется один и тот же запрос, который на каждой странице отдает один и тот же результат.
Данный хак запоминает этот результат и сохраняет его в кеше. В результате чего, не зависимо от количества страниц, запрос на получение количества новостей в разделе будет выполняться только один раз.
UPD: Если используется отложенная публикация (на не наступившую дату), то этот плагин лучше не использовать.
7. Использование {include file='...'} для PHP файлов
Не актуально для DLE 16.0 и старше
Данный раздел скорее можно отнести к категории "микрооптимизации". Однако это не отменяет того факта, что есть что улучшить.
Как показала практика, одно подключение элементарнейшего php файла (даже пустого) в шаблоне - по времени занимает порядка 0.005 секунд.
И это, скажу я вам, достаточно много. Один такой файл особо не сыграет роли (относительно), но если их используется хотя бы 10 шт или он подключается в shortstory.tpl и выводится скажем 30-60 раз. Уже имеем 0.005 * 30 = 0.15с (150-300мс) и это безусловно очень много.
Важное замечание. Речь идет не о времени формирования страницы (или TTFB), а именно о времени которое занимает только DLE.
Причины
Чтобы разобраться в данной проблеме, достаточно изучить файл engine/classes/templates.class.php
Доходим до места подключения php файла и видим строку:
$tpl = new dle_template();
Получается не важно, что находится внутри подключаемого php файла - для него всегда будет создаваться свой экземпляр класса шаблоназитора.
Посмотрев в конструктор класса, мы видим, что там так же создается новый экземпляр класса mobiledetect и соответственно выполняется определение устройства пользователя. Вот именно на это бесполезное действие и тратятся те злополучные 5мс.
Решение
Решение, на самом деле, достаточно простое и на мой взгляд - очевидное.
Мы не будет создавать новый экземпляр класса, просто создадим копию текущего и обнулим значения переменных.
Для этого нужно найти строки:
$tpl = new dle_template();
$tpl->dir = TEMPLATE_DIR;
И заменить их на:
$tpl = clone $this;
$tpl->template = $tpl->copy_template = '';
$tpl->block_data = $tpl->data = $tpl->result = [];
Всё. В результате этого мы полностью сохраняем функционал, но уменьшаем скорость формирования php файла с 5мс до 0.1мс
На этом, пожалуй, пока все. Спасибо что дочитали до конца (если это так :)), буду рад вашим комментариям и предложениям.
С уважением,
Олег Александрович a.k.a. Sander