Configuración de sesiones pegajosas en equilibradores de carga

Las sesiones pegajosas (persistencia de sesión) aseguran que las solicitudes del mismo cliente se enruten al mismo servidor backend durante la duración de una sesión. Esto previene la pérdida de sesión cuando las aplicaciones mantienen estado en memoria en lugar de usar almacenamiento de sesión centralizado. Esta guía cubre persistencia basada en cookies en Nginx y HAProxy, técnicas de hash de IP, cookies específicas de aplicación y alternativas de replicación de sesión.

Tabla de contenidos

  1. Descripción general de sesiones pegajosas
  2. Persistencia basada en cookies en Nginx
  3. Persistencia basada en cookies en HAProxy
  4. Equilibrio de carga con hash de IP
  5. Cookies específicas de aplicación
  6. Replicación de sesión
  7. Tiempos de espera de afinidad de sesión
  8. Prueba de sesiones pegajosas
  9. Solución de problemas

Descripción general de sesiones pegajosas

Las sesiones pegajosas utilizan múltiples mecanismos:

  1. Basado en cookies: El proxy establece/modifica la cookie para enrutar solicitudes
  2. Basado en IP: Hash de IP de cliente para determinar servidor
  3. Basado en cookie de aplicación: Usa cookie existente de aplicación para enrutamiento
  4. IP de origen + Puerto: Hash de fuente de conexión y puerto

Limitaciones de sesiones pegajosas:

  • Previene cambios en distribución de carga
  • Dificulta el mantenimiento del servidor
  • Reduce capacidad efectiva
  • Aumenta latencia de solicitud (cálculos de hash)
  • Pérdida de sesión en falla del servidor

Mejores alternativas:

  • Almacenamiento de sesión distribuido (Redis, Memcached)
  • Diseño de aplicación sin estado
  • Base de datos de sesión (PostgreSQL, MySQL)
  • Caché centralizado

Usar sesiones pegajosas solo cuando sea necesario para aplicaciones con estado.

Persistencia basada en cookies en Nginx

Nginx requiere módulos de terceros para persistencia nativa basada en cookies. Usar la directiva sticky o implementar con map y ruta:

Usando módulo Sticky (si está compilado)

upstream backend {
    least_conn;
    
    server 192.168.1.100:8000;
    server 192.168.1.101:8000;
    server 192.168.1.102:8000;
    
    sticky cookie srv_route expires=1h domain=.example.com path=/ httponly secure;
}

server {
    listen 443 ssl http2;
    server_name app.example.com;
    
    ssl_certificate /etc/nginx/ssl/example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;
    
    location / {
        proxy_pass http://backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Usando enrutamiento basado en mapa (sin módulo)

# Define upstream servers with identifiers
upstream backend_1 { server 192.168.1.100:8000; }
upstream backend_2 { server 192.168.1.101:8000; }
upstream backend_3 { server 192.168.1.102:8000; }

# Create map to determine backend based on session ID
map $cookie_session_id $upstream {
    # Hash the session cookie to one of three backends
    ~*^(?<prefix>[a-f0-9]{2}) $prefix;
    default "00";
}

server {
    listen 80;
    server_name app.example.com;
    
    # Extract session hash value
    set $session_route $upstream;
    
    location / {
        # Route based on session hash
        if ($session_route ~* "^00$") { proxy_pass http://backend_1; }
        if ($session_route ~* "^01$") { proxy_pass http://backend_2; }
        if ($session_route ~* "^02$") { proxy_pass http://backend_3; }
        
        # Set session cookie if not exists
        add_header Set-Cookie "session_id=$request_id; Path=/; HttpOnly; Max-Age=3600" always;
        
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Usando hash consistente

Implementar hash consistente para mejor distribución:

# Lua-based consistent hashing (requires ngx_lua module)
upstream backend {
    server 192.168.1.100:8000;
    server 192.168.1.101:8000;
    server 192.168.1.102:8000;
}

# Nginx Lua script for consistent hashing
location / {
    set_by_lua_file $backend_pool /etc/nginx/lua/consistent_hash.lua $cookie_session_id;
    proxy_pass http://$backend_pool;
}

# Create /etc/nginx/lua/consistent_hash.lua
# Local consistent hash function
local function crc32(data)
    local CRC32_POLY = 0xEDB88320
    local crc = 0xFFFFFFFF
    for i = 1, #data do
        crc = bit.bxor(crc, string.byte(data, i))
        for _ = 1, 8 do
            if bit.band(crc, 1) == 1 then
                crc = bit.bxor(bit.rshift(crc, 1), CRC32_POLY)
            else
                crc = bit.rshift(crc, 1)
            end
        end
    end
    return bit.bxor(crc, 0xFFFFFFFF)
end

local session_id = ngx.arg[1]
local servers = {"192.168.1.100", "192.168.1.101", "192.168.1.102"}

local hash = crc32(session_id)
local selected = servers[hash % #servers + 1]
return selected .. ":8000"

Persistencia basada en cookies en HAProxy

HAProxy proporciona persistencia nativa basada en cookies:

Persistencia básica de cookies

global
    log stdout local0
    stats socket /run/haproxy/admin.sock

defaults
    mode http
    timeout connect 5000
    timeout client 50000
    timeout server 50000

frontend web_in
    bind *:80
    default_backend web_servers

backend web_servers
    balance roundrobin
    
    # Enable cookie-based persistence
    cookie SERVERID insert indirect secure httponly
    
    server srv1 192.168.1.100:8000 check cookie srv1
    server srv2 192.168.1.101:8000 check cookie srv2
    server srv3 192.168.1.102:8000 check cookie srv3

Parámetros:

  • SERVERID: Nombre de cookie
  • insert: Añadir nueva cookie si falta
  • indirect: No eliminar cookie establecida por HAProxy
  • secure: Establecer bandera segura para HTTPS
  • httponly: Establecer bandera HttpOnly
  • cookie srv1: Identificador del servidor

Configuración avanzada de cookies

backend web_servers
    balance roundrobin
    
    # Cookie with domain, path, and expiration
    cookie SERVERID insert indirect secure httponly nocache domain .example.com path /
    
    # Use existing application cookie for routing
    appsession JSESSIONID len 52 timeout 1h
    
    server srv1 192.168.1.100:8000 check cookie srv1
    server srv2 192.168.1.101:8000 check cookie srv2
backend web_servers
    balance roundrobin
    
    cookie SERVERID insert indirect httponly
    
    # Primary servers
    server srv1 192.168.1.100:8000 check cookie srv1
    server srv2 192.168.1.101:8000 check cookie srv2
    
    # Backup servers (if session lost)
    server srv3 192.168.1.102:8000 check cookie srv3 backup
    server srv4 192.168.1.103:8000 check cookie srv4 backup

Sesiones pegajosas con múltiples rutas

backend web_servers
    balance roundrobin
    cookie SERVERID insert indirect secure httponly
    
    stick-table type string len 32 size 100k expire 30m
    stick on cookie(JSESSIONID)
    
    server srv1 192.168.1.100:8000 check cookie srv1
    server srv2 192.168.1.101:8000 check cookie srv2
    server srv3 192.168.1.102:8000 check cookie srv3

Equilibrio de carga con hash de IP

Hash de IP (enrutamiento basado en IP de origen) proporciona persistencia sin cookies:

Hash de IP en Nginx

upstream backend {
    ip_hash;
    
    server 192.168.1.100:8000 weight=3;
    server 192.168.1.101:8000 weight=2;
    server 192.168.1.102:8000 weight=1;
}

server {
    listen 80;
    server_name app.example.com;
    
    location / {
        proxy_pass http://backend;
        # Important: Use X-Forwarded-For only if trusted sources
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Características del hash de IP:

  • Determinista: La misma IP de cliente siempre se enruta al mismo servidor
  • No requiere cookies
  • Sobrevive transiciones proxy/NAT (si está detrás del mismo NAT)
  • Servidor inactivo causa remapeo para aproximadamente 1/N clientes

Hash de origen en HAProxy

backend web_servers
    balance source
    
    server srv1 192.168.1.100:8000 check
    server srv2 192.168.1.101:8000 check
    server srv3 192.168.1.102:8000 check

Con seguimiento de conexión de cliente:

backend web_servers
    balance source
    
    # Track connections by source IP
    stick-table type ip size 100k expire 1h
    stick on src
    
    server srv1 192.168.1.100:8000 check
    server srv2 192.168.1.101:8000 check

Cookies específicas de aplicación

Enrutamiento basado en cookies de sesión de aplicación:

HAProxy appsession

backend web_servers
    # Use existing JSESSIONID (Java)
    appsession JSESSIONID len 52 timeout 1h
    
    server srv1 192.168.1.100:8000 check
    server srv2 192.168.1.101:8000 check
    server srv3 192.168.1.102:8000 check
# Map application session ID to backend
map $cookie_phpsessionid $php_backend {
    ~(?P<hash>.+) http://backend;
}

upstream backend {
    server 192.168.1.100:8000;
    server 192.168.1.101:8000;
    server 192.168.1.102:8000;
}

server {
    listen 80;
    
    location / {
        proxy_pass http://backend;
        proxy_set_header X-Session-ID $cookie_phpsessionid;
    }
}
frontend web_in
    bind *:80
    
    # Extract customer ID from session cookie
    set-var(sess.customer_id) cookie(sessionid)
    
    use_backend gold_servers if { var(sess.customer_id) -m reg -i ^gold_ }
    use_backend silver_servers if { var(sess.customer_id) -m reg -i ^silver_ }
    default_backend bronze_servers

backend gold_servers
    balance roundrobin
    server srv1 192.168.1.110:8000 check
    server srv2 192.168.1.111:8000 check

backend silver_servers
    balance roundrobin
    server srv3 192.168.1.120:8000 check

backend bronze_servers
    balance roundrobin
    server srv4 192.168.1.130:8000 check

Replicación de sesión

Alejarse de sesiones pegajosas replicando sesiones:

Almacenamiento de sesión basado en Redis

Configurar aplicación para usar Redis:

# Install Redis
sudo apt install redis-server
sudo systemctl start redis-server

# Verify Redis
redis-cli ping

Ejemplo con Spring Boot (Java):

# application.yml
spring:
  session:
    store-type: redis
  redis:
    host: localhost
    port: 6379
    timeout: 2000ms

Ejemplo con Node.js (Express):

const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');

const redisClient = createClient();
redisClient.connect();

app.use(session({
    store: new RedisStore({ client: redisClient }),
    secret: 'secret-key',
    resave: false,
    saveUninitialized: false,
    cookie: { 
        secure: true, 
        maxAge: 1800000 
    }
}));

Almacenamiento de sesión en Memcached

Usar Memcached para caché de sesión distribuido:

# Install Memcached
sudo apt install memcached
sudo systemctl start memcached

Configurar aplicación (ejemplo PHP):

<?php
ini_set('session.save_handler', 'memcached');
ini_set('session.save_path', 'localhost:11211');

session_start();
$_SESSION['user_id'] = 123;
?>

Almacenamiento de sesión en base de datos

Almacenar sesiones en base de datos compartida:

-- Create sessions table
CREATE TABLE sessions (
    id VARCHAR(255) PRIMARY KEY,
    user_id INT,
    data TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    expires_at TIMESTAMP,
    INDEX(expires_at)
);

Configurar HAProxy para usar backend de base de datos:

backend session_db
    mode tcp
    balance roundrobin
    server db1 192.168.1.200:5432 check
    server db2 192.168.1.201:5432 check

Tiempos de espera de afinidad de sesión

Configurar duraciones de persistencia de sesión:

Tiempo de espera en Nginx

upstream backend {
    least_conn;
    keepalive 32;
    keepalive_timeout 60s;
    
    server 192.168.1.100:8000;
    server 192.168.1.101:8000;
}

# Sticky using Traefik-style header
map $http_x_session_id $session_backend {
    ~(?P<sid>.+) http://backend;
}

server {
    location / {
        proxy_pass http://backend;
        
        # Session timeout
        proxy_read_timeout 30s;
        proxy_send_timeout 30s;
        proxy_connect_timeout 5s;
    }
}

Tiempo de espera en HAProxy

backend web_servers
    balance roundrobin
    
    # Cookie expires in 1 hour
    cookie SERVERID insert indirect max-age 3600
    
    # Stick table expires in 30 minutes
    stick-table type ip size 100k expire 1800s
    stick on src
    
    timeout server 30s
    timeout connect 5s
    
    server srv1 192.168.1.100:8000 check inter 2000

Prueba de sesiones pegajosas

Probar persistencia basada en cookies:

# Extract session cookie
COOKIE=$(curl -s -c - http://app.example.com/ | grep -i server | awk '{print $NF}')

# Make multiple requests with same cookie
for i in {1..5}; do
    curl -s -b "SERVERID=$COOKIE" http://app.example.com/ | grep -i server
done

Probar enrutamiento con hash de IP:

# Multiple requests from same IP should go to same server
for i in {1..5}; do
    curl -s http://app.example.com/ | head -1
done

# Test from different IPs (using VPN or proxy)
for ip in 1.2.3.4 5.6.7.8 9.10.11.12; do
    curl -s --interface $ip http://app.example.com/ | head -1
done

Verificar replicación de sesión:

# Check Redis sessions
redis-cli
> KEYS *
> GET session:*

# Check session count
> DBSIZE

# Monitor session access
redis-cli MONITOR

Solución de problemas

Verificar configuración de sesiones pegajosas:

# Nginx check
nginx -T | grep -A 10 "upstream"

# HAProxy check
haproxy -f /etc/haproxy/haproxy.cfg -c
echo "show backend" | socat - /run/haproxy/admin.sock

Monitorear cookies de sesión:

# Capture cookie traffic
tcpdump -A -s 1024 'tcp port 80' | grep -i "set-cookie"

# Monitor cookie with curl
curl -v -c cookies.txt http://app.example.com/ | head -20
cat cookies.txt

# Verify cookie attributes
curl -i http://app.example.com/ | grep -i "set-cookie"

Verificar enrutamiento del servidor:

# Add tracking headers in proxy
# Test multiple requests capture the Server header
for i in {1..10}; do
    curl -s -b "SERVERID=srv1" http://app.example.com/ | grep -i "X-Backend-Server"
done

# Check HAProxy stats
curl http://localhost:8404/stats | grep -i server

Probar escenarios de pérdida de sesión:

# Kill a backend server
ssh 192.168.1.100 "sudo systemctl stop application"

# Attempt request with existing session
curl -b "SERVERID=srv1" http://app.example.com/

# Verify failover to backup
curl -b "SERVERID=srv1" http://app.example.com/ | grep -i server

Conclusión

Las sesiones pegajosas permiten despliegues de aplicaciones con estado pero limitan la escalabilidad y flexibilidad operativa. Aunque la persistencia basada en cookies y hash de IP resuelven problemas de sesión inmediatos, el almacenamiento de sesión distribuido con Redis o Memcached proporciona escalabilidad y resiliencia superiores. Evalúa arquitectura de aplicación sin estado como la solución preferida, implementando sesiones pegajosas solo cuando sea necesario y con políticas de tiempo de espera claras y mecanismos de respaldo.