Страничное кеширование в 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, может просто не срабатывать при высоком кешхите.


    Заключение


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

    Подробнее
    Реклама
    Комментарии 14
    • 0
      Из статьи непонятно, а чем тот же Varnish плох? При грамотной настройке производительность его будет выше, зачем городить своё?
      • +1
        Разные подходы к реализации. Varnish определенно хорош, но бывают задачи, когда его божественного VCL недостаточно для формирования ключа (например необходим запрос к ресурсам сервера).
        А вообще данный вопрос может перерасти в холивар:)
        • +1
          ну тогда стоит написать плагин для формирования этого самого ключа, а кешом пусть занимается Varnish. Такой подход применяют для очень крупных сайтов (не только на Вордпрессе), приходилось заниматься подобным для Magento-магазинов
          • 0
            Видимо вы плохо знакомы с функцией ESI, которая позволяет обновлять выборочно блоки на странице в зависимости от ситуации. Например если пользователь залогинен, или если у него в корзине появился товар, то varnish подгрузит и закеширует html блок для конкретного юзера (или группы пользователей). Ведь не зря в Magento используют его для кеширования всего, и никаких проблем с динамическими данными нет вообще.

            ps: тут довольно наглядно про фичу ruhighload.com/index.php/2010/01/22/%D0%BA%D0%B5%D1%88%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B8%D1%86-%D1%83%D1%81%D0%BA%D0%BE%D1%80%D1%8F%D0%B5%D0%BC-%D1%81%D0%B0%D0%B9%D1%82-%D0%B2-100/
            • +1
              Magento изначально поддерживает ESI, в WordPress такой возможностей без граблей нет. И по сугубо субъективному мнению между esi и ajax подгрузкой блоков, я бы выбрал второе.
              Опять повторюсь, какой смысл обсуждать Varnish и Magento в посте про написание плагина под WordPress?
        • 0
          Как обстоят дела с обновлением страниц после добавления новых комментариев? Или у Вас они просто отключены?
          • 0
            Не бывает универсальных решений, все зависит от того какие комментарии вы используете, если это дискусс или фейсбук, то нет необходимости инвалидировать кеш. Если вы используете не сторонние комментарии, то есть много решений от ajax до инвалидации кеша всей страници.
          • 0
            В файл advanced-cache.php прописываем:
            Я правильно вас понимаю, вы модифицируете код ядра?
            • +1
              нет, advanced-cache.php — это плагин, его нужно поместить в папку wp-content и WordPress сам его подтянет
          • 0
            Из названия статьи думал, что кеширование будет без php отдавать кеш. Рассмастривали ли вы такой вариант? На какие бы хуки бы вы зацепились, чтобы стартовать вывод в буфер в начале и считать буфер в конце?
            • 0
              Можно отдавать без php, например использовать Varnish. В данной статье описаны средства самого WordPress для реализации кеширования.
              Хуки для перехваты буфера указаны статье.
              • 0
                Я тоже так думал)
                У меня nginx и сам всё неплохо кэширует!

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.