Comparación de Estrategias de Caché del Servidor
La caché del lado del servidor es la herramienta más poderosa para escalar aplicaciones web sin aumentar la infraestructura, pero elegir la estrategia incorrecta puede resultar en datos obsoletos, complejidad innecesaria o memoria malgastada. Redis, Varnish, la caché de Nginx y Memcached tienen características, casos de uso y modelos de invalidación distintos que determinan cuándo usar cada uno.
Requisitos Previos
- Servidor Linux (Ubuntu 20.04+, Debian 11+, CentOS 8+)
- Nginx instalado
- Docker (para pruebas rápidas)
- Acceso root o sudo
- Aplicación web para probar (PHP, Node.js, Python, etc.)
Caché con Nginx (FastCGI y Proxy)
Cuándo usar: Sitios con contenido semi-estático, WordPress, aplicaciones PHP. La caché más transparente para la aplicación.
# Crear directorio de caché
sudo mkdir -p /var/cache/nginx/{fastcgi,proxy}
sudo chown -R www-data:www-data /var/cache/nginx/
# /etc/nginx/conf.d/cache-config.conf
# Zona de caché FastCGI (para PHP)
fastcgi_cache_path /var/cache/nginx/fastcgi
levels=1:2
keys_zone=FASTCGI_CACHE:20m
max_size=2g
inactive=24h
use_temp_path=off;
# Zona de caché de proxy (para Node.js, Python, etc.)
proxy_cache_path /var/cache/nginx/proxy
levels=1:2
keys_zone=PROXY_CACHE:20m
max_size=2g
inactive=24h
use_temp_path=off;
# Configuración del VirtualHost con caché
server {
listen 443 ssl http2;
server_name tudominio.com;
# Lógica de decisión: ¿cachear o no?
set $skip_cache 0;
if ($request_method = POST) { set $skip_cache 1; }
if ($query_string != "") { set $skip_cache 1; }
if ($http_cookie ~* "(logged_in|cart|session)") { set $skip_cache 1; }
if ($request_uri ~* "/admin|/api|/checkout") { set $skip_cache 1; }
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_cache FASTCGI_CACHE;
fastcgi_cache_key "$scheme$host$request_uri";
fastcgi_cache_valid 200 301 302 1h;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache_use_stale error timeout updating;
add_header X-Cache-Status $upstream_cache_status;
}
}
# Monitorizar el hit rate de la caché Nginx
# Contar HITs vs MISSes en el log
grep "X-Cache-Status:" /var/log/nginx/access.log | \
awk '{print $NF}' | sort | uniq -c | sort -rn
# Limpiar la caché completa
sudo rm -rf /var/cache/nginx/fastcgi/*
sudo nginx -s reload
Varnish Cache
Cuándo usar: Sites de alto tráfico con contenido HTML dinámico que cambia con moderada frecuencia. Excele con múltiples backends y lógica de invalidación compleja.
# Instalar Varnish
sudo apt install -y varnish
# Configurar Varnish como proxy inverso frente a Nginx
# Nginx escuchará en un puerto diferente (8080)
sudo nano /etc/nginx/sites-available/tudominio.conf
# Cambiar: listen 80; → listen 8080;
# /etc/varnish/default.vcl - Configuración de Varnish
vcl 4.1;
# Backend: el servidor de aplicación (Nginx)
backend default {
.host = "127.0.0.1";
.port = "8080";
.connect_timeout = 5s;
.first_byte_timeout = 60s;
.between_bytes_timeout = 10s;
}
# Lógica de recepción de peticiones
sub vcl_recv {
# Eliminar cookies de seguimiento irrelevantes para el caché
if (req.http.Cookie) {
set req.http.Cookie = regsuball(
req.http.Cookie,
"(^|;\s*)(_ga|_gid|_fbp|utm_[^=]*)=[^;]*",
""
);
if (req.http.Cookie == "") {
unset req.http.Cookie;
}
}
# No cachear peticiones autenticadas
if (req.http.Authorization || req.http.Cookie ~ "session|logged_in") {
return(pass);
}
# No cachear métodos no-GET
if (req.method != "GET" && req.method != "HEAD") {
return(pass);
}
# No cachear WebSockets
if (req.http.Upgrade ~ "(?i)websocket") {
return(pipe);
}
}
# Configurar el TTL de respuestas cacheadas
sub vcl_backend_response {
# Cachear durante 5 minutos para HTML dinámico
if (beresp.http.Content-Type ~ "text/html") {
set beresp.ttl = 5m;
set beresp.grace = 1h; # Servir caché obsoleta hasta 1h si el backend falla
}
# Cachear recursos estáticos durante 1 año
if (bereq.url ~ "\.(css|js|png|jpg|gif|ico|woff2)$") {
set beresp.ttl = 365d;
unset beresp.http.Set-Cookie;
}
# Si la respuesta tiene Set-Cookie, no cachear
if (beresp.http.Set-Cookie) {
set beresp.uncacheable = true;
}
}
# Añadir cabecera indicando si viene del caché
sub vcl_deliver {
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT (" + obj.hits + ")";
} else {
set resp.http.X-Cache = "MISS";
}
}
# Configurar Varnish para escuchar en el puerto 80
sudo tee /etc/varnish/varnish.params << 'EOF'
VARNISH_VCL_CONF=/etc/varnish/default.vcl
VARNISH_LISTEN_PORT=80
VARNISH_STORAGE="malloc,256m" # Caché en RAM: 256MB
VARNISH_ADMIN_LISTEN_ADDRESS=127.0.0.1
VARNISH_ADMIN_LISTEN_PORT=6082
EOF
sudo systemctl restart varnish
# Invalidar caché de Varnish para una URL específica
varnishadm ban req.url ~ "^/blog/"
# Ver estadísticas en tiempo real
varnishstat -1 | grep -E "cache_hit|cache_miss|n_object"
Redis como Caché de Aplicación
Cuándo usar: Caché de objetos de aplicación, sesiones, resultados de consultas de base de datos. Flexible, con estructuras de datos ricas y persistencia opcional.
# Instalar Redis
sudo apt install -y redis-server
# Configuración optimizada para caché de aplicación
sudo tee /etc/redis/redis.conf.d/app-cache.conf << 'EOF'
# Solo caché - sin persistencia para máximo rendimiento
save ""
appendonly no
# Política de eliminación cuando se llena la memoria
maxmemory 1gb
maxmemory-policy allkeys-lru # Eliminar los menos usados recientemente
# Optimizaciones de red
tcp-backlog 511
timeout 0
tcp-keepalive 300
EOF
sudo systemctl restart redis
# Probar la conexión
redis-cli ping
redis-cli info memory | grep used_memory_human
Ejemplo de implementación de caché Redis en Node.js:
// cache.js - Módulo de caché con Redis
const redis = require('redis');
const client = redis.createClient({
socket: {
host: 'localhost',
port: 6379,
}
});
client.connect();
// Función de caché con TTL configurable
async function cacheGet(key) {
try {
const data = await client.get(key);
return data ? JSON.parse(data) : null;
} catch (err) {
console.error('Redis GET error:', err);
return null; // Fallback: no usar caché
}
}
async function cacheSet(key, value, ttlSeconds = 300) {
try {
await client.setEx(key, ttlSeconds, JSON.stringify(value));
} catch (err) {
console.error('Redis SET error:', err);
}
}
// Patrón Cache-Aside
async function getWithCache(key, fetchFn, ttl = 300) {
const cached = await cacheGet(key);
if (cached) return cached;
const data = await fetchFn();
await cacheSet(key, data, ttl);
return data;
}
module.exports = { cacheGet, cacheSet, getWithCache };
Memcached
Cuándo usar: Caché de objetos simples con muy alta concurrencia. Más ligero que Redis, sin estructuras de datos ricas ni persistencia.
# Instalar Memcached
sudo apt install -y memcached libmemcached-tools
# Configurar Memcached
sudo tee /etc/memcached.conf << 'EOF'
-d
-m 512 # 512MB de memoria
-p 11211
-u memcache
-l 127.0.0.1 # Solo local
-c 1024 # Máximo de conexiones simultáneas
-t 4 # Número de hilos
EOF
sudo systemctl restart memcached
# Verificar el estado
memcstat --servers=localhost
# Ver estadísticas
echo "stats" | nc localhost 11211
Patrones de Invalidación de Caché
# Estrategia 1: Invalidación por tiempo (TTL)
# La más simple - el caché expira automáticamente
# Redis: SET key value EX 3600 (expira en 1 hora)
# Estrategia 2: Invalidación por evento (Event-driven)
# Invalida el caché cuando cambian los datos
# Ejemplo con Redis en Python
cat > /tmp/cache_invalidation.py << 'EOF'
import redis
r = redis.Redis(host='localhost', decode_responses=True)
def update_product(product_id, new_data):
# Actualizar en base de datos
db.update('products', {'id': product_id}, new_data)
# Invalidar el caché del producto
r.delete(f'product:{product_id}')
# También invalidar listas que puedan contener este producto
r.delete('products:list:*') # Patrón - usar con cuidado
r.delete('products:featured')
EOF
# Estrategia 3: Cache-Aside con versiones
# Añadir versión a la clave para invalidar fácilmente
redis-cli SET "user:123:v2" '{"name":"Juan","email":"[email protected]"}' EX 3600
# Para invalidar: incrementar la versión
redis-cli INCR "user:123:version"
# Estrategia 4: Tags de caché (para invalidación en grupo)
# Asociar respuestas con tags para invalidar por categoría
redis-cli SADD "tag:product:123" "page:/producto/123" "page:/home" "fragment:featured"
redis-cli SMEMBERS "tag:product:123"
# Al actualizar el producto 123, invalidar todas las páginas del set
Estrategias TTL
# Guía de TTL según tipo de contenido:
# Datos estáticos de referencia (raramente cambian)
redis-cli SET "config:site:name" "Mi Empresa" EX 86400 # 24 horas
# Listados de productos (cambian con frecuencia moderada)
redis-cli SET "products:list" "$JSON_DATA" EX 300 # 5 minutos
# Páginas de detalle (cambian ocasionalmente)
redis-cli SET "page:/producto/123" "$HTML_DATA" EX 1800 # 30 minutos
# Sesiones de usuario
redis-cli SET "session:abc123" "$SESSION_DATA" EX 7200 # 2 horas (se renueva con cada petición)
# Datos de tiempo real (contadores, precios en vivo)
redis-cli SET "stock:product:123" "45" EX 30 # 30 segundos
# Resultados de API externa (tasa de cambio, clima)
redis-cli SET "exchange:EUR:USD" "1.08" EX 3600 # 1 hora
# Verificar el TTL actual de una clave
redis-cli TTL "products:list"
# -1: sin expiración, -2: la clave no existe, >0: segundos restantes
# Renovar el TTL (para sesiones activas)
redis-cli EXPIRE "session:abc123" 7200
Benchmarks y Comparación
# Benchmark de Redis vs Memcached
# Instalar redis-benchmark
redis-benchmark -h localhost -p 6379 -n 100000 -c 50 -q
# Benchmark de Memcached
apt install -y memtier-benchmark
memtier_benchmark -s localhost -p 11211 -P memcache -n 100000 -c 50
# Medir el hit rate de la caché de Nginx
cat << 'EOF' > /usr/local/bin/nginx-cache-stats.sh
#!/bin/bash
LOG_FILE="/var/log/nginx/access.log"
TOTAL=$(wc -l < $LOG_FILE)
HITS=$(grep 'X-Cache-Status: HIT' $LOG_FILE | wc -l)
MISSES=$(grep 'X-Cache-Status: MISS' $LOG_FILE | wc -l)
HIT_RATE=$(echo "scale=2; $HITS * 100 / ($HITS + $MISSES)" | bc)
echo "=== Estadísticas de Caché Nginx ==="
echo "Total peticiones: $TOTAL"
echo "Cache HITs: $HITS"
echo "Cache MISSes: $MISSES"
echo "Hit Rate: ${HIT_RATE}%"
EOF
chmod +x /usr/local/bin/nginx-cache-stats.sh
# Resumen comparativo:
# Nginx FastCGI Cache: mejor para PHP/WordPress, transparente para la app
# Varnish: mejor para alto tráfico con HTML complejo, invalidación avanzada
# Redis: mejor para datos de aplicación, sesiones, tiempo real
# Memcached: mejor para caché simple de alto volumen, menor overhead
Solución de Problemas
Hit rate bajo en la caché de Nginx:
# Analizar las razones del BYPASS
grep "BYPASS" /var/log/nginx/access.log | head -20
# Verificar qué cookies están causando el bypass
grep "BYPASS" /var/log/nginx/access.log | awk '{print $11}' | sort | uniq -c
Redis consume demasiada memoria:
# Ver las claves más grandes
redis-cli --bigkeys
# Ver los patrones de claves y su count
redis-cli --scan --pattern '*' | awk -F: '{print $1}' | sort | uniq -c | sort -rn
# Eliminar claves expiradas manualmente
redis-cli DEBUG SLEEP 0
redis-cli MEMORY DOCTOR
Varnish sirve contenido obsoleto:
# Purgar una URL específica
varnishadm 'ban req.url == "/pagina-problematica"'
# Purgar todo el caché
varnishadm 'ban req.url ~ "."'
# Ver los bans activos
varnishadm 'ban.list'
Conclusión
No existe una única estrategia de caché óptima para todas las aplicaciones: Nginx FastCGI Cache es ideal para aplicaciones PHP con mínima configuración, Varnish brilla en sitios de alto tráfico que requieren lógica de invalidación compleja, Redis es la elección perfecta para caché de objetos de aplicación con estructuras de datos ricas, y Memcached sigue siendo competitivo para casos simples de clave-valor de muy alta concurrencia. En la práctica, los mejores resultados se obtienen combinando estas herramientas en capas, usando cada una donde tiene ventaja competitiva.


