HTTP Кэширование

Природа насыщенных (богатых) веб-приложений подразумевает, что они динамические. Вне зависимости от того, насколько эффективно ваше приложение, каждый запрос будет содержать работы больше чем отдача простого статического файла.

И для большинства веб-приложений это вполне нормально. Symfony2 очень быстр и, если вы не делаете чего-то действительно тяжеловесного, каждый запрос будет обрабатываться быстро и не создавая стрессовых ситуаций на сервере.

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

Кэширование на плечах гигантов

Наиболее эффективным способом увеличить быстродействие приложения является кэширование страницы целиком и затем, в обход приложения, отдавать кэшированные данные для каждого запроса. Конечно же, это не всегда возможно применить, особенно для очень динамично меняющихся сайтов... или всё же возможно? В этой главе вы увидите, как работает система кэширования Symfony2 и почему мы считаем это наилучшим решением из возможных.

Система кэширования Symfony2 отличается от других, так как она полагается на простоту и мощь HTTP кэширования, как это определено в спецификации HTTP (см. Спецификация протокола HTTP). Вместо того чтобы изобретать кэширование заново, Symfony2 пользуется стандартом, который определяет базовые коммуникации в Web. Как только вы поймёте основополагающие модели HTTP валидации и истечения срока для кэша, вы будете готовы к управлению системой кэширования Symfony2.

С целью изучения того, как кэшировать в Symfony2, мы пройдём четыре шага:

  • Шаг 1: кэширующий шлюз, или обратный прокси-сервер (reverse proxy), это независимый слой, который располагается перед вашим приложением. Обратный прокси кэширует ответы по мере их поступления от приложения и отвечает на запросы при помощи кэшированных ответов, не подключая приложение. Symfony2 содержит свой собственный обратный прокси, но вы также можете использовать любой обратный прокси на ваш выбор.
  • Шаг 2: заголовки HTTP кэша используются для коммуникации кэширующего шлюза и любого другого кэшера, который может находиться между вашим приложением и клиентом. Symfony2 содержит типовую конфигурацию по умолчанию и мощный интерфейс для работы с заголовками кэша.
  • Шаг 3: окончание срока действия и валидация HTTP кэша - это две модели, используемые для определения является ли кэшированный контент свежим (и может повторно браться из кэша) или же просроченным (и его необходимо пересоздать при помощи приложения).
  • Шаг 4: Edge Side Includes (ESI) позволяют использовать HTTP кэш для независимого кэширования фрагментов страниц (даже вложенных фрагментов). При помощи ESI вы можете кэшировать всю страницу на 60 минут, но встроенную боковую панель лишь на 5 минут.

Так как HTTP кэширование не является достоянием лишь Symfony, существует множество статей по данной теме. Если вы новичок в HTTP кэшировании, мы настоятельно рекомендуем вам прочитать статью Ryan Tomayko: Things Caches Do. Другим исчерпывающим руководством является Cache Tutorial от Mark Nottingham.

Кэширование при помощи кэширующего шлюза

При кэшировании при помощи HTTP, кэш полностью отделён от вашего приложения и располагается между вашим приложением и клиентом, выполнившем запрос.

Работа кэша заключается в приёме запроса от клиента и передаче его вашему приложению. Кэш также будет получать ответ от вашего приложения и перенаправлять его далее к клиенту. Кэш является посредником в клиент-серверных коммуникациях между клиентом и вашим приложением.

По пути, кэш будет сохранять каждый ответ, который полагает “кэшируемым” (см. Введение в HTTP кэширование). Если этот же ресурс будет запрошен ещё раз, кэш отправит сохранённый (кэшированный) ответ клиенту, игнорируя ваше приложение.

Этот тип кэширования известен под именем “кэширующего HTTP шлюза”. Существует много кэшеров такого типа, например: Varnish, Squid в режиме обратного прокси, а также обратный прокси Symfony2.

Типы кэширования

Но кэширующим шлюзом типы кэшеров не исчерпываются. Фактически, заголовки HTTP кэша, отправляемые вашим приложением, могут быть получены и использованы тремя различными типами кэшеров:

  • Кэш браузера: Каждый браузер имеет свой собственный локальный кэш, который в основном используется, когда вы нажимаете кнопку “back”, а также кэш картинок и прочих ресурсов. Кэш браузера - это личный кэш, который не используется никем более.
  • Кэширующие прокси: Прокси - это кэш общего доступа, так как за одним таким прокси может находиться много клиентов. Такие прокси как правило устанавливаются большими компаниями и Интернет-провайдером для уменьшения времени доступа к ресурсам и снижению сетевого трафика.
  • Кэширующие шлюзы: Как и прокси, они также представляют собой кэш общего доступа, но на стороне сервера. Устанавливаемые администраторами, они делают сайты более масштабируемыми, надёжными и быстрыми.

Tip

Кэширующие шлюзы иногда называют кэширующими обратными прокси, суррогатными кэшерами и даже HTTP акселераторами.

Note

Значимость личного кэша по сравнению с кэшем общего доступа становится более заметной, если мы говорим о кэшировании ответов, содержащих контент, относящийся к конкретному пользователю (например, информация о счёте).

Каждый ответ от вашего приложения будет проходить через первый тип кэша или же через оба - первый и второй. Эти кэши находятся вне вашего контроля, но следуют указаниям для HTTP кэша, которые есть в ответе.

Обратный прокси Symfony2

Symfony2 содержит обратный прокси (также называемый кэширующим шлюзом), написанный на PHP. Активируйте его и кэшируемые ответы вашего приложения начнут кэшироваться надлежащим образом. Его установка очень проста. Каждое новое приложение Symfony2 содержит уже настроенное кэширующее ядро (AppCache), которое служит оболочкой для ядра по умолчанию (AppKernel). Кэширующее ядро и есть тот самый обратный прокси.

Для того чтобы активировать кэширование, модифицируйте код фронт-контроллера таким образом, чтобы он использовал кэширующее ядро:

<?php
// web/app.php

require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';
require_once __DIR__.'/../app/AppCache.php';

use Symfony\Component\HttpFoundation\Request;

$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
// wrap the default AppKernel with the AppCache one
$kernel = new AppCache($kernel);
$kernel->handle(Request::createFromGlobals())->send();

Кэширующее ядро немедленно начнёт действовать в качестве обратного прокси - будет кэшировать ответы вашего приложения и отправлять их клиенту.

Tip

Кэширующее ядро имеет особый метод getLog(), который возвращает строковое представление того, что происходит на кэширующем уровне. В dev окружении вы можете использовать его для отладки и проверки вашей стратегии кэширования:

error_log($kernel->getLog());

Объект AppCache имеет конфигурацию по умолчанию, но вы можете конфигурировать и настраивать его опции посредством переопределения метода getOptions():

<?php
// app/AppCache.php
class AppCache extends Cache
{
    protected function getOptions()
    {
        return array(
            'debug'                  => false,
            'default_ttl'            => 0,
            'private_headers'        => array('Authorization', 'Cookie'),
            'allow_reload'           => false,
            'allow_revalidate'       => false,
            'stale_while_revalidate' => 2,
            'stale_if_error'         => 60,
        );
    }
}

Tip

Для изменения опции debug переопределять getOptions() не обязательно, так как она автоматически принимает значение параметра debug от AppKernel.

Ниже представлен список основных опций:

  • default_ttl: Время (в секундах), в течение которого кэшированный элемент считается свежим, если ответ не содержит точных данных о его “свежести”. Явно указанные заголовки Cache-Control или Expires перезаписывают это значение (по умолчанию 0);
  • private_headers: Набор заголовков запроса, которые активируют “приватный” Cache-Control для ответов, которые явно не указывают поведение “приватный” или “публичный” посредством директивы Cache-Control (по умолчанию Authorization и Cookie);
  • allow_reload: Определяет, может ли клиент форсировать обновление кэша при помощи директивы Cache-Control “no-cache” в запросе. Установите её в true для следования спецификации RFC 2616 (по умолчанию false);
  • allow_revalidate: Определяет, может ли клиент форсировать перепроверку кэша при помощи директивы Cache-Control “max-age=0” в запросе. Установите её в true для следования спецификации RFC 2616 (по умолчанию false);
  • stale_while_revalidate: Определяет число секунд по умолчанию (квантификация времени производится в секундах, так как TTL (time to live) ответа измеряется в секундах) во время которого кэш будет немедленно возвращать просроченный ответ, пока производится его фоновая перепроверка (по умолчанию 2); эта опция переопределяется расширением HTTP Cache-Control - stale-while-revalidate (см. RFC 5861);
  • stale_if_error: Определяет число секунд по умолчанию (квантификация времени производится в секундах, так как TTL (time to live) ответа измеряется в секундах), во время которого кэш может обслуживать просроченный ответ, если возникает ошибка (по умолчанию 60). Эта опция переопределяется расширением HTTP Cache-Control - stale-if-error (см. RFC 5861)

Если debug имеет значение true, Symfony2 автоматически добавляет в ответ заголовок X-Symfony-Cache, содержащий полезную информацию о числе срабатываний кэша и о числе не найденных ответов в кэше.

Note

Быстродействие обратного прокси Symfony2 не зависит от сложности приложения. Это достигается за счёт того, что ядро приложения загружается лишь в том случае, когда к нему требуется перенаправить входящий запрос.

Введение в HTTP кэширование

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

Tip

Имейте в виду, что “HTTP” это не более чем язык (простой текстовый язык), который веб клиенты (например, браузеры) и веб серверы используют для коммуникаций между собой. Когда мы говорим об HTTP кэшировании, мы говорим о части этого языка, которая позволяется клиентам и серверам обмениваться информацией, относящейся к кэшированию.

Спецификация HTTP содержит четыре заголовка, относящихся к кэшированию:

  • Cache-Control
  • Expires
  • ETag
  • Last-Modified

Наиболее важным и многосторонним является заголовок Cache-Control, который на самом деле является коллекцией разнообразной информации о кэшировании.

Note

Каждый из заголовков будет детально рассмотрен в секции Модели кэширования в HTTP: expiration и validation.

Заголовок Cache-Control

Заголовок Cache-Control уникален за счёт того, что он содержит не одно конкретное значение, а много различных данных о кэшируемости ответа. Каждая новая порция данных отделяется запятой:

Cache-Control: private, max-age=0, must-revalidate

Cache-Control: max-age=3600, must-revalidate

Symfony предоставляет методы для более удобного управления заголовком Cache-Control:

<?php
//...

$response = new Response();

// пометить ответ как public или private
$response->setPublic();
$response->setPrivate();

// установить max age для private и shared ответов
$response->setMaxAge(600);
$response->setSharedMaxAge(600);

// установить специальную директиву Cache-Control
$response->headers->addCacheControlDirective('must-revalidate', true);

Публичные (public) vs Частные (private) ответы

Кэширующие шлюзы и прокси рассматривают “общие” кэши как кэшированный контент, который используется более чем одним пользователем. Если будет случайно сохранён ответ, специфичный для отдельного пользователя, впоследствии он может быть отправлен множеству различных пользователей. Представьте, что информация о вашем счёте была кэширована и будет отправлена любому пользователю, который запросит свою собственную страницу со счётом!

Для того чтобы корректно обработать эту ситуацию, каждый ответ может быть объявлен публичным или же частным:

  • public: Публичный ответ может кэшироваться как частным, так и публичным кэшами;
  • private: Частный ответ подразумевает что он целиком или же его часть предназначена для одного единственного пользователя и не должен кэшироваться публичными кэшерами.

Symfony действует консервативно и помечает каждый ответ как частный. Для того чтобы получить преимущества от использования публичных кэшеров (в том числе и обратного прокси Symfony2), ответ должен быть помечен как публичный (public).

Безопасные методы

HTTP кэширование работает лишь для “безопасных” HTTP методов (таких как GET и HEAD). Под безопасностью этих методов понимается, что вы никогда не измените состояние приложения при обработке таких запросов (при этом вы, конечно, можете логгировать информацию, кэшировать данные и т.д.). Это ограничение имеет два следствия:

  • Вы никогда не должны изменять состояние вашего приложения, отвечая на GET или HEAD запрос. Даже если вы не используете кэширующий шлюз, наличие прокси-кэша означает, что любой GET или HEAD запрос может как попасть в ваше приложение, так и не попасть (прокси вернёт кэшированные данные, не затрагивая приложение).
  • Ни в коем случае не кэшируйте PUT, POST и DELETE методы. Эти методы предназначены для изменения состояния приложения (например, удаления записи из блога). Если их кэшировать, то часть запросов на изменение состояния приложения не будут достигать его.

Правила кэширования и значения по умолчанию

HTTP 1.1 по умолчанию разрешает кэширование, если явно не указан заголовок Cache-Control. На практике, большинство кэшеров ничего не делают, если запросы имеют куки, авторизационный заголовок, используют небезопасные методы (т.е. PUT, POST, DELETE), или когда ответ имеет перенаправляющий статус-код (например, 301 или 302).

Symfony2 автоматически устанавливает разумно-консервативный заголовок Cache-Control, если разработчик не задал правила кэширования явно. Эти умолчания следуют следующим правилам:

  • Если не определены заголовки кэширования (Cache-Control, Expires, ETag или Last-Modified), Cache-Control устанавливается в значение no-cache, то есть ответ кэшироваться не будет;
  • Если Cache-Control пустой (но присутствует любой другой кэширующий заголовок), его значение устанавливается в private, must-revalidate;
  • Если присутствует хотя бы одна директива Cache-Control и явно не указаны директивы public или private, Symfony2 добавляет директиву private автоматически (за исключением случая, когда установлен s-maxage).

Модели кэширования в HTTP: expiration и validation

Спецификация HTTP определяет две модели кэширования:

  • Первая - модель “окончания срока действия” (expiration), вы просто указываете как долго ответ будет “свежим”, включая заголовки Cache-Control и/или Expires. Кэшеры, которые поддерживают эту модель, не будут выполнять некоторый запрос до тех пор, пока его кэшированная версия не достигнет окончания срока действия (expiration) и не станет “просроченной”.
  • Когда страницы очень быстро меняются, часто бывает необходимо использовать модель валидации (validation). При использовании этой модели кэшер сохраняет ответ, но при каждом последующем запросе он запрашивает сервер - является ли кэшированный ответ валидным или нет. Приложение использует некоторый уникальный идентификатор ответа (заголовок Etag) и/или временную метку (заголовок Last-Modified) для проверки изменилась ли страница с момента её кэширования.

Целью обоих этих моделей является следующая: не генерировать один и тот же ответ дважды, если в кэше уже есть “свежий” ответ, сохранённый там ранее.

HTTP Expiration - окончание строка действия

Модель окончания срока действия более эффективная и простая из двух поддерживаемых моделей кэширования и должна использоваться везде, где это возможно. Когда ответ кэшируется со сроком окончания действия, кэш будет хранить ответ и возвращать его на клиент напрямую, не затрагивая приложение, пока срок действия не окончится.

Модель окончания срока действия может быть задействована с использованием двух похожих HTTP заголовков: Expires или Cache-Control.

Окончание срока действия при помощи заголовка Expires

Следуя спецификации HTTP, “заголовок Expires содержит дату/время, после которого этот ответ будет считаться просроченным”. Заголовок Expires может быть установлен при помощи метода setExpires() класса Response. Он принимает экземпляр DateTime в качестве аргумента:

<?php
//...
$date = new DateTime();
$date->modify('+600 seconds');

$response->setExpires($date);

Результирующий заголовок будет выглядеть следующим образом:

Expires: Thu, 01 Mar 2011 16:00:00 GMT

Note

Метод setExpires() автоматически конвертирует дату в зону GMT, как того требует спецификация.

Заголовок Expires имеет 2 ограничения. Первое, часы на веб-сервере и и часы кэшера (например, браузера) должны быть синхронизированными. Второе, следует из спецификации и гласит, что “HTTP/1.1 серверы никогда не должны устанавливать дату Expires более чем на один год вперёд”.

Окончание срока действия при помощи заголовка Cache-Control

Поскольку заголовок Expires имеет ограничения, вы должны использовать заголовок Cache-Control. Вспоминайте, что заголовок Cache-Control используется для указания различных директив, относящихся к кэшированию. Для окончания срока действия имеются две директивы, max-age и s-maxage. Первая используется всеми кэшерами, в то время как вторая используется лишь “общими” (shared) кэшами:

<?php
//...
// Устанавливаем число секунд, после которого ответ более не будет считаться свежим
$response->setMaxAge(600);

// Тоже что и выше, но только для общих кэшей
$response->setSharedMaxAge(600);

Заголовок Cache-Control будет иметь следующий формат (также там могут быть и другие директивы):

Cache-Control: max-age=600, s-maxage=600

Валидация

Когда некоторый ресурс должен быть обновлён, в связи с тем, что произошли изменения в данных, лежащих в его основе, модель окончания срока действия становится несостоятельной. При подходе, используемом в модели окончания срока действия, кэш не обратится к приложению для обновления ответа пока данные не становятся просроченными (т.е. когда истечёт срок действия кэшированного ответа).

Модель валидации решает эту проблему. С её помощью кэш также продолжает сохранять ответы. Различие заключается в том, что для каждого запроса, кэш запрашивает приложение изменился или нет запрашиваемый ресурс. Если кэш ещё валиден, ваше приложение должно вернуть статус код 304 и не возвращать контент. Это означает, что кэш ещё валиден и можно возвращать кэшированный ответ.

С этой моделью вы, прежде всего, сохраняете пропускную способность вашего интернет-канала, так как страница целиком не отсылается дважды тому же клиенту (вместо этого будет отправлен ответ со статус кодом 304). Но, если вы аккуратно проектируете ваше приложение, мы можете получить необходимый минимум данных, необходимых для того чтобы отправить статус код 304 и сохранить также ресурсы CPU и/или оперативной памяти (см. ниже реализацию этого варианта).

Tip

Статус 304 означает “Not Modified”. Это важный статус, так как вместе с ним не отправляется запрошенный контент. Вместо этого, ответ состоит из небольшого набора указаний, которые сообщают кэшу, что можно использовать сохранённую ранее версию.

Как и в случае с моделью окончания срока действия, есть два HTTP заголовка, которые могут быть использованы для реализации модели валидации: ETag и Last-Modified.

Валидация при помощи заголовка ETag

Заголовок ETag - это строковый заголовок (называемый “entity-tag”), который единственным образом идентифицирует представление целевого ресурса. Он генерируется и устанавливается всецело внутри вашего приложения, так что вы можете понять, к примеру, соответствует ли кэшированный ресурс /about тому, который ваше приложение собирается вернуть. Заголовок ETag похож на отпечатки пальцев и используется для быстрого определения эквивалентны ли две версии ресурса. Как и отпечатки пальцев, каждый ETag должен быть уникальным для любого представления одного и того же ресурса.

Давайте взглянем на простую реализацию, которая генерирует ETag в виде md5 хэша от контента:

<?php
//...
public function indexAction()
{
    $response = $this->render('MyBundle:Main:index.html.twig');
    $response->setETag(md5($response->getContent()));
    $response->isNotModified($this->getRequest());

    return $response;
}

Метод Response::isNotModified() сравнивает ETag, отправленный в запросе (Request) с этим же тагом в ответе (Response). Если они совпадают, этот метод автоматически устанавливает для Response статус код 304.

Этот алгоритм достаточно простой и вполне типичный, но вам нужно создать экземпляр Response целиком, перед тем как вы получите возможность сравнить ETag’и, а это весьма расточительно. Другими словами, этот подход сохраняет пропускную способность, но не ресурсы CPU.

В секции Оптимизация вашего кода при помощи метода валидации мы покажем как можно использовать валидацию более интеллигентно и определять валидность кэша без излишних затрат ресурсов сервера.

Tip

Symfony2 также поддерживает “слабые” ETag’и - для этого надо передать true в качестве второго аргумента в метод :method:`Symfony\\Component\\HttpFoundation\\Response::setETag`.

Валидация при помощи заголовка Last-Modified

Заголовок Last-Modified - это второй возможный способ валидации. Следуя спецификации HTTP, “Заголовок Last-Modified содержит дату и время, когда представление ресурса было изменено в последний раз, по версии исходного сервера”. Другими словами, приложение принимает решение о том, должен ли быть обновлён кэшированный контент, основываясь на том, изменялся ли он со времени кэширования.

Например, вы можете использовать дату последнего обновления для всех объектов, необходимых для создания представления ресурса в качестве значения заголовка Last-Modified:

<?php
//...
public function showAction($articleSlug)
{
    // ...

    $articleDate = new \DateTime($article->getUpdatedAt());
    $authorDate = new \DateTime($author->getUpdatedAt());

    $date = $authorDate > $articleDate ? $authorDate : $articleDate;

    $response->setLastModified($date);
    $response->isNotModified($this->getRequest());

    return $response;
}

Метод Response::isNotModified() сравнивает заголовок If-Modified-Since, отправленный в запросе с заголовком Last-Modified, установленном в ответе. Если они идентичны, Response будет установлен статус код 304.

Note

Заголовок запроса If-Modified-Since соответствует заголовку Last-Modified последнего ответа, отправленного клиенту для некоторого ресурса. Таким образом, клиент и сервер общаются друг с другом и определяют был ли ресурс обновлён с момента его кэширования.

Оптимизация вашего кода при помощи метода валидации

Основная цель любой стратегии кэширования - понизить нагрузку на приложение. Иными словами, чем меньше делает ваше приложение для того, чтобы вернуть ответ 304, тем лучше. Метод Response::isNotModified() именно этим и занимается при использовании простого и эффективного шаблона:

<?php
//...
public function showAction($articleSlug)
{
    // Получаем минимум информации для вычисления
    // значений для заголовков ETag или Last-Modified
    // (основываясь на запросе Request, данных, получаемых из базы данных
    // или же из хранилища ключ-значение)
    $article = // ...

    // Создаём ответ Response с заголовком ETag и/или Last-Modified
    $response = new Response();
    $response->setETag($article->computeETag());
    $response->setLastModified($article->getPublishedAt());

    // Проверяем, что ответ не модифицировался для этого запроса
    if ($response->isNotModified($this->getRequest())) {
        // возвращаем ответ 304
        return $response;
    } else {
        // делаем дополнительные действия, например, получаем дополнительные данные
        $comments = // ...

        // или отображаем шаблон при помощи $response, который был создан ранее
        return $this->render(
            'MyBundle:MyController:article.html.twig',
            array('article' => $article, 'comments' => $comments),
            $response
        );
    }
}

Если ответ Response не модифицировался, метод isNotModified() автоматически устанавливает статус код ответа в 304, удаляет контент и удаляет некоторые заголовки, которые не должны присутствовать в ответе 304 (см. метод :method:`Symfony\\Component\\HttpFoundation\\Response::setNotModified`).

Вариации ответа

Ранее вы узнали, что каждый URI имеет единственное представление целевого ресурса. По умолчанию, HTTP кэширование выполняется с использованием URI ресурса в качестве ключа к значению кэша. Если два человека запросят один и тот же URI для кэшируемого ресурса, второй клиент получит уже кэшированную версию.

Иногда этого не достаточно и требуется кэшировать различные версии одного и того же URI, основываясь на значении одного или нескольких заголовков. Например, если вы сжимаете страницы для клинентов, которые поддерживают сжатие, любой URI будет иметь два представления: одно для клиентов, поддерживающих сжатие, и одно для тех кто не поддерживает. Это определяется на основе значения заголовка запроса Accept-Encoding.

В этом случае, вам необходимо хранить обе версии ответа для некоторого ресурса - сжатую и не сжатую и возвращать ее, основываясь на значении заголовка запроса Accept-Encoding. Этого можно достичь при помощи заголовка ответа Vary, который является списком (разделители - запятые) различных заголовков, чьи значения переключают различные представления запрошенного ресурса:

Vary: Accept-Encoding, User-Agent

Tip

Заголовок Vary из примера выше позволяет кэшировать различные версии для каждого ресурса, основываясь на URI и значении заголовков запроса Accept-Encoding и User-Agent.

Объект Response предоставляет простой интерфейс для управления заголовком Vary:

<?php
//...
// устанавливаем один заголовок vary
$response->setVary('Accept-Encoding');

// устанавливаем несколько заголовков vary
$response->setVary(array('Accept-Encoding', 'User-Agent'));

Метод setVary() принимает в качестве параметра имя заголовка или же массив наименований заголовков, на основании значений которых необходимо варьировать ответ.

Окончание срока действия и валидация

Вы можете использовать окончание срока действия совместно с валидацией в одном и том же экземпляре Response. Если окончание срока действия работает раньше валидации, вы сможете получить лучшие преимущества от обеих моделей. Другими словами, используя совместно модели окончание срока действия и валидации вы можете проинструктировать кэш хранить контент пока с некоторым интервалом осуществляется (окончание срока действия) проверка, что контент всё ещё валиден.

Другие методы класса Response

Класс Response содержит также другие методы для работы с кэшем. Пример ниже иллюстрирует самые часто употребляемые из них:

<?php
// пометить ответ как "просроченный"
$response->expire();

// Форсировать возврат ответа 304 без контента
$response->setNotModified();

В дополнение к этому, все основные HTTP относящиеся к кэшу, могут быть установлены при помощи одного метода setCache():

<?php
// Установить заголовки для кэширования одним вызовом
$response->setCache(array(
    'etag'          => $etag,
    'last_modified' => $date,
    'max_age'       => 10,
    's_maxage'      => 10,
    'public'        => true,
    // 'private'    => true,
));

Использование ESI (Edge Side Includes)

Кэширующие шлюзы - это отличный способ сделать ваш сайт более производительным. Но они также имеют и одно ограничение: они могут кэшировать лишь страницы целиком. Если вы по каким-то причинам не можете кэшировать страницы целиком или в случае когда страница имеет несколько динамических частей, вы вышли из зоны удачи. К счастью, Symfony2 предоставляет решение для этих случаев, основанное на технологии ESI, или Edge Side Includes. Компания Akamaï создала эту спецификацию почти 10 лет назад, и она позволяет иметь для отдельных частей страницы различные стратегии кэширования.

Спецификация ESI описывает таги, которые вы можете добавить в ваши страницы для общения с кэширующим шлюзом. В Symfony2 реализован лишь один таг - include, так как это наиболее полезный таг вне контекста Akamaï:

<html>
    <body>
        Some content

        <!-- Подключаем контент другой страницы -->
        <esi:include src="http://..." />

        More content
    </body>
</html>

Note

Обратите внимание, в примере выше, что для ESI тага указан полный URL. ESI таг представляет собой фрагмент страницы, который можно получить по этому URL.

При обработке запроса, кэширующий шлюз получает страницу целиком из своего кэша или же запрашивает его у приложения. Если ответ содержит один или более ESI тагов, они обрабатываются тем же образом. Другими словами, кэширущий шлюз получает включённые фрагменты страниц из своего кэша, либо запрашивает эти фрагменты у приложения. Когда все ESI таги обработаны, шлюз включает все фрагменты в основную страницу и отправляет итоговый контент клиенту.

Всё это происходит незаметно на уровне кэширующего шлюза (т.е. вне вашего приложения). Как вы увидите далее, если вы захотите использовать преимущества, которые предоставляют ESI таги, Symfony2 позволит вам подключать их не прилагая особых усилий.

Использование ESI в Symfony2

Во-первых, перед использованием ESI, убедитесь, что вы активировали их в настройках приложения:

Теперь, предположим, что у вас есть страница, которая по большей части статическая, за исключением новостей, расположенных под контентом. При помощи ESI вы можете кэшировать новости независимо от остальной страницы.

<?php
// ...
public function indexAction()
{
    $response = $this->render('MyBundle:MyController:index.html.twig');
    $response->setSharedMaxAge(600);

    return $response;
}

В этом примере вы устанавливаете для всей страницы время жизни кэша в 10 минут. Затем, подключите новости в шаблон при помощи встраивания действия. Это можно сделать при помощи хелпера render (см. Внедрение контроллеров).

Так как встроенный контент поступает из другой страницы (или контроллера в данном случае), Symfony2 использует стандартный хэлпер render для конфигурирования ESI тага:

Указав параметр standalone равный true, вы говорите Symfony2, что действие должно отображаться как ESI таг. Вы возможно удивлены - зачем использовать хелпер, вместо того, чтобы написать ESI таг самостоятельно. Это необходимо для того, чтобы ваше приложение работало даже если не установлен никакой кэширующий шлюз. Давайте разберём, как работает эта конструкция.

Когда опция standalone имеет значение false (по умолчанию), Symfony2 объединяет контент подключённой страницы с контентом основной перед отправкой ответа на клиент. Но когда standalone имеет значение true, и если Symfony2 определяет, что кэширующий шлюз, через который работает приложение, поддерживает ESI, генерится ESI таг. Но если шлюз не обнаружен или же он не поддерживает ESI, Symfony2 будет объединять контент подключённой страницы с контентом основной также, как это было бы выполнено при значении standalone равном false.

Note

Symfony2 определяет, поддерживает ли шлюз ESI, при помощи другой спецификации Akamaï, которая поддерживается обратным прокси Symfony2 “из коробки”.

Теперь для встроенного действия вы можете указать собственные правила кэширования, независимо от главной страницы:

<?php
public function newsAction()
{
  // ...

  $response->setSharedMaxAge(60);
}

При помощи ESI кэш страницы будет валидным в течение 600 секунд, но компонент новостей будет кэшироваться только на 60 секунд.

Требованием, при использовании ESI, является следующее: встроенное действие должно быть доступно через некоторый URL, чтобы кэширующий шлюз мог получить его контент независимо от остальной страницы. Конечно, действие не может быть доступным без маршрута, который указывает на него. Symfony2 заботится и об этом при помощи базового маршрута и контроллера. Чтобы ESI таг include работал, вы должны определить маршрут _internal:

Tip

Так как маршрут позволяет получить доступ к вашему действию при помощи URL, вы возможно захотите защитить его при помощи брандмауэра Symfony2 (разрешив доступ по IP вашего обратного прокси). См. секцию Защита по IP главы Безопасность.

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

Note

При использовании ESI, помните, что вам всегда необходимо использовать директиву s-maxage вместо max-age. Это необходимо, так как браузер получает агрегированный ресурс, следовательно, он не заботится о вложенных компонентах и будет подчиняться директиве max-age и кэшировать страницу целиком, чего вы точно не захотите.

Хелпер render поддерживает две важных опции:

  • alt: используется в качестве атрибута alt тэга ESI, который позволяет указать альтернативный URL, который будет использован, если src не будет найден;
  • ignore_errors: при значении true, атрибут onerror будет добавлен к ESI тагу. Его значение будет равно continue, что будет означать удаление ESI тага в случае ошибки на уровне кэширующего шлюза.

Очистка (аннулирование) кэша

“В науке о компьютерах есть лишь две сложные вещи: аннулирование кэша и вопросы именования.” –Phil Karlton

Вы не должны заботиться об аннулировании кэша, так как аннулирование уже заложено в модели кэширования HTTP. Если вы используете модель валидации, вам не нужно ничего аннулировать по определению; если вы используете окончание срока действия и требуется аннулировать ресурс, это означает, что ранее вы для этого ресурса установили срок окончания далеко в будущее.

Note

Так как аннулирование кэша - это тема, специфичная для каждого конкретного обратного прокси, если вы специально не побеспокоились об этом - то с лёгкостью сможете переключаться между различными прокси ничего не меняя в коде вашего приложения.

На самом же деле, любой обратный прокси предоставляет способ для очистки кэша, но вы должны стараться избегать этого, насколько возможно. Наиболее типичный путь для очистки кэша для некоторого URL - запросить чего при помощи специального HTTP метода PURGE.

Ниже вы увидите как настроить обратный прокси Symfony2 для поддержки HTTP метода PURGE:

<?php
// app/AppCache.php
class AppCache extends Cache
{
    protected function invalidate(Request $request)
    {
        if ('PURGE' !== $request->getMethod()) {
            return parent::invalidate($request);
        }

        $response = new Response();
        if (!$this->getStore()->purge($request->getUri())) {
            $response->setStatusCode(404, 'Not purged');
        } else {
            $response->setStatusCode(200, 'Purged');
        }

        return $response;
    }
}

Caution

Вы должны защитить метод PURGE каким-либо образом, чтобы не допускать возможности очистки кэша случайными людьми.

Summary

Symfony2 создан таким образом, чтобы следовать проверенным правилам “движения” по дорогам HTTP. Кэширование - не исключение. Настройка системы кэширования Symfony2 подразумевает близкое знакомство с моделью кэширования HTTP и её эффективное использование. Это означает, что вместо того, чтобы полагаться только на документацию Symfony2 и примеры кода, вы получаете доступ к целому миру знаний, относящихся к кэшированию в HTTP и кэширующим шлюзам, таким как Varnish.

Дополнительная информация в книге рецептов:

  • /cookbook/cache/varnish