HTTP кеш

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

Природа насичених веб-додатків означає що вони динамічні. Незалежно від того, наскільки ваш додаток ефективний, кожен запит буде вміщати більше роботи ніж віддача простого статистичного файлу. 

І для більшості веб-додатків це цілком нормально. Symfony - швидкий фреймворк, і якщо ви не виконуєте дуже важких операцій, кожен запит буде швидко оброблятися, без складних ситуацій на сервері.  

Проте, коли ваш сайт буде збільшуватися, нагрузка може перетворитися у проблему. Робота, яка зазвичай виконується для кожного запиту, повинна тепер виконуватися лише один раз. Цього можна досягнути за допомогою кешування.  

Кешування на плечах гігантів

Найбільш ефективний шлях збільшення швидкості роботи роботи додатку - це спробувати кешувати сторінку повністю і потім, в обході додатку, віддавати кешовані дані для кожного запиту. Звичайно, такий спосіб не завжди можна використати, особливо для сайтів, що часто динамічно змінюються… чи все таки можливо? У цьому розділі ви побачите, як працює система кешування у Symfony і чому ми вважаємо, що Symfony -  це найкращий вибір з усіх можливих.

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

Ми пройдемо 4 етапи, для того, аби вивчити, як кешувати у Symfony:

1. Шлюз кешування, або зворотний проксі-сервер (reverse proxy) - це незалежний шар, який розміщений перед вашим додатком. Зворотний проксі-сервер кешує відповіді, коли вони поступають від додатку і відповідає на запити за допомогою кешовани відповідей, і при цьому не підключає додаток. Symfony містить власний проксі-сервер, але ви можете обрати будь-який за бажанням.

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

3. Закінчення терміну дії і валідація кешу - це дві моделі, які використовуються для визначення чи кешований контент свіжий (і може братися з укшу знову), чи його термін дії вже закінчився (і необхідно його створювати заново, за допомогою додатку).

4. Edge Side Includes (ESI) дозволяють використовувати HTTP кеш для незалежного кешування елементів сторінок (навіть вкладених елементів). За допомогою ESI ви можете прокешувати цілу сторінку за 60 хвилин, проте вкладену бокову панель лише за 5 хвилин.

Кешування HTTP не є повністю заслугою Symfony, адже існує безліч статей на дану тему. Якщо тема HTTP кешування для вас нова, ми рекомендуємо почитати статтю Раєна Томейко (Ryan Tomayko): Things Caches Do. Ще одне цікаве джерело це Cache Tutorial від Марка Нотінгема (Mark Nottingham).

Кешування за допомогою шлюзу кешування

При використанні НTTP при кешуванні, кеш повністю відділений від вашого додадку і знаходиться між додадком і клієнтом, який виконав запит.

Робота кешу - приймати запит від клієнта і передавати його вашому додатку. Також, кеш буде отримувати відповідь від вашого додатку і перенаправляти його далі до клієнта. Кеш грає роль “посередника” в клієнтсько-серверних комунікаціях між клієнтом і вашим додатком.  

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

Цей тип кешування відомий як “HTTP шлюз кешування”. Існує багато кешерів такого типу, наприклад: Varnish, Squid в режимі оберненого проксі,  а також обернений проксі Symfony.

Типи кешування

Проте шлюз кешування не єдиний у своєму роді. Фактично, заголовками HTTP кеша, які відправляє ваш додаток, можуть користуватися, а також отримувати, три різні типи кешерів:

Кеш браузера: Кожен браузер має локальний кеш, який в основному використовується, коли ви натискаєте кнопку “back”, або для зображень та інших ресурсів. Кеш браузера - приватний кеш, який ніким більше не використовується.  

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

Шлюзи кешування: так само як і проксі, вони є кешами загального доступу, проте не на стороні сервера. Вони встановлюються адміністраторами, роблять сайти більш еластичними, надійними і швидкими.  

Шлюзи кешування деколи називають кешованими оберненими проксі, сурогатними кешерами і навіть HTTP акселераторами.  

Значення приватного кеша у порівнянні з кешом загального доступу, стає помітнішим, коли ми говоримо про кешування відповідей, які містять контент, і які відносяться до конкретного користувача (наприклад, інформація про рахунок).   

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

Обернений проксі у Symfony

Symfony містить обернений проксі ( його також називають шлюзом кешування), написаний на PHP. Активуйте його і кешовані відповіді вашого додатку почнуть кешуватися як слід. Встановити його просто. Кожен додаток у Symfony містить налаштоване ядро кешування (AppCache), яке є оболонкою для ядра за замовчуванням (AppKernel). Ядро кешування і є тим самим оберененим проксі.   

Щоб активувати кешування, модифікуйте код фронт-контролера так, щоб він використовував ядро кешування:

// web/app.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);

$request = Request::createFromGlobals();

$response = $kernel->handle($request);
$response->send();

$kernel->terminate($request, $response);

Ядро кешування відразу почне діяти як обернений проксі - буде кешувати відповіді вашого додатка і відправляти їх клієнту.  

Якщо ви використовуєте опцію  framework.http_method_override, щоб прочитати метод HTTP з параметра a_method, клацніть зверху на посилання для зміни параметрів.  

Ядро кешування містить особливий метод getLog(), який повертає рядкове представлення того, що відбувається на рівні кешування. В середовищі dev ви можете використовувати його для дебагу і перевірки стратегії кешування.

error_log($kernel->getLog());

Об’єкт AppCache має конфігурацію за замовчуванням, але ви можете налаштовувати його опції за допомогою перевизначення метода getOptions():

 
// app/AppCache.php
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;

class AppCache extends HttpCache
{
    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,
        );
    }
}


Для зміни опції 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-while-revalidate (див. RFC 5861).

Якщо debug має значення true, Symfony автоматично добавляє у відповідь заголовок   X-Symfony-Cache, який містить корисну інформацію про кількість спрацьовування кеша  і про кількість незнайдених відповідей у кеші. 

Використовуємо другий обернений проксі

Обернений проксі у Symfony - це хороший інструмент для використання підчас розробки чи під час вигрузки вашого сайту на віртуальний хостинг, де ви не зможете встановити нічого, окрім PHP коду. Проте проксі на PHP ніколи не працюватиме швидше за проксі на C. Тому ми рекомендуємо вам використовувати Varnish або Squid на ваших серверах, якщо це можливо. Хороша новина - переключення з одного проксі сервера на другий виконується напрочуд легко і вам не потрібно змінювати код у вашому додатку. Просто почніть працювати з оберненим проксі у Symfony і змініть його на Varnish, коли трафік збільшиться.

Більше про використання Varnish у Symfony, читайте у Довіднику.  

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

Вступ до HTTP кешування

Щоб отримати користь від кешування, ваш додаток повинен мати молживість повідомити, які відповіді можуть бути кешованими, а також правила, які будуть вказувати коли і як закінчується придатність даного кеша. Цього можна досягнути за допомогою HTTP заголовківдля кешування відповідей.  

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

HTTP містить 4 заголовки, які відносяться до кешування:  

Cache-Control

Expires

ETag

Last-Modified

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

Кожен з заголовків ми пояснимо у підрозділі Моделі кешування в HTTP: Validation Invalidation.

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

Заголовок Cache-Control унікальний за рахунок того, що він вміщує не одне конкретне значення, а багато різноманітних даних про кешування відповіді. Кожна нова частина фнформації відділяється комою:

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

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

Symfony надає інші методи для більш зручного керування заголовком Cache-Control:

 
// ...

use Symfony\Component\HttpFoundation\Response;

$response = new Response();

// mark the response as either public or private
$response->setPublic();
$response->setPrivate();

// set the private or shared max age
$response->setMaxAge(600);
$response->setSharedMaxAge(600);

// set a custom Cache-Control directive
$response->headers->addCacheControlDirective('must-revalidate', true);

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

Публічні (public) та приватні (private) відповіді

Шлюзи кешування і проксі вважаються “спільними” кешами, тому що кешований контент доступний більш ніж одному користувачеві. Якщо буде випадково збережена відповідь, специфічна для певного користувача, врешті решт вона може бути відправлена багатьом користувачам. Уявіть, що інформація про ваш акаунт була кешована і буде відправлена будь-якому користувачеві, який відсилав запит на власний акаунт!  

Щоб уникнути такої ситуації, кожна відповідь повинна бути публічною або приватною:  

public: вказує що відповідь може кешуватися як приватними так і публічними кешами.

private: приватна відповідь повністю, або певна її частина, призначена для одного користувача і не повинна кешуватися публічними кешерами.

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

Безпечні методи

HTTP кешування працює лише для “безпечних” HTTP методів (таких як GET і HEAD). Ці методи мають статус “безпечних”, і це означає, що ви ніколи не зміните стан додатку обробляючи такі запити (при цьому ви можете логувати нформацію, кешувати дані і т. д.). Таке обмеження має наступні наслідки:  

  • Ви ніколи не повинні змінювати стан вашого додатку, відповідаючи на запит GET, або HEAD. Навіть якщо ви не використовуєте шлюз кешування, наявність проксі-кеша означає, що як попасти, як і не попасти до вашого додатку.  

  • Ні в якому разі не кешуйте методи PUT, POST, або DELETE. Вони призначені для зміни стану додатку (наприклад, видалення запису з блогу). Якщо їх кешувати, то частина запитів на зміну стану не будуть його досягати.   

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

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

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

  • Якщо заголовки кешування не визначені (Cache-Control, Expires, ETag або Last-Modified), Cache-Control отримує значення no-cache, тобто відповідь не буде кешуватися;

  • Якщо Cache-Control пустий (проте присутній інший заголовок кешування) яго значення private, must-revalidate;

  • Проте, якщо присутня хоть одна директива Cache-Control, і явно не вказані директиви public або private, Symfony автоматично додає директиву private (виняток: коли встановлений s-maxage).

Моделі кешування в HTTP: validation та invalidation

Специфікація HTTP визначає дві моделі кешування:  

  • Перша  модель “закінчення терміну дійсності” (expiration), ви просто вказуєте як довго відповідь буде “свіжою” включаючи заголовки Cache-Control і/або Expires. Кешери, які підтримують дану модель, не будуь виконувати запит то того моменту, поки його версія кешування не досягатиме закінчення терміну дійсності і не стане непридатною;

  • Коли сторінки динамічті, тобто часто змінюються, потрібно використовувати модель валідації (validation). При використанні даної моделі, кешер зберігає відповідь, але при кожному наступному запиті він надсилає запит серверу - чи кешована відповідь дійсна (валідна). Додаток використовує унікальний ідентифікатор відповіді (заголовок Etag) і/або часову мітку (заголовок Last-Modified), щоб перевірити чи змінилася сторінка з моменту її кешування.

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

Читаємо специфікацію HTTP

Специфікація HTTP визначає просту, але потужну мову, за допомогою якої можна створювати комунікації клієнт-сервер у мережі. Модель запит-відповідь визначає усю вашу роботу, як веб-розробника. Нажаль, читати оригінальну специфікацію - RFC 2616 - не так просто.

На даний момент існує ініціатива (HTTP Bis) переписати RFC 2616. Дана спрорба - не мета описати нову версію HTTP, а зосереджена на поясненні оригінальної специфікації HTTP. Структура специфікації покращилась (вона розділена на 7 частин) все що стосується HTTP кешування - розміщено у двох окремих частинах (P4 - Conditional Requests та P6 - Caching: Browser and intermediary caches).

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

Expiration - закінчення терміну придатності

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

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

Закінчення терміну придатності за допомогою заголовка Expires

Згідно з специфікацією HTTP, "заголовок Expires містить дату/час, після якого відповідь вважатиметься непридатною”. Заголовок Expires може бути встановлений за допомогою метода setExpires() класу Response. ВІн приймає екземпляр DateTime instance у якості аргумента:

$date = new DateTime();
$date->modify('+600 seconds');

$response->setExpires($date);

Остаточний заголовок HTTP виглядатиме наступним чином:

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

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

 

Зверніть увагу, що в усіх версіях HTTP перед версією 1.1, оригінальному серверу не потрібно було надсилати заголовок Date. Як наслідок, кеш (наприклад, браузер) покладається на місцевий годинник, щоб оцінити заголовок Expires, роблячи розрахунки в часі вразливими до відхилення. Ще одне обмеження заголовка Expires, згідно зі специфікацією - “HTTP/1.1 сервери не повинні встановлювати дату Expires більш ніж на один рік наперед.

Закінцення терміну придатності з заголовком Cache-Control

Оскільки заголовок Expires містить обмеження, слід використовувати заголовок  Cache-Control. Згадайте, заголовок Cache-Control використовується для того, щоб вказати різні директиви, які відносяться до кешування. Для закінчення терміну дії існує дві директиви,  max-age і s-maxage. Перша використовується усіма кешерами, в той час як друга - лише загальними (shared) кешами:

// Sets the number of seconds after which the response
// should no longer be considered fresh
$response->setMaxAge(600);

// Same as above but only for shared caches
$response->setSharedMaxAge(600);

Заголовок Cache-Control виглядатиме наступним чином (він може мати додаткові директиви):

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

Валідація

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

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

З цією моделлю, перш за все ви зберігаєте пропускну здатність  

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

Статус 304 означає "Not Modified". Це важливий статус, так як разом з ним не відправляється контент, на який дали запит. Натомість, відповідь містить невелику кількість вказівок, які повідомляють кеш, що можна використовувати збережену раніше версію.  

Так само як з терміном закінчення придатності, існує два HTTP заголовка, які можуть використовуватися для втілення моделі валідації: ETag і Last-Modified.

Валідація за допомогою заголовка ETag

Заголовок ETag - є рядковим заголовком (називається "entity-tag"), який єдиним способом ідентифікує представлення цільового ресурсу. Він генерується і встановлюється повністю всередині вашого додатку, так як ви зможете зрозуміти, наприклад чи відповідає кешований ресурс  /about тому, який збирається повернути ваш додаток. Заголовок ETag подібний на відбитки пальців і використовується для швидкого визначення чи дві версії ресурса еквівалентні. Як і відбитки пальців, кожен ETag повинен бути унікальним для будь-якого представлення одного й того ж ресурса.  

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

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

use Symfony\Component\HttpFoundation\Request;

class DefaultController extends Controller
{
    public function homepageAction(Request $request)
    {
        $response = $this->render('static/homepage.html.twig');
        $response->setETag(md5($response->getContent()));
        $response->setPublic(); // make sure the response is public/cacheable
        $response->isNotModified($request);

        return $response;
    }
}

Метод  isNotModified() порівнює If-None-Match, відправлений у запиті (Request) з заголовком ETag у Response. Якщо вони співрпадають, цей метод автоматично встановлює для Response статус код 304.

Кеш встановлює заголовок If-None-Match у запиті до кешованої відповіді заголовка  ETag, перед тим як надіслати запит назад до додатку. Ось таким чином кеш та сервер спілкуються один з одним, і вирішують чи ресурс був оновлений з моменту його кешування.  

Цей алгоритм є достатньо простим і нічим особливим не відрізняється, але вам слід створити екземпляр Response повністю, перед тим, як ви отримаєте можливість вирахувати ETag, а цього недостатньо. Іншими словами, цей підхід зберігає пропускну здатність, але не ресурси CPU.

У секції “Оптимізуємо код за допомогою метода валідації”, ми покажемо як можна розумніше використовувати валідацію, і визначати валідність кеша без лишньої роботи.  

Symfony також використовує слабкі ETags-и - для цього потрібно передати true у якості другого аргумента в метод setEtag().

Поділитися