Budowa własnego serwera CDN: pobrać raz, wydać tysiąc razy

Opublikowano 8 min czytania

Serwer CDN pobiera każdy plik raz ze źródła i potem wydaje go tysiąc razy samodzielnie. Serwer źródłowy widzi już tylko ułamek zapytań, a odwiedzający dostają krótkie czasy ładowania. Tak się taki serwer buduje.

Każdy plik, który odwiedzający ładuje z Twojej strony, Twój serwer źródłowy pobiera z systemu plików, pakuje w odpowiedź i wysyła w drogę. Przy dziesięciu odwiedzających dziennie nikt tego nie zauważy. Przy dziesięciu tysiącach jednoczesnych odwiedzających to właśnie jest powód, dla którego strona staje się wolna, choć sama aplikacja nie ma nic do roboty.

Serwer CDN staje przed nią. Pobiera każdy plik raz ze źródła, a potem wydaje go sam, tyle razy, ile jest potrzebny. Źródło widzi dzięki temu już tylko ułamek zapytań, a odwiedzający dostają swoje pliki z serwera, który nie robi nic poza wydawaniem. Ten artykuł pokazuje, ile to naprawdę daje, jak postawić taki serwer na nginx i na co zwrócić uwagę przy transferze i lokalizacji.

Co serwer CDN właściwie robi

Przebieg opowiada się w trzech zdaniach:

  • Odwiedzający pyta serwer CDN o plik.
  • Jeśli plik już tam leży, wychodzi natychmiast. To jest trafienie.
  • Jeśli go tam nie ma, serwer CDN pobiera go ze źródła, odkłada i wydaje. To jest chybienie i zdarza się na plik tylko raz w okresie ważności.

Zasada jest ta sama co przy serwerze streamingowym: pobrać raz, wydać wielokrotnie. Różnica jest tylko taka, że serwer streamingowy rozdziela bieżący strumień danych, a serwer CDN gotowe pliki.

Rachunek: jak mocno cache odciąża

Decydujący jest współczynnik trafień, czyli udział zapytań, na które cache odpowiada sam. Przy milionie zapytań na dobę odciążenie wygląda tak:

Współczynnik trafieńZapytania u źródłaOdciążenie
50 %500.000czynnik 2
80 %200.000czynnik 5
95 %50.000czynnik 20
99 %10.000czynnik 100

Dla plików statycznych 95 procent i więcej to przypadek normalny, bo zdjęcia, skrypty i fonty zmieniają się rzadko. Skok z 95 na 99 procent jest przy tym cenniejszy, niż liczby każą przypuszczać: dzieli pozostałe obciążenie jeszcze raz przez pięć.

Jednocześnie przesuwa się transfer. Źródło prawie nic już nie przesyła, serwer CDN tym więcej. Przy 500.000 wywołań zdjęć na dobę po 400 KB każde daje to 200 GB dziennie, czyli około 6 TB w miesiącu, i to tylko za zdjęcia. Dlatego przy węźle CDN ważną liczbą nie jest procesor, a łącze i wolumen.

Kiedy własny serwer CDN się opłaca

Komercyjni dostawcy CDN rozliczają każdy przesłany gigabajt. To jest wygodne i przy małym wolumenie tanie. Od pewnej ilości danych proporcje się odwracają:

PrzypadekWłasny serwer CDNRozliczenie za gigabajt
Kilka GB w miesiącuprawie się nie opłacataniej
Wiele TB w miesiącustała cena miesięcznarośnie liniowo
Duże pliki do pobrania, archiwa wideowyraźna przewagaszybko staje się drogie
Pełna kontrola nad regułami i protokołamicałkowitaograniczona

Dochodzi do tego punkt, który trudno wyrazić w liczbach: przy własnym węźle sam decydujesz, co jest cachowane, jak długo i kiedy znika. Nie jesteś przywiązany do reguł obcego dostawcy i możesz wydawać także rzeczy, które nie mieszczą się w obcych schematach.

Budowa z nginx jako cache

Instalacja

apt update
apt install -y nginx

Więcej nie trzeba, cache jest w nginx wbudowany. Utwórz katalog, w którym mają leżeć pliki:

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

Konfiguracja

Cache opisuje się raz w bloku http, a potem używa w poszczególnych lokalizacjach:

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

Wartości znaczą po kolei tyle: keys_zone=cdn:100m wystarcza na około 800.000 wpisów, max_size ogranicza miejsce na dysku, inactive=30d wyrzuca pliki, o które nikt nie pytał od 30 dni, a use_temp_path=off unika zbędnego przenoszenia między katalogami.

Poniżej właściwy serwer:

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;

        # łączenie wielu jednoczesnych chybień w jedno zapytanie
        proxy_cache_lock on;
        proxy_cache_lock_timeout 10s;

        # wydawanie dalej przy problemach ze źródłem
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;

        # ciasteczka sesji nie mogą rozsadzić cache
        proxy_ignore_headers Set-Cookie;
        proxy_hide_header Set-Cookie;

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

Trzy linie z tego zasługują na szczególną uwagę. proxy_cache_lock dba o to, żeby przy nowym pliku nie pytało źródła stu odwiedzających jednocześnie, a tylko jeden, podczas gdy pozostali czekają. proxy_cache_use_stale utrzymuje wydawanie treści, gdy źródło startuje ponownie albo odpowiada wolno. A ukrycie Set-Cookie zapobiega temu, żeby jedna jedyna odpowiedź z ciasteczkiem sesji uczyniła cały cache bezużytecznym.

Sprawdzenie, czy działa

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

Przy pierwszym wywołaniu stoi tam MISS, przy drugim HIT. Jeśli i za drugim razem stoi MISS, przyczyną jest prawie zawsze jeden z trzech zwykłych podejrzanych: Set-Cookie w odpowiedzi, Cache-Control: no-store ze źródła albo brakująca informacja o długości.

Co jest cachowane, a co nie

Czyste rozwiązanie polega na tym, że mówi to samo źródło. Ono zna swoje treści, cache nie. Ustawia do tego odpowiednie nagłówki:

TreśćNagłówek u źródła
Plik z sumą kontrolną w nazwieCache-Control: public, max-age=31536000, immutable
Zdjęcie albo plik do pobrania bez wersjonowaniaCache-Control: public, max-age=86400
HTML strony publicznejCache-Control: public, max-age=300
Obszar po zalogowaniu, koszykCache-Control: private, no-store

Jeśli chcesz decydować w samym cache, da się to zrobić przez osobne lokalizacje dla każdego typu plików:

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

Celowe pozbywanie się treści

Najprostsza droga to w ogóle nie musieć usuwać: kto każdemu zmienionemu zdjęciu nadaje nową nazwę pliku, na przykład logo.a1b2c3.png, nie ma tego problemu. Stary plik kiedyś sam wypada z cache.

Jeśli to niemożliwe, przy nginx pomaga darmowy moduł ngx_cache_purge. Bez dodatkowego modułu ścieżkę da się policzyć z klucza cache, bo nginx używa jego sumy MD5 jako nazwy pliku:

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"

Na codzienne potrzeby wystarcza zwykle mały skrypt, który wykonuje te linie dla listy ścieżek.

Duże pliki i wideo

Przy plikach powyżej kilkuset megabajtów potrzebujesz rozkładu na bloki, inaczej cache przy każdym skoku w filmie pobiera cały plik od nowa:

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

W ten sposób nginx pobiera tylko te bloki, o które naprawdę pytano. Gdy widz przeskoczy na środek filmu, przez łącze wędruje kilka megabajtów zamiast kilku gigabajtów.

TLS, HTTP/2 i HTTP/3

Węzeł CDN wydaje wiele małych plików do wielu połączeń, a właśnie tam nowsze protokoły się opłacają. HTTP/2 załatwia http2 on;. HTTP/3 daje przede wszystkim coś na połączeniach komórkowych, bo zmiana sieci nie zrywa połączenia:

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

Nie zapomnij przy tym otworzyć w firewallu portu UDP 443. To najczęstszy powód, dla którego HTTP/3 jest skonfigurowany, a jednak nikt z niego nie korzysta.

Kilka lokalizacji

Pojedynczy węzeł daje już największą część zysku, czyli odciążenie źródła. Drugi zysk, krótkie trasy do odwiedzającego, przychodzi dopiero z kilkoma lokalizacjami.

Rozdzielanie działa przez DNS: ta sama nazwa wskazuje, zależnie od pochodzenia odwiedzającego, na inny węzeł. Potrzebujesz do tego dostawcy DNS z przypisaniem geograficznym. Wszystkie węzły dostają tę samą konfigurację i to samo źródło, nic o sobie nie wiedzą, i właśnie to czyni całą konstrukcję tak odporną: gdy jeden węzeł padnie, wyjmujesz go z DNS, a pozostałe przejmują ruch.

KernelHost oferuje lokalizacje w Niemczech, na zachodnim i wschodnim wybrzeżu USA oraz w Singapurze i Korei Południowej. Przegląd z opóźnieniami znajdziesz pod lokalizacje serwerów. Jako reguła praktyczna: jeden węzeł na każdy kontynent, na którym siedzi znacząca część Twojej publiczności.

Mierzyć, nie zgadywać

Zapisuj status cache do protokołu, wtedy możesz w każdej chwili przeliczyć współczynnik trafień:

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

Wynik da się ocenić jedną linijką:

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

Jeśli udział HIT przy plikach statycznych leży poniżej 90 procent, szukanie przyczyny prawie zawsze się opłaca: zwykle są to ciasteczka w odpowiedziach, za krótkie okresy ważności albo zapytania ze zmiennymi parametrami na końcu adresu, które dla cache za każdym razem wyglądają jak nowy plik.

Jaki serwer pasuje

Węzeł CDN to maszyna do wydawania treści. Kolejność ważnych cech wygląda więc inaczej niż przy serwerze aplikacyjnym:

  1. Transfer i łącze. Tu powstaje całe obciążenie. Węzeł w ruchu regularnie przesyła kilka terabajtów w miesiącu.
  2. Dysk. NVMe ma wyraźną przewagę, bo cache składa się z wielu małych losowych odczytów. Wielkość zależy od tego, jak duża część Twoich zasobów jest gorąca, a nie od tego, jak duże są zasoby w całości.
  3. RAM. System operacyjny używa wolnej pamięci jako drugiego cache przed dyskiem. Więcej RAM znaczy tu wprost więcej tempa.
  4. Procesor. Najmniej krytyczny, potrzebny jest tylko do TLS, a to nowoczesne procesory robią przy okazji.

Dokładnie na ten przypadek zrobione są nasze serwery root z nielimitowanym transferem: prawdziwe 2 × 1 Gbit/s na serwer bez dławienia, nielimitowany wolumen w stałej cenie miesięcznej, NVMe klasy centrum danych w RAID oraz ochrona przed zalewaniem o wydajności 3,2 Tbps od razu po uruchomieniu. Udostępnienie trwa niecałe 30 sekund, z pełnym dostępem root, IPv4 i siecią IPv6 /64. Ponieważ rozliczenie działa na zasadzie przedpłaty z możliwością rezygnacji co miesiąc, możesz zacząć od jednego węzła i w każdej chwili dodać drugą lokalizację, gdy Twoja publiczność się rozłoży.

Najczęstsze pytania

Co dokładnie robi serwer CDN?
Siedzi między Twoimi odwiedzającymi a Twoim serwerem źródłowym. Pierwsze zapytanie o plik przekazuje do źródła i odkłada przy tym odpowiedź. Wszystkie dalsze zapytania o ten sam plik odpowiada już sam, bez zawracania głowy źródłu.
Jak mocno to naprawdę odciąża?
To zależy od współczynnika trafień. Przy 95 procentach Twoje źródło widzi już tylko co dwudzieste zapytanie, obciążenie spada więc do jednej dwudziestej. Przy 80 procentach to nadal jedna piąta. Dla plików statycznych, takich jak zdjęcia, skrypty, fonty i pliki do pobrania, 95 procent i więcej to norma.
Czy potrzebuję do tego komercyjnego dostawcy?
Niekoniecznie. Własny serwer CDN opłaca się przede wszystkim przy dużych plikach i dużym wolumenie, bo komercyjni dostawcy rozliczają każdy przesłany gigabajt. Przy serwerze z nielimitowanym transferem setny terabajt kosztuje tyle samo co pierwszy, czyli nic dodatkowo.
Które pliki cachować, a których nie?
Wszystko, co dla wszystkich odwiedzających wygląda tak samo: zdjęcia, wideo, CSS, JavaScript, fonty, PDF, pliki instalacyjne. Nie powinieneś cachować stron spersonalizowanych, koszyków, obszarów po zalogowaniu i wszystkiego, co nosi ciasteczko sesji. Decyzję najlepiej podejmuje źródło przez nagłówek Cache-Control.
Jak długo cache ma trzymać pliki?
Dla plików z numerem wersji albo sumą kontrolną w nazwie spokojnie rok, te się nigdy nie zmieniają. Dla zdjęć i plików do pobrania bez wersjonowania typowe są godziny do dni. Dla HTML, jeśli w ogóle go cachujesz, minuty. Ważna jest nie dokładna liczba, a to, żebyś miał sposób na celowe usunięcie pojedynczych plików.
Jak usunąć plik z cache?
Najprościej przez nazwę pliku: kto przy każdej zmianie nadaje nową nazwę, w ogóle nie musi nic usuwać. Jeśli to niemożliwe, przy nginx pomaga albo komercyjny moduł purge, albo darmowy ngx_cache_purge. W razie potrzeby usuwasz plik cache wprost w systemie plików, ścieżkę da się policzyć z klucza cache.
Co się dzieje, gdy serwer źródłowy padnie?
Przy właściwych ustawieniach cache wydaje treści dalej, nawet gdy wpisy są już przedawnione. Dbają o to proxy_cache_use_stale z error i timeout oraz proxy_cache_background_update. Twoja strona pozostaje dzięki temu dostępna, podczas gdy źródło jest serwisowane albo startuje ponownie.
Czy działa to także dla dużych plików wideo?
Tak, ale tylko z ustawieniem slice. Bez niego cache przy każdym skoku w filmie pobiera cały plik od nowa ze źródła. Przez slice nginx rozkłada plik na bloki, na przykład po jednym megabajcie, i odkłada tylko te części, o które naprawdę pytano.
Ile lokalizacji potrzebuję?
Zacznij od jednej, i to tam, gdzie siedzi większość Twoich odwiedzających. Druga lokalizacja opłaca się dopiero wtedy, gdy znacząca część publiczności siedzi na innym kontynencie. Rozdzielanie działa wtedy przez DNS, każdy odwiedzający trafia na najbliższy węzeł.
Po czym poznam, że cache działa?
Wypisz zmienną upstream_cache_status jako nagłówek. Przy drugim wywołaniu tego samego pliku musi tam stać HIT. Jeśli zapiszesz tę samą wartość dodatkowo do protokołu, możesz w każdej chwili przeliczyć współczynnik trafień. Poniżej 90 procent przy plikach statycznych zwykle coś jest nie tak z nagłówkami albo z ciasteczkami.

CDN Caching nginx Reverse Proxy Performance HTTP Unlimited Traffic