W tym poście chce Ci przybliżyć podstawy Varnisha. Przejdziesz przez konfigurację lokalną Dockera oraz najważniejszą część tj. samą konfigurację Varnisha.
Wiedza ta na pewno Ci jeszcze nie pozwoli postawić Varnisha na produkcji ani poprawnie skonfigurować środowiska dla produkcji ale będzie to dobry wstęp do tego aby zrozumieć jak to wszystko mniej więcej działa.
Wpis jest częścią serii o Varnish’u.
Cały kod możesz znaleźć [TUTAJ] oraz [TUTAJ]
Środowisko lokalne
Zacznijmy od konfiguracji Dockera. Będzie ona banalnie prosta. Będzie w niej FPM, Nginx oraz temat naszego wpisu tj. Varnish.
version: '3'
services:
nginx:
image: nginx:latest
ports:
- '80:80'
volumes:
- './:/app'
- '.docker/site.conf:/etc/nginx/conf.d/default.conf'
depends_on:
- fpm
links:
- fpm
fpm:
image: php:8.1-fpm
volumes:
- './:/app'
varnish:
image: varnish:latest
depends_on:
- nginx
volumes:
- .docker/varnish.vcl:/etc/varnish/default.vcl
links:
- nginx
ports:
- '5000:80'
Nie będę się tutaj rozpisywać odnośnie całej konfiguracji dockera bo wykracza to poza ramę wpisu ale postaram się kiedyś zrobić serię na ten temat. 😉
Dla nas kluczowymi informacjami jest to że pod linkiem http://localhost/ znajdziesz odpowiedź bez cache tj. od razu z Nginx a pod linkiem http://localhost:5000/ już z obsługą Varnisha.
Zacznijmy od najprostszej konfiguracji Varnisha
vcl 4.1;
backend default {
.host = "nginx";
.port = "80";
}
Powyższa konfiguracja to tak naprawdę przekazanie Varnishowi jedynie hosta oraz portu do którego ma stukać w przypadku braku danych.
Mając konfiguracje musimy przygotować sobie kilka przykładowych endpointów dzięki którym będziemy widzieć jak się zachowuje Varnish.
Aby było nam wygodniej patrzeć jak się zachowuje cache każdy endpoint będzie zwracał nam aktualną data + godzinę.
<?php
declare(strict_types=1);
namespace App\Controller;
use DateTime;
use DateTimeZone;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class HomeController extends AbstractController
{
#[Route('/')]
#[Cache(maxage: 30, public: true, mustRevalidate: false)]
public function index(): Response
{
return $this->generateResponse();
}
#[Route('/revalidate')]
#[Cache(maxage: 20, public: true, mustRevalidate: true, staleWhileRevalidate: 5, staleIfError: 10)]
public function revalidate(): Response
{
return $this->generateResponse();
}
#[Route('/without-cache')]
public function withoutCache(): Response
{
return $this->generateResponse();
}
private function generateResponse(): Response
{
return new Response(
(new DateTime())
->setTimezone(new DateTimeZone('Europe/Warsaw'))
->format('Y-m-d H:i:s:v')
);
}
}
Pierwszym miejscem jest nasza strona główna. Ma zostać ona zacachowana na 20 sekund a w przypadku braku odpowiedzi nie musi być rewalidowana.
Następny punkt jest dostępny pod adresem localhost:5000/revalidate. Gdy cache wygaśnie musi on zostać zrewalidowany. Zapewne zauważyłeś dwa ciekawe argumenty przekazane w atrybucie Cache tj. staleWhileRevalidate czyli nagłówek HTTP stale-while-revalidate oraz staleIfError tj. nagłówek stale-if-error.
Pierwszy nagłówek odpowiada za czas przez który można wykorzystywać wygaśnięty cache zaś stale-if-error odpowiada za czas przez który można wykorzystywać wygaśnięty cache w przypadku błędu serwera np. braku jego dostępności.
Ostatnia strona jest dostępna pod linkiem localhost:5000/without-cache. Serwuje ona nam zawsze świeżą odpowiedź.
Co zrobi Varnish gdy Nginx padnie
docker-compose -f docker-compose.varnish.yml stop nginx
W momencie gdy padnie nam nginx, fpm itd. Varnish będzie odpowiadał do momentu aż cache wygaśnie. Po wygaśnięciu podejmie próbę pobrania świeżej wartości ale zakończy się to rzuceniem 503. 504 itp. dla użytkownika bo nasz serwer nie odpowiada. Gdy nasza padnięta usługa wstanie i serwer zwróci poprawną odpowiedź Varnish od razu rozpocznie jej serwowanie użytkownikowi.
Ważna informacją też jest to że Varnish nie będzie zapisywał do cache niczego co zwróci kod błędu np. 504.
Dodajmy healtcheck
vcl 4.1;
backend default {
.host = "nginx";
.port = "80";
.probe = {
.url = "/health";
.timeout = 3s;
.interval = 10s;
.window = 5;
.threshold = 3;
}
}
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class HealthCheckController extends AbstractController
{
#[Route('health')]
public function index(): Response
{
return new JsonResponse(['status' => 'ok']);
}
}
Jak widzisz HealthCheckController jest banalnie prosty, zresztą konfiguracja tak samo.
W tym przykładzie Varnish będzie sprawdzał zdrowie serwera (w configu: probe.inverval) co 10 sekund, z timeoutem (w configu: probe.timeout) ustawionym na 3 sekundy. Każda próba będzie wysyłała request pod adres (w configu: probe.url) /health. W przypadku gdy 3 z ostatnich 5 prób się powiedzie, uznajemy serwer za zdrowy. W momencie gdy będzie 2/5, 1/5 lub 0/5 prób udanych, serwer zostanie oznaczony jako chory.
Jakie korzyści da Varnish z używania health checka
W porównaniu do poprzedniego przypadku jak przywrócisz Nginx do żywych zapewne zauważysz że Varnish od razu nie zwróci nam odpowiedzi. Zwróci ją nam dopiero gdy serwer zostanie oznaczony jako zdrowy.
Jest to bardzo dobre rozwiązanie ponieważ gdyby udało nam się podnieść usługę na dosłownie kilka sekund Varnish mógłby wrzucić w cache coś czego byśmy nie chcieli np. jakiś błąd pod 200-tką zwróconą itd. Takie zachowanie mogłoby również zrobić w przysłowiowego konia samego użytkownika że już wszystko działa.
Podsumowanie
Mam nadzieję że ten wpis dobrze wprowadził Cię w podstawy koncept Varnisha. W dalszych wpisach będziemy poszerzać tą wiedzę tak aby zrozumieć większość podstawowej wiedzy. 🙂
Następny wpis z serii Varnishu możesz znaleźć [KLIKNIJ TUTAJ]