Страничное кеширование в WordPress

в 17:55, , рубрики: caching, php, redis, wordpress

image

В последнее время на Хабре появилось довольно много постов по данной теме, но по своей сути их можно назвать: «Смотрите, я поставил Varnish / W3 Total Cache и держу миллион запросов на «Hello world» страничке». Данная же статья рассчитана больше на гиков, желающих познать, как же это все работает и написать собственный плагин для страничного кеширования.

Зачем?

Стандартный вопрос, который вознивает у каждого разработчика перед созданием велосипеда уже существующего функционала. Действительно, готовых плагинов уйма и многие из них довольно качественные, но нужно понимать что в первую очередь они рассчитаны на статические блоги. Что же делать, если у вас не стандартный WordPress сайт?

Приступим

Какие инструменты предоставляет нам 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
      Позволяет реализовать страничное кеширование, то что нам и нужно!

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(). Данный вариант немного медленней предыдущего, но мы получаем безграничные возможности для тонкой настройки страничного кеша.

О чем не стоит забывать

  1. Данные примеры очень упрощены, если будете их использовать в своих проектах — не поленитесь добавить условия для кеширования:
    • только GET запросы
    • только, если на странице нет ошибок
    • только, если нет set-cookie
    • только, если статус 200 или 301
  2. Эффективность кеша напрямую зависит от его времени жизни. Увеличивая $timeout, потрудитесь продумать инвалидацию кеша при изменении данных.
  3. WP Cron запускается позже advanced-cache.php, может просто не срабатывать при высоком кешхите.

Заключение

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

Автор: Pingbull

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js