Валидация HTTP-кеша

Дата обновления перевода 2023-01-16

Валидация HTTP-кеша

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

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

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

Tip

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

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

Дата обновления перевода 2023-01-16

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

Tip

Вы также можете определить заголовки HTTP-кеширования для окончания срока действия и валидации, используя аннотации. См. документацию FrameworkExtraBundle.

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

Заголовок HTTP ETag ("entity-tag") - это необязательный HTTP-заголовок, чьё значение является произвольной стокой, которая уникально идентифицирует представление целевого ресурса. Он полностью генерируется и устанавливается вашим приложением так, что вы можете сказать, к примеру, является ли ресурс /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 также поддерживает слабый ETag, передавая 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', [
            'article' => $article,
            'comments' => $comments,
        ], $response);
    }
}

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