Montar un servidor CDN propio: recoger una vez, entregar mil veces

Publicado el 9 min de lectura

Un servidor CDN recoge cada archivo una vez del origen y lo entrega después mil veces él mismo. El servidor de origen ve solo una fracción de las peticiones y los visitantes obtienen tiempos de carga cortos. Así montas uno.

Cada archivo que un visitante carga de tu página lo saca tu servidor de origen del sistema de archivos, lo mete en una respuesta y lo manda. Con diez visitantes al día nadie lo nota. Con diez mil visitantes simultáneos, eso es precisamente la razón de que la página se vuelva lenta, aunque la aplicación en sí no tenga nada que hacer.

Un servidor CDN se coloca por delante. Recoge cada archivo una vez del origen y lo entrega después él mismo, tantas veces como haga falta. El origen ve por eso solo una fracción de las peticiones, y los visitantes reciben sus archivos de un servidor que no hace nada más que entregar. Este artículo muestra cuánto aporta eso de verdad, cómo montas un servidor así con nginx y qué importa en cuanto a tráfico y ubicación.

Qué hace realmente un servidor CDN

El proceso se cuenta en tres frases:

  • Un visitante pide un archivo al servidor CDN.
  • Si ya está allí, sale de inmediato. Eso es un acierto.
  • Si no está allí, el servidor CDN la recoge del origen, la guarda y la entrega. Eso es un fallo, y ocurre por archivo solo una vez por periodo de validez.

El principio es el mismo que en un servidor de streaming: recoger una vez, entregar muchas. La única diferencia es que un servidor de streaming reparte un flujo de datos en marcha y un servidor CDN, archivos terminados.

La cuenta: cuánto alivia una caché

Lo decisivo es la tasa de aciertos, es decir la parte de las peticiones que responde la propia caché. Con un millón de peticiones al día, el alivio se ve así:

Tasa de aciertosPeticiones en el origenAlivio
50 %500.000factor 2
80 %200.000factor 5
95 %50.000factor 20
99 %10.000factor 100

Para los archivos estáticos, el 95 por ciento y más es lo normal, porque las imágenes, los scripts y las fuentes cambian poco. El salto del 95 al 99 por ciento es además más valioso de lo que dejan suponer las cifras: divide la carga restante otra vez entre cinco.

Al mismo tiempo, el tráfico se desplaza. El origen transmite ya casi nada, y el servidor CDN tanto más. Con 500.000 descargas de imágenes al día de 400 KB cada una, eso son 200 GB diarios, o sea unos 6 TB al mes, solo en imágenes. Por eso, en un nodo CDN la cifra importante no es la CPU, sino el enlace y el volumen.

Cuándo vale la pena un servidor CDN propio

Los proveedores comerciales de CDN facturan por gigabyte transferido. Eso es cómodo y, con poco volumen, barato. A partir de cierta cantidad, la relación se da la vuelta:

CasoServidor CDN propioFacturación por gigabyte
Pocos GB al mesapenas vale la penamás barata
Muchos TB al mesprecio mensual fijocrece de forma lineal
Descargas grandes, archivos de vídeoclaramente en ventajase vuelve cara enseguida
Control total sobre reglas y registroscompletolimitado

A esto se añade un punto difícil de cuantificar: con un nodo propio decides tú mismo qué se cachea, cuánto tiempo y cuándo desaparece. No estás atado a las reglas de un proveedor ajeno y puedes entregar también cosas que no encajan en esquemas ajenos.

Montaje con nginx como caché

Instalar

apt update
apt install -y nginx

No hace falta más, la caché viene integrada en nginx. Crea el directorio en el que deben quedar los archivos:

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

Configurar

La caché se describe una vez en el bloque http y se usa después en cada uno de los distintos bloques 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";

Los valores significan en detalle: keys_zone=cdn:100m alcanza para unas 800.000 entradas, max_size limita el espacio en el disco, inactive=30d tira los archivos que nadie ha pedido en 30 días, y use_temp_path=off evita un traslado innecesario entre directorios.

Debajo, el servidor propiamente dicho:

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;

        # agrupar varios fallos simultáneos en una sola petición
        proxy_cache_lock on;
        proxy_cache_lock_timeout 10s;

        # seguir entregando si hay problemas en el origen
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_background_update on;

        # las cookies de sesión no deben reventar la caché
        proxy_ignore_headers Set-Cookie;
        proxy_hide_header Set-Cookie;

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

Tres líneas de ahí merecen atención especial. proxy_cache_lock hace que, con un archivo nuevo, no pidan cien visitantes al origen a la vez, sino solo uno, mientras los demás esperan. proxy_cache_use_stale mantiene la entrega en marcha mientras el origen se reinicia o responde despacio. Y ocultar Set-Cookie evita que una única respuesta con cookie de sesión deje inservible toda la caché.

Comprobar si surte efecto

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

En la primera llamada aparece ahí MISS; en la segunda, HIT. Si la segunda vez vuelve a aparecer MISS, casi siempre es cosa de uno de los tres sospechosos habituales: un Set-Cookie en la respuesta, un Cache-Control: no-store del origen o la falta del dato de longitud.

Qué se cachea y qué no

La solución limpia es que lo diga el propio origen. Él conoce sus contenidos, la caché no. Para eso envía las cabeceras adecuadas:

ContenidoCabecera en el origen
Archivo con suma de comprobación en el nombreCache-Control: public, max-age=31536000, immutable
Imagen o descarga sin versionadoCache-Control: public, max-age=86400
HTML de una página públicaCache-Control: public, max-age=300
Área con sesión iniciada, carritoCache-Control: private, no-store

Si quieres decidirlo en la caché misma, se hace con bloques location propios por tipo de archivo:

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

Quitar contenidos de forma selectiva

El camino más sencillo es no tener que borrar nada: quien da a cada imagen modificada un nombre de archivo nuevo, por ejemplo logo.a1b2c3.png, no tiene ese problema. El archivo antiguo sale de la caché por sí solo en algún momento.

Si eso no es posible, en nginx ayuda el módulo libre ngx_cache_purge. Sin módulo adicional, la ruta se puede calcular a partir de la clave de caché, porque nginx usa su suma MD5 como nombre de archivo:

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"

Para el día a día basta casi siempre un pequeño script que ejecute estas líneas para una lista de rutas.

Archivos grandes y vídeos

Con archivos de más de unos cientos de megabytes necesitas la división en bloques, porque si no la caché vuelve a cargar el archivo completo en cada salto dentro del vídeo:

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

Así, nginx recoge solo los bloques que de verdad se piden. Si un espectador salta a la mitad de una película, por la línea viajan unos pocos megabytes en lugar de varios gigabytes.

TLS, HTTP/2 y HTTP/3

Un nodo CDN entrega muchos archivos pequeños a muchas conexiones, y justo ahí es donde se amortizan los protocolos más nuevos. HTTP/2 queda resuelto con http2 on;. HTTP/3 aporta sobre todo en conexiones de telefonía móvil, porque un cambio de red no corta la conexión:

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

No olvides abrir además el UDP 443 en el firewall. Es la razón más frecuente de que HTTP/3 esté configurado y aun así nadie lo use.

Varias ubicaciones

Un solo nodo aporta ya la mayor parte de la ganancia, es decir el alivio del origen. La segunda ganancia, los caminos cortos hasta el visitante, llega solo con varias ubicaciones.

El reparto va por DNS: el mismo nombre apunta a un nodo distinto según la procedencia del visitante. Necesitas para eso un proveedor de DNS con asignación geográfica. Todos los nodos reciben la misma configuración y el mismo origen, no saben nada el uno del otro, y precisamente eso hace que el montaje sea tan robusto: si un nodo cae, lo quitas del DNS y los demás se hacen cargo.

KernelHost ofrece ubicaciones en Alemania, en la costa oeste de EE. UU., en la costa este de EE. UU. y además en Singapur y Corea del Sur. Encontrarás un resumen con latencias en Ubicaciones de servidores. Como regla general: un nodo por cada continente en el que esté una parte apreciable de tu público.

Medir en lugar de adivinar

Escribe el estado de la caché en el registro, así puedes contar la tasa de aciertos en cualquier momento:

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

Evaluarlo se puede con una sola línea:

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

Si la proporción de HIT en archivos estáticos está por debajo del 90 por ciento, buscar la causa vale la pena casi siempre: suelen ser cookies en las respuestas, periodos de validez demasiado cortos o peticiones con parámetros cambiantes al final de la dirección, que para la caché parecen cada vez un archivo nuevo.

Qué servidor encaja

Un nodo CDN es una máquina de entrega. El orden de las propiedades importantes es por eso distinto que en un servidor de aplicaciones:

  1. Tráfico y enlace. Aquí se genera toda la carga. Un nodo transmite en funcionamiento varios terabytes al mes con regularidad.
  2. Disco. NVMe está claramente en ventaja, porque la caché consiste en muchos accesos aleatorios pequeños. El tamaño depende de cuánto de tu fondo está caliente, no de lo grande que sea el fondo en total.
  3. RAM. El sistema operativo usa la memoria libre como segunda caché por delante del disco. Más RAM significa aquí directamente más velocidad.
  4. CPU. Lo menos crítico, solo se necesita para TLS, y eso lo hacen los procesadores modernos de pasada.

Justo para este caso están hechos nuestros servidores root con tráfico ilimitado: 2 × 1 Gbit/s reales por servidor sin limitación de velocidad, volumen ilimitado a precio mensual fijo, NVMe de centro de datos en RAID y 3,2 Tbps de protección antifloods de serie. La puesta a disposición dura menos de 30 segundos, con acceso root completo, IPv4 y una red IPv6 /64. Como la facturación es de prepago y cancelable mensualmente, puedes empezar con un nodo y añadir en cualquier momento una segunda ubicación, cuando tu público se reparta.

Preguntas frecuentes

¿Qué hace exactamente un servidor CDN?
Se sitúa entre tus visitantes y tu servidor de origen. La primera petición de un archivo la pasa al origen y guarda la respuesta al hacerlo. Todas las peticiones siguientes del mismo archivo las responde él mismo, sin molestar al origen.
¿Cuánto alivia eso en realidad?
Depende de la tasa de aciertos. Con el 95 por ciento, tu origen ve solo una de cada veinte peticiones, así que la carga cae a una veinteava parte. Con el 80 por ciento sigue siendo una quinta parte. Para archivos estáticos como imágenes, scripts, fuentes y descargas, el 95 por ciento y más es lo normal.
¿Necesito para eso un proveedor comercial?
No necesariamente. Un servidor CDN propio vale la pena sobre todo con archivos grandes y mucho volumen, porque los proveedores comerciales facturan por gigabyte transferido. En un servidor con tráfico ilimitado, el terabyte número cien cuesta lo mismo que el primero, es decir nada adicional.
¿Qué archivos debo cachear y cuáles no?
Todo lo que tiene el mismo aspecto para todos los visitantes: imágenes, vídeos, CSS, JavaScript, fuentes, PDF, archivos de instalación. No deberías cachear páginas personalizadas, carritos, áreas de acceso ni nada que lleve una cookie de sesión. La decisión la toma mejor el origen a través de la cabecera Cache-Control.
¿Cuánto tiempo debe guardar la caché los archivos?
Para archivos con número de versión o suma de comprobación en el nombre, tranquilamente un año, porque no cambian nunca. Para imágenes y descargas sin versionado es habitual de horas a días. Para HTML, si lo cacheas, minutos. Lo importante no es la cifra exacta, sino tener una forma de borrar archivos concretos de manera selectiva.
¿Cómo borro un archivo de la caché?
Lo más sencillo es por el nombre de archivo: quien asigna un nombre nuevo en cada cambio no necesita borrar nada. Si eso no es posible, en nginx ayudan o bien el módulo Purge comercial o bien el libre ngx_cache_purge. En caso de apuro borras el archivo de caché directamente en el sistema de archivos, la ruta se puede calcular a partir de la clave de caché.
¿Qué pasa si el servidor de origen se cae?
Bien configurada, la caché sigue entregando, incluso cuando las entradas ya han caducado. De eso se encargan proxy_cache_use_stale con error y timeout, y proxy_cache_background_update. Tu página se mantiene así accesible mientras se hace el mantenimiento del origen o este se reinicia.
¿Funciona también con archivos de vídeo grandes?
Sí, pero solo con el ajuste de slice. Sin él, la caché vuelve a cargar del origen el archivo completo en cada salto dentro del vídeo. Con slice, nginx divide el archivo en bloques de, por ejemplo, un megabyte y guarda solo las partes que realmente se piden.
¿Cuántas ubicaciones necesito?
Empieza con una, y en el sitio donde está la mayoría de tus visitantes. Una segunda ubicación solo vale la pena cuando una parte apreciable del público está en otro continente. El reparto va entonces por DNS, y cada visitante acaba en el nodo más cercano.
¿Cómo reconozco que la caché funciona?
Saca la variable upstream_cache_status como cabecera. En la segunda llamada al mismo archivo tiene que aparecer ahí HIT. Si escribes además ese mismo valor en el registro, puedes contar la tasa de aciertos en cualquier momento. Por debajo del 90 por ciento en archivos estáticos, suele haber algo que no cuadra con las cabeceras o con las cookies.

CDN Caching nginx Reverse Proxy Performance HTTP Unlimited Traffic