Użycie pamięci podręcznej w celu zwiększenia wydajności
Problemy z wydajnością mogą pojawić się wraz ze wzrostem popularności aplikacji. Typowe problemy: nie zrobiłeś indeksów baz danych, a ze strony idzie grad zapytań SQL. Z pustą bazą danych nie będziesz miał żadnych problemów, ale mogą się one pojawić wraz z większym natężeniem ruchu i wzrostem ilości danych.
Dodawanie nagłówków pamięci podręcznej HTTP (ang. HTTP cache headers)
Wykorzystanie strategii użycia pamięci podręcznej HTTP to świetny sposób na maksymalizację wydajności dla użytkowników końcowych przy niewielkim nakładzie pracy. Dodaj zwrotny serwer pośredniczący pamięci podręcznej (ang. reverse proxy cache) w środowisku produkcyjnym oraz użyj CDN do buforowania na węźle krańcowym dla jeszcze lepszej wydajności.
Zapiszmy stronę domową w pamięci podręcznej na godzinę:
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')]
Metoda setSharedMaxAge()
konfiguruje wygaśnięcie pamięci podręcznej dla zwrotnego serwera pośredniczącego (ang. reverse proxy). Użyj setMaxAge()
do kontrolowania pamięci podręcznej przeglądarki. Czas wyrażany jest w sekundach (1 godzina = 60 minut = 3600 sekund).
Buforowanie strony konferencji jest wyzwaniem, ponieważ jest ona bardzo dynamiczna. Każdy może dodać komentarz w dowolnej chwili i nikt nie chce czekać godzinę, aby zobaczyć go online. W takich przypadkach należy stosować strategię walidacji HTTP.
Aktywacja Symfony HTTP Cache Kernel
Aby przetestować strategię pamięci podręcznej HTTP, włącz zwrotny serwer pośredniczący (ang. reverse proxy) Symfony HTTP, ale tylko dla środowiska "development" (dla środowiska "production" użyjemy bardziej "wydajnego" rozwiązania):
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
Oprócz tego, że jest to pełnoprawny zwrotny serwer pośredniczący (ang. reverse proxy) HTTP, dodaje on (poprzez klasę HttpCache
) kilka przydatnych informacji o debugowaniu jako nagłówki HTTP. Bardzo pomaga to w sprawdzaniu poprawności nagłówków pamięci podręcznej, które ustawiliśmy.
Możesz sprawdzić jego działanie na stronie głównej:
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
W przypadku pierwszego żądania, serwer pamięci podręcznej mówi, że był to miss
(brak wpisu w pamięci podręcznej) i że wykonał akcję store
(buforowania odpowiedzi). Sprawdź nagłówek cache-control
(kontrola pamięci podręcznej), aby zobaczyć konfigurację odpowiedzialną za strategię pamięci podręcznej.
W przypadku kolejnych żądań odpowiedź jest przechowywana w pamięci podręcznej. Również age
(czas, który upłynął od ostatniego zapisu) został zaktualizowany:
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
Unikanie zapytań SQL za pomocą ESI
Nasłuchiwacz (ang. listener) TwigEventSubscriber
wstrzykuje globalną zmienną do Twiga ze wszystkimi obiektami konferencji. Czyni to dla każdej strony witryny. Jest to prawdopodobnie świetne miejsce do optymalizacji.
Nie będziesz dodawał nowych konferencji codziennie, więc kod odpytuje o dokładnie te same dane z bazy danych w kółko.
Możemy chcieć buforować nazwy konferencji i slugi użwajac Symfony Cache, ale, kiedy tylko jest to możliwe, lubię polegać na infrastrukturze buforowania HTTP.
Jeśli chcesz zapisać w pamięci podręcznej fragment strony, przenieś go poza bieżące żądanie HTTP, tworząc żądanie cząstkowe. ESI jest do tego idealnym rozwiązaniem. ESI jest sposobem osadzenia wyniku żądania HTTP w innym żądaniu.
Utwórz kontroler zwracający tylko fragment kodu HTML, który wyświetla konferencje:
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
{
Utwórz odpowiedni szablon:
Otwórz /conference/header
aby sprawdzić, czy wszystko działa poprawnie.
Czas na magiczną sztuczkę! Zaktualizuj szablon Twig, aby wywołać kontroler, który właśnie stworzyliśmy:
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 %}
Et voilà. Odśwież stronę, a witryna nadal wyświetla się tak samo.
Tip
Użyj panelu "Request / Response" profilera Symfony, aby dowiedzieć się więcej o głównym żądaniu i żądaniach cząstkowych.
Teraz za każdym razem, gdy wejdziesz na stronę w przeglądarce, wykonywane są dwa żądania HTTP, jedno dla nagłówka i jedno dla strony głównej. Pogorszyliśmy wydajność. Gratulacje!
Wywołanie HTTP nagłówka konferencji jest obecnie wykonywane wewnętrznie przez Symfony, dzięki czemu unikamy zewnętrznego połączenia HTTP. Oznacza to również, że nie ma możliwości skorzystania z nagłówków pamięci podręcznej HTTP.
Skonwertuj połączenie na "prawdziwe" połączenie HTTP za pomocą ESI.
Po pierwsze, włącz obsługę 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
Następnie użyj render_esi
zamiast 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 %}
Jeśli Symfony wykryje zwrotny serwer pośredniczący (ang. reverse proxy), który wie, jak radzić sobie z ESI – włącza obsługę automatycznie. Jeśli nie, to wraca do renderowania żądania cząstkowego synchronicznie.
Ponieważ zwrotny serwer pośredniczący (ang. reverse proxy) Symfony obsługuje ESI, sprawdźmy jego logi (najpierw wyczyść pamięć podręczną – zobacz "Usuwanie" poniżej):
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
Odśwież kilka razy: odpowiedź /
jest buforowana, a /conference_header
już nie. Osiągnęliśmy coś wspaniałego: mamy całą stronę w pamięci podręcznej, ale wciąż jedna z jej części jest dynamiczna.
Nie tego jednak chcemy. Zapisz stronę z nagłówkiem w pamięci podręcznej na godzinę, niezależnie od wszystkiego innego:
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')]
Pamięć podręczna jest teraz włączona dla obu żądań:
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
Nagłówek x-symfony-cache
zawiera dwa elementy: żądanie główne /
oraz żądanie cząstkowe (conference_header
ESI). Oba znajdują się w pamięci podręcznej (fresh
).
Strategia pamięci podręcznej może być odmienna dla strony głównej i jej ESI. Jeśli mamy stronę "o nas", możemy chcieć ją przechować przez tydzień w pamięci podręcznej i nadal mieć nagłówek aktualizowany co godzinę.
Usuń nasłuchiwacz (ang. listener), bo już go nie potrzebujemy:
1
$ rm src/EventSubscriber/TwigEventSubscriber.php
Oczyszczanie pamięci podręcznej HTTP na potrzeby testów
Testowanie strony internetowej w przeglądarce lub za pomocą testów automatycznych staje się nieco trudniejsze w przypadku warstwy pamięci podręcznej.
Możesz ręcznie wyczyścić całą pamięć podręczną HTTP usuwając katalog var/cache/dev/http_cache/
:
1
$ rm -rf var/cache/dev/http_cache/
To podejście nie działa za dobrze, jeśli chcesz tylko unieważnić niektóre adresy URL lub jeśli chcesz włączyć unieważnienie pamięci podręcznej do testów funkcjonalnych. Dodajmy mały, dostępny tylko dla konta administracyjnego, punkt końcowy HTTP, aby unieważnić niektóre adresy 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');
+ }
}
Nowy kontroler został ograniczony do metody PURGE
HTTP. Metoda ta nie znajduje się w standardzie HTTP, ale jest powszechnie stosowana do unieważniania pamięci podręcznej.
Domyślnie parametry trasy nie mogą zawierać /
, ponieważ rozdzielają segmenty URL. Ograniczenie to można pominąć w odniesieniu do ostatniego parametru trasy, na przykład uri
. ustawiając własny wzór wymagań (.*
).
Sposób w jaki otrzymujemy instancję HttpCache
może wyglądać nieco dziwnie; używamy klasy anonimowej, ponieważ dostęp do klasy "rzeczywistej" nie jest możliwy. Instancja HttpCache
owija prawdziwe jądro, które nie jest świadome warstwy pamięci podręcznej, tak jak powinno być.
Unieważnienie pamięci podręcznej strony głównej i nagłówka konferencji za pośrednictwem następujących połączeń 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
Podpolecenie symfony var:export SYMFONY_PROJECT_DEFAULT_ROUTE_URL
zwraca bieżący adres URL lokalnego serwera WWW.
Note
Kontroler nie posiada nazwy trasy, ponieważ nigdy nie będziemy się do niego odwoływać w kodzie.
Grupowanie podobnych tras z użyciem prefiksu
Dwie trasy w kontrolerze panelu administracyjnego mają ten sam prefiks /admin
. Zamiast powtarzać go na wszystkich trasach, należy zrefaktorować trasy w celu skonfigurowania prefiksu na samej klasie:
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()) {
Zapisywanie do pamięci podręcznej operacji obciążających procesor/pamięć
Nie mamy na stronie internetowej algorytmów intensywnie wykorzystujących procesor czy pamięć. Aby porozmawiać o lokalnej pamięci podręcznej, stwórzmy polecenie, które wyświetla informację o etapie, nad którym pracujemy, a dokładniej mówiąc, nazwę tagu Git dołączoną do aktualnego zatwierdzenia (ang. commit) Gita.
Komponent Symfony Process pozwala uruchomić polecenie i otrzymać wynik jego działania (dane ze standardowego strumienia wyjścia i standardowego strumienia błędów).
Zaimplementuj polecenie:
Note
Możesz użyć make:command
do stworzenia polecenia:
1
$ symfony console make:command app:step:info
A jeśli chcemy przechowywać dane wyjściowe przez kilka minut? Dodaj do projektu moduł Symfony Cache.
I użyj pamięci podręcznej:
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;
}
Proces jest teraz wywoływany tylko wtedy, gdy element app.current_step
nie znajduje się w pamięci podręcznej.
Profilowanie i porównywanie wydajności
Nigdy nie dodawaj pamięci podręcznej bez zastanowienia. Należy pamiętać, że dodanie pamięci podręcznej zwiększa poziom złożoności. A ponieważ źle nam idzie zgadywanie, co będzie szybkie, a co powolne, możesz znaleźć się w sytuacji, w której pamięć podręczna spowolni Twoją aplikację.
Zawsze zmierz wpływ dodawania pamięci podręcznej za pomocą narzędzia do profilowania, takiego jak Blackfire.
Zapoznaj się z etapem "Wydajność", aby dowiedzieć się więcej o tym, jak można użyć narzędzia Blackfire do przetestowania kodu przed wdrożeniem.
Konfiguracja zwrotnego serwera pośredniczącego pamięci podręcznej (ang. reverse proxy cache) w środowisku produkcyjnym
Zamiast używać zwrotnego proxy Symfony na produkcji, użyjemy bardziej "wydajnego" zwrotnego proxy Varnish.
Dodaj Varnish do usług 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
Użyj serwera Varnish jako głównego punktu wejścia na trasach:
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}/" }
Na koniec, utwórz plik config.vcl
do konfiguracji Varnish:
Włączenie wsparcia ESI na serwerze Varnish
Obsługa ESI na Varnish powinna być włączona bezpośrednio dla każdego żądania. W celu uniwersalizacji Symfony wykorzystuje standardowe nagłówki Surrogate-Capability
i Surrogate-Control
do negocjowania wsparcia ESI:
Oczyszczanie (ang. purging) pamięci podręcznej Varnish.
Unieważnienie pamięci podręcznej w środowisku produkcyjnym prawdopodobnie nigdy nie będzie potrzebne, z wyjątkiem celów awaryjnych i raczej na gałęziach innych niż master
. Jeśli musisz często czyścić pamięć podręczną, prawdopodobnie oznacza to, że strategia użycia pamięci podręcznej powinna zostać poprawiona (poprzez obniżenie TTL lub poprzez zastosowanie strategii walidacji zamiast strategii wygaśnięcia).
W każdym razie, zobaczmy jak skonfigurować serwer Varnish do unieważnienia pamięci podręcznej:
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 {
W rzeczywistości, prawdopodobnie należałoby zastosować ograniczenie po IP, jak to opisano w dokumentacji Varnish.
Oczyść (ang. purge) teraz kilka adresów 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
Adresy URL wyglądają nieco dziwnie, ponieważ adresy zwracane przez env:url
już kończą się na /
.