Валідація HTTP-кешу

Дата оновлення перекладу 2023-09-19

Валідація HTTP-кешу

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

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

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

Tip

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

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

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

Tip

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

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

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

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

Щоб побачити просту реалізацію, згенеруйте ETag як md5 змісту:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Controller/DefaultController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends AbstractController
{
    public function homepage(Request $request): Response
    {
        $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 із заголовком відповіді ETag. Якщо вони співпадають, метод автоматично встановлює у Response статус-код 304.

Note

При використання mod_deflate або mod_brotli в Apache 2.4, оригінальне значення ETag модифікується (наприклад, якщо ETag був foo, Apache перетворює його на foo-gzip або foo-br), що порушує валідацію, засновану на ETag.

Ви можете контролювати цю поведінку з директивами DeflateAlterETag та BrotliAlterETag. Як варіант, ви можете використати наступну конфігурацію Apache, щоб залишити як оригінальний ETag, так і модифікований, при стисканні відповідей:

1
RequestHeader edit "If-None-Match" '^"((.*)-(gzip|br))"$' '"$1", "$2"'

Note

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

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

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

Tip

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/Controller/ArticleController.php
namespace App\Controller;

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

class ArticleController extends AbstractController
{
    public function show(Article $article, Request $request): Response
    {
        $author = $article->getAuthor();

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

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

        $response = new Response();
        $response->setLastModified($date);
        // Встановити відповідь як публічну. Інакше вона буде приватною за замовчуванням.
        $response->setPublic();

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

        // ... зробити більше роботи, щоб наповнити відповідь повним змістом

        return $response;
    }
}

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

Note

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// src/Controller/ArticleController.php
namespace App\Controller;

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

class ArticleController extends AbstractController
{
    public function show(string $articleSlug, Request $request): Response
    {
        // Отримати мінімум інформації для обчислення
        // значення ETag або Last-Modified
        // (засновуючись на Request, дані вилучаються з бази даних
        // або, наприклад, сховища ключових значень)
        $article = ...;

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

        // Встановити відповідь як публічну. Інакше вона буде приватною за замовчуванням.
        $response->setPublic();

        // Перевірити, щоб Response не була змінена для заданого Request
        if ($response->isNotModified($request)) {
            // негайно повернути відповідь 304
            return $response;
        }

        // зробити більше роботи - наприклад, вилучити більше даних
        $comments = ...;

        // або відобразити шаблон з $response, який ви вже розпочали
        return $this->render('article/show.html.twig', array(
            'article' => $article,
            'comments' => $comments,
        ), $response);
    }
}

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