Costruire un proprio server CDN: prelevare una volta, consegnare mille volte

Pubblicato il 9 min di lettura

Un server CDN preleva ogni file una volta dall'origine e poi lo consegna mille volte da sé. Il server di origine vede solo una frazione delle richieste, i visitatori ottengono tempi di caricamento brevi. Ecco come costruirne uno.

Ogni file che un visitatore carica dal tuo sito, il tuo server di origine lo preleva dal filesystem, lo impacchetta in una risposta e lo spedisce. Con dieci visitatori al giorno non se ne accorge nessuno. Con diecimila visitatori contemporanei è esattamente questo il motivo per cui il sito diventa lento, anche se l'applicazione vera e propria non ha nulla da fare.

Un server CDN si mette davanti. Preleva ogni file una volta dall'origine e poi lo consegna da sé, tutte le volte che serve. L'origine vede così solo una frazione delle richieste, e i visitatori ricevono i loro file da un server che non fa altro che consegnare. Questo articolo mostra quanto porti davvero, come si configura un server di questo tipo con nginx e cosa conta in termini di traffico e sede.

Che cosa fa davvero un server CDN

Il funzionamento si racconta in tre frasi:

  • Un visitatore richiede un file al server CDN.
  • Se è già presente, parte subito. Questo è un hit.
  • Se non è presente, il server CDN la preleva dall'origine, la memorizza e la consegna. Questo è un miss, e per ogni file avviene una sola volta per ciascun periodo di validità.

Il principio è lo stesso di un server di streaming: prelevare una volta, consegnare molte volte. La differenza è solo che un server di streaming distribuisce un flusso di dati in corso, mentre un server CDN distribuisce file già pronti.

Il conto: quanto alleggerisce una cache

Decisiva è la hit rate, cioè la quota di richieste a cui risponde la cache stessa. Con un milione di richieste al giorno l'alleggerimento è questo:

Hit rateRichieste all'origineAlleggerimento
50 %500.000fattore 2
80 %200.000fattore 5
95 %50.000fattore 20
99 %10.000fattore 100

Per i file statici il 95 per cento e oltre è la normalità, perché immagini, script e font cambiano raramente. Il salto dal 95 al 99 per cento vale più di quanto i numeri lascino intuire: riduce il carico residuo ancora a un quinto.

Allo stesso tempo il traffico si sposta. L'origine trasferisce quasi nulla, il server CDN tanto di più. Con 500.000 richieste di immagini al giorno da 400 KB ciascuna si tratta di 200 GB al giorno, cioè circa 6 TB al mese, solo per le immagini. Per questo in un nodo CDN il numero importante non è la CPU, ma connettività e volume.

Quando conviene un server CDN proprio

I provider CDN commerciali fatturano per gigabyte trasferito. È comodo e, con poco volume, conveniente. Oltre una certa quantità il rapporto si capovolge:

CasoServer CDN proprioFatturazione per gigabyte
Pochi GB al meseconviene pocopiù conveniente
Molti TB al meseprezzo mensile fissocresce in modo lineare
Download grandi, archivi videochiaramente in vantaggiodiventa presto costosa
Controllo completo su regole e protocollicompletolimitato

A questo si aggiunge un punto difficile da quantificare: con un nodo proprio decidi tu cosa viene messo in cache, per quanto tempo e quando sparisce. Non sei legato alle regole di un provider esterno e puoi consegnare anche contenuti che negli schemi altrui non rientrano.

Architettura con nginx come cache

Installazione

apt update
apt install -y nginx

Non serve altro, la cache è integrata in nginx. Crea la directory in cui andranno i file:

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

Configurazione

La cache viene descritta una volta nel blocco http e poi usata nei singoli location:

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

I valori nel dettaglio: keys_zone=cdn:100m basta per circa 800.000 voci, max_size limita lo spazio sul disco, inactive=30d butta via i file che nessuno ha richiesto per 30 giorni e use_temp_path=off evita uno spostamento inutile tra directory.

Sotto, il server vero e proprio:

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;

        # raggruppa i miss simultanei in una sola richiesta
        proxy_cache_lock on;
        proxy_cache_lock_timeout 10s;

        # continua a consegnare in caso di problemi all'origine
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;

        # i cookie di sessione non devono rovinare la cache
        proxy_ignore_headers Set-Cookie;
        proxy_hide_header Set-Cookie;

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

Tre righe meritano attenzione particolare. proxy_cache_lock fa in modo che per un file nuovo non siano cento visitatori a interrogare l'origine contemporaneamente, ma uno solo, mentre gli altri attendono. proxy_cache_use_stale mantiene attiva la consegna mentre l'origine si riavvia o risponde lentamente. E l'eliminazione di Set-Cookie impedisce che una sola risposta con cookie di sessione renda inutilizzabile tutta la cache.

Verificare che funzioni

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

Alla prima chiamata compare MISS, alla seconda HIT. Se anche la seconda volta compare MISS, la causa è quasi sempre uno dei tre soliti sospetti: un Set-Cookie nella risposta, un Cache-Control: no-store dall'origine oppure l'indicazione di lunghezza mancante.

Cosa va in cache e cosa no

La soluzione pulita è che lo dica l'origine stessa. L'origine conosce i propri contenuti, la cache no. Per questo imposta gli header adeguati:

ContenutoHeader all'origine
File con checksum nel nomeCache-Control: public, max-age=31536000, immutable
Immagine o download senza versionamentoCache-Control: public, max-age=86400
HTML di una pagina pubblicaCache-Control: public, max-age=300
Area riservata, carrelloCache-Control: private, no-store

Se vuoi decidere nella cache stessa, lo puoi fare con location separate per tipo di file:

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

Rimuovere contenuti in modo mirato

La via più semplice è non dover cancellare affatto: chi dà un nome di file nuovo a ogni immagine modificata, per esempio logo.a1b2c3.png, non ha il problema. Il file vecchio esce dalla cache da sé, prima o poi.

Se non è possibile, con nginx aiuta il modulo libero ngx_cache_purge. Senza modulo aggiuntivo il percorso si calcola dalla chiave di cache, perché nginx ne usa la somma MD5 come nome del file:

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"

Per l'uso quotidiano basta di solito un piccolo script che esegue queste righe per un elenco di percorsi.

File grandi e video

Con i file oltre alcune centinaia di megabyte ti serve la suddivisione in blocchi, altrimenti a ogni salto nel video la cache scarica di nuovo l'intero file:

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

Così nginx preleva solo i blocchi davvero richiesti. Se uno spettatore salta a metà di un film, sulla linea passano pochi megabyte invece di diversi gigabyte.

TLS, HTTP/2 e HTTP/3

Un nodo CDN consegna molti file piccoli a molte connessioni, ed è proprio lì che i protocolli più recenti rendono. HTTP/2 si risolve con http2 on;. HTTP/3 porta vantaggi soprattutto sulle connessioni mobili, perché un cambio di rete non fa cadere la connessione:

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

Non dimenticare di aprire la UDP 443 nel firewall. È il motivo più frequente per cui HTTP/3 risulta configurato e nessuno lo usa comunque.

Più sedi

Un singolo nodo porta già la parte maggiore del vantaggio, cioè l'alleggerimento dell'origine. Il secondo vantaggio, percorsi brevi verso il visitatore, arriva solo con più sedi.

La distribuzione passa dal DNS: lo stesso nome punta a un nodo diverso a seconda della provenienza del visitatore. Ti serve per questo un provider DNS con assegnazione geografica. Tutti i nodi ricevono la stessa configurazione e la stessa origine, non sanno nulla l'uno dell'altro, ed è proprio questo a rendere l'architettura così robusta: se un nodo cade, lo togli dal DNS e gli altri subentrano.

KernelHost offre sedi in Germania, sulla costa ovest degli Stati Uniti, sulla costa est degli Stati Uniti e inoltre a Singapore e in Corea del Sud. Una panoramica con le latenze la trovi alla pagina Sedi dei server. Come regola empirica: un nodo per ogni continente su cui si trova una parte rilevante del tuo pubblico.

Misurare invece di indovinare

Scrivi lo stato della cache nel log, così puoi contare la hit rate in qualsiasi momento:

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

L'analisi si fa con una sola riga:

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

Se la quota di HIT sui file statici sta sotto il 90 per cento, cercare la causa conviene quasi sempre: in genere sono cookie nelle risposte, validità troppo brevi oppure richieste con parametri variabili alla fine dell'indirizzo, che per la cache sembrano ogni volta un file nuovo.

Quale server serve

Un nodo CDN è una macchina da consegna. L'ordine delle caratteristiche importanti è quindi diverso rispetto a un server applicativo:

  1. Traffico e connettività. È qui che nasce tutto il carico. Un nodo in esercizio trasferisce regolarmente diversi terabyte al mese.
  2. Disco. NVMe è chiaramente in vantaggio, perché la cache è fatta di molti accessi piccoli e casuali. La dimensione dipende da quanta parte dei tuoi contenuti è calda, non da quanto siano grandi i contenuti nel complesso.
  3. RAM. Il sistema operativo usa la memoria libera come seconda cache davanti al disco. Qui più RAM significa direttamente più velocità.
  4. CPU. La meno critica, serve solo per il TLS, e i processori moderni lo gestiscono senza sforzo.

I nostri server root a traffico illimitato sono fatti esattamente per questo caso: 2 × 1 Gbit/s reali per server senza limitazioni, volume illimitato a prezzo mensile fisso, NVMe datacenter in RAID e 3,2 Tbps di protezione antiflood di serie. L'attivazione richiede meno di 30 secondi, con accesso root completo, IPv4 e una rete IPv6 /64. Poiché la fatturazione è PrePaid e disdicibile ogni mese, puoi iniziare con un nodo e aggiungere in qualsiasi momento una seconda sede, quando il tuo pubblico si distribuisce.

Domande frequenti

Che cosa fa esattamente un server CDN?
Si trova tra i tuoi visitatori e il tuo server di origine. La prima richiesta di un file la inoltra all'origine e ne memorizza la risposta. A tutte le richieste successive dello stesso file risponde da sé, senza disturbare l'origine.
Quanto alleggerisce davvero?
Dipende dalla hit rate. Al 95 per cento la tua origine vede solo una richiesta su venti, quindi il carico scende a un ventesimo. All'80 per cento è ancora un quinto. Per i file statici come immagini, script, font e download, il 95 per cento e oltre è la norma.
Mi serve per questo un provider commerciale?
Non necessariamente. Un server CDN proprio conviene soprattutto con file grandi e volumi elevati, perché i provider commerciali fatturano per gigabyte trasferito. Con un server a traffico illimitato il centesimo terabyte costa come il primo, cioè niente in più.
Quali file conviene mettere in cache e quali no?
Tutto ciò che è identico per tutti i visitatori: immagini, video, CSS, JavaScript, font, PDF, file di installazione. Non mettere in cache le pagine personalizzate, i carrelli, le aree riservate e tutto ciò che porta un cookie di sessione. La decisione è meglio che la prenda l'origine, tramite l'header Cache-Control.
Per quanto tempo la cache deve conservare i file?
Per i file con numero di versione o checksum nel nome anche un anno, perché non cambiano mai. Per immagini e download senza versionamento sono normali ore o giorni. Per l'HTML, se lo metti in cache, minuti. Non conta il numero esatto, conta avere un modo per cancellare singoli file in modo mirato.
Come cancello un file dalla cache?
La via più semplice passa dal nome del file: chi assegna un nome nuovo a ogni modifica non ha bisogno di cancellare. Se non è possibile, con nginx aiuta il modulo purge commerciale oppure il modulo libero ngx_cache_purge. In caso di necessità cancelli il file di cache direttamente nel filesystem, perché il percorso si calcola dalla chiave di cache.
Che cosa succede se il server di origine non risponde?
Se è impostato bene, la cache continua a consegnare, anche quando le voci sono formalmente scadute. Se ne occupano proxy_cache_use_stale con error e timeout, insieme a proxy_cache_background_update. Il tuo sito resta così raggiungibile mentre l'origine viene sottoposta a manutenzione o si riavvia.
Funziona anche con file video di grandi dimensioni?
Sì, ma solo con l'impostazione slice. Senza di essa, a ogni salto nel video la cache scarica di nuovo l'intero file dall'origine. Con slice nginx divide il file in blocchi, per esempio da un megabyte, e memorizza solo le parti davvero richieste.
Quante sedi mi servono?
Inizia con una, e precisamente dove si trova la maggior parte dei tuoi visitatori. Una seconda sede conviene solo quando una parte rilevante del pubblico si trova in un altro continente. La distribuzione passa poi dal DNS, e ogni visitatore finisce sul nodo più vicino.
Da che cosa capisco che la cache funziona?
Fai restituire la variabile upstream_cache_status come header. Alla seconda chiamata dello stesso file deve comparire HIT. Se scrivi lo stesso valore anche nel log, puoi contare la hit rate in qualsiasi momento. Sotto il 90 per cento con i file statici, di solito c'è qualcosa che non va negli header o nei cookie.

CDN Caching nginx Reverse Proxy Performance HTTP Unlimited Traffic