• Блог
  • Оптимизация больших сайтов. ч.2

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

В этой части поговорим о подходах к кэшированию ресурсов. Это важный элемент оптимизации. Ресурсы — это страницы сайта. И у них разные цели. Одни страницы можно полностью заменить на статические. Например, документация, страницы о компании. Другие выводят динамически генерируемый контент — информационные ленты, каталоги товаров и т.п. Но самый частый случай — когда генерируется только часть контента. Например, статьи или страница товара. Сам текст, как правило, статический. Бывают случаи, когда в него вставляют вызов чанков или сниппетов. Но в основном это простой текст. И на этой странице есть динамические блоки — это могут быть комментарии, блок с новостями, похожими статьями, аналогичными или просмотренными товарами, отзывами, информация из других разделов, акции, ну и т.д. А если таких блоков на странице несколько, генерация такой страницы отнимает приличные ресурсы у сервера. И как оптимизировать такие страницы? Начнём по порядку.

На данном этапе мы можем управлять двумя уровнями кэширования — кэшем всей страницы и кэшированием элементов MODX, указанных на этой странице. Для начала разберёмся с кэшем страницы.

Кэш страницы

Для управления кэшированием у страницы есть 2 чекбокса — «Кэшируемый» и «Очистить кэш». Первый всегда должен быть включён. В этом случае MODX генерирует файл страницы, в котором находится почти готовая HTML страница. «Почти», потому что в ней остаются некэшируемые вызовы тегов MODX. Что это даёт? Минус 4 запроса к БД — получить ресурс, шаблон, все TV и источник файлов. Плюс не тратится время на парсинг кэшируемых тегов. Они уже распарсены и их вызов заменён на результат на самой странице. Плюс кэшируются права доступа к ресурсу, если ресурс закрытый. А это тоже запросы к БД.

Чекбокс «Очистить кэш» в целях оптимизации нужно снять. Почему, я рассказывал в прошлой статье. Если этот чекбокс оставить включённым, то MODX очистит кэш следующих разделов:

  • auto_publish;
  • db;
  • context_settings;
  • resource.

Так как мы отключили этот чекбокс, то нам нужно самим позаботиться об инвалидации кэша. Идём по порядку. Если на сайте используется автопубликация ресурсов, то нам придётся перегенерировать кэш автопубликации самостоятельно. Кэширование базы данных должно быть отключено, поэтому мы пропускаем этот пункт. При сохранении ресурса MODX всегда пересоздаёт кэш контекста. Так что второй раз его пересоздавать не нужно. Я считаю, что это баг и мы его обошли.

Мнение!

Конечно, опытный разработчик возразит, что проблема с двойным кэшированием контекста справедлива только при сохранении существующего ресурса. Если сохранить новый ресурс, то скрытого обновления кэша не будет. Но тут я могу сказать, что, как правило, редко бывает так, чтобы ресурс при создании сразу на 100% был готов к публикации. Обычно его создают неопубликованным. А в дальнейшем дорабатывают, вычитывают, редактируют. Так что исходим из того, что всегда сохраняется существующий ресурс.

А вот с кэшем ресурсов придётся немного потрудиться. MODX, как и любые другие приложения, предлагает типовые настройки для сайта. Дальше задача по настройке ложится на администратора. Поэтому и с кэшированием подход типовой — при сохранении ресурса удаляется кэш всех ресурсов. Для чего это сделано? У вас могут быть страницы, использующие данные других ресурсов. И они могут быть закэшированы на этих страницах. Например, список последних новостей или комментариев. Поэтому MODX на всякий случай удаляет всё. В этом случае при первом запросе к ресурсам MODX будет вынужден парсить страницы заново. При большой посещаемости это может вызвать серьёзную нагрузку. Поэтому очищать кэш у таких страниц мы будем вручную и избирательно — не у всех таких страниц нужно чистить кэш. Об этом поговорим в разделе про кэширование тегов.

Делать это мы будем в плагине, который использует наш оптимизированный кэш-менеджер.

switch($modx->event->name) {
    case 'OnDocFormSave':
    case 'OnResourceDelete':
        if ($modx->cacheManager instanceof OptCacheManager) {
            switch ($mode){
                case 'upd':
                    // Чистим кэш сохраняемого ресурса.
                    $modx->cacheManager->clearResourceCache($resource);
                    break;
            }
        }
        // Генерируем кэш автопубликаций.
        $modx->cacheManager->refresh([
            'auto_publish' => ['contexts' => ['web']],
        ]);
        break;
    case 'OnResourceAutoPublish':
        break;
}

if ($modx->cacheManager instanceof OptCacheManager) {
    // Указываем ресурсы, у которых нужно очистить кэш.
    $ids = [1,10];  
    foreach ($ids as $id) {
        $modx->cacheManager->clearResourceCache($id, true); // Второй параметр отвечает за генерацию кэша указанной страницы.
    }
}
// Удаляем кэши элементов
foreach(['chunks', 'snippets'] as $folder) {
    array_map('unlink', glob(MODX_CORE_PATH . "cache/optimization/$folder/*.php"));
}

В этом плагине при сохранении или удалении ресурса обновляется кэш автопубликаций, а также удаляются и пересоздаются кэши 2 страниц, id которых равны 1 и 10 (чисто для примера). А также, если сохраняется существующий ресурс, то удаляется и его кэш. При автопубликации ресурса также удаляются кэши указанных страниц. В большинстве случаев этого плагина будет достаточно. Но и он может быть оптимизирован — удалять кэши только тех страниц, которые зависят от сохраняемого ресурса. Про кэши элементов поговорим чуть ниже.

Важно!

При сохранении любого элемента в админке MODX польностью очищает кэш сайта. Так что, если вы решили поменять какой-то незначительный чанк, которые используется на одной странице, или написать комментарий в сниппете, обязательно снимите чекбокс «Очистить кэш».

Кэширование тегов

Кэшировать нужно всё возможное — меню (если оно генерируется), галерею картинок, TV и всё, что не требует изменений при обновлении страницы. Т.е. всё, что должно оставаться неизменённым при нажатии F5. Ну кроме, конечно, данных текущего пользователя. Их кэшировать нельзя. Часто есть агрегатная страница, на которой собираются различные блоки типа последних новостей, статей, комментариев, отзывов, товаров и т.п. (как правило это главная страница сайта). Её контент не меняется при обновлении страницы, но данные должны быть актуальными. Как правило, такие блоки собираются сниппетами, и если не кэшировать эти сниппеты, то страница будет грузиться неприлично долго.

Из опыта

На главной странице сайта, у которого 160 тыс. ресурсов, выводится 8 блоков из разных разделов. Генерация такой страницы из БД занимала 5,5 сек., из кэша (до оптимизации) — 1,2 сек., после оптимизации — 0,004 сек.

Есть 2 подхода к кэширование таких страниц. Первый — кэшировать абсолютно все сниппеты. Таким образом получается практически статическая страница. А за актуальностью информации на такой странице следит плагин. В моём последнем проекте я выбрал именно этот подход для главной страницы. При сохранении ресурса её кэш удаляется и сразу пересоздаётся, чтобы первый пользователь не ждал 5 сек., пока она откроется. Но если у вас таких блоков немного и они повторяются на разных страницах, то в этом случае можно применить второй подход — кэширование результата сниппета или чанка. Вот пример сниппета CacheOptimizer, который является обёрткой для сниппетов или чанков, которые нужно закэшировать. Он использует библиотеку modHelpers.


/**
 * @var modX $modx
 * @var array $scriptProperties
 * @var string $type             Тип элемента - 'chunk' или 'snippet'.
 * @var string $name             Название элемента.
 * @var string $key              Ключ кэширования (название файла кэша).
 * @var int $cacheTime           Время кэширования в секундах. 0 - без ограничения времени.
 */
if (empty($name)) {
    return '';
}
$name = preg_replace('~[\W]~', '', str_replace('-', '_', $name));
unset($scriptProperties['name'], $scriptProperties['type'], $scriptProperties['cacheTime']);
// Если ключ не указан, то он формируется автоматически.
if (empty($key)) {
    if (!empty($scriptProperties)) {
        $key = $name . '_' . md5($name . implode(',', $scriptProperties));
    } else {
        $key = $name;  // При условии, что внутри элемента не используются разные данные, зависящие от ресурса.
    }
}

$options = [
    xPDO::OPT_CACHE_KEY => "optimization/{$type}s", // Папка с данными
];
if ((int)$cacheTime > 0) {
    $options[xPDO::OPT_CACHE_EXPIRES] = (int)$cacheTime;
}

return cache()->remember($key, $options, function() use ($modx, $name, $type, $scriptProperties) {
    $service = ($pdoTools = $modx->getService('pdoTools')) ? $pdoTools : $modx;
    $output = '';
    switch ($type) {
        case 'chunk':
            $output = $service->getChunk($name, $scriptProperties);
            break;
        case 'snippet':
            $name = '!' . ltrim($name, '!');
            $output = $service->runSnippet($name, $scriptProperties);
    }
    return $output;
});

Все вызовы чанков или сниппетов, которые нужно закэшировать, оборачиваем в этот сниппет

{'!CacheOptimizer' | snippet : [
    'type' => 'snippet',
    'name' => 'pdoResources',
    'cacheTime' => 0,  
    ...  // Далее указываем параметры сниппета pdoResources
]}

В итоге, в папке core/cache/ будет создан раздел optimization, в котором по папкам snippets и chunks будут разложены кэши элементов. Если вернуться к плагину, то станет понятно, что в самом конце удаляются именно эти кэши.

Тут стоит ещё обратить внимание на время кэширования. Если у вас выводится список статей или комментариев, у которых указаны, например, время публикации в формате «5 минут назад» или количество просмотров, то кэшировать такие данные нужно на какое-то ограниченное время — 0 в данном случае не подойдёт. Если возможно, то лучше убрать такие данные или использовать нейтральные форматы.

Из опыта

На странице новости выводится блок «Читайте также» с 4-мя случайными новостями. И при обновлении страницы в браузере новости должны меняться. До оптимизации страница открывалась за 1,5 сек. Если этот блок закэшировать, то на всех страницах новостей будут одни и те же случайные новости. Даже если ограничить по времени.

Проблема

Запрос строится с сортировкой RAND(). Для MySql это сложная операция. Особенно для большого набора данных. А данных было много — новости за полгода. В память они не умещаются, поэтому MySql вынужден создавать временный файл и в нём проводить все манипуляции — добавить случайное значение для каждой строчки, отсортировать по нему и отобрать нужное количество.

Решение

Первый шаг — уменьшение периода с полугода до 2 месяцев. Это дало сокращение времени запроса на 0,4 сек. Второй шаг — кэширование запроса, но по определённому алгоритму. Из базы данных выбирается 30 записей, кэшируются сниппетом на 5 минут (можно и больше), дальше массив с данными перемешивается с помощью функции shuffle() и выводятся только первые 4 элемента. Таким образом, новости всегда разные, а страница отдаётся за 0,3 сек.

Есть ещё и третий подход — для таких блоков использовать AJAX. Из готового можно взять AjaxSnippet. Но если у вас высоконагруженный сайт, то нагрузка на сервер заметно вырастет. Такая оптимизация решает только одну задачу — удовлетворение пользователя. Правда за счёт бедных электронов. :) Поэтому я бы советовал использовать AJAX только если первые 2 подхода применить не получается.

Заключение

Я постарался осветить основные подходы к кэшированию ресурсов. Возможно для решения конктретной задачи вам придётся доработать предложенные решения или придумать своё. Это нормально. А в следующей статье мы поговорим об оптимизации библиотеки pdoTools. Если вы читали предыдущую статью, то уже знаете, что многие сниппеты из этой библиотеки будут работать некорректно из-за того, что карта ресурсов неполная. Да и сама эта библиотека не готова к работе с большим набором данных. Будем это исправлять.

Предыдущая статья «Оптимизация больших сайтов. ч.1».

1   403

Комментарии ()

  1. Александр 20 июня 2021, 14:16 # 0
    Круто! Спасибо.

    А если чанк, который использует сниппет, изменить, то придется только вручную удалить кеш или сохранить страницу, чтобы очистился кеш для чанков?

    Есть 2 подхода к кэширование таких страниц. Первый — кэшировать абсолютно все сниппеты.
    Как именно? Все элементы через сниппет CacheOptimize?

    При сохранении ресурса её кэш удаляется и сразу пересоздаётся,
    Только страницы которые указаны в массиве $ids = [1,10], правильно?

    1. Сергей Шлоков 20 июня 2021, 17:41 # 0
      А если чанк, который использует сниппет, изменить, то придется только вручную удалить кеш или сохранить страницу, чтобы очистился кеш для чанков?
      Точно. Можно повесить плагин на сохранение чанков и сниппетов.

      Как именно? Все элементы через сниппет CacheOptimize?
      Имелось ввиду вызов сниппетов без знака "!".

      Только страницы которые указаны в массиве $ids = [1,10], правильно?
      Да, но эти цифры только для примера.

    Вы должны авторизоваться, чтобы оставлять комментарии.

    Выделите опечатку и нажмите Ctrl + Enter, чтобы отправить сообщение об ошибке.