Кэш в Drupal от А до Я
Введение
Страницы сайтов становятся всё больше по размеру, и встаёт вопрос асинхронной загрузки контента на них. Но если для изображений существует библиотека для их простой загрузки после загрузки основной страницы, то с асинхронной загрузкой HTML частей страницы (блоков), всё намного сложней. Например: для реализации асинхронной загрузки блоков в Drupal без потери производительности сайта необходимо понимать, как работает кэширование, как его использовать и как оптимизировать. А уже после реализовать по намеченному плану «Lazy Loading» загрузку блоков сайта.
Кэш в Drupal — введение
Кэш в Drupal — это промежуточный буфер, хранящий часто запрашиваемые данные, которые могут быть возвращены пользователю с наименьшими затратами системных ресурсов и максимальной скоростью. Этот буфер может представлять из себя таблицу(ы) в базе данных, файл с данными на жёстком диске или набор ключ-значение в сетевом журналируемом хранилище данных. Подойдёт любой вариант хранения информации, который можно интегрировать с PHP для кэширования веб-приложения на Drupal.
Один из примеров использования кэша в Drupal — это кэш меню. В большинстве случаев меню не является часто изменяемой частью сайта. Не имеет смысла каждый раз перестраивать (получать все пункты, их зависимость друг от друга, выводить их в шаблон) меню для вывода пользователю при его заходе на новую страничку сайта. Тем более, что если это меню каталога из 1000 пунктов, оно может перестраиваться для вывода достаточно долгое время. Зачем же заставлять пользователя ждать? Поэтому Drupal кэширует однажды построенное меню на указанное администратором время и выводит его пользователю из кэша, избегая долгой операции по перестраиванию меню.
Drupal содержит встроенный кэш (в своих базовых модулях), который можно настроить на странице: /admin/settings/performance. Нужно не забывать его периодически чистить, когда вы добавляете новую функциональность в каких-либо своих разработках (будь то шаблоны или модули).
Кэш можно очистить несколькими способами:
- С помощью кнопки «Очистить кэш» на странице /admin/settings/performance
- Установить модуль admin_menu и выбрать в самой левой вкладке Flush all caches (Очистить все кэши)
- С помощью ссылки «Empty cache» (очистить кэш) блока Devel Block (модуль Devel)
- Выполнив команду Drush: drush cache-clear theme (чистится только кэш темы)
- Программно, вызвав функцию drupal_rebuild_theme (чистится только кэш темы)
Кэш в Drupal — сегменты кэша
Кэш Drupal разбит по сегментам, а не хранится в одном единственном месте. По умолчанию каждый сегмент кэша хранится в виде отдельной таблицы в базе данных. Это позволяет выносить часто обновляемые сегменты кэша в другие системы хранения кэша, например в Memcached или Boost. Так же это повышает производительность работы с кэшем — в меньших объёмах данных работа с записями происходят быстрее. Более того, при определённых действиях кэш может очищаться частично (сегментно).
Drupal насчитывает множество различных сегментов кэша. Сегменты кэша похожи для версий 7.XX и 6.XX:
1. {cache} — сегмент для общего хранилища кэша. Сюда попадают данные, которые невозможно классифицировать, либо же нет смысла создавать под них новый сегмент кэша.
2. {cache_block} — добавляется при включении модуля Block (входит в ядро). При загрузке региона темы Drupal производится загрузка данных по всем блокам этого региона и при необходимости производится построение блока или отображение его из кэша, пропуская вызов хука hook_block_view(). Стоит учесть, что кэширование для блоков отключается, если включаются модули по работе с доступами к материалу, использующие hook_node_access(). Так же необходимо знать, что при программном создании блока через hook_block_info() можно управлять параметрами кэширования для блока (подробнее — в документации).
3. {cache_filter} — модуль Filter создает свою таблицу для хранения кэша для обработанного фильтрами текста. Чем сложнее фильтр, тем больше процессорного времени тратится на обработку текста. Поэтому по возможности все вызовы check_markup() кэшируются. Cache ID для таблицы {cache_filter} собирается по правилу название_формата: язык: хэш_текста.
4. {cache_form} — если остальные кэшируемые данные хранятся для ускорения работы сайта, то этот кэш на производительность никак не влияет. Он необходим, чтобы формы, построенные с помощью Forms API, были абсолютно безопасными с точки зрения уязвимостей. Каждый раз при построении формы она сохраняется в сегменте {cache_form}. Если форм и посетителей много, то {cache_form} имеет свойство разрастаться до внушительных размеров, если не запускать cron для очистки кэша каждый час-два.
5. {cache_menu} — включается при включении модуля Menu и является хранилищем ссылок из всех меню, созданными через интерфейс. Cache ID строится по правилу links: имя_меню:tree-data: язык: хэш_параметров.
6. {cache_page} — хранит закэшированные данные страниц для анонимных пользователей. Если найден кэш для текущей страницы, то будут вызваны только 2 хука: hook_boot() и hook_exit(). Остальные же хуки (включая hook_init() и прочие) будут пропущены. Это именно тот кэш, который включается на сайте в разделе настроек производительности (admin/config/development/performance) галочкой «Кэшировать страницы для анонимных пользователей».
7. {cache_update} — модуль Update manager добавляет данный сегмент. Он хранит данные по всем релизам для включенных модулей.
Сегменты кэша, имеющиеся в Drupal версии 7.XX (нет в версии 6.XX):
1. {cache_path} — хранит соответствие между системным путём и его алиасами для более быстрого поиска алиаса по системному пути.
2. {cache_image} — зарезервирована модулем Image и может использоваться как хранение сведений о проведении различных манипуляций над изображениями.
3. {cache_bootstrap} — сегмент кэша, в котором хранятся данные, инициализируемые при загрузке Drupal.
4. {cache_field} — в данном сегменте хранятся данные по всем полям (fields). Cache ID формируется по правилу field: тип_сущности:id_сущности.
Так же сторонние модули могут создавать свои сегменты кэша. Например, сегменты для модулей hacked, l10n_update, token, views:
- cache_hacked
- cache_l10n_update
- cache_token
- cache_views
- cache_views_data
Стоит упомянуть и объектный кэш Ctools’a, который не относится к ядру и создаётся модулем CTools. Объектный кэш CTools’a — это сегмент кэша, который предоставляет своё пространство под хранение больших объектов, которые редактируются в данный момент. Например, кэш изменённого представления до его сохранения в модуле Views хранится именно в объектном кэше CTools’a. В отличии от других сегментов кэша он имеет дополнительное поле sid (Session ID) — идентификатор текущей сессии пользователя. Благодаря ему изменённые данные видны только изменившему объект пользователю. Этот сегмент не имеет поля expire и не удаляется при очистке кэша через интерфейс, но очищается раз в сутки по Cron с удалением из этого сегмента кэша недельной давности.
Жизненный цикл страницы
При запросе страницы у веб-сервера браузером пользователя происходит следующее (на примере Drupal версии 7.XX):
Производится первичная загрузка ядра Drupal с одним из параметров:
- DRUPAL_BOOTSTRAP_CONFIGURATION: Инициализирует только конфигурацию.
- DRUPAL_BOOTSTRAP_PAGE_CACHE: Инициализация слоя кэширования.
- DRUPAL_BOOTSTRAP_DATABASE: Инициализация слоя базы данных.
- DRUPAL_BOOTSTRAP_VARIABLES: Инициализация слоя переменных.
- DRUPAL_BOOTSTRAP_SESSION: Инициализация работы с сессиями.
- DRUPAL_BOOTSTRAP_PAGE_HEADER: Инициализация слоя работы с заголовками.
- DRUPAL_BOOTSTRAP_LANGUAGE: Инициализация слоя работы с языком страницы.
- DRUPAL_BOOTSTRAP_FULL: Полностью загружает Drupal. А также добавляет функции проверки и исправления введенных данных.
Подробнее можно посмотреть в функции drupal_bootstrap() из файла bootstrap.inc расположенного в папке includes.
При выполнении drupal_bootstrap() с параметром DRUPAL_BOOTSTRAP_FULL производится:
- Подключение файлов системных функций.
- Инициализация всех слоёв и первичных настроек.
- Подключение файлов всех включённых модулей.
- Инициализация переменных и системных функций для работы с путями и их алиасами в Drupal.
- Инициализация включённой темы оформления.
- Производится выполнении module_invoke_all(). Реализует API для загрузки и взаимодействия с модулями Drupal, регистрируя все хуки текущих включённых модулей.
После этого производится вызов функции menu_execute_active_handler(), которая определяет навигационные меню и преобразует запросы страниц в вызовы функций, привязанные к путям на сайте. Также внутри данной функции производится вызов:
- Функции drupal_deliver_html_page(), которая возвращает данные страницы в виде HTML в браузер пользователя. Внутри этой функции вызывается функция drupal_render(), которая не только выводит данные, но и сохраняет в один из сегментов кэша и достаёт их оттуда при их наличии вместо повторной генерации страницы с использованием шаблонизатора.
- Функции drupal_page_footer(), которая устанавливает кэш страницы ('cache_path' и 'cache_bootstrap'), если это необходимо, и позволяет модулям реагировать на закрытии страницы по hook_exit (). Тут же при необходимости производится запуск Cron.
В общем случае:
- Производится инициализация всех необходимых переменных и функций.
- Производится проверка, имеется ли кэш по данному URL ({cache_page}). Если имеется, то он возвращается, иначе производятся дальнейшие действия.
- Производится проверка имеется ли кэш полей ({cache_field}), контента ({cache_filter}), меню ({cache_menu}), блоков ({cache_block}), а так же изображений ({cache_image}) и алиасов ({cache_path}). Если кэш не имеется, то производится операция по получение и обработки необходимых данных с сохранением в кэш. Полученные данные передаются в функцию темизации.
- Функция темизации строит страницу и кэширует её по данному URL ({cache_page}).
- Данные возвращаются пользователю.
Программная работа с кэшем в Drupal 7.X
Самым распространённым вариантом работы с кэшем является сохранение данных разрабатываемого модуля в кэш используя функцию cache_set. А также извлечение их из него, используя функцию cache_get.
<?php
// Проверяем, имеется ли кэш с именем: my_module_data.
if ($cache = cache_get('my_module_data')) {
// Возвращаем его данные, если он имеется.
return $cache->data;
}
else {
// Если кэш отсутствует, реализуем построение данных.
$my_data = 'Тестовые данные для кэширования';
// Сохраняем данные в кэш с именем: my_module_data сегмента: {cache}.
cache_set('my_module_data', $my_data, 'cache');
// Возвращаем данные.
return $my_data;
}
?>
Очистить данные кэша с именем my_module_data можно, вызвав одну из функций:
<?php
// Очистим данные кэшей с истёкшим сроком годности
// и времени (если кэшировались на определённое время).
cache_clear_all();
// Полностью очистим сегмент {my_module_data}.
cache_clear_all('*', 'my_module_data', TRUE);
// Удалим из сегмента {my_module_data} записи,
// у которых Cache ID начинается c 'my_module'.
cache_clear_all('my_module', 'my_module_data', TRUE);
?>
Через интерфейс администратора сайта это можно сделать на странице: example.com/admin/config/development/performance нажав кнопку Очистить кэш.
Если необходимо, чтобы данные были закэшированы на определённое время и не зависели от нажатия кнопки очистки кэша на странице example.com/admin/config/development/performance, то достаточно добавить в функцию cache_set дополнительный параметр — на сколько секунд кэшировать данные.
<?php
cache_set('my_module_data', $my_data, 'cache', time() + 360);
?>
Drupal позволяет создавать свои сегменты кэша для хранения данных. Создадим для этого модуль: mymodule. Для этого в каталоге сайта: ./sites/default/modules создадим папку: mymodule. В ней создадим файл описания модуля mymodule.info с содержимым:
name = My module
description = "Тестовый модуль для создания своего сегмента кэша."
core = 7.x
version = 7.x-1.x-dev
files[] = mymodule.module
Так же создадим файл mymodule.install в котором, используя hook_schema(), создадим новую таблицу для хранения данных своего сегмента кэша:
<?php
/**
* Implements hook_schema().
*/
function mymodule_schema() {
// Копируем схему таблицы сегмента: {cache}.
// Это позволяет не описывать самим поля своей таблицы
// так как нам необходима идентичная таблица.
$schema['mymodule'] = drupal_get_schema_unprocessed('system', 'cache');
$schema['mymodule']['description'] = 'Cache table stores some example data.';
return $schema;
}
Последний шаг перед тем как начать использовать функцию cache_set с указанием созданного нами сегмента — создать фал mymodule.module и позаботиться об автоматическом сбросе кэша при нажатии на кнопку очистки кэша на странице example.com/admin/config/development/performance.
<?php
/**
* Implements hook_flush_caches().
*/
function mymodule_flush_caches() {
// Возвращаем имя собственного сегмента для очистки данных кэша в нём.
return array('mymodule');
}
Включаем модуль на странице example.com/аdmin/modules, после чего, например, сохраним в собственный сегмент кэша созданного модулем, данные:
<?php
cache_set('my_module_data', 'Строка сохранённая в собственный сегмент кэша', 'mymodule');
?>
Существует малоизвестная функция cache_is_empty, с помощью которой можно узнать, хранятся ли в кэше с заданным именем какие-либо данные:
<?php
cache_is_empty('my_module');
?>
Как работать с кэшем в Drupal 6.X и 8.X, можно всегда посмотреть в подмодуле cache_example, модуля: examples со страницы: www.drupal.org/project/examples.
Вынесение кэша из базы данных
Кэш сегментов можно перенести, например в Memcached или Redis.
У каждого хранилища имеются свои сторонники. Помогавший в написании статьи Evgeniy Maslovskiy (Spleshka, www.drupal.org/u/spleshka) является сторонником Memcached, и на его сайте подробно описана интеграция Memcached и Drupal. В описанном им модуле интеграции с Memcached имеются дополнительные плюсы перед другими решениями: это обход подключения к БД при получении кэша и обход вызовов hook_exit() и hook_boostrap() в других модулях.
В данной статье рассмотрим вынесение кэша в Redis, который мне нравится более простой настройкой по сравнению с Memcached. А сравнительные тесты скорости обоих хранилищ рассмотрим в другой статье.
Настройку Redis на сервере можно найти в интернет. Модуль интеграции с Drupal скачаем со страницы www.drupal.org/project/redis и распакуем в директорию ./sites/all/modules. После чего внесём изменения в конфигурационный файл Drupal ./sites/default/settings.php, добавив в него строки:
$conf['redis_client_interface'] = 'PhpRedis';
$conf['redis_client_host'] = $relationships['redis'][0]['host'];
$conf['redis_client_port'] = $relationships['redis'][0]['port'];
// Имя используемой библиотеки в PHP для соединения с Redis.
$conf['redis_client_interface'] = 'PhpRedis';
$conf['cache_backends'][] = 'sites/all/modules/redis/redis.autoload.inc';
$conf['cache_default_class'] = 'Redis_Cache';
// Пример как сегмент кэша {cache_form} оставить для хранения в базе данных.
$conf['cache_class_cache_form'] = 'DrupalDatabaseCache';
// Пример как собственный сегмент кэша созданы в модуле ранее в статье: {mymodule}
// перенести для хранения в Redis.
$conf['cache_class_mymodule'] = 'Redis_Cache';
Не забывайте читать файл README.txt в папке модуля, так как в нём описаны все настройки модуля.
Где это можно применить?
Один из вариантов применения описанных знаний вынесение части данных сайта в блоки загружающиеся после основной загрузки страницы, как для ускорения загрузки сайта, так и для общего ускорения работы сайта. Достаточно лишь взять за основу модуль Ajax Blocks, интегрировать его с модулем High-performance JavaScript callback handler, а кэш данных поместить в своё хранилище в Redis, используя данную статью.
Заключение
В данную статью не попало описание большого количества модулей для кэширования, написанных сообществом и размещённых на www.drupal.org. Если вам понравилась статья, голосуйте за неё, и тогда в следующую статью я постараюсь включить бонус в виде обзора кэширования в Drupal 8. А так же рассмотрю тему использования в Drupal в качестве хранилища кэша Redis или Elasticsearch с фронтендом на Silex и AngularJS.