HTTP кеш

17/05/2016 0 symfony, розробка

Валідація за допомогою заголовка Last-Modified

Заголовок Last-Modified - це другий можливий спосіб валідації. Згідно з специфікацією HTTP, "заголовок Last-Modified містить дату і час, коли представлення ресурса було змінено в останній раз, згідно версії вихідного сервера.” Інакше кажучи, додаток приймає рішення чи повин бути оновленим кешований контент, базуючись на тому, чи він мінявся з часу його кешування.  

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

// src/AppBundle/Controller/ArticleController.php
namespace AppBundle\Controller;

// ...
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Entity\Article;

class ArticleController extends Controller
{
    public function showAction(Article $article, Request $request)
    {
        $author = $article->getAuthor();

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

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

        $response = new Response();
        $response->setLastModified($date);
        // Set response as public. Otherwise it will be private by default.
        $response->setPublic();

        if ($response->isNotModified($request)) {
            return $response;
        }

        // ... do more work to populate the response with the full content

        return $response;
    }
}

Метод isNotModified() порівнює заголовок If-Modified-Since, відправлений в запиті з заголовком Last-Modified, встановленим у відповіді. Якщо вони іденитичні, Response отримає статус код 304.  

Кеш налаштовує заголовок If-Modified-Since на запит до Last-Modified, початкової кешованої відповіді, перш ніж відсилати запит додатку назад. Таким чином кеш і сервер спілкуються один з одним і вирішують, чи ресурс був оновлений від часу його кшування.  

Оптимізуємо код за допомогою метода валідації

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

// src/AppBundle/Controller/ArticleController.php
namespace AppBundle\Controller;

// ...
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

class ArticleController extends Controller
{
    public function showAction($articleSlug, Request $request)
    {
        // Get the minimum information to compute
        // the ETag or the Last-Modified value
        // (based on the Request, data is retrieved from
        // a database or a key-value store for instance)
        $article = ...;

        // create a Response with an ETag and/or a Last-Modified header
        $response = new Response();
        $response->setETag($article->computeETag());
        $response->setLastModified($article->getPublishedAt());

        // Set response as public. Otherwise it will be private by default.
        $response->setPublic();

        // Check that the Response is not modified for the given Request
        if ($response->isNotModified($request)) {
            // return the 304 Response immediately
            return $response;
        }

        // do more work here - like retrieving more data
        $comments = ...;

        // or render a template with the $response you've already started
        return $this->render('article/show.html.twig', array(
            'article' => $article,
            'comments' => $comments
        ), $response);
    }
}

Якщо ввідповідь (Response) не модифікована, метод  isNotModified() автоматично встановлює статус код на 304, видаляє контент і деякі заголовки, які не повинні бути присутніми у відповіді 304 (див. метод setNotModified()).

Варіації відповіді (Response)

Раніше ви дізнались, що кожен URI (уніфікований ідентифікатор ресурсів) має єдине предаставлення цільового ресурса. За замовчуванням, HTTP кешування відбувається з використанням ресурса URI в якості ключа до знань кеша. Якщо два користувача дадуть запит на один і той самий URI кешованого  ресурса, другий клієнт отримає кешовану версію.  

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

У цьому випадку, вам потрібно зберігати  обидві версії для ресурса - стиснуту і нестиснуту, та повертати її базуючись на значенні заголовка запиту Accept-Encoding. Цього ефекту можна досягнути використавши зоголовок відповіді Vary, який є списком (розділений комами), різноманітних заголовків, чиї значення перемикають різні представлення ресурса, на якй дали запит.  

 Vary: Accept-Encoding, User-Agent

Заголовок Vary, поданий у прикладц вище, дозволяє кешувати різні версії для кожного ресурса, базуючись на URI і значенні заголовків запиту Accept-Encoding і User-Agent.  

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

// set one vary header
$response->setVary('Accept-Encoding');

// set multiple vary headers
$response->setVary(array('Accept-Encoding', 'User-Agent'));

Метод setVary() приймає в якості параметра ім’я заголовка або масиву заголовків, на основі яких потрібно варіювати відповідь.  

Закінчення терміну дії та валідація

Ви можете використовувати закінчення терміну дії і валідацію в одному екзкмплярі Response. Якщо закінчення терміну дії працює раніше валідації, ви зможете отримати кращі переваги обох моделей. Тобто, використовуючи їх разом ви зможете проінструктурувати кеш таким чином, щоб він зберігав контент доти, поки з деяким інтервалом втконується перевірка, що контент досі валідний.  

Ви також можете визначити кешовані заголовки HTTP для закінчення терміну дії і валідації, використовуючи анотації. Детальніше у документації: FrameworkExtraBundle documentation.

More Response Methods

Клас Response містить також і інші методи для роботи з кешом. Наведемо приклад найкорисніших з них:

// Marks the Response stale
$response->expire();

// Force the response to return a proper 304 response with no content
$response->setNotModified();

Додатково, всі основні HTTP заголовки, які відносяться до кеша, можна встановити, залучивши всього один метод  setCache() :

// Set cache settings in one call
$response->setCache(array(
    'etag'          => $etag,
    'last_modified' => $date,
    'max_age'       => 10,
    's_maxage'      => 10,
    'public'        => true,
    // 'private'    => true,
));

Анулювання (інвалідація) кеша

"В комп’ютерних науках є лише дві складні речі: анулювання кеша і питання іменування.” -  Філ Карлтон (Phil Karlton)

Як тільки URL-адреса стає кешована шлюзом кешування, кеш більше не проситиме додаток цього контенту. Це дає можливість кешу швидко давати відповіді і зменшувати навантаження на ваш додаток. Проте. є ризик надіслати застарілий контент. Вихід з цієї дилеми - використати довгі кешовані життєві цикли, і активно повідомляти шлюз кешування про зміни у контенті. Обернені проксі зазвичай дають каналові можливість отримати такі нотифікації, зазвичай через спеціальні запити HTTP.   

Оскільки процес анулювання кеша потужний, шукайте можливостей уникнути  його. Якщо у вас не вийде щось анулювати, застарілі кеші будуть ще дуже довго обслуговуватись. Натомість, використайте короткі життєві цикли кеша, або модель валідації, і налаштуйте ваші контроллери так, аби вони могли зробити ефективну перевірку валідації, як описано у попередньому підрозділі.  

Більше того, оскільки процес анулювання є специфічною темою для кожного типу оберного проксі, застосовуючи цей концепт, ви будете прив’язані до спеціального оберненого проксі, інакше вам потрібно буде прикласти додаткових зусиль, щоб підтримувати різноманітні проксі.  

Проте, інколи вам навпаки буде потрібний цей процес, який відбувається під час явного анулювання. Для інвалідації, ваш додаток повинен визначити коли змінюється контент, і повідомити кеш про те, що він повинен видалити URL-адреси, в яких знаходяться дані цього кеша.

Якщо ви бажаєте користуватися анулюванням кеша, перегляньте пакет  FOSHttpCacheBundle. Він надає сервіси для допомоги з різними концептами анулювання, а також документує конфігурацію для декількох звичайних кешованих проксі.  

Якщо один контент відповідає одній URL, модель PURGE добре справляється з роботою. Ви відсилаєте запит кешованому проксі з методом PURGE (використання слова "PURGE" умовне, технічно, тут можна використати будь-яке) замість GET і змушуєте кешований проксі визначити це та забрати дані з кеша, і тому, вам не потрібно звертатись до додатка за відповіддю.  

Ось приклад, як можна налаштувати обернений проксі у Symfony для підтримки метода  HTTP PURGE:

// app/AppCache.php

use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
// ...

class AppCache extends HttpCache
{
    protected function invalidate(Request $request, $catch = false)
    {
        if ('PURGE' !== $request->getMethod()) {
            return parent::invalidate($request, $catch);
        }

        if ('127.0.0.1' !== $request->getClientIp()) {
            return new Response(
                'Invalid HTTP method',
                Response::HTTP_BAD_REQUEST
            );
        }

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

        return $response;
    }
}

Вам потрібно певним чином оберігати метод HTTP PURGE, щоб будь-які користувачі не могли чистити ваші кешовані дані.  

Очищення (англ. Purge) дає команду кешу відмовитися від ресурсу у всіх його варіантах (згідно з заголовком Vary, як подпно у прикладі зверху). Альтернативою цьому сгугує оновлення контенту.  Оновлення означає, що кешованому проксі доручено скасувати свій локальний кеш і знову витягти контент. Таким чином, у нас вже буде доступний контент у кеші. Недоліком оновлення є те, що варіанти не є недійсними.

У багатьох додатках, той самий контент використовується на багатьох сторінках з різними URL-адресами. Більш гнучкі концепції існують для наступних випадків:  

  • Беннінг робить недійсними відповіді, що збігаються з регулярними виразами на  URL або на інших показниках;

  • Тегування кеша дозволяє вам додати тег до контенту, який використовується у відповіді, щоб ви могли зробити недійсними усі URL-и, які містять такий контент.  

Використання ESI (Edge Side Includes)

Шлюзи кешування - це хороший спосіб допомогти вашому веб-сайту працювати краще. Але вони мають обмеження: вони  кешують сторінки повністю. Якщо ви, по певній причині не можете кешувати сторінки повністю, або якщо сторінка має декілька динамічних частин, вам не дуже пощастило. На щастя, Symfony має рішення на такі випадки, яке має за основу технологію ESI або Edge Side Include. Компанія Akamai написала цю специфікацію майже 10 років тому, і саме вона дозволяє користуватися різними стратегіями кешування для окремих частин сторінки.

Специфікація ESI описує теги, які ви можете додати до ваших сторінок щоб “поспілкуватись” зі шлюзом кешування. У Symfony реалізований лише один тег -   include, твк як це найкорисніший тег поза контекстом Akamai:

<!DOCTYPE html>
<html>
    <body>
        <!-- ... some content -->

        <!-- Embed the content of another page here -->
        <esi:include src="http://..." />

        <!-- ... more content -->
    </body>
</html>

Зверніть увагу, що для тега ESI URL-адреса вказана повністю. Тег ESI - це фрагмент сторінки, який можно отримати за цією URL-адресою.

При обробці запиту, шлюз кешування повністю отримує сторінку з свого кеша, або дає на нього запит додатку. Якщо відповідь містить один або більше ESI тег, вони обробляються таким самим способом. Тобто, шлюз кешування отримує фрагменти сторінок свого кеша, або дає запит додатку на ці фрагменти. Коли всі теги оброблені, шлюз додає всі фрагментив основну сторінку і віжправляє готовий контент клієнту.

Весь процес відбувається непомітно, на рівні шлюза кешування (поза нашим додатком). Якщо ви хочете використовувати переваги, які надають теги ESI, Symfony дозволить вам підключати їх не витрачаючи багато зусиль.

Використовуємо ESI у Symfony

Спочатку, перед використанням ESI, переконайтесь, що ви активували їх в налаштуваннях додатку:

YAML

# app/config/config.yml
framework:
    # ...
    esi: { enabled: true }

XML

<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/symfony"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:framework="http://symfony.com/schema/dic/symfony"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony
        http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">

    <framework:config>
        <!-- ... -->
        <framework:esi enabled="true" />
    </framework:config>
</container>

PHP

// app/config/config.php
$container->loadFromExtension('framework', array(
    // ...
    'esi' => array('enabled' => true),
));

Тепер, уявімо що у вас є сторінка, яка в більшості статична, за винятком новин, розміщених під контентом. За допомогою ESI, ви зможете кешувати новини незалежно від решти частин сторінки.

// src/AppBundle/Controller/DefaultController.php

// ...
class DefaultController extends Controller
{
    public function aboutAction()
    {
        $response = $this->render('static/about.html.twig');
        // set the shared max age - which also marks the response as public
        $response->setSharedMaxAge(600);

        return $response;
    }
}

В даному прикладі, ми встановлюємо для усієї сторінки час життя кеша на 10 хвилин. Пізніше підключаємо новини до шаблону за допомогою вбудування дії. Це можна втілити за допомогою хелпера render (див. підрозділ “Підключення контролерів”).

Так як вбудований контент надходить з іншої сторінки (або віз контролера, як в нашому випадку), Symfony використовує стандартний хелпер render для конфігурації тега ESI:

TWIG
{# app/Resources/views/static/about.html.twig #}

{# you can use a controller reference #}
{{ render_esi(controller('AppBundle:News:latest', { 'maxPerPage': 5 })) }}

{# ... or a URL #}
{{ render_esi(url('latest_news', { 'maxPerPage': 5 })) }}

PHP

<!-- app/Resources/views/static/about.html.php -->

<!-- you can use a controller reference -->
<?php echo $view['actions']->render(
    new \Symfony\Component\HttpKernel\Controller\ControllerReference(
        'AppBundle:News:latest',
        array('maxPerPage' => 5)
    ),
    array('strategy' => 'esi')
) ?>

<!-- ... or a URL -->
<?php echo $view['actions']->render(
    $view['router']->url('latest_news', array('maxPerPage' => 5)),
    array('strategy' => 'esi'),
) ?>

Вказавши рендерер esi (через функцію Twig render_esi), ви вказуєте Synfony, що саме повинно відображатись як ESI тег. Ви можливо здивовані - навіщо використовувати хелпер, якщо можна просто написати цей тег самостійно. Це нобхідно для того, щоб ваш додаток працював як зі шлюзом кешування, так і без нього.  

Пізніше ви побачите, що змінна maxPerPage яку ви використовуєте доступна у якості аргумента вашому контролеру (тобто, $maxPerPage). Змінні, які проходять через  render_esi стають частиною ключа кеша, тому ваші кеші будуть унікальними для кожної комбінації змінних та значень.  

Коли ви використовуватимете функцію render (або встановлюватимете рендерер у  inline), Symfony буде об’єднувати контент включеної сторінки з головною, перш ніж надіслати відповідь клієнту. Проте, якщо ви використовуєте рендерер esi  (render_esi), і якщо Symfony визначає, що він “спілкується” з шлюзом кешування, який підтримує ESI, тоді даний рендерер генерує тег ESI. Але якщо немає ніякого шлюзу кешування, або якщо він не підтримує ESI, Symfony просто об’єднає контент сторінки з головною, так само, як і у тому випадку, коли б ви використали render.

Symfony визначає, чи підтримує шлюз ESI, за допомогою другої специфікації  Akamai, яка пітримується оберненим проксі Symfony.

Тепер для вбудованої дії ви можете вказати власні правила кешування, незалежні від головної сторінки.

// src/AppBundle/Controller/NewsController.php
namespace AppBundle\Controller;

// ...
class NewsController extends Controller
{
    public function latestAction($maxPerPage)
    {
        // ...
        $response->setSharedMaxAge(60);

        return $response;
    }
}

За допомогою ESI, кеш сторінки буде валідним протягом 600 секунд, але кеш компонента новин кешуватиметься тільки 60 секунд.  

Вимога при використанні тега ESI: вбудована дія повинна бути доступна через певний URL, щоб шлюз кешування міг отримати його крнтент незалежно він інших частин сторінки. Symfony попіклується про все: згенерує унікальну URL-адресу, для будь якого контролера, створить відповідний маршрут за допомогою  FragmentListener, який повинен бути включений у вашу конфігурацію.

YAML

# app/config/config.yml
framework:
    # ...
    fragments: { path: /_fragment }

XML

<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:doctrine="http://symfony.com/schema/dic/framework"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony
        http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">

    <!-- ... -->
    <framework:config>
        <framework:fragments path="/_fragment" />
    </framework:config>
</container>

PHP

// app/config/config.php
$container->loadFromExtension('framework', array(
    // ...
    'fragments' => array('path' => '/_fragment'),
));

Чудова перевага рендерера ESI, це те що ви зможете зробити власний додаток динамічним настільки, наскільки потрібно, водночас лише трішки задіявши до роботи додаток.

Слухач відповідає лише локальній IP адресі або “довіреним проксі”.

Як тільки ви почнете використовувати ESI, не забувайте використовувати директиву  s-maxage замість max-age. Оскільки браузер отримує лише комплексний ресурс, він нічого не знає про під-компоненти, і тому він виконуватиме команди директиви max-age і кешувати цілу сторінку. А вам це зовсім не потрібно.  

Помічник render_esi підтримує такі дві корисні опції:  

alt

Використовується як властивість alt з тегом ESI, який дозволяє вказати альтернативну  URL-адресу, яку можна використати у тому випадку, коли програма не знайшла src .

ignore_errors

Якщо властивості onerror дати тип “true”, вона буде додана до ESI зі значенням continue вказуючи, що в разі виходу з ладу, шлюз кешування просто забере тег ESI.

Підсумки

Symfony була створена для того, щоб виконувати правила дорожнього руху: HTTP. Ешування - це не виняток. Якщо ви опановуєте систему кешування у Symfony, це означає що ви все більше ознайомлюєтесь з моделями кешування  HTTP і ефективно їх використовує. А це в свою чергу допомагає не покладатись на документацію Symfony і шаблони коду, а отримати доступ до світу знань відносно кешування HTTP та шлюзу кешування, такого як Varnish.

Більше дізнавайтесь у Довіднику

Як використовувати Varnish для пришвидшення роботи сайт

 

Поділитися