Varnish, xkey ja cachen tyhjennys sivustoillani

You are currently viewing Varnish, xkey ja cachen tyhjennys sivustoillani

Cache on tehokas työkalu, mutta sen hankaluudet tulevat vastaan siinä vaiheessa, kun pitäisi jotain saada pois ja muutettua.

Cachen invalidisointi (BAN) ja poisto (PURGE) ovat ehkä tärkeimpiä asioita osata. Jos mikä tahansa muuttuu, vaikka vain julkaisisi uuden artikkelin tai podcastin, niin vähintään etusivu ja sivupalkit täytyy saada päivitettyä. Siellä yleensä listat tuoreimmista julkaisuista näkyvät. Mutta samalla päivittyy myös kategorian ja tagien arkistot. Entä RSS-syöte?

Jos noita kaikkia ei käsitellä, niin niissä näkyy cachetettu vanha sisältö ilman uutta juttua.

Tietokonemaailman viisaudet

Ennenkuin jatkan eteenpäin: niin muistutus eräästä IT-sanonnasta:

There are only two hard things in Computer Science: cache invalidation and naming things.

On toinenkin lainalaisuus, joka ei naurata yhtään. Ehkä siksi, että se ei ole vitsi, vaan fakta.

Pystymme muokkaamaan ja tyhjentämään Varnishin cachen, mutta meillä ei ole minkäänlaisia työkaluja vaikuttaa kävijän laitteen cacheen. Selaimet elävät omaa elämäänsä ja joko noudattavat tai eivät noudata ohjeita. Tai tekevät kuten Apple/Safari: cachettaa aggressiivisesti ihan oman päänsä mukaan.

BAN ei poista

BAN on se mitä kutsutaan cachen invalidisoinniksi. BAN ei poista cachetettua objektia (url ja sivu ovat vain yksi objekti muiden joukossa), vaan merkitsee sen päivitettäväksi.

Se on siis jonossa, mutta edelleen käytettävissä. Ja sitä käytetäänkin. Varnish käyttää bannia eräänlaisena puskurina ladatessaan päivitettyä sisältöä, jotta kävijän ei tarvitse odotella.

Kun objekti merkitään BAN, niin sitä ei poisteta cachesta. Kun seuraava kävijä pyytää samaa, sanotaan vaikka WordPressin sivua, niin hänelle tarjotaan sivu cachesta, ja samaan aikaan aloitetaan tuoreen version nouto backendiltä. Kävijän ei tarvitse odotella, vaan saa heti sivunsa — mutta ei uutta, vaan cachetetun vanhan.

Sinä aikana Varnish korvaa muistissaan vanhan version uudella. Joten sitä seuraava kävijä saa uuden ja tuoreen kappaleen, myös cachesta.

Tässä on pieni koukku takana. Tuo pätee, jos cachetetun objektin säilytysaikaa on jäljellä. Ilmaisu on TTL, time to live.

Armon ongelma

Käytössä on myös sellainen asia kuin grace. Eräällä tavalla armonaika. Jos objektin TTL on nolla, niin sitä ei poistetakaan heti välimuistista, vaan säilytysaikaa jatketaan sen verran mitä gracelle on suotu.

Tuossa on se etu, että jos jostain syystä backend on kuormitettuna, on tolkuton kävijäpiikki tai jopa alhaalla, niin objekti pidetään cachessa yli TTL-ajan.

Tuolla on kuitenkin bannin suhteen vähemmän haluttu vaikutus. Jos BAN tehtiin TTL-ajan ollessa plussan puolella, niin taustahaku toimi ja seuraava sai päivitetyn version. Mutta jos siirrytään gracen puolelle, niin tuota ei tehdä. Jokainen BAN-merkitty pyyntö jää ilman taustahakua koko grace-ajan ajaksi jonoon odottamaam. Joten aina tulee hit, jos objekti on cachessa.

Uusi versio haetaan vasta grace-ajan päätyttyä normaalilla return(miss) tuloksella.

  • BAN hakee ja tarjoilee uuden version, jos TTL > 0
  • Jos TTL <= 0 ja grace > 0, niin aina tulee cachesta ja aidosti BAN ei gracen voimassaoloaikana tapahdu
  • Kun grace = 0 tehdään normaali miss, ja BAN olikin tarpeeton

Tuolla on merkitystä silloin kun käyttää lyhyitä TTL aikoja ja näkyvän pitkää gracea. Leikitään, että grace on 1h. Tehdään BAN objektille, joka on grace-ajalla. Silloin uusi versio tarjoillaan vasta tunnin kuluttua.

Minulla tuolla ei ole pääsääntöisesti merkitystä. Minulla on yleinen TTL vuosi. Ei siksi, että minulla cache milloinkaan pääsisi noin vanhaksi, koska taatusti väliin tulee joku Varnishin päivityksen takia tehty ja cachen nollaava systemctl restart varnish tai jopa serverin buutti.

Se on pitkä siksi, että minun ei missään vaiheessa tarvitse miettiä TTL-aikoja, vaan jokainen objekti pysyy taatusti cachessa.

On minulla lyhyempiäkin aikoja käytössä. Podcast- ja RSS-syötteillä on muistaakseni 24 tuntia. Tuo on tehty bottien takia, koska noita kysellään niin usein, enkä halua, että joka kerta paukutellaan sinänsä turhan takia backendiä.

Normaalisti staattisia tiedostoja ei kannata viedä cacheen. Varnishin yksi tarkoitus on vähentää backendin kuormaa ja koska staattiset tiedostot annetaan ilman backendin työtä, niin cache on turha. En halua maksaa CDN:stä, levyni on hitaampi kuin RAM ja minulla on nykyiseen tarpeeseen riittävästi muistia, niin moiset, kuten CSS-tyylitiedostot, javascriptit ja kuvat, pääsevät myös välimuistiin.

Kuvat ovat pidemmällä TTL-ajalla, koska niitä aniharvoin muokataan. Sen sijaan WordPressin ja pluginien päivitykset muuttavat usein myös CSS- ja JS-tiedostoja, niin ne vaativat lyhyemmän TTL-ajan. Muistaakseni on nyt viikko.

Tuo on eräällä tavalla kompromissi. Minulla ei ole minkäänlaisia mahdollisuuksia tarkistaa mitkä ovat muuttuneet ja päivittyneet, niin annan kävijöiden nauttia vielä hieman pidempään vanhasta. Asia muuttuu, jos päivitys korjaa jonkun selvän vian. Silloin häviää koko domain.

PURGE poistaa heti

PURGE sen sijaan poistaa objektin heti. Silloin seuraava kysyjä saa suoraan miss ja sitä seuraava hit, normaalin kaavan mukaan. Siinä ei enää merkitse TTL:t ja gracet mitään.

Miksi sitten yleensä kuitenkin tehdään BAN, ei PURGE? Energian säästämiseksi, eräällä tavalla. PURGE tehdään heti, kun BAN jää jonoon ja tehdään sitten kun sen aika on (paitsi jos odotusaikana kukaan ei ole objektia pyytänyt, TTL kuluu loppuun, grace käynnistyy jne.).

Järjestelmä pääsee silloin tekemään banneja hitaammin taustalla. Purge sen sijaan on prosessoitava samantien, vaikka palveltavana olisi miljoona kävijää.

Minä teen purgen kahdessa tapauksessa.

Jos minulla on tarve poistaa kaikki määrättyyn domainiin tai urliin liittyvä. BAN on hankala, koska mukana voi olla jotain objekteja, joita en tajunnut poistaa. Lisäksi PURGE-poistot ovat luonteeltaan usein sellaisia, että haluan tai minun tarvitsee siivota osumat cachesta heti, eikä joskus.

Toinen tapaus on cachen lämmitys. Se tarkoittaa sitä, että lataan etukäteen sivusisältöä cacheen. Pakotan noudot backendille, mutta jos cachessa jo vaikka määrätty sivu, niin sitä ei kuitenkaan laitettaisi välimuistiin. Haluaisin cacheen kuitenkin tuoreimman mahdollisen version.

Joten teen ensin purgen ja hävitän sen urlin cachesta, ja vasta sitten noudan sisällön backendiltä.

Voi sen tehdä bannillakin. Sen jälkeen ban.list on täysin lukukelvoton, koska siellä on parhaimmillaan/pahimmillaan satoja urlia jonossa odottamassa päivitystään. Toki ne häviävät ajan myötä, mutta lämmitykseen käytettävissä urleissa on mukana myös virheellisiä — haen ne API:lla Matomosta. Koska viallisia osoitteita ei kukaan pyydä, niin ne eivät myöskään häviä milloinkaan listalta.

Joskus käytetään ilmaisuja soft ja hard purge. Hard purge on aito poistaminen. Sen sijaan soft purge on täysin sama kuin BAN. Usein syy käyttää purgea kumpaankin on kirjoittanisen helppous, eli laiskuus — ja siksi minä teen juurikin noin. Varnishin VCL:ään laitettu logiikka sitten päättää kumpi tapahtuu.

Xkey helpottaa elämää

xkey on headereissa, tarkkaan ottaen yleensä X-Cache-Keys mukana liikkuva merkintä, joka kertoo Varnishille mikä tarvitsee BAN tai PURGE.

Kun päivitän vaikkapa WordPress-artikkelin ja muutan samalla sen tageja ja kategorian, niin saan tehtyä bannin käyttämällä xkey-avaimia frontpage ja sidebar (siellä yleensä piilee artikkelilistat) sekä tag ja category, jolloin päivitetään kummankin arkistot. Samalla article-id bannaa juuri sen artikkelin.

Jos tarve on purgelle, niin käytän avainta url.

Minun ei siis tarvitse tietää jokaisen bannattavan objektin nimeä. Riittää, että annan oikean xkey-tagin. Silloin esimerkiksi sidebar bannaa cachesta jokaisen komponentin, joka liittyy sivupalkkiin.

Useimmiten xkey-tagit laitetaan backendillä. Minulla on WordPresseissä snippet, joka tuon tekee. Mutta sen pystyy toki asettamaan Varnishissakin, jos haluaa — on vain perin työlästä.

Minulla tulee kaikki muu WordPressistä paitsi xkey-tagi url — ihan siksi, että se on helpoin asettaa Varnishissa. WordPress ei myöskään tiedä mikä postausten lopullinen url on, koska sen tekee vähintään backendin web-server ja se sijaitsee WordPressin jälkeen.

WordPressin snippet

Toki voit laittaa tämän tiedostoon functions.php, mutta siinä ei ole mitään järkeä. Eivät nämä asiat ole teemariippuvaisia. Joten käytä snippet pluginia.

Tämä lisää headerin X-Cache-Keys jossa xkey-tagit majailevat.

https://gist.github.com/eksiscloud/dddff3f52a99fd0058fab76bb8eebd97

Olisi kuitenkin mukavaa, kun ei tarvitse tehdä banneja käsin. Joten automatisoin tuon. Aina kun WordPressin postausta muokataan, niin julkaisun jälkeen lähtee sopiva pyyntö Varnishille ja se tekee bannit.

Tämäkin laitetaan snippet lisäosaan.

https://gist.github.com/eksiscloud/3e3fd041c092365a78ece344d46cee9d

Toki jos haluaa tai tarvitsee tehdä bannin/purgen käsin Varnishissa, niin ne onnistuu tällä scriptillä.

https://gist.github.com/eksiscloud/0ca57f2e59ccd4792d0a0ec044d35929

Varnishin muutokset

Varnishillekin täytyy kertoa mitä se banneilla ja purgeilla oikein tekeen.

Muista: tämä toimii vain jos käytössä on vmod, joka mahdollistaa xkeyn. Se ei ole Varnishin sisäänrakennettu ominaisuus. Googleta ja asenna ohjeiden mukaan.

Kannattaa ehkä käyttää sub vcl rakennetta. Tosin se on hieman kaksipiippuinen asia. Toisaalta ali-vcl:ien käyttö helpottaa lohkojen muokkaamista. Toisaalta se vaikeuttaa koko paketin selaamista. Tee niin tai näin, niin aina voittaa ja häviää.

Täältä voit vilkaista miten minä olen asiat rakentanut:

https://github.com/eksiscloud/Varnish_7.x-multiple_sites/tree/main

vcl_recv

    ## Normal no-go rules
    if (req.http.X-Bypass != "true") {
        return(synth(405, "Forbidden"));
    }

    if (!req.http.xkey-purge) {
        return(synth(400, "Missing xkey"));
    }

    ## Use only allowed xkey-tags
    if (
        req.http.xkey-purge !~ "^frontpage$" &&
        req.http.xkey-purge !~ "^sidebar$" &&
        req.http.xkey-purge !~ "^url-.*" &&
        req.http.xkey-purge !~ "^article-[0-9]+$" &&
        req.http.xkey-purge !~ "^domain-[a-z0-9-]+$"&&
        req.http.xkey-purge !~ "^tag-[a-z0-9-]+$" &&
        req.http.xkey-purge !~ "^category-[a-z0-9-]+$"
    ) {
        std.log("⛔ Unknown xkey: " + req.http.xkey-purge);
        return(synth(404, "Unknown xkey tag: " + req.http.xkey-purge));
    }

    ## When BAN happens
    if (req.method == "BAN") {
        ban("obj.http.xkey ~ " + req.http.xkey-purge);
        return(synth(200, "Banned: " + req.http.xkey-purge));
    }

    # Using hard PURGE for xkey-urls
    if (req.method == "PURGE") {
        if (req.http.xkey-purge && req.http.xkey-purge ~ "^url-") {
            xkey.purge(req.http.xkey-purge);
            return(synth(200, "Hard purged: " + req.http.xkey-purge));
        }
	if (req.http.xkey-purge && req.http.xkey-purge ~ "^domain-") {
            xkey.purge(req.http.xkey-purge);
            return(synth(200, "Hard purged: " + req.http.xkey-purge));
	}

        # Soft fallback-purge
        ban("obj.http.xkey ~ " + req.http.xkey-purge);
        return(synth(200, "Soft purged: " + req.http.xkey-purge));
    }

    ## another PURGE
    return(hash);

Käytän Nginxin asettamaa X-Bypass headeria rajoittamaan kuka tuonne pääsee. Koska teen tuon sub-vcl:nä, niin rajoitan pääsyn vain metodeille BAN ja PURGE. Jos et samaa kikkaa käytä, niin sinun pitää laittaa kaikki tuo sallivan if-lauseen sisälle.

Ehtolauseet kommentille Use only allowed xkey-tags tekevät tietysti myös tuon, mutta aidosti ne estävät kirjoitusvirheet. Muutoin fronpage lähtisi myös banniin, mutta ei tekisi mitään muuta kuin olisi häviämättä ban.list joukosta.

vcl_backend_response

    ## Set xkey
    if (beresp.http.X-Cache-Tags) {
        set beresp.http.xkey = beresp.http.X-Cache-Tags;
    }

    ## Add domain-xkey if not already there
    if (bereq.http.host) {
        if (bereq.http.host == "www.katiska.eu" && !std.strstr(beresp.http.xkey, "domain-katiska")) {
            set beresp.http.xkey += ",domain-katiska";
        } else if (bereq.http.host == "www.poochierevival.info" && !std.strstr(beresp.http.xkey, "domain-poochie")) {
            set beresp.http.xkey += ",domain-poochie";
        } else if (bereq.http.host == "www.eksis.one" && !std.strstr(beresp.http.xkey, "domain-eksis")) {
            set beresp.http.xkey += ",domain-eksis";
        } else if (bereq.http.host == "jagster.eksis.one" && !std.strstr(beresp.http.xkey, "domain-jagster")) {
            set beresp.http.xkey += ",domain-jagster";
        } else if (bereq.http.host == "dev.eksis.one" && !std.strstr(beresp.http.xkey, "domain-dev")) {
            set beresp.http.xkey += ",domain-dev";
        }
    }

    ## Add xkey for tags if not already there
    if (bereq.url ~ "^/") {
        # This trick must be done beacuse strings can't be joined with regex-operator
        set beresp.http.X-URL-CHECK = "url-" + bereq.url;
    
        if (!std.strstr(beresp.http.xkey, beresp.http.X-URL-CHECK)) {
            set beresp.http.xkey += "," + beresp.http.X-URL-CHECK;
        }

    unset beresp.http.X-URL-CHECK;
    }

Omituisuudestaan huolimatta tuo tekee hyvin simppelit asiat. Ensin se siirtää xkeyt headerille X-Cache-Tags.

Sitten varmistetaan, että domain-xkey on varmasti siellä, eikä sitä laiteta mukaan kahteen kertaan.

Loppu varmistaa tag-xkeyn kanssa ihan saman. Se oli pakko kierrättää väliaikaisen headerin kautta, koska muutoin en saanut stringijonoon liitettyä regexiä. Tai ainakaan en keksinyt muuta tapaa.

Muuta ei tarvita.

Yhteenveto

Annoin tekoälylle tiedot käyttämästäni systeemistä (jossa ei ole mitään ihmeellistä, aika tavanomainen) ja käskin sen tehdä siitä cheat sheetin. Kyllä, tällaisiin tylsiin rutiiniasioihin käytän AI:ta ihan ilman estoja — säästää aikaa ja vaivaa.

Käytetyt tagityypit (xkey)

Tyyppi Esimerkki Asettaa Kuvaus
article-<ID> article-12345 WordPress Artikkelin yksilöllinen tunniste
tag-<slug> tag-energia WordPress Artikkelin tagit
category-<slug> category-koira (ei vielä käytössä) Artikkelin kategoria
sidebar sidebar WordPress Näkyy esim. ”tuoreimmat” listauksessa
frontpage frontpage WordPress Etusivun näkymät
domain-<nimi> domain-poochie, domain-katiska Varnish Sivustokohtainen tunniste domainin perusteella
url-<polku> url-/koira/koiraa-ei-kuulu-jatkuvasti-aktivoida/ Varnish URL-pohjainen xkey, generoi Varnish vcl_backend_response
feed, activitypub ei käytössä Nämä ohitetaan tai pass suoraan

Headerit ja niiden merkitykset

Header Lähde Käyttötarkoitus
x-cache-tags WordPress Semanttiset tunnisteet (artikkeli, tagit, sidebar jne.)
xkey Varnish Lopullinen tunnistevälimuistia varten (sisältää domain + url + x-cache-tags)
xkey-purge Manuaalinen (curl/script) Purge-tagi, jolla poistetaan objektit xkey perusteella
Cache-Control: no-cache Manuaalinen Pakottaa cache-missin (jos luotettu pyytäjä)
X-Bypass: true Manuaalinen/Nginx Ohittaa välimuistin täysin
X-Real-IP Nginx Todellinen IP, jota Varnish käyttää luotetun pyytäjän tunnistukseen

Toiminnot

Tilanne Ratkaisu Varnishin käsittely
Artikkelia muokataan ban("obj.http.xkey ~ article-<ID>") Automaattinen WordPressissä
Uusi artikkeli julkaistaan ban("obj.http.xkey ~ frontpage|sidebar") Automaattinen WordPressissä
Tagia tai kategoriaa muokataan ban("obj.http.xkey ~ tag-<slug>") Tuki tagille toteutettu
Koko domainin cache tyhjennetään xkey.purge("domain-<nimi>") Manuaalinen tai script
Yksittäinen sivu poistetaan xkey.purge("url-/polku/") Automaattinen lisäys Varnishissa
Väärä xkey käytössä 404 Unknown xkey tag Turva, toteutettu vcl_recv/vcl_synth
Luotettu pyytäjä testaa cache miss Cache-Control: no-cache + X-Real-IP hash_always_miss = true

Erityishuomiot

  • ban() ei poista objektia heti — se merkitsee sen invalidiksi, mutta säilyttää sen kunnes TTL tai grace menee umpeen.
  • xkey.purge() poistaa objektit heti — ei jää graceen.
  • grace-aika voi aiheuttaa, että vanha versio tarjoillaan, vaikka uusi olisi taustalla.
  • vcl_backend_response-lohko ajetaan vain backend-haussa, ei välimuistiosumassa.

Esimerkkikäskyt

Hard purge (välitön poisto)

curl -X PURGE https://www.katiska.eu/ -H "xkey-purge: article-12345"

BAN soft purge

curl -X BAN https://www.katiska.eu/ -H "X-Cache-Tags: article-12345"

Tyhjennä koko domainin cache

curl -X PURGE https://www.katiska.eu/ -H "xkey-purge: domain-katiska"

Jakke Lehtonen

Teen B2B-markkinoille sisällöntuottoa sekä UX-testauksia. Samaan liittyy myös koulutukset yrityksille ja webmaailman kanssa muutoin painiville. Serverien sielunelämää on joutunut ohessa opettelmaan. Toinen puoli toiminnasta on koirien ravitsemuksen ja ruokinnan suunnittelua sekä varsinkin omistajien kouluttamista hoitamaan koiriaan oikein ja vielä paremmin. Profiili: Jakke Lehtonen

Keskustele foorumilla Katiskan foorumi

WordPressin kommentit: