В последнее время на Хабре появилось довольно много постов по данной теме, но по своей сути их можно назвать: «Смотрите, я поставил Varnish / W3 Total Cache и держу миллион запросов на «Hello world» страничке». Данная же статья рассчитана больше на гиков, желающих познать, как же это все работает и написать собственный плагин для страничного кеширования.
Зачем?
Стандартный вопрос, который возникает у каждого разработчика перед созданием
Приступим
Какие инструменты предоставляет нам WordPress?
Как все знают, данная CMS позволяет легко расширять свою функциональность с помощью плагинов, но не все знают, что есть несколько типов плагинов:
- обычные плагины
находятся в wp-content/plugins
администратор может их свободно устанавливать, активировать и деактивировать; - обязательные плагины
находятся в wp-content/mu-plugins
данные плагины включаются автоматически и не могут быть деактивированы; - системные плагины
находятся в wp-content
позволяют переопределять классы ядра или внедрять в них собственный функционал;
к ним относятся:
- sunrise.php
Подгружается в самом начале инициализации ядра. Чаще всего используется для domain mapping; - db.php
Позволяет переопределять стандартный класс для работы с базой данных; - object-cache.php
Позволяет переопределись стандартный класс объектного кеширования, например если захотите использовать Memcached или Redis; - advanced-cache.php
Позволяет реализовать страничное кеширование, то что нам и нужно!
- sunrise.php
advanced-cache.php
Для того, чтобы данный плагин начал функционировать, его нужно поместить в директорию wp-content, а в wp-config.php добавить строку:
define('WP_CACHE', true);
Если заглянуть в код WordPress, то можно увидеть, что данный скрипт подгружается на раннем этапе загрузки платформы.
// wp-settings.php:63
// For an advanced caching plugin to use. Uses a static drop-in because you would only want one.
if ( WP_CACHE )
WP_DEBUG ? include( WP_CONTENT_DIR . '/advanced-cache.php' ) : @include( WP_CONTENT_DIR . '/advanced-cache.php' );
Также, после загрузки ядра, CMS попытается вызвать функцию wp_cache_postload(), но о ней позже.
// wp-settings.php:226
if ( WP_CACHE && function_exists( 'wp_cache_postload' ) )
wp_cache_postload();
Хранилище
Для хранения кеша лучше всего использовать быстрые хранилища, так как от их скорости напрямую зависит скорость отдачи контента из кеша. Я бы не советовал использовать MySql или файловую систему, гораздо лучше с этим справятся Memcached, Redis или другие хранилища, использующие оперативную память.
Мне лично нравится Redis, так как им довольно просто пользоваться, имеет хорошие показатели скорости чтения\записи и как приятный бонус — сохраняет копию данных на жесткий диск, что позволят не терять информацию при перезагрузке сервера.
$redis = new Redis();
// подключение к серверу
$redis->connect( 'localhost' );
// сохранить данные $value под ключем $key на время $timeout
$redis->set( $key, $value, $timeout );
// получить данные по ключу $key
$redis->get( $key );
// удалить данные по ключу $key
$redis->del( $key );
Разумеется это не полный перечень методов, весь список API можно изучить на официальном сайте, но для большинства задач этого достаточно.
Если на сайте используется прокаченный объектный кеш (object-cache.php), то имеет смысл использовать его API:
wp_cache_set( $key, $value, $group, $timeout );
wp_cache_get( $key, $group );
wp_cache_delete( $key, $group );
Простейшое страничное кеширование
Код нарочно упрощен, многие проверки убраны, дабы не путать читателя лишними конструкциями и сфокусироватся на логике самого кеширования. В файл advanced-cache.php прописываем:
// если как хранилище используется объектный кеш, то его нужно инициализировать вручную,
// поскольку на данном этапе загрузки он еще не загружен
wp_start_object_cache();
// формируем ключ
// чаще всего это URL страницы
$key = 'host:' . md5( $_SERVER['HTTP_HOST'] ) . ':uri:' . md5( $_SERVER['REQUEST_URI'] );
// берем данные из кеша по ключу
if( $data = wp_cache_get( $key, 'advanced-cache' ) ) {
// если данные существуют, отображаем их и завершаем выполнение
$html = $data['html'];
die($html);
}
// если данных нет, продолжаем выполнение
// не сохраняем в кеш запросы админ панели
if( ! is_admin() ) {
// перехватываем буфер вывода
ob_start( function( $html ) use( $key ) {
$data = [
'html' => $html,
'created' => current_time('mysql'),
'execute_time' => timer_stop(),
];
// после генерации страницы сохраняем данные в кеш на 10 минут
wp_cache_set($key, $data, 'advanced-cache', MINUTE_IN_SECONDS * 10);
return $html;
});
}
Все, вы получили простейший рабочий страничный кеш, теперь рассмотрим каждый участок детальнее.
Создание ключа
$key = 'host:' . md5( $_SERVER['HTTP_HOST'] ) . ':uri:' . md5( $_SERVER['REQUEST_URI'] );
В данном случае ключем является URL страницы. Использование глобальной переменной $_SERVER и хеширования нельзя назвать лучшей практикой, но для простого примера подойдет. Советую добавлять разделяющие участки строки как «host:» и «uri:», так как их удобно использовать в регулярных выражениях. Например получить все ключи по определенному хосту: $keys = $redis->keys( 'host:' . md5( 'site.com' ) . ':*' );
Выдача из кеша
// берем данные из кеша по ключу
if( $data = wp_cache_get( $key, 'advanced-cache' ) ) {
// если данные существуют, отображаем их и завершаем выполнение
$html = $data['html'];
die($html);
}
Тут все просто, если кеш уже создан, то выдаем его пользователю и завершаем выполнение.Сохранение в кеш
PHP функция ob_start перехватывает весь последующий вывод в буфер и позволяет обработать его в конце работы скрипта. Простыми словами мы получаем весь контент сайта в переменной $html.
ob_start( function( $html ) {
// $html - HTML код готовой страницы
return $html;
}
Далее сохраняем данные в кеш:
$data = [
'html' => $html,
'created' => current_time('mysql'),
'execute_time' => timer_stop(),
];
wp_cache_set($key, $data, 'advanced-cache', MINUTE_IN_SECONDS * 10);
Есть смысл сохранять не только HTML, но и прочую полезную информацию: время создания кеша и тд. Очень рекомендую сохранять HTTP заголовки, хотя бы Content-Type и посылать их при выдаче из кеша.
Совершенствуем
В примере выше мы использовали функцию is_admin() для исключения кеширования админ панели, но данный способ не очень практичен по двум причинам:
- запросы на admin-ajax.php не попадают в кеш;
- если администратор первым посетит страницу, то в кеш попадет его «admin bar» и прочие вредные для пользователей вещи;
Наилучшим решением для простого сайта будет вообще не использовать кеш для залогиненых пользователей (администраторов). Так как advanced-cache.php выполняется до полной загрузки ядра, мы не можем пользоваться функцией is_user_logged_in() , но можем определить наличие аутентификации по cookie (как известно WordPress не использует сессии).
// проверяем наличие cookie wordpress_logged_in_*
$is_logged = count( preg_grep( '/wordpress_logged_in_/', array_keys( $_COOKIE ) ) ) > 0;
// сохраняем кеш только не залогиненых пользователей
if( ! $is_logged ) {
ob_start( function( $html ) use( $key ) {
// ....
return $html;
});
}
Усложняем задачу
Допустим, наш сайт отдает разный контент для пользователей из разных регионов или стран. В данном случае ключем кеша должен быть не только URL страницы, но и регион:
$region = get_regeon_by_client_ip( $_SERVER['REMOTE_ADDR'] );
$key = 'host:' . md5( $_SERVER['HTTP_HOST'] ) . ':uri:' . md5( $_SERVER['REQUEST_URI'] ) . ':region:' . md5( $region );
По данному принципу мы можем формировать разный кеш разным группам пользователей по любым параметрам.
wp_cache_postload()
Данная функция вызывается после загрузки ядра и ее тоже удобно использовать в некоторых случаях.
По опыту скажу, что такой вариант работает гораздо стабильней:
function wp_cache_postload() {
add_action( 'wp', function () {
ob_start( function( $html ) {
// ...
return $html;
});
}, 0);
}
На момент вызова wp_cache_postload(), функция add_action уже существует и ей можно пользоваться.
Бывают ситуации, когда для формирования ключа кеша нужны данные, которые невозможно получить из cookie, IP и прочих доступных на этапе инициализации ресурсов. Например нужно генерировать индивидуальный кеш для каждого пользователя (иногда это имеет смысл).
function wp_cache_postload() {
$key = 'host:' . md5( $_SERVER['HTTP_HOST'] ) . ':uri:' . md5( $_SERVER['REQUEST_URI'] )
. ':user:' . get_current_user_id();
if( $data = wp_cache_get( $key, 'advanced-cache' ) ) {
$html = $data['html'];
die($html);
}
add_action( 'wp', function () {
ob_start( function( $html ) {
// ...
return $html;
});
}, 0);
}
Как видно в примере, вся логика помещена в тело wp_cache_postload и тут уже доступны все функции платформы, включая get_current_user_id(). Данный вариант немного медленней предыдущего, но мы получаем безграничные возможности для тонкой настройки страничного кеша.
О чем не стоит забывать
- Данные примеры очень упрощены, если будете их использовать в своих проектах — не поленитесь добавить условия для кеширования:
- только GET запросы
- только, если на странице нет ошибок
- только, если нет set-cookie
- только, если статус 200 или 301
- Эффективность кеша напрямую зависит от его времени жизни. Увеличивая $timeout, потрудитесь продумать инвалидацию кеша при изменении данных.
- WP Cron запускается позже advanced-cache.php, может просто не срабатывать при высоком кешхите.
Заключение
Нет ничего сложного в написании собственного страничного кеширования. Разумеется, в этом нет смысла для типичного сайта, но если вы породили монстра — данный материал должен оказаться полезным.