Eigenen CDN-Server aufbauen: einmal holen, tausendfach ausliefern

Veröffentlicht am 8 Min. Lesezeit

Ein CDN-Server holt jede Datei einmal vom Ursprung und liefert sie danach tausendfach selbst aus. Der Ursprungsserver sieht nur noch einen Bruchteil der Anfragen, die Zuschauer bekommen kurze Ladezeiten. So baust du einen auf.

Jede Datei, die ein Besucher von deiner Seite lädt, holt sich dein Ursprungsserver aus dem Dateisystem, packt sie in eine Antwort und schickt sie los. Bei zehn Besuchern am Tag merkt das niemand. Bei zehntausend gleichzeitigen Besuchern ist genau das der Grund, warum die Seite langsam wird, obwohl die eigentliche Anwendung gar nichts zu tun hat.

Ein CDN-Server setzt sich davor. Er holt jede Datei einmal vom Ursprung und liefert sie danach selbst aus, so oft sie gebraucht wird. Der Ursprung sieht dadurch nur noch einen Bruchteil der Anfragen, und die Besucher bekommen ihre Dateien von einem Server, der nichts anderes tut als ausliefern. Dieser Artikel zeigt, wie viel das wirklich bringt, wie du so einen Server mit nginx aufsetzt und worauf es bei Traffic und Standort ankommt.

Was ein CDN-Server wirklich macht

Der Ablauf ist in drei Sätzen erzählt:

  • Ein Besucher fragt eine Datei beim CDN-Server an.
  • Liegt sie dort schon, geht sie sofort raus. Das ist ein Treffer.
  • Liegt sie nicht dort, holt der CDN-Server sie beim Ursprung, legt sie ab und liefert sie aus. Das ist ein Fehltreffer, und er passiert pro Datei nur einmal je Gültigkeitsdauer.

Das Prinzip ist dasselbe wie bei einem Stream-Server: einmal holen, vielfach ausliefern. Der Unterschied ist nur, dass ein Stream-Server einen laufenden Datenstrom verteilt und ein CDN-Server fertige Dateien.

Die Rechnung: wie stark ein Cache entlastet

Entscheidend ist die Trefferquote, also der Anteil der Anfragen, die der Cache selbst beantwortet. Bei einer Million Anfragen am Tag sieht die Entlastung so aus:

TrefferquoteAnfragen am UrsprungEntlastung
50 %500.000Faktor 2
80 %200.000Faktor 5
95 %50.000Faktor 20
99 %10.000Faktor 100

Für statische Dateien sind 95 Prozent und mehr der Normalfall, denn Bilder, Skripte und Schriften ändern sich selten. Der Sprung von 95 auf 99 Prozent ist dabei wertvoller, als die Zahlen vermuten lassen: Er senkt die verbleibende Last noch einmal auf ein Fünftel.

Gleichzeitig verschiebt sich der Traffic. Der Ursprung überträgt kaum noch etwas, der CDN-Server dafür umso mehr. Bei 500.000 Bildabrufen am Tag mit je 400 KB sind das 200 GB täglich, also rund 6 TB im Monat, allein für Bilder. Deshalb ist bei einem CDN-Knoten nicht die CPU die wichtige Zahl, sondern Anbindung und Volumen.

Wann sich ein eigener CDN-Server lohnt

Kommerzielle CDN-Anbieter rechnen pro übertragenem Gigabyte ab. Das ist bequem und bei wenig Volumen günstig. Ab einer gewissen Menge dreht sich das Verhältnis:

FallEigener CDN-ServerAbrechnung pro Gigabyte
Wenige GB im Monatlohnt kaumgünstiger
Viele TB im Monatfixer Monatspreiswächst linear mit
Große Downloads, Videoarchiveklar im Vorteilwird schnell teuer
Volle Kontrolle über Regeln und Protokollevollständigeingeschränkt

Dazu kommt ein Punkt, der sich schlecht beziffern lässt: Bei einem eigenen Knoten entscheidest du selbst, was gecacht wird, wie lange, und wann es verschwindet. Du bist nicht an die Regeln eines fremden Anbieters gebunden und kannst auch Dinge ausliefern, die in fremde Schemata nicht passen.

Aufbau mit nginx als Cache

Installieren

apt update
apt install -y nginx

Mehr braucht es nicht, der Cache ist in nginx eingebaut. Lege das Verzeichnis an, in dem die Dateien liegen sollen:

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

Konfigurieren

Der Cache wird einmal im http-Block beschrieben und dann in den einzelnen Orten benutzt:

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

Die Werte bedeuten im Einzelnen: keys_zone=cdn:100m reicht für etwa 800.000 Einträge, max_size begrenzt den Platz auf der Platte, inactive=30d wirft Dateien weg, die 30 Tage niemand angefragt hat, und use_temp_path=off vermeidet ein unnötiges Verschieben zwischen Verzeichnissen.

Darunter der eigentliche 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;

        # mehrere gleichzeitige Fehltreffer zu einer Anfrage buendeln
        proxy_cache_lock on;
        proxy_cache_lock_timeout 10s;

        # bei Problemen am Ursprung weiter ausliefern
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;

        # Sitzungs-Cookies duerfen den Cache nicht sprengen
        proxy_ignore_headers Set-Cookie;
        proxy_hide_header Set-Cookie;

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

Drei Zeilen daraus verdienen besondere Beachtung. proxy_cache_lock sorgt dafür, dass bei einer neuen Datei nicht hundert Besucher gleichzeitig den Ursprung anfragen, sondern nur einer und die übrigen warten. proxy_cache_use_stale hält die Auslieferung aufrecht, während der Ursprung neu startet oder langsam antwortet. Und das Ausblenden von Set-Cookie verhindert, dass eine einzige Antwort mit Sitzungs-Cookie den ganzen Cache unbrauchbar macht.

Prüfen, ob es greift

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

Beim ersten Aufruf steht dort MISS, beim zweiten HIT. Steht auch beim zweiten Mal MISS, liegt es fast immer an einem der drei üblichen Verdächtigen: ein Set-Cookie in der Antwort, ein Cache-Control: no-store vom Ursprung oder eine fehlende Längenangabe.

Was gecacht wird und was nicht

Die saubere Lösung ist, dass der Ursprung es selbst sagt. Er kennt seine Inhalte, der Cache nicht. Dafür setzt er passende Header:

InhaltHeader am Ursprung
Datei mit Prüfsumme im NamenCache-Control: public, max-age=31536000, immutable
Bild oder Download ohne VersionierungCache-Control: public, max-age=86400
HTML einer öffentlichen SeiteCache-Control: public, max-age=300
Angemeldeter Bereich, WarenkorbCache-Control: private, no-store

Willst du im Cache selbst entscheiden, geht das über eigene Orte je Dateityp:

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

Inhalte gezielt loswerden

Der einfachste Weg ist, gar nicht löschen zu müssen: Wer jedem geänderten Bild einen neuen Dateinamen gibt, etwa logo.a1b2c3.png, hat das Problem nicht. Die alte Datei fällt irgendwann von selbst aus dem Cache.

Geht das nicht, hilft bei nginx das freie Modul ngx_cache_purge. Ohne Zusatzmodul lässt sich der Pfad aus dem Cache-Schlüssel berechnen, weil nginx dessen MD5-Summe als Dateinamen verwendet:

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"

Für den Alltag genügt meist ein kleines Skript, das diese Zeilen für eine Liste von Pfaden ausführt.

Große Dateien und Videos

Bei Dateien über einigen hundert Megabyte brauchst du die Zerlegung in Blöcke, sonst lädt der Cache bei jedem Sprung im Video die ganze Datei erneut:

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

Damit holt nginx nur die Blöcke, die wirklich angefragt werden. Springt ein Zuschauer in die Mitte eines Films, wandern wenige Megabyte über die Leitung statt mehrerer Gigabyte.

TLS, HTTP/2 und HTTP/3

Ein CDN-Knoten liefert viele kleine Dateien an viele Verbindungen aus, genau dort zahlen sich die neueren Protokolle aus. HTTP/2 ist mit http2 on; erledigt. HTTP/3 bringt vor allem auf Mobilfunkverbindungen etwas, weil ein Netzwechsel die Verbindung nicht abreißen lässt:

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

Vergiss dabei nicht, UDP 443 in der Firewall zu öffnen. Das ist der häufigste Grund, warum HTTP/3 eingerichtet ist und trotzdem niemand es nutzt.

Mehrere Standorte

Ein einzelner Knoten bringt schon den größten Teil des Gewinns, nämlich die Entlastung des Ursprungs. Der zweite Gewinn, kurze Wege zum Besucher, kommt erst mit mehreren Standorten.

Die Verteilung läuft über DNS: Derselbe Name zeigt je nach Herkunft des Besuchers auf einen anderen Knoten. Du brauchst dafür einen DNS-Anbieter mit geografischer Zuordnung. Alle Knoten bekommen dieselbe Konfiguration und denselben Ursprung, sie wissen nichts voneinander, und genau das macht den Aufbau so robust: Fällt ein Knoten aus, nimmst du ihn aus dem DNS, und die übrigen übernehmen.

KernelHost bietet Standorte in Deutschland, an der US-Westküste, an der US-Ostküste sowie in Singapur und Südkorea. Eine Übersicht mit Latenzen findest du unter Serverstandorte. Als Faustregel: Ein Knoten je Kontinent, auf dem ein nennenswerter Teil deines Publikums sitzt.

Messen statt raten

Schreibe den Cache-Status ins Protokoll, dann kannst du die Trefferquote jederzeit auszählen:

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

Auswerten lässt sich das mit einem Einzeiler:

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

Liegt der Anteil von HIT bei statischen Dateien unter 90 Prozent, lohnt die Suche nach der Ursache fast immer: meist sind es Cookies in den Antworten, zu kurze Gültigkeiten oder Anfragen mit wechselnden Parametern am Ende der Adresse, die für den Cache jedes Mal wie eine neue Datei aussehen.

Welcher Server passt

Ein CDN-Knoten ist eine Auslieferungsmaschine. Die Reihenfolge der wichtigen Eigenschaften sieht deshalb anders aus als bei einem Anwendungsserver:

  1. Traffic und Anbindung. Hier entsteht die gesamte Last. Ein Knoten überträgt im Betrieb regelmäßig mehrere Terabyte im Monat.
  2. Platte. NVMe ist deutlich im Vorteil, weil der Cache aus vielen kleinen zufälligen Zugriffen besteht. Die Größe richtet sich danach, wie viel von deinem Bestand heiß ist, nicht danach, wie groß der Bestand insgesamt ist.
  3. RAM. Das Betriebssystem nutzt freien Speicher als zweiten Cache vor der Platte. Mehr RAM bedeutet hier direkt mehr Tempo.
  4. CPU. Am wenigsten kritisch, sie wird nur für TLS gebraucht, und das erledigen moderne Prozessoren nebenbei.

Genau für diesen Fall sind unsere Unlimited-Traffic-Rootserver gemacht: echte 2 × 1 Gbit/s pro Server ohne Drosselung, unbegrenztes Volumen zum festen Monatspreis, Datacenter-NVMe im RAID und 3,2 Tbps Floodschutz ab Werk. Die Bereitstellung dauert unter 30 Sekunden, mit vollem Root-Zugriff, IPv4 und einem /64-IPv6-Netz. Weil auf PrePaid-Basis und monatlich kündbar abgerechnet wird, kannst du mit einem Knoten anfangen und jederzeit einen zweiten Standort dazunehmen, wenn dein Publikum sich verteilt.

Häufige Fragen

Was macht ein CDN-Server genau?
Er sitzt zwischen deinen Besuchern und deinem Ursprungsserver. Die erste Anfrage nach einer Datei reicht er an den Ursprung weiter und legt die Antwort dabei ab. Alle weiteren Anfragen nach derselben Datei beantwortet er selbst, ohne den Ursprung zu behelligen.
Wie stark entlastet das wirklich?
Das hängt an der Trefferquote. Bei 95 Prozent sieht dein Ursprung nur noch jede zwanzigste Anfrage, die Last fällt also auf ein Zwanzigstel. Bei 80 Prozent ist es immer noch ein Fünftel. Für statische Dateien wie Bilder, Skripte, Schriften und Downloads sind 95 Prozent und mehr normal.
Brauche ich dafür einen kommerziellen Anbieter?
Nicht zwingend. Ein eigener CDN-Server lohnt sich vor allem bei großen Dateien und viel Volumen, weil kommerzielle Anbieter pro übertragenem Gigabyte abrechnen. Bei einem Server mit unbegrenztem Traffic kostet das hundertste Terabyte dasselbe wie das erste, nämlich nichts zusätzlich.
Welche Dateien soll ich cachen und welche nicht?
Alles, was für alle Besucher gleich aussieht: Bilder, Videos, CSS, JavaScript, Schriften, PDF, Installationsdateien. Nicht cachen solltest du personalisierte Seiten, Warenkörbe, Anmeldebereiche und alles, was ein Sitzungs-Cookie trägt. Die Entscheidung trifft am besten der Ursprung über den Cache-Control-Header.
Wie lange soll der Cache Dateien behalten?
Für Dateien mit Versionsnummer oder Prüfsumme im Namen ruhig ein Jahr, die ändern sich nie. Für Bilder und Downloads ohne Versionierung sind Stunden bis Tage üblich. Für HTML, falls du es überhaupt cachst, Minuten. Wichtig ist nicht die genaue Zahl, sondern dass du einen Weg hast, einzelne Dateien gezielt zu löschen.
Wie lösche ich eine Datei aus dem Cache?
Am einfachsten über den Dateinamen: Wer bei jeder Änderung einen neuen Namen vergibt, braucht gar kein Löschen. Geht das nicht, hilft bei nginx entweder das kommerzielle Purge-Modul oder das freie ngx_cache_purge. Notfalls löschst du die Cache-Datei direkt im Dateisystem, der Pfad lässt sich aus dem Cache-Schlüssel berechnen.
Was passiert, wenn der Ursprungsserver ausfällt?
Richtig eingestellt liefert der Cache dann weiter aus, auch wenn die Einträge eigentlich abgelaufen sind. Dafür sorgen proxy_cache_use_stale mit error und timeout sowie proxy_cache_background_update. Deine Seite bleibt damit erreichbar, während der Ursprung gewartet wird oder neu startet.
Funktioniert das auch für große Videodateien?
Ja, aber nur mit der Slice-Einstellung. Ohne sie lädt der Cache bei jedem Sprung im Video die ganze Datei neu vom Ursprung. Mit der Direktive slice zerlegt nginx die Datei in Blöcke von zum Beispiel einem Megabyte und legt nur die wirklich angefragten Teile ab.
Wie viele Standorte brauche ich?
Fang mit einem an, und zwar dort, wo die meisten deiner Besucher sind. Ein zweiter Standort lohnt sich erst, wenn ein nennenswerter Teil des Publikums auf einem anderen Kontinent sitzt. Die Verteilung läuft dann über DNS, jeder Besucher landet beim nächstgelegenen Knoten.
Woran erkenne ich, dass der Cache funktioniert?
Gib die Variable upstream_cache_status als Header aus. Beim zweiten Aufruf derselben Datei muss dort HIT stehen. Schreibst du denselben Wert zusätzlich ins Protokoll, kannst du die Trefferquote jederzeit auszählen. Unter 90 Prozent bei statischen Dateien stimmt meist etwas mit den Headern oder den Cookies nicht.

CDN Caching nginx Reverse Proxy Performance HTTP Unlimited Traffic