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"
Keskustele foorumilla Katiskan foorumi