Drupal постоянно ругают за его медлительность, за огромное количество запросов к базе данных и неповоротливость. Наиболее часто это решается с помощью Memcached или Varnish. В этой статье я хотел бы добавить еще несколько советов, использование которых позволит не сделать Drupal еще медленее. Тех, кому это интересно, прошу пожаловать под кат.
Вместе с функциональностью сайта растет и количество потребляемой памяти и количество SQL запросов, необходимых для выполнения полной загрузки Drupal. Если вам необходимо выполнить всего один SQL запрос с помощью AJAX, Drupal может тратить большое количество времени для полной загрузки и исполнения кода, который возможно никогда не будет использован в данном запросе. Модуль JS позволяет решить эту проблему, предоставляя альтернативный способ загрузки Drupal только до уровня, необходимого для выполнения конкретной задачи. В том числе позволяя подключать необходимые файлы и модули для обработки запроса.
Drupal загружает себя при каждом запросе, проходя ряд фаз начальной загрузки. Все фазы загрузки определены в файле bootstarp.inc:
Например, если вам необходимо использовать только функцию variable_get() в AJAX колбеке, будет достаточно уровня DRUPAL_BOOTSTRAP_VARIABLES, а если вам требуется доступ к текущему объекту пользователя $user вам необходимо использовать DRUPAL_BOOTSTRAP_SESSION и т.д.
Для работы с модулем JS достаточно реализовать хук hook_js(), в котором описать какие модули необходимо подключить, какую фазу бутстрапа использовать:
Важно понимать, что достаточно сложно выполнять контроль прав доступа пользователя на начальных этапах загрузки Drupal, поэтому необходимо внимательней следить за безопасностью своего кода.
Очень часто необходимо добавить поля из профиля пользователя в ноду и наиболее простое решение для этого использовать хук template_preprocess_node():
Но при отображении большого количества нод такой подход создаст большое количество запросов к базе данных. Получить такую же функциональность без ущерба производительности можно с помощью хука hook_entity_prepare_view():
После этого $entity->account будет доступно в препроцессе:
Когда код выполняется несколько раз в течении одного запроса, очень удобно использовать статические переменные для кеширования. (подробнее о статических переменных в PHP можно прочитать тут и тут). Ядро Drupal предоставляет отличное решение для реализации статического кеширования — функцию drupal_static(). Функция drupal_static() предоставляет центральную статическую переменную для хранения данных. Первый вызов drupal_static() вернет NULL, но любые изменения в этой переменной будут сохранены для следующего вызова этой функции. Таким образом мы можем проверить была ли уже установлена переменная и получить ее моментально практически не выполняя при этом никакой работы.
Переменные в Drupal хранятся в специальной таблице в формате: имя — сериализованное значение. При каждом запросе все переменные загружаются из кеша в глобальную переменную $conf.
Во время сохранения каждой переменной происходит следующее:
При большом количестве переменных это может занять достаточно много времени. В Drupal реализована система блокировки и любые долго выполняющиеся операции, параллельно которым, скорее всего, будут поступать другие запросы, должны пытаться перед началом работы получить блокировку. Если предыдущий запрос очистил кеш переменных, следующий запрос будет его перестраивать, поэтому очень частое использование функции variable_set() может привести к массовой блокировке таблицы, из-за того, что десятки запросов ждут новой записи кеша таблицы переменных, который может стать устаревшим прежде чем его извлекут для использования.
Drupal хранит сессии пользователей в базе данных, а не в файлах, поэтому если на сайтах с большой посещаемостью эта таблица может очень быстро разрастись до огромных размеров. Если таблица сессий стала очень большой, можно увеличить частоту сборки мусора для PHP-сессий в settings.php:
Надеюсь, что не все из этого списка является совсем уж очевидными вещами и это кому-нибудь пригодится.
Используем JS модуль
Вместе с функциональностью сайта растет и количество потребляемой памяти и количество SQL запросов, необходимых для выполнения полной загрузки Drupal. Если вам необходимо выполнить всего один SQL запрос с помощью AJAX, Drupal может тратить большое количество времени для полной загрузки и исполнения кода, который возможно никогда не будет использован в данном запросе. Модуль JS позволяет решить эту проблему, предоставляя альтернативный способ загрузки Drupal только до уровня, необходимого для выполнения конкретной задачи. В том числе позволяя подключать необходимые файлы и модули для обработки запроса.
Drupal загружает себя при каждом запросе, проходя ряд фаз начальной загрузки. Все фазы загрузки определены в файле bootstarp.inc:
- DRUPAL_BOOTSTRAP_CONFIGURATION: На этой фазе заполняется внутрений массив конфигураций Drupal, устанавливается базовый URL, анализируется файл settings.php и т.д.
- DRUPAL_BOOTSTRAP_PAGE_CACHE: Попытка предоставить страницу из кеша, если включен режим кеширование страниц для анонимных пользователей.
- DRUPAL_BOOTSTRAP_DATABASE: Определяется тип базы данных и устанавливается соединение для выполнения запросов к базе данных.
- DRUPAL_BOOTSTRAP_VARIABLES: Инициализация системы переменных.
- DRUPAL_BOOTSTRAP_SESSION: Инициализация обработки сессий.
- DRUPAL_BOOTSTRAP_PAGE_HEADER: Задание заголовка страницы.
- DRUPAL_BOOTSTRAP_LANGUAGE: Определение языка страницы.
- DRUPAL_BOOTSTRAP_FULL: Загрузка модулей и инициализация темы.
Например, если вам необходимо использовать только функцию variable_get() в AJAX колбеке, будет достаточно уровня DRUPAL_BOOTSTRAP_VARIABLES, а если вам требуется доступ к текущему объекту пользователя $user вам необходимо использовать DRUPAL_BOOTSTRAP_SESSION и т.д.
Для работы с модулем JS достаточно реализовать хук hook_js(), в котором описать какие модули необходимо подключить, какую фазу бутстрапа использовать:
/**
* Implements hook_js().
*/
function js_example_js() {
return array(
'results' => array(
'callback' => 'js_example_ajax_results', // Функция обратного вызова необходимая для отображения результатов
'includes' => array('unicode', 'locale', 'language'), // Какие файлы необходимо загрузить из директории /includes
'dependencies' => array(), // Список модулей, которые необходимо загрузить
'bootstrap' => DRUPAL_BOOTSTRAP_DATABASE, // Необходимая фаза загрузки Drupal
'file' => 'js_example.ajax.inc', // Файл в котором находится callback
'access callback' => 'js_example_ajax_results_access', // Функция обратного вызова для проверки прав доступа
'access arguments' => array(), // Список аргументов для access callback
'page arguments' => array(), // Список аргументов для callback
'skip_hook_init' => TRUE, // Флаг, который позволяет пропустить выполнение hook_init(),
'i18n' => FALSE, // Флаг, который позволяет включить или выключить поддержку i18n
),
);
}
Важно понимать, что достаточно сложно выполнять контроль прав доступа пользователя на начальных этапах загрузки Drupal, поэтому необходимо внимательней следить за безопасностью своего кода.
Множественная загрузка сущностей
Очень часто необходимо добавить поля из профиля пользователя в ноду и наиболее простое решение для этого использовать хук template_preprocess_node():
template_preprocess_node(&$variables) {
$node = $variables['node'];
$variables['account'] = user_load($node->uid);
}
Но при отображении большого количества нод такой подход создаст большое количество запросов к базе данных. Получить такую же функциональность без ущерба производительности можно с помощью хука hook_entity_prepare_view():
hook_entity_prepare_view($entities, $type, $langcode) {
if ($type != 'node') {
return;
}
$uids = array();
foreach ($entities as $entity) {
$uids[] = $entity->uid;
}
$accounts = user_load_multiple($uids);
foreach ($entities as $entity) {
$entity->account = $accounts[$entity->uid];
}
}
После этого $entity->account будет доступно в препроцессе:
template_preprocess_node(&$vars) {
$account = $vars['node']->account;
}
Используйте drupal_static()
Когда код выполняется несколько раз в течении одного запроса, очень удобно использовать статические переменные для кеширования. (подробнее о статических переменных в PHP можно прочитать тут и тут). Ядро Drupal предоставляет отличное решение для реализации статического кеширования — функцию drupal_static(). Функция drupal_static() предоставляет центральную статическую переменную для хранения данных. Первый вызов drupal_static() вернет NULL, но любые изменения в этой переменной будут сохранены для следующего вызова этой функции. Таким образом мы можем проверить была ли уже установлена переменная и получить ее моментально практически не выполняя при этом никакой работы.
function my_module_function() {
$foo = &drupal_static(__FUNCTION__);
global $user;
if (!isset($foo[$user->uid])) {
$foo[$user->uid] = something_expensive();
}
return $foo[$user->uid];
}
Частое использование variable_set() влияет на производительность
Переменные в Drupal хранятся в специальной таблице в формате: имя — сериализованное значение. При каждом запросе все переменные загружаются из кеша в глобальную переменную $conf.
Во время сохранения каждой переменной происходит следующее:
- Обновляется запись в базе данных
- Очищается кеш
- Когда следующий запрос обнаруживает, что нет кеша для таблицы переменных, все переменные загружаются, и записываются в кеш.
При большом количестве переменных это может занять достаточно много времени. В Drupal реализована система блокировки и любые долго выполняющиеся операции, параллельно которым, скорее всего, будут поступать другие запросы, должны пытаться перед началом работы получить блокировку. Если предыдущий запрос очистил кеш переменных, следующий запрос будет его перестраивать, поэтому очень частое использование функции variable_set() может привести к массовой блокировке таблицы, из-за того, что десятки запросов ждут новой записи кеша таблицы переменных, который может стать устаревшим прежде чем его извлекут для использования.
Сокращение таблицы сессий
Drupal хранит сессии пользователей в базе данных, а не в файлах, поэтому если на сайтах с большой посещаемостью эта таблица может очень быстро разрастись до огромных размеров. Если таблица сессий стала очень большой, можно увеличить частоту сборки мусора для PHP-сессий в settings.php:
ini_set('session.gc_maxlifetime', 86400);
Надеюсь, что не все из этого списка является совсем уж очевидными вещами и это кому-нибудь пригодится.