Een eigen CDN-server opzetten: één keer halen, duizend keer uitleveren

Gepubliceerd op 9 min leestijd

Een CDN-server haalt elk bestand één keer bij de oorsprong en levert het daarna duizend keer zelf uit. De oorsprongsserver ziet nog maar een fractie van de aanvragen, de bezoekers krijgen korte laadtijden. Zo zet u er een op.

Elk bestand dat een bezoeker van uw site opvraagt, haalt uw oorsprongsserver uit het bestandssysteem, stopt het in een antwoord en stuurt het weg. Bij tien bezoekers per dag merkt niemand dat. Bij tienduizend gelijktijdige bezoekers is precies dat de reden waarom de site langzaam wordt, terwijl de eigenlijke toepassing helemaal niets te doen heeft.

Een CDN-server gaat ervoor staan. Hij haalt elk bestand één keer bij de oorsprong en levert het daarna zelf uit, zo vaak als nodig is. De oorsprong ziet daardoor nog maar een fractie van de aanvragen, en de bezoekers krijgen hun bestanden van een server die niets anders doet dan uitleveren. Dit artikel laat zien hoeveel dat werkelijk oplevert, hoe u zo'n server met nginx opzet en waar het bij dataverkeer en locatie op aankomt.

Wat een CDN-server precies doet

Het verloop is in drie zinnen verteld:

  • Een bezoeker vraagt een bestand aan bij de CDN-server.
  • Ligt het er al, dan gaat het er direct uit. Dat is een treffer.
  • Ligt het er niet, dan haalt de CDN-server het bij de oorsprong, slaat het op en levert het uit. Dat is een misser, en die gebeurt per bestand maar één keer per geldigheidsduur.

Het principe is hetzelfde als bij een streamserver: één keer halen, vele keren uitleveren. Het verschil is alleen dat een streamserver een lopende datastroom verdeelt en een CDN-server kant-en-klare bestanden.

De rekening: hoe sterk een cache ontlast

Beslissend is het trefferpercentage, dus het deel van de aanvragen dat de cache zelf beantwoordt. Bij een miljoen aanvragen per dag ziet de ontlasting er zo uit:

TrefferpercentageAanvragen bij de oorsprongOntlasting
50 %500.000factor 2
80 %200.000factor 5
95 %50.000factor 20
99 %10.000factor 100

Voor statische bestanden zijn 95 procent en meer het normale geval, want afbeeldingen, scripts en lettertypen veranderen zelden. De stap van 95 naar 99 procent is daarbij waardevoller dan de getallen laten vermoeden: hij deelt de resterende belasting nog een keer door vijf.

Tegelijk verschuift het dataverkeer. De oorsprong draagt bijna niets meer over, de CDN-server des te meer. Bij 500.000 afbeeldingen per dag van elk 400 KB is dat 200 GB per dag, dus ongeveer 6 TB per maand, alleen al voor afbeeldingen. Daarom is bij een CDN-knooppunt niet de CPU het belangrijke getal, maar de verbinding en het volume.

Wanneer een eigen CDN-server de moeite waard is

Commerciële CDN-aanbieders rekenen per overgedragen gigabyte af. Dat is gemakkelijk en bij weinig volume voordelig. Vanaf een bepaalde hoeveelheid draait de verhouding om:

GevalEigen CDN-serverAfrekenen per gigabyte
Een paar GB per maandnauwelijks de moeitevoordeliger
Vele TB per maandvaste maandprijsgroeit lineair mee
Grote downloads, videoarchievenduidelijk in het voordeelwordt snel duur
Volledige controle over regels en logboekenvolledigbeperkt

Daar komt een punt bij dat zich slecht in cijfers laat uitdrukken: bij een eigen knooppunt bepaalt u zelf wat er gecacht wordt, hoe lang, en wanneer het verdwijnt. U zit niet vast aan de regels van een externe aanbieder en kunt ook dingen uitleveren die in andermans schema's niet passen.

Opbouw met nginx als cache

Installeren

apt update
apt install -y nginx

Meer is er niet nodig, de cache zit in nginx ingebouwd. Maak de map aan waarin de bestanden moeten komen te staan:

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

Configureren

De cache wordt één keer in het http-blok beschreven en daarna in de afzonderlijke locatieblokken gebruikt:

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";

De waarden betekenen afzonderlijk het volgende: keys_zone=cdn:100m is genoeg voor ongeveer 800.000 items, max_size begrenst de ruimte op de schijf, inactive=30d gooit bestanden weg die 30 dagen door niemand zijn aangevraagd, en use_temp_path=off voorkomt onnodig verplaatsen tussen mappen.

Daaronder de eigenlijke server:

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;

        # meerdere gelijktijdige missers tot een aanvraag bundelen
        proxy_cache_lock on;
        proxy_cache_lock_timeout 10s;

        # bij problemen bij de oorsprong blijven uitleveren
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;

        # sessiecookies mogen de cache niet opblazen
        proxy_ignore_headers Set-Cookie;
        proxy_hide_header Set-Cookie;

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

Drie regels daaruit verdienen bijzondere aandacht. proxy_cache_lock zorgt ervoor dat bij een nieuw bestand niet honderd bezoekers tegelijk de oorsprong aanvragen, maar slechts één, en dat de overige wachten. proxy_cache_use_stale houdt de uitlevering overeind terwijl de oorsprong opnieuw start of langzaam antwoordt. En het verbergen van Set-Cookie voorkomt dat één enkel antwoord met sessiecookie de hele cache onbruikbaar maakt.

Controleren of het werkt

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

Bij de eerste aanroep staat daar MISS, bij de tweede HIT. Staat er ook de tweede keer MISS, dan komt het bijna altijd door een van de drie gebruikelijke verdachten: een Set-Cookie in het antwoord, een Cache-Control: no-store van de oorsprong of een ontbrekende lengteopgave.

Wat er gecacht wordt en wat niet

De nette oplossing is dat de oorsprong het zelf zegt. Hij kent zijn inhoud, de cache niet. Daarvoor zet hij passende headers:

InhoudHeader bij de oorsprong
Bestand met controlesom in de naamCache-Control: public, max-age=31536000, immutable
Afbeelding of download zonder versienummerCache-Control: public, max-age=86400
HTML van een openbare paginaCache-Control: public, max-age=300
Ingelogd gedeelte, winkelwagenCache-Control: private, no-store

Wilt u in de cache zelf beslissen, dan gaat dat via eigen locatieblokken per bestandstype:

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;
}

Inhoud doelgericht verwijderen

De eenvoudigste weg is helemaal niet te hoeven wissen: wie elke gewijzigde afbeelding een nieuwe bestandsnaam geeft, bijvoorbeeld logo.a1b2c3.png, heeft dat probleem niet. Het oude bestand valt op een gegeven moment vanzelf uit de cache.

Kan dat niet, dan helpt bij nginx de vrije module ngx_cache_purge. Zonder extra module kunt u het pad uit de cachesleutel berekenen, omdat nginx daarvan de MD5-som als bestandsnaam gebruikt:

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"

Voor dagelijks gebruik is meestal een klein script genoeg dat deze regels voor een lijst met paden uitvoert.

Grote bestanden en video's

Bij bestanden van meer dan een paar honderd megabyte hebt u de opdeling in blokken nodig, anders laadt de cache bij elke sprong in de video het hele bestand opnieuw:

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";
}

Daarmee haalt nginx alleen de blokken die werkelijk worden aangevraagd. Springt een kijker naar het midden van een film, dan gaan er een paar megabyte over de lijn in plaats van meerdere gigabyte.

TLS, HTTP/2 en HTTP/3

Een CDN-knooppunt levert veel kleine bestanden aan veel verbindingen uit, en juist daar betalen de nieuwere protocollen zich uit. HTTP/2 is met http2 on; geregeld. HTTP/3 levert vooral op mobiele verbindingen iets op, omdat een netwerkwissel de verbinding niet verbreekt:

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

Vergeet daarbij niet UDP 443 in de firewall te openen. Dat is de meest voorkomende reden waarom HTTP/3 wel is ingesteld en toch niemand het gebruikt.

Meerdere locaties

Eén enkel knooppunt levert al het grootste deel van de winst op, namelijk de ontlasting van de oorsprong. De tweede winst, korte afstanden naar de bezoeker, komt pas met meerdere locaties.

De verdeling loopt via DNS: dezelfde naam wijst afhankelijk van de herkomst van de bezoeker naar een ander knooppunt. U hebt daarvoor een DNS-aanbieder met geografische toewijzing nodig. Alle knooppunten krijgen dezelfde configuratie en dezelfde oorsprong, ze weten niets van elkaar, en precies dat maakt de opzet zo robuust: valt een knooppunt uit, dan haalt u het uit de DNS, en de overige nemen het over.

KernelHost biedt locaties in Duitsland, aan de Amerikaanse westkust, aan de Amerikaanse oostkust en in Singapore en Zuid-Korea. Een overzicht met latenties vindt u onder Serverlocaties. Als vuistregel: één knooppunt per continent waarop een noemenswaardig deel van uw publiek zit.

Meten in plaats van gokken

Schrijf de cachestatus naar het logboek, dan kunt u het trefferpercentage op elk moment natellen:

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

Analyseren kan met een eenregelig commando:

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

Ligt het aandeel HIT bij statische bestanden onder 90 procent, dan is zoeken naar de oorzaak bijna altijd de moeite waard: meestal zijn het cookies in de antwoorden, te korte geldigheidsduren of aanvragen met wisselende parameters aan het eind van het adres, die er voor de cache elke keer als een nieuw bestand uitzien.

Welke server past

Een CDN-knooppunt is een uitlevermachine. De volgorde van de belangrijke eigenschappen ziet er daarom anders uit dan bij een applicatieserver:

  1. Dataverkeer en verbinding. Hier ontstaat de volledige belasting. Een knooppunt draagt in bedrijf regelmatig meerdere terabyte per maand over.
  2. Schijf. NVMe is duidelijk in het voordeel, omdat de cache uit veel kleine willekeurige toegangen bestaat. De grootte hangt ervan af hoeveel van uw inhoud warm is, niet van hoe groot de inhoud in totaal is.
  3. RAM. Het besturingssysteem gebruikt vrij geheugen als tweede cache voor de schijf. Meer RAM betekent hier direct meer snelheid.
  4. CPU. Het minst kritisch, die is alleen voor TLS nodig, en dat doen moderne processors er met gemak bij.

Precies voor dat geval zijn onze Unlimited-Traffic-rootservers gemaakt: echte 2 × 1 Gbit/s per server zonder beperking, onbeperkt volume voor een vaste maandprijs, datacenter-NVMe in RAID en 3,2 Tbps floodbescherming standaard. Het klaarzetten duurt minder dan 30 seconden, met volledige roottoegang, IPv4 en een /64-IPv6-netwerk. Omdat er op PrePaid-basis wordt afgerekend en het abonnement maandelijks opzegbaar is, kunt u met één knooppunt beginnen en er altijd een tweede locatie bij nemen als uw publiek zich verdeelt.

Veelgestelde vragen

Wat doet een CDN-server precies?
Hij zit tussen uw bezoekers en uw oorsprongsserver. De eerste aanvraag voor een bestand geeft hij door aan de oorsprong, en daarbij slaat hij het antwoord op. Alle verdere aanvragen voor hetzelfde bestand beantwoordt hij zelf, zonder de oorsprong lastig te vallen.
Hoeveel ontlasting levert dat werkelijk op?
Dat hangt van het trefferpercentage af. Bij 95 procent ziet uw oorsprong nog maar elke twintigste aanvraag, de belasting valt dus terug op een twintigste. Bij 80 procent is het nog steeds een vijfde. Voor statische bestanden zoals afbeeldingen, scripts, lettertypen en downloads zijn 95 procent en meer normaal.
Heb ik daarvoor een commerciële aanbieder nodig?
Niet noodzakelijk. Een eigen CDN-server is vooral de moeite waard bij grote bestanden en veel volume, omdat commerciële aanbieders per overgedragen gigabyte afrekenen. Bij een server met onbeperkt dataverkeer kost de honderdste terabyte hetzelfde als de eerste, namelijk niets extra.
Welke bestanden moet ik cachen en welke niet?
Alles wat voor alle bezoekers hetzelfde is: afbeeldingen, video's, CSS, JavaScript, lettertypen, PDF, installatiebestanden. Niet cachen moet u gepersonaliseerde pagina's, winkelwagens, inloggedeelten en alles wat een sessiecookie meedraagt. De beslissing neemt het best de oorsprong, via de Cache-Control-header.
Hoe lang moet de cache bestanden bewaren?
Voor bestanden met een versienummer of controlesom in de naam rustig een jaar, die veranderen nooit. Voor afbeeldingen en downloads zonder versienummer zijn uren tot dagen gebruikelijk. Voor HTML, als u het al cacht, minuten. Belangrijk is niet het precieze getal, maar dat u een manier hebt om afzonderlijke bestanden doelgericht te wissen.
Hoe wis ik een bestand uit de cache?
Het eenvoudigst via de bestandsnaam: wie bij elke wijziging een nieuwe naam toekent, hoeft helemaal niets te wissen. Kan dat niet, dan helpt bij nginx ofwel de commerciële purge-module ofwel de vrije ngx_cache_purge. In het uiterste geval wist u het cachebestand direct in het bestandssysteem, het pad is uit de cachesleutel te berekenen.
Wat gebeurt er als de oorsprongsserver uitvalt?
Goed ingesteld blijft de cache dan uitleveren, ook als de items eigenlijk verlopen zijn. Daar zorgen proxy_cache_use_stale met error en timeout en daarnaast proxy_cache_background_update voor. Uw site blijft daarmee bereikbaar terwijl aan de oorsprong onderhoud wordt gedaan of hij opnieuw start.
Werkt dat ook voor grote videobestanden?
Ja, maar alleen met de slice-instelling. Zonder die instelling laadt de cache bij elke sprong in de video het hele bestand opnieuw bij de oorsprong. Met slice deelt nginx het bestand op in blokken van bijvoorbeeld één megabyte en slaat alleen de werkelijk aangevraagde delen op.
Hoeveel locaties heb ik nodig?
Begin met één, en wel daar waar de meeste van uw bezoekers zitten. Een tweede locatie is pas de moeite waard als een noemenswaardig deel van het publiek op een ander continent zit. De verdeling loopt dan via DNS, iedere bezoeker komt bij het dichtstbijzijnde knooppunt uit.
Waaraan merk ik dat de cache werkt?
Geef de variabele upstream_cache_status als header uit. Bij de tweede aanroep van hetzelfde bestand moet daar HIT staan. Schrijft u dezelfde waarde ook naar het logboek, dan kunt u het trefferpercentage op elk moment natellen. Onder 90 procent bij statische bestanden is er meestal iets niet in orde met de headers of de cookies.

CDN Caching nginx Reverse Proxy Performance HTTP Unlimited Traffic