lukaszstaniszewski.pl

blog programistyczny

lukaszstaniszewski.pl

blog programistyczny

Varnish – czyszczenie cache – cz. 4

Zapewne nie raz spotkałeś się z sytuacją w której przez cache nie mogłeś zmienić szybko danych widocznych dla użytkownika. W ramach tego wpisu dowiesz się w jaki sposób usunąć cache dla jednego jak i wielu adresów.

 

Całą użytą konfiguracje możesz znaleźć [TUTAJ].

Wpis jest częścią serii Varnish’u

Konfiguracja PURGE

Zacznijmy od skonfigurowania obsługi metody PURGE.  Żeby doszło do wyczyszczenia cache wystarczy że w vcl_revc wykonasz return(purge). Po zwróceniu purge wykonywany jest podprogram vcl_hash. Niczym się to nie różni od zwykłego requestu. Kiedy vcl_hash wywoła return(lookup) Varnish wyczyści obiekt, a następnie wywoła vcl_purge.

vcl 4.1;

backend default {
    .host = "nginx";
    .port = "80";

    .probe = {
        .url = "/health";
        .timeout = 3s;
        .interval = 10s;
        .window = 5;
        .threshold = 3;
    }
}

acl purge {
    "localhost";
    "fpm";
}

sub vcl_recv {
    if (req.method == "PURGE") {
        if (!client.ip ~ purge) {
            return(synth(405,"Not allowed."));
        }

        return (purge);
    }
}

Jak się pewnie domyśliłeś w pierwszej kolejności musimy zabezpieczyć się przed sytuacją w której użytkownik ma prawo wyczyścić nam cache. Służy do tego lista acl (PS fpm to nazwa hosta w dockerze). W liście znajdują się adresy z których możemy wywoływać metody PURGE oraz później BAN.

W jaki sposób usuwać pojedynczy adres

<?php

namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\HttpClient\HttpClientInterface;

#[AsCommand(
    name: 'purge:url',
    description: 'Purge in varnish url',
)]
class PurgeUrlCommand extends Command
{
    private const URL = 'url';

    private const VARNISH_URL = 'http://varnish';

    private const DATA = [
        [
            'method' => 'GET',
            'label' => 'Create cache',
        ],
        [
            'method' => 'GET',
            'label' => 'Check cache is working',
        ],
        [
            'method' => 'PURGE',
            'label' => 'Purge',
        ],
        [
            'method' => 'GET',
            'label' => 'Check purge is working',
        ],
        [
            'method' => 'GET',
            'label' => 'Check after purge cache is working',
        ],
    ];

    public function __construct(private readonly HttpClientInterface $httpClient)
    {
        parent::__construct(null);
    }

    protected function configure(): void
    {
        $this
            ->addArgument(self::URL, InputArgument::OPTIONAL, 'Url for purge')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $url = sprintf(
            '%s%s',
            self::VARNISH_URL,
            $input->hasArgument(self::URL) ? $input->getArgument(self::URL) : ''
        );

        foreach (self::DATA as $data) {
            $output->writeln($data['label']);
            $response = $this->httpClient->request($data['method'], $url);

            $output->writeln($response->getContent());

            sleep(1);
        }

        return Command::SUCCESS;
    }
}

Teraz przyszła najprzyjemniejsza część nauki tj. testy konfiguracji, wystarczy że odpalisz command (jeżeli korzystasz z repozytorium wskazanego na samym początku wpisu) php bin/console purge:url i możesz zobaczyć efekt konfiguracji Varnisha. Poniżej możesz zobaczyć rezultat wykonanej komendy.

Create cache
2022-09-20 19:47:54:002

Check cache is working
2022-09-20 19:47:54:002

Purge
<!DOCTYPE html>
<html>
  <head>
    <title>200 Purged</title>
  </head>
  <body>
    <h1>Error 200 Purged</h1>
    <p>Purged</p>
    <h3>Guru Meditation:</h3>
    <p>XID: 11</p>
    <hr>
    <p>Varnish cache server</p>
  </body>
</html>

Check purge is working
2022-09-20 19:48:37:207

Check after purge cache is working
2022-09-20 19:48:37:207

A co gdy chcemy usunąć wiele podobnych adresów

Do czyszczenia wielu adresów służą bany. Funkcja ta przeszukuje pamięć zgodnie z podanymi warunkami np. tak jak w naszej konfiguracji tj. po wyrażeniu regularnych oraz hoście. Możesz dodać tutaj również swoje inne warunki.

Jednym z ważnych aspektów optymalizacyjnych dla banów jest świadomość że bany starsze niż najstarsze obiekty w pamięci podręcznej są odrzucane bez oceny. Jeśli masz dużo obiektów z długim czasem TTL, możesz nagromadzić wiele banów. Skutkiem tego będzie spadek wydajności (tutaj warto wspomnieć o ban lurker ale w tej kwestii odsyłam Cię do dokumentacji :)). Ilość nałożonych banów możesz sprawdzić za pomocą commanda varnishadm ban.list.

Present bans:
1663696139.700177     0 -  req.http.host == varnish && req.url ~ ^\/many\/test

Pora na konfiguracje, będziemy bazować na konfiguracji z poprzednich wpisów. 🙂

vcl 4.1;

backend default {
    .host = "nginx";
    .port = "80";

    .probe = {
        .url = "/health";
        .timeout = 3s;
        .interval = 10s;
        .window = 5;
        .threshold = 3;
    }
}

acl purge {
    "localhost";
    "fpm";
}

sub vcl_recv {
    if (req.method == "PURGE") {
        if (!client.ip ~ purge) {
            return(synth(405,"Not allowed."));
        }

        return (purge);
    }

    if (req.method == "BAN") {
        if (!client.ip ~ purge) {
            return (synth(405));
        }

        if (!req.http.X-Purge-Regex) {
            return (purge);
        }

        ban("req.http.host == " + req.http.host + " && req.url ~ " + req.http.X-Purge-Regex);

        return(synth(200, "Ban added"));
    }
}

W porównaniu do PURGE bany możesz zakładać również z poziomu CLI. Służy do tego command varnishadm ban.

Pora wyczyścić cache

<?php

namespace App\Command;

use Exception;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

#[AsCommand(
    name: 'ban',
    description: 'Ban urls by regex',
)]
class BanCommand extends Command
{
    private const URLS = 'urls';

    private const REGEX = 'regex';

    private const VARNISH_URL = 'http://varnish';

    public function __construct(private readonly HttpClientInterface $httpClient)
    {
        parent::__construct(null);
    }

    protected function configure(): void
    {
        $this
            ->addArgument(self::REGEX,InputArgument::REQUIRED, 'Regex')
            ->addArgument(self::URLS, InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Urls')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $urls = $input->getArgument(self::URLS);

        $output->writeln('Create cache');
        $this->callToUrls($urls, $output);

        $output->writeln('Test is working cache');
        $this->callToUrls($urls, $output);

        $output->writeln('Create ban');
        $this->ban($urls, $input->getArgument(self::REGEX), $output);

        $output->writeln('Check if ban working');
        $this->callToUrls($urls, $output);

        $output->writeln('Check if cache working after ban');
        $this->callToUrls($urls, $output);

        return Command::SUCCESS;
    }

    private function callToUrls(array $urls, OutputInterface $output): void
    {
        foreach ($urls as $url) {
            $response = $this->httpClient->request(
                'GET',
                sprintf('%s%s', self::VARNISH_URL, $url)
            );

            $output->writeln($response->getContent());

            sleep(1);
        }
    }

    private function ban(mixed $urls, string $regex, OutputInterface $output): void
    {
        $response = $this->httpClient->request(
            'BAN',
            sprintf('%s%s', self::VARNISH_URL, current($urls)),
            [
                'headers' => [
                    'X-Purge-Regex' => $regex,
                ],
            ],
        );

        $output->writeln($response->getContent());
    }
}

php bin/console ban "^\/many\/test-" /many/test-1 /many/test-2 /many/foo / /many/bar

Przyszła pora sprawdzić również jak działa metoda BAN. Poniżej możesz zobaczyć rezultat wykonanej komendy.

Create cache
test-1 2022-09-20 19:51:58:333
test-2 2022-09-20 19:51:48:600
foo 2022-09-20 19:51:49:800
2022-09-20 19:51:51:015
bar 2022-09-20 19:51:52:160

Test is working cache
test-1 2022-09-20 19:51:58:333
test-2 2022-09-20 19:51:48:600
foo 2022-09-20 19:51:49:800
2022-09-20 19:51:51:015
bar 2022-09-20 19:51:52:160

Create ban
<!DOCTYPE html>
<html>
  <head>
    <title>200 Ban added</title>
  </head>
  <body>
    <h1>Error 200 Ban added</h1>
    <p>Ban added</p>
    <h3>Guru Meditation:</h3>
    <p>XID: 32808</p>
    <hr>
    <p>Varnish cache server</p>
  </body>
</html>

Check if ban working
test-1 2022-09-20 19:51:58:333
test-2 2022-09-20 19:53:48:844
foo 2022-09-20 19:51:49:800
2022-09-20 19:51:51:015
bar 2022-09-20 19:51:52:160

Check if cache working after ban
test-1 2022-09-20 19:51:58:333
test-2 2022-09-20 19:53:48:844
foo 2022-09-20 19:53:49:994
2022-09-20 19:51:51:015
bar 2022-09-20 19:51:52:160

Podsumowanie

Mam nadzieję że ten wpis pokazał Ci jak możesz czyścić cache w Varnish. Pominąłem tutaj kwestie optymalizacyjne ponieważ ta seria ma na celu przedstawić różne aspekty podstaw Varnisha a nie wszystkich aspektów jednak zachęcam Cię do poświęcenia jeszcze 10 minut na przestudiowanie dokumentacji 🙂

Źródła:

Varnish – czyszczenie cache – cz. 4
Przewiń na górę