Construiește propriul server CDN: aduci o dată, livrezi de mii de ori

Publicat pe 9 min de citit

Un server CDN aduce fiecare fișier o dată de la origine și apoi îl livrează singur de mii de ori. Serverul de origine vede doar o fracțiune din cereri, iar vizitatorii primesc timpi de încărcare scurți. Așa construiești unul.

Fiecare fișier pe care un vizitator îl încarcă de pe site-ul tău este scos de serverul de origine din sistemul de fișiere, împachetat într-un răspuns și trimis la drum. La zece vizitatori pe zi nu observă nimeni asta. La zece mii de vizitatori simultani, exact acesta este motivul pentru care site-ul devine lent, deși aplicația propriu-zisă nu are nimic de făcut.

Un server CDN se așază în fața lui. Aduce fiecare fișier o singură dată de la origine și apoi îl livrează singur, de câte ori este nevoie. Originea vede astfel doar o fracțiune din cereri, iar vizitatorii primesc fișierele de la un server care nu face nimic altceva decât să livreze. Articolul arată cât aduce asta în realitate, cum ridici un astfel de server cu nginx și ce contează la trafic și la locație.

Ce face în realitate un server CDN

Mersul lucrurilor se spune în trei propoziții:

  • Un vizitator cere un fișier de la serverul CDN.
  • Dacă acesta se află deja acolo, pleacă imediat. Acesta este un hit.
  • Dacă nu se află acolo, serverul CDN îl aduce de la origine, îl depune și îl livrează. Acesta este un miss și se întâmplă pentru fiecare fișier o singură dată pe durată de valabilitate.

Principiul este același ca la un server de streaming: aduci o dată, livrezi de multe ori. Diferența este doar că un server de streaming distribuie un flux de date în curs, iar un server CDN fișiere gata făcute.

Socoteala: cât de mult ușurează un cache

Decisivă este rata de hit, adică partea din cereri la care răspunde cache-ul singur. La un milion de cereri pe zi, reducerea sarcinii arată așa:

Rata de hitCereri la origineReducerea sarcinii
50%500.000factor 2
80%200.000factor 5
95%50.000factor 20
99%10.000factor 100

Pentru fișierele statice, 95 la sută și peste este cazul normal, pentru că imaginile, scripturile și fonturile se schimbă rar. Saltul de la 95 la 99 la sută este mai valoros decât lasă să se creadă cifrele: împarte încă o dată la cinci sarcina rămasă.

În același timp, traficul se mută. Originea nu mai transferă aproape nimic, în schimb serverul CDN cu atât mai mult. La 500.000 de accesări de imagini pe zi, cu 400 KB fiecare, asta înseamnă 200 GB zilnic, deci în jur de 6 TB pe lună, numai pentru imagini. De aceea, la un nod CDN cifra importantă nu este procesorul, ci conectivitatea și volumul.

Când merită un server CDN propriu

Furnizorii comerciali de CDN facturează pe gigabyte transferat. Este comod și, la volum mic, ieftin. De la o anumită cantitate, raportul se inversează:

CazServer CDN propriuFacturare pe gigabyte
Câțiva GB pe lunăaproape că nu meritămai ieftină
Mulți TB pe lunăpreț lunar fixcrește liniar
Descărcări mari, arhive videoclar în avantajdevine repede scumpă
Control deplin asupra regulilor și protocoalelorcompletlimitat

Mai apare un punct greu de pus în cifre: la un nod propriu decizi tu ce se pune în cache, cât timp și când dispare. Nu ești legat de regulile unui furnizor străin și poți livra și lucruri care nu se potrivesc în scheme străine.

Montajul cu nginx pe post de cache

Instalare

apt update
apt install -y nginx

Nu trebuie mai mult, cache-ul este integrat în nginx. Creează directorul în care să stea fișierele:

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

Configurare

Cache-ul se descrie o dată în blocul http și apoi se folosește în blocurile location individuale:

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

Valorile înseamnă, pe rând: keys_zone=cdn:100m ajunge pentru circa 800.000 de intrări, max_size limitează spațiul pe disc, inactive=30d aruncă fișierele pe care nu le-a cerut nimeni timp de 30 de zile, iar use_temp_path=off evită o mutare inutilă între directoare.

Dedesubt, serverul propriu-zis:

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;

        # mai multe miss-uri simultane se strâng într-o singură cerere
        proxy_cache_lock on;
        proxy_cache_lock_timeout 10s;

        # la probleme la origine se livrează mai departe
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;

        # cookie-urile de sesiune nu trebuie să spargă cache-ul
        proxy_ignore_headers Set-Cookie;
        proxy_hide_header Set-Cookie;

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

Trei linii de aici merită atenție deosebită. proxy_cache_lock face ca, la un fișier nou, să nu ceară o sută de vizitatori originea în același timp, ci doar unul, iar ceilalți să aștepte. proxy_cache_use_stale menține livrarea în timp ce originea repornește sau răspunde încet. Iar ascunderea lui Set-Cookie împiedică un singur răspuns cu cookie de sesiune să facă tot cache-ul inutilizabil.

Verifici dacă prinde

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

La primul apel scrie acolo MISS, la al doilea HIT. Dacă și a doua dată scrie MISS, aproape mereu este unul dintre cei trei suspecți obișnuiți: un Set-Cookie în răspuns, un Cache-Control: no-store de la origine sau o lungime care lipsește.

Ce se pune în cache și ce nu

Soluția curată este ca originea să spună singură. Ea își cunoaște conținutul, cache-ul nu. Pentru asta pune headerele potrivite:

ConținutHeader la origine
Fișier cu sumă de control în numeCache-Control: public, max-age=31536000, immutable
Imagine sau descărcare fără versionareCache-Control: public, max-age=86400
HTML-ul unei pagini publiceCache-Control: public, max-age=300
Zonă autentificată, coș de cumpărăturiCache-Control: private, no-store

Dacă vrei să decizi în cache însuși, asta merge prin blocuri location proprii pentru fiecare tip de fișier:

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

Scoți conținut în mod țintit

Calea cea mai simplă este să nu fie nevoie să ștergi deloc: cine dă fiecărei imagini schimbate un nume nou de fișier, de pildă logo.a1b2c3.png, nu are problema asta. Fișierul vechi cade cândva singur din cache.

Dacă nu se poate, la nginx ajută modulul liber ngx_cache_purge. Fără modul suplimentar, calea se poate calcula din cheia de cache, pentru că nginx folosește suma MD5 a acesteia ca nume de fișier:

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"

Pentru uzul zilnic ajunge de obicei un script mic, care execută aceste linii pentru o listă de căi.

Fișiere mari și videoclipuri

La fișiere de peste câteva sute de megabytes ai nevoie de descompunerea în blocuri, altfel cache-ul încarcă din nou fișierul întreg la fiecare salt în video:

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

Astfel nginx aduce doar blocurile care sunt cerute cu adevărat. Dacă un spectator sare în mijlocul unui film, pe linie trec câțiva megabytes în loc de mai mulți gigabytes.

TLS, HTTP/2 și HTTP/3

Un nod CDN livrează multe fișiere mici către multe conexiuni, exact acolo se amortizează protocoalele mai noi. HTTP/2 este rezolvat cu http2 on;. HTTP/3 aduce ceva mai ales pe conexiunile mobile, pentru că o schimbare de rețea nu lasă conexiunea să se rupă:

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

Nu uita să deschizi UDP 443 în firewall. Acesta este cel mai frecvent motiv pentru care HTTP/3 este configurat și totuși nimeni nu îl folosește.

Mai multe locații

Un singur nod aduce deja cea mai mare parte din câștig, anume ușurarea originii. Al doilea câștig, drumuri scurte către vizitator, vine abia cu mai multe locații.

Distribuția merge prin DNS: același nume arată, în funcție de proveniența vizitatorului, către un alt nod. Pentru asta ai nevoie de un furnizor DNS cu atribuire geografică. Toate nodurile primesc aceeași configurație și aceeași origine, nu știu nimic unul despre altul, și exact asta face montajul atât de robust: dacă un nod cade, îl scoți din DNS, iar celelalte preiau.

KernelHost oferă locații în Germania, pe Coasta de Vest a SUA, pe Coasta de Est a SUA, precum și în Singapore și Coreea de Sud. O privire de ansamblu cu latențe găsești la locațiile serverelor. Ca regulă de bază: un nod pe fiecare continent în care stă o parte demnă de menționat din publicul tău.

Măsori, nu ghicești

Scrie starea cache-ului în jurnal, apoi poți număra rata de hit oricând:

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

Se poate evalua cu o singură linie:

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

Dacă partea de HIT la fișierele statice stă sub 90 la sută, căutarea cauzei merită aproape mereu: de obicei sunt cookie-uri în răspunsuri, valabilități prea scurte sau cereri cu parametri schimbători la capătul adresei, care arată pentru cache de fiecare dată ca un fișier nou.

Ce server se potrivește

Un nod CDN este o mașină de livrare. Ordinea proprietăților importante arată de aceea altfel decât la un server de aplicație:

  1. Trafic și conectivitate. Aici apare toată sarcina. Un nod transferă în funcționare, în mod regulat, mai mulți terabytes pe lună.
  2. Discul. NVMe este clar în avantaj, pentru că cache-ul este făcut din multe accesări mici și aleatorii. Mărimea se orientează după cât din fondul tău este fierbinte, nu după cât de mare este fondul în total.
  3. RAM. Sistemul de operare folosește memoria liberă ca al doilea cache în fața discului. Mai mult RAM înseamnă aici direct mai multă viteză.
  4. Procesorul. Cel mai puțin critic, este folosit doar pentru TLS, iar asta o rezolvă procesoarele moderne în treacăt.

Exact pentru acest caz sunt făcute serverele root cu trafic nelimitat de la noi: 2 × 1 Gbit/s reali pe server, fără limitare de viteză, volum nelimitat la preț lunar fix, NVMe de centru de date în RAID și 3,2 Tbps protecție antiflood din fabrică. Livrarea durează mai puțin de 30 de secunde, cu acces root complet, IPv4 și o rețea IPv6 /64. Pentru că facturarea se face pe bază PrePaid și se poate anula lunar, poți începe cu un nod și adăuga oricând o a doua locație, atunci când publicul tău se împrăștie.

Întrebări frecvente

Ce face exact un server CDN?
Stă între vizitatorii tăi și serverul tău de origine. Prima cerere pentru un fișier o trece mai departe către origine și depune totodată răspunsul. Toate cererile următoare pentru același fișier le răspunde singur, fără să deranjeze originea.
Cât de mult ușurează asta în realitate?
Depinde de rata de hit. La 95 la sută, originea ta vede doar fiecare a douăzecea cerere, deci sarcina scade la o douăzecime. La 80 la sută este tot o cincime. Pentru fișiere statice, cum sunt imaginile, scripturile, fonturile și descărcările, 95 la sută și peste este ceva normal.
Am nevoie pentru asta de un furnizor comercial?
Nu obligatoriu. Un server CDN propriu merită mai ales la fișiere mari și volum mult, pentru că furnizorii comerciali facturează pe gigabyte transferat. La un server cu trafic nelimitat, al sutălea terabyte costă la fel ca primul, adică nimic în plus.
Ce fișiere să pun în cache și ce nu?
Tot ce arată la fel pentru toți vizitatorii: imagini, videoclipuri, CSS, JavaScript, fonturi, PDF, fișiere de instalare. Nu ar trebui să pui în cache pagini personalizate, coșuri de cumpărături, zone de autentificare și tot ce poartă un cookie de sesiune. Decizia cel mai bine o ia originea, prin headerul Cache-Control.
Cât timp să păstreze cache-ul fișierele?
Pentru fișierele cu număr de versiune sau sumă de control în nume, liniștit un an, acelea nu se schimbă niciodată. Pentru imagini și descărcări fără versionare sunt obișnuite ore până la zile. Pentru HTML, dacă îl pui în cache, minute. Important nu este cifra exactă, ci să ai o cale de a șterge țintit fișiere individuale.
Cum șterg un fișier din cache?
Cel mai simplu prin numele fișierului: cine dă un nume nou la fiecare modificare nu are nevoie de ștergere. Dacă nu se poate, la nginx ajută fie modulul Purge comercial, fie ngx_cache_purge, cel liber. La nevoie ștergi fișierul de cache direct în sistemul de fișiere, calea se poate calcula din cheia de cache.
Ce se întâmplă dacă serverul de origine cade?
Setat corect, cache-ul livrează mai departe, chiar dacă intrările au expirat de fapt. De asta se ocupă proxy_cache_use_stale cu error și timeout, precum și proxy_cache_background_update. Site-ul tău rămâne astfel accesibil în timp ce originea este întreținută sau repornește.
Funcționează și pentru fișiere video mari?
Da, dar numai cu setarea slice. Fără ea, cache-ul încarcă la fiecare salt în video fișierul întreg din nou de la origine. Cu slice, nginx descompune fișierul în blocuri de, de exemplu, un megabyte și depune doar părțile cerute cu adevărat.
De câte locații am nevoie?
Începe cu una, și anume acolo unde se află cei mai mulți dintre vizitatorii tăi. O a doua locație merită abia când o parte demnă de menționat din public stă pe un alt continent. Distribuția merge apoi prin DNS, fiecare vizitator ajunge la nodul cel mai apropiat.
Cum recunosc că cache-ul funcționează?
Scoate variabila upstream_cache_status ca header. La al doilea apel al aceluiași fișier trebuie să scrie acolo HIT. Dacă scrii aceeași valoare și în jurnal, poți număra rata de hit oricând. Sub 90 la sută la fișierele statice, de obicei nu este în regulă ceva cu headerele sau cu cookie-urile.

CDN Caching nginx Reverse Proxy Performance HTTP Unlimited Traffic