Кешування для підвищення продуктивності
Проблеми з продуктивністю можуть виникнути разом зі зростанням популярності. Кілька типових прикладів: відсутність індексів бази даних або безліч SQL-запитів на сторінку. У вас не буде жодних проблем із порожньою базою даних, але зі збільшенням кількості трафіку та дедалі більшим обсягом даних, в якийсь момент, вони можуть виникнути.
Додавання заголовків HTTP-кешу
Використання стратегій HTTP-кешування є відмінним способом досягнення максимальної продуктивності для кінцевих користувачів без особливих зусиль. Додайте зворотний проксі-кеш у продакшн, щоб увімкнути кешування і використовуйте CDN для досягнення ще кращих результатів продуктивності.
Закешуймо головну сторінку на одну годину:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -33,9 +33,12 @@ class ConferenceController extends AbstractController
#[Route('/', name: 'homepage')]
public function index(ConferenceRepository $conferenceRepository): Response
{
- return new Response($this->twig->render('conference/index.html.twig', [
+ $response = new Response($this->twig->render('conference/index.html.twig', [
'conferences' => $conferenceRepository->findAll(),
]));
+ $response->setSharedMaxAge(3600);
+
+ return $response;
}
#[Route('/conference/{slug}', name: 'conference')]
Метод setSharedMaxAge()
встановлює термін дії кешу для зворотних проксі. Використовуйте setMaxAge()
, щоб контролювати кеш браузера. Час встановлюється у секундах (1 година = 60 хвилин = 3600 секунд).
Кешувати сторінку конференції складніше, оскільки вона більш динамічна. В будь-який момент хтось може додати коментар, і ніхто не захоче чекати одну годину, щоб побачити його на сайті. В таких випадках, використовуйте стратегію HTTP-валідації.
Активація ядра HTTP-кешу Symfony
Щоб перевірити стратегію HTTP-кешу, увімкніть зворотний зворотний HTTP-проксі Symfony, але тільки в середовищі "розробки" (для "продакшн" середовища ми будемо використовувати "надійніше" рішення):
1 2 3 4 5 6 7 8 9 10
--- a/config/packages/framework.yaml
+++ b/config/packages/framework.yaml
@@ -22,3 +22,7 @@ when@test:
test: true
session:
storage_factory_id: session.storage.factory.mock_file
+
+when@dev:
+ framework:
+ http_cache: true
Крім того, що це повноцінний зворотний HTTP-проксі, HTTP-проксі Symfony (за допомогою класу HttpCache
) додає деяку корисну інформацію налагодження у якості HTTP-заголовків. Це дуже допомагає при валідації встановлених нами заголовків кешу.
Перевірте це на головній сторінці:
1
$ curl -s -I -X GET https://127.0.0.1:8000/
1 2 3 4 5 6 7 8 9 10 11
HTTP/2 200
age: 0
cache-control: public, s-maxage=3600
content-type: text/html; charset=UTF-8
date: Mon, 28 Oct 2019 08:11:57 GMT
x-content-digest: en63cef7045fe418859d73668c2703fb1324fcc0d35b21d95369a9ed1aca48e73e
x-debug-token: 9eb25a
x-debug-token-link: https://127.0.0.1:8000/_profiler/9eb25a
x-robots-tag: noindex
x-symfony-cache: GET /: miss, store
content-length: 50978
Для найпершого запиту кеш-сервер повідомляє вам, що відбувся miss
(промах) і що він виконав store
(збереження), щоб закешувати відповідь. Перевірте заголовок cache-control
, щоб побачити налаштовану стратегію кешу.
Для наступних запитів відповідь закешовано (період
(age
) також оновлено):
1 2 3 4 5 6 7 8 9 10 11
HTTP/2 200
age: 143
cache-control: public, s-maxage=3600
content-type: text/html; charset=UTF-8
date: Mon, 28 Oct 2019 08:11:57 GMT
x-content-digest: en63cef7045fe418859d73668c2703fb1324fcc0d35b21d95369a9ed1aca48e73e
x-debug-token: 9eb25a
x-debug-token-link: https://127.0.0.1:8000/_profiler/9eb25a
x-robots-tag: noindex
x-symfony-cache: GET /: fresh
content-length: 50978
Уникнення SQL-запитів за допомогою ESI
Слухач TwigEventSubscriber
впроваджує глобальну змінну в Twig для всіх об'єктів конференції. Це відбувається для кожної окремої сторінки веб-сайту. Мабуть, це прекрасне місце для оптимізації.
Ви не будете додавати нові конференції щодня, тому код запитує одні й ті ж дані з бази даних знову і знову.
Ми, можливо, захочемо закешувати імена конференцій і "slugs" за допомогою кешу Symfony, але всякий раз, коли це можливо, я вважаю за краще покладатися на інфраструктуру HTTP-кешування.
Коли ви хочете закешувати фрагмент сторінки, перемістіть його за межі поточного HTTP-запиту, створивши підзапит. ESI ідеально підходить для цього. ESI — це спосіб вбудувати результат одного HTTP-запиту в інший.
Створіть контролер, який повертає лише фрагмент HTML, що відображає конференції:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -41,6 +41,14 @@ class ConferenceController extends AbstractController
return $response;
}
+ #[Route('/conference_header', name: 'conference_header')]
+ public function conferenceHeader(ConferenceRepository $conferenceRepository): Response
+ {
+ return new Response($this->twig->render('conference/header.html.twig', [
+ 'conferences' => $conferenceRepository->findAll(),
+ ]));
+ }
+
#[Route('/conference/{slug}', name: 'conference')]
public function show(Request $request, Conference $conference, CommentRepository $commentRepository, string $photoDir): Response
{
Створіть відповідний шаблон:
Перейдіть за посиланням /conference_header
, щоб перевірити, що все працює правильно.
Час розкрити хитрість! Оновіть макет Twig, щоб викликати щойно створений контролер:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -16,11 +16,7 @@
<body>
<header>
<h1><a href="{{ path('homepage') }}">Guestbook</a></h1>
- <ul>
- {% for conference in conferences %}
- <li><a href="{{ path('conference', { slug: conference.slug }) }}">{{ conference }}</a></li>
- {% endfor %}
- </ul>
+ {{ render(path('conference_header')) }}
<hr />
</header>
{% block body %}{% endblock %}
І вуаля! Оновіть сторінку, і веб-сайт все одно відображатиметься так само.
Tip
Використовуйте панель профілювальника Symfony "Request / Response", щоб дізнатися більше про основний запит і його підзапити.
Тепер, щоразу, коли ви переходите на сторінку в браузері, виконуються два HTTP-запити: один для заголовка й один для головної сторінки. Ви погіршили продуктивність. Вітаю!
HTTP-виклик заголовка конференції на даний момент виконується всередині Symfony, тому HTTP-транзакція не відбувається. Це також означає, що немає можливості скористатися заголовками HTTP-кешу.
Перетворіть виклик на "реальний" HTTP за допомогою ESI.
По-перше, увімкніть підтримку ESI:
1 2 3 4 5 6 7 8 9 10 11
--- a/config/packages/framework.yaml
+++ b/config/packages/framework.yaml
@@ -12,7 +12,7 @@ framework:
cookie_samesite: lax
storage_factory_id: session.storage.factory.native
- #esi: true
+ esi: true
#fragments: true
php_errors:
log: true
Потім використовуйте render_esi
замість render
:
1 2 3 4 5 6 7 8 9 10 11
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -16,7 +16,7 @@
<body>
<header>
<h1><a href="{{ path('homepage') }}">Guestbook</a></h1>
- {{ render(path('conference_header')) }}
+ {{ render_esi(path('conference_header')) }}
<hr />
</header>
{% block body %}{% endblock %}
Якщо Symfony виявить зворотний проксі, який знає, як поводитися з ESI, він вмикає підтримку автоматично (якщо ні, то він переходить до синхронної обробки підзапиту).
Оскільки зворотний проксі Symfony підтримує ESI, перевірмо його журнали (спочатку видаліть кеш — див. "Очищення" нижче):
1
$ curl -s -I -X GET https://127.0.0.1:8000/
1 2 3 4 5 6 7 8 9 10 11 12
HTTP/2 200
age: 0
cache-control: must-revalidate, no-cache, private
content-type: text/html; charset=UTF-8
date: Mon, 28 Oct 2019 08:20:05 GMT
expires: Mon, 28 Oct 2019 08:20:05 GMT
x-content-digest: en4dd846a34dcd757eb9fd277f43220effd28c00e4117bed41af7f85700eb07f2c
x-debug-token: 719a83
x-debug-token-link: https://127.0.0.1:8000/_profiler/719a83
x-robots-tag: noindex
x-symfony-cache: GET /: miss, store; GET /conference_header: miss
content-length: 50978
Оновіть кілька разів: відповідь за посиланням /
закешовано, а за посиланням /conference_header
— ні. Ми досягли чогось значного: маючи всю сторінку в кеші, одна частина все ще залишається динамічною.
Але це зовсім не те, чого ми хочемо. Закешуйте хедер сторінки на одну годину, незалежно від усього іншого:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
--- a/src/Controller/ConferenceController.php
+++ b/src/Controller/ConferenceController.php
@@ -44,9 +44,12 @@ class ConferenceController extends AbstractController
#[Route('/conference_header', name: 'conference_header')]
public function conferenceHeader(ConferenceRepository $conferenceRepository): Response
{
- return new Response($this->twig->render('conference/header.html.twig', [
+ $response = new Response($this->twig->render('conference/header.html.twig', [
'conferences' => $conferenceRepository->findAll(),
]));
+ $response->setSharedMaxAge(3600);
+
+ return $response;
}
#[Route('/conference/{slug}', name: 'conference')]
Кеш тепер увімкнено для обох запитів:
1
$ curl -s -I -X GET https://127.0.0.1:8000/
1 2 3 4 5 6 7 8 9 10 11
HTTP/2 200
age: 613
cache-control: public, s-maxage=3600
content-type: text/html; charset=UTF-8
date: Mon, 28 Oct 2019 07:31:24 GMT
x-content-digest: en15216b0803c7851d3d07071473c9f6a3a3360c6a83ccb0e550b35d5bc484bbd2
x-debug-token: cfb0e9
x-debug-token-link: https://127.0.0.1:8000/_profiler/cfb0e9
x-robots-tag: noindex
x-symfony-cache: GET /: fresh; GET /conference_header: fresh
content-length: 50978
Заголовок x-symfony-cache
містить два елементи: основний запит /
і підзапит (ESI conference_header
). Обидва знаходяться в кеші (fresh
).
Стратегія кешу головної сторінки та її ESI можуть бути різними. Якщо у нас є сторінка "about", ми можемо зберегти її в кеш на тиждень, і при цьому заголовок буде оновлюватися щогодини.
Видаліть слухача, оскільки він нам більше не потрібен:
1
$ rm src/EventSubscriber/TwigEventSubscriber.php
Очищення HTTP-кешу для тестування
Тестування веб-сайту в браузері або за допомогою автоматичних тестів стає трохи складнішим з шаром кешування.
Ви можете вручну видалити весь HTTP-кеш, видаливши каталог var/cache/dev/http_cache/
:
1
$ rm -rf var/cache/dev/http_cache/
Ця стратегія погано працює, якщо ви хочете інвалідувати лише деякі URL-адреси або якщо ви хочете інтегрувати інвалідацію кешу у свої функціональні тести. Додаймо невелику, доступну лише для адміністратора, кінцеву точку HTTP, щоб інвалідувати деякі URL-адреси:
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
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -36,3 +36,5 @@ services:
tags:
- { name: 'doctrine.orm.entity_listener', event: 'prePersist', entity: 'App\Entity\Conference'}
- { name: 'doctrine.orm.entity_listener', event: 'preUpdate', entity: 'App\Entity\Conference'}
+
+ Symfony\Component\HttpKernel\HttpCache\StoreInterface: '@http_cache.store'
--- a/src/Controller/AdminController.php
+++ b/src/Controller/AdminController.php
@@ -8,6 +8,8 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\HttpCache\StoreInterface;
+use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Workflow\Registry;
@@ -52,4 +54,16 @@ class AdminController extends AbstractController
'comment' => $comment,
]));
}
+
+ #[Route('/admin/http-cache/{uri<.*>}', methods: ['PURGE'])]
+ public function purgeHttpCache(KernelInterface $kernel, Request $request, string $uri, StoreInterface $store): Response
+ {
+ if ('prod' === $kernel->getEnvironment()) {
+ return new Response('KO', 400);
+ }
+
+ $store->purge($request->getSchemeAndHttpHost().'/'.$uri);
+
+ return new Response('Done');
+ }
}
Новий контролер був обмежений лише HTTP-методом PURGE
. Цей метод не входить у стандарт HTTP, але він широко використовується для інвалідації кешів.
За замовчуванням параметри маршруту не можуть містити символ /
, оскільки він розділяє сегменти URL-адреси. Ви можете перевизначити це обмеження для останнього параметра маршруту, наприклад uri
, встановивши ваш власний шаблон вимог (.*
).
Те, як ми отримуємо екземпляр HttpCache
, також може виглядати дещо дивним; ми використовуємо анонімний клас, оскільки отримати доступ до "реального" неможливо. Екземпляр HttpCache
обертає справжнє ядро, яке не знає про шар кешу, як це і має бути.
Інвалідуйте головну сторінку та хедер конференції за допомогою наступних викликів cURL:
1 2
$ curl -s -I -X PURGE -u admin:admin `symfony var:export SYMFONY_PROJECT_DEFAULT_ROUTE_URL`/admin/http-cache/
$ curl -s -I -X PURGE -u admin:admin `symfony var:export SYMFONY_PROJECT_DEFAULT_ROUTE_URL`/admin/http-cache/conference_header
Підкоманда symfony var:export SYMFONY_PROJECT_DEFAULT_ROUTE_URL
повертає поточну URL-адресу локального веб-сервера.
Note
Контролер не має імені маршруту, оскільки він ніколи не буде згадуватися в коді.
Групування подібних маршрутів за префіксом
Два маршрути в адміністративному контролері мають той самий префікс /admin
. Замість того щоб повторювати його у всіх маршрутах, виконайте рефакторинг маршрутів, щоб налаштувати префікс у самому класі:
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
--- a/src/Controller/AdminController.php
+++ b/src/Controller/AdminController.php
@@ -15,6 +15,7 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Workflow\Registry;
use Twig\Environment;
+#[Route('/admin')]
class AdminController extends AbstractController
{
private $twig;
@@ -28,7 +29,7 @@ class AdminController extends AbstractController
$this->bus = $bus;
}
- #[Route('/admin/comment/review/{id}', name: 'review_comment')]
+ #[Route('/comment/review/{id}', name: 'review_comment')]
public function reviewComment(Request $request, Comment $comment, Registry $registry): Response
{
$accepted = !$request->query->get('reject');
@@ -55,7 +56,7 @@ class AdminController extends AbstractController
]));
}
- #[Route('/admin/http-cache/{uri<.*>}', methods: ['PURGE'])]
+ #[Route('/http-cache/{uri<.*>}', methods: ['PURGE'])]
public function purgeHttpCache(KernelInterface $kernel, Request $request, string $uri, StoreInterface $store): Response
{
if ('prod' === $kernel->getEnvironment()) {
Кешування інтенсивних операцій ЦП/пам'яті
У нас на веб-сайті немає алгоритмів, що інтенсивно використовують ЦП або пам'ять. Щоб поговорити про локальні кеші, створімо команду, яка відображає поточний крок, над яким ми працюємо (точніше, ім'я тегу Git, закріпленого за поточною фіксацією Git).
Компонент Symfony Process дозволяє вам виконати команду й отримати результат (стандартний вивід і вивід помилок).
Реалізуйте команду:
Note
Ви могли б використовувати make:command
, щоб створити команду:
1
$ symfony console make:command app:step:info
Що робити, якщо ми хочемо закешувати вивід на декілька хвилин? Використовуйте кеш Symfony.
І оберніть код логікою кешу:
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
--- a/src/Command/StepInfoCommand.php
+++ b/src/Command/StepInfoCommand.php
@@ -6,16 +6,31 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\Process;
+use Symfony\Contracts\Cache\CacheInterface;
class StepInfoCommand extends Command
{
protected static $defaultName = 'app:step:info';
+ private $cache;
+
+ public function __construct(CacheInterface $cache)
+ {
+ $this->cache = $cache;
+
+ parent::__construct();
+ }
+
protected function execute(InputInterface $input, OutputInterface $output): int
{
- $process = new Process(['git', 'tag', '-l', '--points-at', 'HEAD']);
- $process->mustRun();
- $output->write($process->getOutput());
+ $step = $this->cache->get('app.current_step', function ($item) {
+ $process = new Process(['git', 'tag', '-l', '--points-at', 'HEAD']);
+ $process->mustRun();
+ $item->expiresAfter(30);
+
+ return $process->getOutput();
+ });
+ $output->writeln($step);
return 0;
}
Процес тепер викликається лише в тому випадку, якщо елемент app.current_step
відсутній у кеші.
Профілювання та порівняння продуктивності
Ніколи не додавайте кеш наосліп. Майте на увазі, що додавання деякого кешу додає рівень складності. Складно передбачити, що буде працювати швидко, а що повільно, ви можете опинитися в ситуації, коли кеш робить ваш застосунок повільнішим.
Завжди вимірюйте вплив додавання кешу за допомогою інструменту профілювання, такого як Blackfire.
Зверніться до кроку про "Продуктивність", щоб дізнатися більше про те, як ви можете використовувати Blackfire для тестування коду перед розгортанням.
Налаштування зворотного проксі-кешу в продакшн
Замість використання зворотного проксі Symfony у середовищі розробки ми збираємося використовувати "надійніший" зворотний проксі Varnish.
Додайте Varnish у сервіси Platform.sh:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
--- a/.platform/services.yaml
+++ b/.platform/services.yaml
@@ -2,3 +2,12 @@
database:
type: postgresql:13
disk: 1024
+
+varnish:
+ type: varnish:6.0
+ relationships:
+ application: 'app:http'
+ configuration:
+ vcl: !include
+ type: string
+ path: config.vcl
Використовуйте Varnish у якості основної точки входу в маршрути:
1 2 3 4 5 6
--- a/.platform/routes.yaml
+++ b/.platform/routes.yaml
@@ -1,2 +1,2 @@
-"https://{all}/": { type: upstream, upstream: "app:http" }
+"https://{all}/": { type: upstream, upstream: "varnish:http", cache: { enabled: false } }
"http://{all}/": { type: redirect, to: "https://{all}/" }
Нарешті, створіть файл config.vcl
, щоб налаштувати Varnish:
Увімкнення підтримки ESI у Varnish
Підтримка ESI у Varnish має бути увімкнена явно для кожного запиту. Щоб охопити всі запити, Symfony використовує стандартні заголовки Surrogate-Capability
та Surrogate-Control
для узгодження підтримки ESI:
Очищення кешу Varnish
Інвалідація кешу у продакшн, ймовірно, ніколи не знадобиться, за винятком особливих потреб і, можливо, у не-master
гілках. Якщо вам потрібно часто очищати кеш, це, ймовірно, означає, що стратегія кешу має бути змінена (шляхом зниження TTL або використовуючи стратегію валідації замість стратегії закінчення терміну дії).
У будь-якому випадку, подивімося, як налаштувати Varnish для інвалідації кешу:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
--- a/.platform/config.vcl
+++ b/.platform/config.vcl
@@ -1,6 +1,13 @@
sub vcl_recv {
set req.backend_hint = application.backend();
set req.http.Surrogate-Capability = "abc=ESI/1.0";
+
+ if (req.method == "PURGE") {
+ if (req.http.x-purge-token != "PURGE_NOW") {
+ return(synth(405));
+ }
+ return (purge);
+ }
}
sub vcl_backend_response {
У реальному житті ви, ймовірно, обмежите IP-адреси, як описано в документації по Varnish.
Тепер очистьте кеш для деяких URL-адрес:
1 2
$ curl -X PURGE -H 'x-purge-token: PURGE_NOW' `symfony cloud:env:url --pipe --primary`
$ curl -X PURGE -H 'x-purge-token: PURGE_NOW' `symfony cloud:env:url --pipe --primary`conference_header
URL-адреси виглядають дещо дивно через те, що адреси, повернені env:url
, вже закінчуються символом /
.