Monter son propre serveur CDN : récupérer une fois, livrer mille fois

Publié le 9 min de lecture

Un serveur CDN récupère chaque fichier une fois depuis l'origine, puis le livre mille fois lui-même. Le serveur d'origine ne voit plus qu'une fraction des requêtes, et les visiteurs obtiennent des temps de chargement courts. Voici comment en monter un.

Chaque fichier qu'un visiteur charge depuis votre site, votre serveur d'origine le récupère dans le système de fichiers, l'emballe dans une réponse et l'envoie. Avec dix visiteurs par jour, personne ne le remarque. Avec dix mille visiteurs simultanés, c'est exactement la raison pour laquelle le site devient lent, alors que l'application proprement dite n'a rien à faire.

Un serveur CDN se place devant. Il récupère chaque fichier une fois depuis l'origine et le livre ensuite lui-même, aussi souvent qu'il est demandé. L'origine ne voit de ce fait plus qu'une fraction des requêtes, et les visiteurs reçoivent leurs fichiers d'un serveur qui ne fait rien d'autre que livrer. Cet article montre ce que cela apporte vraiment, comment monter un tel serveur avec nginx et ce qui compte en matière de trafic et d'emplacement.

Ce que fait réellement un serveur CDN

Le déroulement se raconte en trois phrases :

  • Un visiteur demande un fichier au serveur CDN.
  • S'il est déjà là, il part aussitôt. C'est un hit.
  • S'il n'est pas là, le serveur CDN le récupère à l'origine, le dépose et le livre. C'est un miss, et il ne se produit qu'une fois par fichier et par durée de validité.

Le principe est le même que pour un serveur de streaming : récupérer une fois, livrer de multiples fois. La seule différence est qu'un serveur de streaming distribue un flux de données en cours et un serveur CDN des fichiers terminés.

Le calcul : de combien un cache soulage

Le taux de hits est décisif, c'est-à-dire la part des requêtes auxquelles le cache répond lui-même. Pour un million de requêtes par jour, le soulagement ressemble à ceci :

Taux de hitsRequêtes à l'origineSoulagement
50 %500 000facteur 2
80 %200 000facteur 5
95 %50 000facteur 20
99 %10 000facteur 100

Pour les fichiers statiques, 95 pour cent et plus sont le cas normal, car les images, les scripts et les polices changent rarement. Le saut de 95 à 99 pour cent est d'ailleurs plus précieux que les chiffres ne le laissent supposer : il divise encore par cinq la charge restante.

En même temps, le trafic se déplace. L'origine ne transfère presque plus rien, le serveur CDN d'autant plus. Pour 500 000 appels d'images par jour à 400 Ko chacun, cela fait 200 Go par jour, soit environ 6 To par mois, rien que pour les images. C'est pourquoi, sur un nœud CDN, le chiffre important n'est pas le CPU, mais la connectivité et le volume.

Quand un serveur CDN à soi vaut la peine

Les prestataires CDN commerciaux facturent au gigaoctet transféré. C'est commode et peu coûteux quand le volume est faible. À partir d'une certaine quantité, le rapport s'inverse :

CasServeur CDN à soiFacturation au gigaoctet
Quelques Go par moisne vaut guère la peinemoins cher
Plusieurs To par moisprix mensuel fixecroît linéairement
Gros téléchargements, archives vidéonettement avantageuxdevient vite coûteux
Contrôle total sur les règles et les protocolescompletlimité

S'y ajoute un point difficile à chiffrer : avec un nœud à vous, vous décidez vous-même de ce qui est mis en cache, pour combien de temps, et quand cela disparaît. Vous n'êtes pas lié aux règles d'un prestataire extérieur et vous pouvez aussi livrer des choses qui n'entrent pas dans des schémas imposés.

Montage avec nginx comme cache

Installer

apt update
apt install -y nginx

Il n'en faut pas plus, le cache est intégré à nginx. Créez le répertoire dans lequel les fichiers doivent se trouver :

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

Configurer

Le cache est décrit une fois dans le bloc http, puis utilisé dans les différents emplacements :

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

Les valeurs signifient, dans le détail : keys_zone=cdn:100m suffit pour environ 800 000 entrées, max_size limite la place sur le disque, inactive=30d jette les fichiers que personne n'a demandés depuis 30 jours, et use_temp_path=off évite un déplacement inutile entre répertoires.

En dessous, le serveur proprement dit :

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;

        # regrouper plusieurs miss simultanés en une seule requête
        proxy_cache_lock on;
        proxy_cache_lock_timeout 10s;

        # continuer à servir en cas de problème à l'origine
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;

        # les cookies de session ne doivent pas faire exploser le cache
        proxy_ignore_headers Set-Cookie;
        proxy_hide_header Set-Cookie;

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

Trois lignes méritent une attention particulière. proxy_cache_lock veille à ce que, pour un fichier nouveau, cent visiteurs n'interrogent pas l'origine en même temps, mais un seul, les autres attendant. proxy_cache_use_stale maintient la livraison pendant que l'origine redémarre ou répond lentement. Et le masquage de Set-Cookie évite qu'une seule réponse portant un cookie de session rende tout le cache inutilisable.

Vérifier que cela prend

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

Au premier appel, il y a MISS, au second HIT. Si MISS apparaît aussi la deuxième fois, cela vient presque toujours de l'un des trois suspects habituels : un Set-Cookie dans la réponse, un Cache-Control: no-store venant de l'origine ou une indication de longueur manquante.

Ce qui est mis en cache et ce qui ne l'est pas

La solution propre est que l'origine le dise elle-même. Elle connaît ses contenus, le cache non. Pour cela, elle pose les en-têtes adéquats :

ContenuEn-tête à l'origine
Fichier avec somme de contrôle dans le nomCache-Control: public, max-age=31536000, immutable
Image ou téléchargement sans versionnageCache-Control: public, max-age=86400
HTML d'une page publiqueCache-Control: public, max-age=300
Espace connecté, panierCache-Control: private, no-store

Si vous voulez décider dans le cache lui-même, cela passe par des emplacements propres à chaque type de fichier :

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

Se débarrasser de contenus précis

Le chemin le plus simple est de ne pas avoir à supprimer : qui donne un nouveau nom de fichier à chaque image modifiée, par exemple logo.a1b2c3.png, n'a pas le problème. L'ancien fichier sort tout seul du cache à un moment ou un autre.

Si ce n'est pas possible, nginx propose le module libre ngx_cache_purge. Sans module supplémentaire, le chemin se calcule à partir de la clé de cache, parce que nginx utilise sa somme MD5 comme nom de fichier :

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"

Pour le quotidien, un petit script qui exécute ces lignes pour une liste de chemins suffit la plupart du temps.

Gros fichiers et vidéos

Pour les fichiers de plus de quelques centaines de mégaoctets, vous avez besoin du découpage en blocs, sinon le cache recharge tout le fichier à chaque saut dans la vidéo :

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

nginx ne récupère ainsi que les blocs réellement demandés. Si un spectateur saute au milieu d'un film, quelques mégaoctets passent par la ligne au lieu de plusieurs gigaoctets.

TLS, HTTP/2 et HTTP/3

Un nœud CDN livre beaucoup de petits fichiers à de nombreuses connexions, c'est exactement là que les protocoles récents sont payants. HTTP/2 est réglé avec http2 on;. HTTP/3 apporte surtout quelque chose sur les connexions mobiles, parce qu'un changement de réseau ne fait pas tomber la connexion :

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

N'oubliez pas d'ouvrir l'UDP 443 dans le pare-feu. C'est la raison la plus fréquente pour laquelle HTTP/3 est configuré et que personne ne l'utilise pour autant.

Plusieurs emplacements

Un seul nœud apporte déjà la plus grande part du gain, à savoir le soulagement de l'origine. Le deuxième gain, des chemins courts vers le visiteur, ne vient qu'avec plusieurs emplacements.

La répartition passe par le DNS : le même nom pointe, selon la provenance du visiteur, vers un nœud différent. Il vous faut pour cela un fournisseur DNS avec attribution géographique. Tous les nœuds reçoivent la même configuration et la même origine, ils ne savent rien l'un de l'autre, et c'est précisément ce qui rend le montage si robuste : si un nœud tombe, vous le retirez du DNS, et les autres prennent le relais.

KernelHost propose des emplacements en Allemagne, sur la côte ouest et sur la côte est des États-Unis ainsi qu'à Singapour et en Corée du Sud. Vous trouverez un aperçu avec les latences sous Emplacements des serveurs. Comme règle empirique : un nœud par continent sur lequel se trouve une part notable de votre public.

Mesurer au lieu de deviner

Écrivez le statut du cache dans le journal, vous pourrez alors compter le taux de hits à tout moment :

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

L'analyse se fait avec une seule ligne :

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

Si la part de HIT sur les fichiers statiques est sous 90 pour cent, chercher la cause vaut presque toujours la peine : ce sont le plus souvent des cookies dans les réponses, des durées de validité trop courtes ou des requêtes aux paramètres changeants en fin d'adresse, qui ressemblent chaque fois à un nouveau fichier pour le cache.

Quel serveur convient

Un nœud CDN est une machine à livrer. L'ordre des propriétés importantes est donc différent de celui d'un serveur applicatif :

  1. Trafic et connectivité. C'est là que naît toute la charge. En exploitation, un nœud transfère régulièrement plusieurs téraoctets par mois.
  2. Disque. Le NVMe est nettement avantageux, parce que le cache se compose de nombreux petits accès aléatoires. La taille se règle sur la part de votre contenu qui est chaude, et non sur la taille totale du contenu.
  3. RAM. Le système d'exploitation utilise la mémoire libre comme deuxième cache devant le disque. Plus de RAM signifie ici directement plus de vitesse.
  4. CPU. Le moins critique, il ne sert que pour TLS, et les processeurs modernes s'en occupent accessoirement.

Nos serveurs root à trafic illimité sont faits exactement pour ce cas : de vrais 2 × 1 Gbit/s par serveur sans bridage, un volume illimité au prix mensuel fixe, du NVMe datacenter en RAID et 3,2 Tbps de protection anti-flood d'origine. La mise à disposition prend moins de 30 secondes, avec un accès root complet, une IPv4 et un réseau IPv6 en /64. Comme la facturation se fait en prépaiement et reste résiliable chaque mois, vous pouvez commencer avec un nœud et ajouter un deuxième emplacement à tout moment, quand votre public se répartit.

Questions fréquentes

Que fait exactement un serveur CDN ?
Il se place entre vos visiteurs et votre serveur d'origine. La première requête pour un fichier, il la transmet à l'origine et dépose la réponse au passage. Toutes les requêtes suivantes pour le même fichier, il y répond lui-même, sans importuner l'origine.
Quel est le soulagement réel ?
Cela dépend du taux de hits. À 95 pour cent, votre origine ne voit plus qu'une requête sur vingt, la charge tombe donc au vingtième. À 80 pour cent, c'est encore un cinquième. Pour les fichiers statiques comme les images, les scripts, les polices et les téléchargements, 95 pour cent et plus sont normaux.
Ai-je besoin d'un prestataire commercial pour cela ?
Pas forcément. Un serveur CDN à soi vaut surtout la peine avec de gros fichiers et beaucoup de volume, parce que les prestataires commerciaux facturent au gigaoctet transféré. Sur un serveur à trafic illimité, le centième téraoctet coûte la même chose que le premier, à savoir rien de plus.
Quels fichiers mettre en cache et lesquels non ?
Tout ce qui est identique pour tous les visiteurs : images, vidéos, CSS, JavaScript, polices, PDF, fichiers d'installation. Ce qu'il ne faut pas mettre en cache : les pages personnalisées, les paniers, les espaces connectés et tout ce qui porte un cookie de session. La décision se prend au mieux à l'origine, via l'en-tête Cache-Control.
Combien de temps le cache doit-il garder les fichiers ?
Pour les fichiers portant un numéro de version ou une somme de contrôle dans leur nom, un an sans hésiter, ils ne changent jamais. Pour les images et les téléchargements sans versionnage, des heures à des jours sont habituels. Pour le HTML, si vous le mettez en cache du tout, des minutes. L'important n'est pas le chiffre exact, mais d'avoir un moyen de supprimer des fichiers précis.
Comment supprimer un fichier du cache ?
Le plus simple passe par le nom de fichier : qui attribue un nouveau nom à chaque modification n'a pas besoin de supprimer. Si ce n'est pas possible, nginx propose soit le module Purge commercial, soit le module libre ngx_cache_purge. En dernier recours, vous supprimez le fichier de cache directement dans le système de fichiers, le chemin se calcule à partir de la clé de cache.
Que se passe-t-il si le serveur d'origine tombe en panne ?
Bien réglé, le cache continue alors de servir, même si les entrées sont en principe expirées. C'est le rôle de proxy_cache_use_stale avec error et timeout, ainsi que de proxy_cache_background_update. Votre site reste ainsi joignable pendant que l'origine est en maintenance ou redémarre.
Cela fonctionne-t-il aussi pour de gros fichiers vidéo ?
Oui, mais uniquement avec le réglage slice. Sans lui, le cache recharge tout le fichier depuis l'origine à chaque saut dans la vidéo. Avec slice, nginx découpe le fichier en blocs d'un mégaoctet par exemple et ne dépose que les parties réellement demandées.
De combien d'emplacements ai-je besoin ?
Commencez par un, et là où se trouve la majorité de vos visiteurs. Un deuxième emplacement ne vaut la peine que lorsqu'une part notable du public se trouve sur un autre continent. La répartition passe alors par le DNS, chaque visiteur arrive sur le nœud le plus proche.
Comment savoir que le cache fonctionne ?
Sortez la variable upstream_cache_status comme en-tête. Au deuxième appel du même fichier, HIT doit y figurer. Si vous écrivez en plus la même valeur dans le journal, vous pouvez compter le taux de hits à tout moment. Sous 90 pour cent sur des fichiers statiques, c'est en général que quelque chose ne va pas avec les en-têtes ou les cookies.

CDN Caching nginx Reverse Proxy Performance HTTP Unlimited Traffic