Bygg en egen CDN-server: hämta en gång, leverera tusenfalt

Publicerad den 8 min läsning

En CDN-server hämtar varje fil en gång från ursprunget och levererar den därefter tusenfalt själv. Ursprungsservern ser bara en bråkdel av förfrågningarna, och besökarna får korta laddningstider. Så bygger du en.

Varje fil som en besökare laddar från din sida hämtar din ursprungsserver ur filsystemet, packar in den i ett svar och skickar iväg den. Vid tio besökare om dagen märker ingen det. Vid tiotusen samtidiga besökare är precis det skälet till att sidan blir långsam, trots att den egentliga applikationen inte har något att göra.

En CDN-server ställer sig framför. Den hämtar varje fil en gång från ursprunget och levererar den därefter själv, så ofta som den behövs. Ursprunget ser därigenom bara en bråkdel av förfrågningarna, och besökarna får sina filer från en server som inte gör något annat än att leverera. Den här artikeln visar hur mycket det verkligen ger, hur du sätter upp en sådan server med nginx och vad som är viktigt när det gäller trafik och plats.

Vad en CDN-server verkligen gör

Förloppet går att berätta i tre satser:

  • En besökare frågar efter en fil hos CDN-servern.
  • Ligger den redan där går den ut omedelbart. Det är en träff.
  • Ligger den inte där hämtar CDN-servern den hos ursprunget, lägger undan den och levererar den. Det är en miss, och den inträffar bara en gång per fil och giltighetstid.

Principen är densamma som hos en streamingserver: hämta en gång, leverera många gånger. Skillnaden är bara att en streamingserver fördelar en löpande dataström och en CDN-server färdiga filer.

Räkningen: hur mycket en cache avlastar

Avgörande är träffkvoten, alltså andelen förfrågningar som cachen besvarar själv. Vid en miljon förfrågningar om dagen ser avlastningen ut så här:

TräffkvotFörfrågningar hos ursprungetAvlastning
50 %500 000Faktor 2
80 %200 000Faktor 5
95 %50 000Faktor 20
99 %10 000Faktor 100

För statiska filer är 95 procent och mer normalfallet, för bilder, skript och typsnitt ändras sällan. Steget från 95 till 99 procent är därvid mer värdefullt än siffrorna låter förmoda: det minskar den återstående lasten till en femtedel en gång till.

Samtidigt förskjuts trafiken. Ursprunget överför nästan ingenting längre, CDN-servern desto mer. Vid 500 000 bildhämtningar om dagen med 400 KB var blir det 200 GB dagligen, alltså omkring 6 TB i månaden, bara för bilder. Därför är det inte processorn som är den viktiga siffran vid en CDN-nod, utan anslutning och volym.

När en egen CDN-server lönar sig

Kommersiella CDN-leverantörer fakturerar per överförd gigabyte. Det är bekvämt och billigt vid liten volym. Från en viss mängd vänder förhållandet:

FallEgen CDN-serverFakturering per gigabyte
Några GB i månadenlönar sig knapptbilligare
Många TB i månadenfast månadsprisväxer linjärt med
Stora nedladdningar, videoarkivklart i fördelblir snabbt dyrt
Full kontroll över regler och loggarfullständigbegränsad

Dessutom finns en punkt som är svår att sätta siffror på: vid en egen nod bestämmer du själv vad som cachas, hur länge, och när det försvinner. Du är inte bunden till en utomstående leverantörs regler och kan även leverera sådant som inte passar in i andras scheman.

Uppbyggnad med nginx som cache

Installera

apt update
apt install -y nginx

Mer behövs inte, cachen är inbyggd i nginx. Lägg upp katalogen där filerna ska ligga:

mkdir -p /var/cache/cdn
chown www-data:www-data /var/cache/cdn

Konfigurera

Cachen beskrivs en gång i http-blocket och används sedan på de enskilda platserna:

proxy_cache_path /var/cache/cdn
                 levels=1:2
                 keys_zone=cdn:100m
                 max_size=200g
                 inactive=30d
                 use_temp_path=off;

proxy_cache_key "$scheme$request_method$host$request_uri";

Värdena betyder i detalj: keys_zone=cdn:100m räcker för omkring 800 000 poster, max_size begränsar utrymmet på disken, inactive=30d slänger filer som ingen har frågat efter på 30 dagar, och use_temp_path=off undviker en onödig flytt mellan kataloger.

Därunder den egentliga servern:

server {
    listen 443 ssl;
    http2 on;
    server_name cdn.example.com;

    ssl_certificate     /etc/letsencrypt/live/cdn.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cdn.example.com/privkey.pem;

    location / {
        proxy_pass https://origin.example.com;
        proxy_set_header Host origin.example.com;
        proxy_http_version 1.1;

        proxy_cache cdn;
        proxy_cache_valid 200 301 302 24h;
        proxy_cache_valid 404 1m;

        # bunta flera samtidiga missar till en enda förfrågan
        proxy_cache_lock on;
        proxy_cache_lock_timeout 10s;

        # fortsätt leverera vid problem hos ursprunget
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;

        # sessionscookies får inte spränga cachen
        proxy_ignore_headers Set-Cookie;
        proxy_hide_header Set-Cookie;

        add_header X-Cache-Status $upstream_cache_status always;
    }
}

Tre rader därur förtjänar särskild uppmärksamhet. proxy_cache_lock ser till att inte hundra besökare frågar ursprunget samtidigt vid en ny fil, utan bara en, medan de övriga väntar. proxy_cache_use_stale håller leveransen uppe medan ursprunget startar om eller svarar långsamt. Och att gömma Set-Cookie hindrar att ett enda svar med sessionscookie gör hela cachen oanvändbar.

Kontrollera att det griper

nginx -t && systemctl reload nginx
curl -sI https://cdn.example.com/bild.jpg | grep -i x-cache-status
curl -sI https://cdn.example.com/bild.jpg | grep -i x-cache-status

Vid det första anropet står det MISS där, vid det andra HIT. Står det MISS även andra gången ligger det nästan alltid hos en av de tre vanliga misstänkta: en Set-Cookie i svaret, ett Cache-Control: no-store från ursprunget eller en saknad längdangivelse.

Vad som cachas och vad som inte gör det

Den rena lösningen är att ursprunget säger det själv. Det känner sitt innehåll, cachen gör det inte. Till det sätter det passande headers:

InnehållHeader hos ursprunget
Fil med kontrollsumma i namnetCache-Control: public, max-age=31536000, immutable
Bild eller nedladdning utan versioneringCache-Control: public, max-age=86400
HTML på en offentlig sidaCache-Control: public, max-age=300
Inloggat område, varukorgCache-Control: private, no-store

Vill du bestämma i cachen själv går det via egna platser per filtyp:

location ~* \.(jpg|jpeg|png|gif|webp|avif|svg|ico|css|js|woff2|mp4|zip)$ {
    proxy_pass https://origin.example.com;
    proxy_cache cdn;
    proxy_cache_valid 200 7d;
    add_header Cache-Control "public, max-age=604800";
    add_header X-Cache-Status $upstream_cache_status always;
}

Bli av med innehåll riktat

Den enklaste vägen är att inte behöva ta bort något alls: den som ger varje ändrad bild ett nytt filnamn, till exempel logo.a1b2c3.png, har inte problemet. Den gamla filen faller så småningom ur cachen av sig själv.

Går det inte hjälper hos nginx den fria modulen ngx_cache_purge. Utan extramodul går sökvägen att räkna ut från cachenyckeln, eftersom nginx använder dess MD5-summa som filnamn:

KEY="httpsGETcdn.example.com/bild.jpg"
H=$(printf '%s' "$KEY" | md5sum | cut -d' ' -f1)
rm -f "/var/cache/cdn/${H: -1}/${H: -3:2}/$H"

I vardagen räcker oftast ett litet skript som kör de här raderna för en lista med sökvägar.

Stora filer och videor

Vid filer över några hundra megabyte behöver du uppdelningen i block, annars laddar cachen om hela filen vid varje hopp i videon:

location /video/ {
    proxy_pass https://origin.example.com;
    proxy_cache cdn;
    proxy_cache_valid 200 206 7d;

    slice 1m;
    proxy_set_header Range $slice_range;
    proxy_cache_key "$uri$slice_range";
}

Därmed hämtar nginx bara de block som verkligen efterfrågas. Hoppar en tittare in i mitten av en film vandrar några få megabyte över ledningen i stället för flera gigabyte.

TLS, HTTP/2 och HTTP/3

En CDN-nod levererar många små filer till många anslutningar, och precis där lönar sig de nyare protokollen. HTTP/2 är avklarat med http2 on;. HTTP/3 ger framför allt något på mobilanslutningar, eftersom ett nätbyte inte river anslutningen:

listen 443 quic reuseport;
listen 443 ssl;
http2 on;
add_header Alt-Svc 'h3=":443"; ma=86400';

Glöm inte att öppna UDP 443 i brandväggen. Det är det vanligaste skälet till att HTTP/3 är inrättat och ingen använder det ändå.

Flera platser

En enda nod ger redan största delen av vinsten, nämligen avlastningen av ursprunget. Den andra vinsten, korta vägar till besökaren, kommer först med flera platser.

Fördelningen löper över DNS: samma namn pekar på olika noder beroende på var besökaren kommer ifrån. Du behöver en DNS-leverantör med geografisk tilldelning för det. Alla noder får samma konfiguration och samma ursprung, de vet ingenting om varandra, och precis det gör uppbyggnaden så robust: faller en nod ifrån tar du den ur DNS, och de övriga tar över.

KernelHost erbjuder platser i Tyskland, på USA:s västkust, på USA:s östkust samt i Singapore och Sydkorea. En översikt med latenser hittar du under Serverplatser. Som tumregel: en nod per kontinent där en nämnvärd del av din publik sitter.

Mäta i stället för att gissa

Skriv cachestatusen till loggen, då kan du räkna ut träffkvoten när som helst:

log_format cdn '$remote_addr $status $body_bytes_sent '
               '$upstream_cache_status "$request"';
access_log /var/log/nginx/cdn.log cdn;

Det går att utvärdera med en enradare:

awk '{print $4}' /var/log/nginx/cdn.log | sort | uniq -c | sort -rn

Ligger andelen HIT under 90 procent vid statiska filer lönar det sig nästan alltid att söka orsaken: oftast är det cookies i svaren, för korta giltighetstider eller förfrågningar med skiftande parametrar i slutet av adressen, som för cachen ser ut som en ny fil varje gång.

Vilken server passar

En CDN-nod är en leveransmaskin. Ordningen på de viktiga egenskaperna ser därför annorlunda ut än hos en applikationsserver:

  1. Trafik och anslutning. Här uppstår hela lasten. En nod överför i drift regelbundet flera terabyte i månaden.
  2. Disk. NVMe är klart i fördel, eftersom cachen består av många små slumpmässiga åtkomster. Storleken rättar sig efter hur mycket av ditt bestånd som är varmt, inte efter hur stort beståndet är totalt.
  3. RAM. Operativsystemet använder ledigt minne som en andra cache framför disken. Mer RAM betyder här direkt mer fart.
  4. Processor. Minst kritisk, den behövs bara för TLS, och det klarar moderna processorer i förbifarten.

Precis för det här fallet är våra rootservrar med obegränsad trafik gjorda: riktiga 2 × 1 Gbit/s per server utan strypning, obegränsad volym till fast månadspris, datacenter-NVMe i RAID och 3,2 Tbps floodskydd från fabrik. Driftsättningen tar under 30 sekunder, med full root-åtkomst, IPv4 och ett /64-IPv6-nät. Eftersom faktureringen sker på PrePaid-basis och går att säga upp varje månad kan du börja med en nod och när som helst lägga till en andra plats, om din publik fördelar sig.

Vanliga frågor

Vad gör en CDN-server egentligen?
Den sitter mellan dina besökare och din ursprungsserver. Den första förfrågan efter en fil skickar den vidare till ursprunget och lägger samtidigt undan svaret. Alla ytterligare förfrågningar efter samma fil besvarar den själv, utan att besvära ursprunget.
Hur mycket avlastar det i praktiken?
Det hänger på träffkvoten. Vid 95 procent ser ditt ursprung bara var tjugonde förfrågan, lasten faller alltså till en tjugondel. Vid 80 procent är det fortfarande en femtedel. För statiska filer som bilder, skript, typsnitt och nedladdningar är 95 procent och mer normalt.
Behöver jag en kommersiell leverantör för det?
Inte nödvändigtvis. En egen CDN-server lönar sig framför allt vid stora filer och mycket volym, eftersom kommersiella leverantörer fakturerar per överförd gigabyte. På en server med obegränsad trafik kostar den hundrade terabyten lika mycket som den första, nämligen inget extra.
Vilka filer ska jag cacha och vilka inte?
Allt som ser likadant ut för alla besökare: bilder, videor, CSS, JavaScript, typsnitt, PDF, installationsfiler. Det du inte bör cacha är personliga sidor, varukorgar, inloggade områden och allt som bär en sessionscookie. Beslutet fattas bäst av ursprunget via Cache-Control-headern.
Hur länge ska cachen behålla filer?
För filer med versionsnummer eller kontrollsumma i namnet gärna ett år, de ändras aldrig. För bilder och nedladdningar utan versionering är timmar till dagar vanligt. För HTML, om du cachar det alls, minuter. Viktigt är inte det exakta talet, utan att du har ett sätt att ta bort enskilda filer riktat.
Hur tar jag bort en fil från cachen?
Enklast via filnamnet: den som ger ett nytt namn vid varje ändring behöver inte ta bort något alls. Går det inte hjälper hos nginx antingen den kommersiella purge-modulen eller det fria ngx_cache_purge. I nödfall tar du bort cachefilen direkt i filsystemet, sökvägen går att räkna ut från cachenyckeln.
Vad händer om ursprungsservern faller ifrån?
Rätt inställd levererar cachen vidare då, även när posterna egentligen har gått ut. Det sköter proxy_cache_use_stale med error och timeout samt proxy_cache_background_update. Din sida förblir därmed nåbar medan ursprunget underhålls eller startar om.
Fungerar det även för stora videofiler?
Ja, men bara med slice-inställningen. Utan den laddar cachen om hela filen från ursprunget vid varje hopp i videon. Med slice delar nginx upp filen i block om till exempel en megabyte och lägger bara undan de delar som verkligen efterfrågas.
Hur många platser behöver jag?
Börja med en, och då där de flesta av dina besökare finns. En andra plats lönar sig först när en nämnvärd del av publiken sitter på en annan kontinent. Fördelningen löper då över DNS, och varje besökare landar hos den närmaste noden.
Hur ser jag att cachen fungerar?
Skriv ut variabeln upstream_cache_status som header. Vid det andra anropet av samma fil måste det stå HIT där. Skriver du dessutom samma värde till loggen kan du räkna ut träffkvoten när som helst. Under 90 procent vid statiska filer betyder oftast att något är fel med headerna eller cookies.

CDN Caching nginx Reverse Proxy Performance HTTP Unlimited Traffic