Un balanceador reparte las peticiones entre varios servidores, pero muchas aplicaciones guardan la sesión del usuario (carrito, login, formularios a medias) en la memoria del servidor que la creó. Si la siguiente petición llega a otro servidor, el usuario pierde la sesión. Las sesiones persistentes, o sticky sessions, hacen que el balanceador envíe siempre al mismo cliente al mismo backend. En esta guía montarás dos backends de prueba en Ubuntu 24.04 y configurarás la persistencia por cookie en HAProxy, y por IP y por cookie en Nginx, comprobando cada caso con curl.
Requisitos previos
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
- Un usuario no root con privilegios
sudo. - Los puertos 80 y 8000 TCP abiertos para las pruebas:
sudo ufw allow 80,8000/tcp.
Para que la guía se pueda seguir en un solo servidor, los dos backends son pequeños servidores web locales en los puertos 8081 y 8082. En producción serán servidores distintos: cambia 127.0.0.1:8081 y 127.0.0.1:8082 por sus IP privadas.
Métodos de persistencia
| Método | Cómo identifica al cliente | Ventajas | Inconvenientes |
|---|---|---|---|
| Cookie insertada por el balanceador | Cookie propia (SERVERID) con el nombre del backend | Precisa, independiente de la IP | Requiere HTTP (o terminar TLS en el balanceador) |
| Hash de IP de origen | IP del cliente | Funciona sin cookies, también en TCP | Muchos usuarios tras la misma IP (NAT, oficinas) caen en el mismo backend; si la IP del cliente cambia, pierde la sesión |
| Tabla de afinidad (stick-table) | IP u otro dato guardado en memoria del balanceador | Flexible, con caducidad configurable | Se pierde al reiniciar si no se sincroniza entre balanceadores |
Paso 1: Preparar los backends de prueba
Instala HAProxy y Nginx:
sudo apt update
sudo apt install haproxy nginx
Nginx trae un sitio por defecto en el puerto 80, que usará HAProxy en esta guía. Desactívalo:
sudo rm /etc/nginx/sites-enabled/default
sudo systemctl reload nginx
Crea una página distinta para cada backend, de forma que la respuesta indique quién la sirvió:
sudo mkdir -p /srv/app1 /srv/app2
echo "app1" | sudo tee /srv/app1/index.html
echo "app2" | sudo tee /srv/app2/index.html
Arranca dos servidores web con systemd-run, que los ejecuta como servicios temporales de systemd:
sudo systemd-run --unit=demo-app1 /usr/bin/python3 -m http.server 8081 --bind 127.0.0.1 --directory /srv/app1
sudo systemd-run --unit=demo-app2 /usr/bin/python3 -m http.server 8082 --bind 127.0.0.1 --directory /srv/app2
Comprueba que responden:
curl -s http://127.0.0.1:8081/
curl -s http://127.0.0.1:8082/
app1
app2
Paso 2: Persistencia por cookie en HAProxy
HAProxy puede insertar su propia cookie con el nombre del servidor que atendió la primera petición. En las siguientes peticiones lee la cookie y envía al cliente al mismo servidor, sin que la aplicación tenga que hacer nada.
Abre la configuración:
sudo nano /etc/haproxy/haproxy.cfg
Deja las secciones global y defaults que trae Ubuntu y añade al final:
frontend fe_web
bind :80
default_backend be_app
backend be_app
balance roundrobin
option redispatch
option httpchk GET /
cookie SERVERID insert indirect nocache httponly
server app1 127.0.0.1:8081 check cookie app1
server app2 127.0.0.1:8082 check cookie app2
Qué hace cada línea relevante:
cookie SERVERID insert: HAProxy añade la cookieSERVERIDen la respuesta.indirect: la cookie no se reenvía a la aplicación ni se vuelve a enviar al cliente si ya la tiene.nocache: añadeCache-Control: privatea las respuestas con la cookie, para que una caché intermedia no la sirva a otros usuarios.httponly: el JavaScript de la página no puede leerla. Si terminas TLS en HAProxy, añade tambiénsecure.cookie app1en cadaserver: el valor que se guarda en la cookie para ese servidor.option httpchkycheck: comprobaciones de salud HTTP. Si un servidor cae, HAProxy ignora las cookies que apuntan a él y reparte esos clientes entre los demás.option redispatch: si la conexión al servidor de la cookie falla antes de que la comprobación de salud lo marque como caído, reintenta en otro.
Valida y recarga:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy
Configuration file is valid
Comprobar la persistencia en HAProxy
Sin cookie, las peticiones se reparten por turnos:
for i in 1 2 3 4; do curl -s http://127.0.0.1/; done
app1
app2
app1
app2
Mira la cabecera que inserta HAProxy en la primera respuesta:
curl -sI http://127.0.0.1/ | grep -i -e set-cookie -e cache-control
set-cookie: SERVERID=app2; path=/; HttpOnly
cache-control: private
Ahora guarda y reenvía la cookie como haría un navegador. Todas las peticiones llegan al mismo backend:
rm -f ~/cookies.txt
for i in 1 2 3 4; do curl -s -b ~/cookies.txt -c ~/cookies.txt http://127.0.0.1/; done
app1
app1
app1
app1
Para probar la conmutación, detén el backend que te ha tocado, espera unos segundos a que la comprobación de salud lo detecte (por defecto, tres fallos seguidos a intervalos de 2 segundos) y repite la petición con la misma cookie:
sudo systemctl stop demo-app1
sleep 10
curl -s -b ~/cookies.txt -c ~/cookies.txt http://127.0.0.1/
app2
HAProxy ha enviado la petición a app2 y ha reescrito la cookie, así que el cliente queda fijado al nuevo servidor. Vuelve a arrancar el backend para los siguientes pasos:
sudo systemd-run --unit=demo-app1 /usr/bin/python3 -m http.server 8081 --bind 127.0.0.1 --directory /srv/app1
Alternativa sin cookies: stick-table por IP
Si el tráfico no es HTTP o no puedes usar cookies, HAProxy puede recordar qué servidor atendió a cada IP en una tabla en memoria. Sustituye la línea cookie del backend y el parámetro cookie de los servidores por:
backend be_app
balance roundrobin
option httpchk GET /
stick-table type ip size 200k expire 30m
stick on src
server app1 127.0.0.1:8081 check
server app2 127.0.0.1:8082 check
Cada IP queda asociada a un servidor durante 30 minutos desde su última petición. Puedes consultar la tabla en tiempo real a través del socket de administración que configura Ubuntu:
echo "show table be_app" | sudo socat stdio /run/haproxy/admin.sock
socat se instala con sudo apt install socat si no lo tienes.
Paso 3: Persistencia por IP en Nginx
La versión libre de Nginx no incluye la directiva sticky, que es exclusiva de NGINX Plus. La forma más sencilla de persistencia es ip_hash, que elige el backend a partir de la IP del cliente. En esta guía Nginx escuchará en el puerto 8000 para convivir con HAProxy.
sudo nano /etc/nginx/sites-available/sticky-demo
upstream app_iphash {
ip_hash;
server 127.0.0.1:8081;
server 127.0.0.1:8082;
}
server {
listen 8000;
location / {
proxy_pass http://app_iphash;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Actívalo y recarga:
sudo ln -s /etc/nginx/sites-available/sticky-demo /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Todas las peticiones desde la misma IP llegan al mismo backend, sin cookies:
for i in 1 2 3 4; do curl -s http://127.0.0.1:8000/; done
app2
app2
app2
app2
Con IPv4, ip_hash usa solo los tres primeros octetos de la dirección, así que todos los clientes de una misma red /24 van al mismo servidor. Si Nginx está detrás de otro proxy o de un CDN, todos los clientes compartirán su IP y la carga no se repartirá; en ese caso usa hash $http_x_forwarded_for consistent; o la persistencia por cookie del siguiente paso.
Paso 4: Persistencia por cookie en Nginx
Sin NGINX Plus puedes conseguir persistencia por cookie combinando dos map: uno que traduce la cookie srv a la dirección del backend y otro que, tras la respuesta, genera la cookie a partir del backend que realmente atendió la petición ($upstream_addr). Sustituye el contenido de /etc/nginx/sites-available/sticky-demo:
sudo nano /etc/nginx/sites-available/sticky-demo
upstream app_pool {
server 127.0.0.1:8081;
server 127.0.0.1:8082;
}
map $cookie_srv $sticky_target {
app1 127.0.0.1:8081;
app2 127.0.0.1:8082;
default app_pool;
}
map $upstream_addr $srv_cookie {
~127\.0\.0\.1:8081$ "srv=app1; Path=/; HttpOnly";
~127\.0\.0\.1:8082$ "srv=app2; Path=/; HttpOnly";
default "";
}
server {
listen 8000;
location / {
proxy_pass http://$sticky_target;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
add_header Set-Cookie $srv_cookie;
error_page 502 504 = @fallback;
}
location @fallback {
proxy_pass http://app_pool;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
add_header Set-Cookie $srv_cookie;
}
}
Cómo funciona:
- Sin cookie,
$sticky_targetvaleapp_pooly Nginx reparte con el balanceo normal del upstream. - Con
srv=app1,proxy_passva directamente a127.0.0.1:8081. add_header Set-Cookie $srv_cookiefija la cookie según el backend que respondió. Si Nginx probó varios servidores,$upstream_addrcontiene la lista completa, por eso elmapusa expresiones regulares ancladas al final ($) para quedarse con el último, el que respondió. Cuando el valor está vacío, Nginx no añade la cabecera.- Si el backend fijado no responde,
error_pagepasa la petición a@fallback, que usa el grupo completo y reescribe la cookie hacia el servidor que sí funciona.
Notacon
proxy_passa una dirección concreta Nginx no aplica los reintentos del upstream. El bloque@fallbackcubre ese caso, pero reenvía la petición completa, incluidas las POST. Si tus peticiones no son idempotentes, tenlo en cuenta o usa HAProxy.
Comprueba y recarga:
sudo nginx -t
sudo systemctl reload nginx
Repite la prueba con un archivo de cookies:
rm -f ~/cookies.txt
for i in 1 2 3 4; do curl -s -b ~/cookies.txt -c ~/cookies.txt http://127.0.0.1:8000/; done
app1
app1
app1
app1
Y comprueba la conmutación deteniendo el backend fijado:
sudo systemctl stop demo-app1
curl -s -b ~/cookies.txt -c ~/cookies.txt http://127.0.0.1:8000/
grep srv ~/cookies.txt
app2
#HttpOnly_127.0.0.1 FALSE / FALSE 0 srv app2
Paso 5: Limpiar el entorno de pruebas
El backend demo-app1 ya está detenido desde la prueba anterior. Detén el otro y elimina el sitio de Nginx:
sudo systemctl stop demo-app2
sudo rm /etc/nginx/sites-enabled/sticky-demo
sudo systemctl reload nginx
Los servicios creados con systemd-run son temporales y desaparecen al detenerse. Si solo quieres uno de los dos balanceadores, desinstala el otro.
¿Necesitas realmente sesiones persistentes?
La persistencia resuelve un problema de la aplicación, no del balanceador, y tiene costes: la carga se reparte peor, y cuando un servidor cae o lo retiras para actualizarlo, sus usuarios pierden la sesión igualmente. La alternativa robusta es que la aplicación guarde las sesiones fuera del proceso, en Redis, Memcached o la base de datos. La mayoría de frameworks lo soportan con un cambio de configuración (por ejemplo, SESSION_DRIVER=redis en Laravel o session.save_handler = redis en PHP con la extensión phpredis). Con sesiones compartidas, cualquier backend puede atender cualquier petición y puedes quitar la persistencia del balanceador.
Las sesiones persistentes siguen siendo útiles para aplicaciones heredadas que no puedes modificar, para WebSockets o conexiones largas con estado y para aprovechar cachés locales de cada servidor.
Solución de problemas
- Las peticiones siguen alternando entre backends. El cliente no devuelve la cookie. Con
curlusa-by-c; en el navegador revisa en las herramientas de desarrollo que la cookie se guarda. Si terminas TLS en el balanceador y la cookie tienesecure, no se envía por HTTP. - Todo el tráfico va a un solo backend con
ip_hash. Los clientes llegan a través de un proxy, CDN o NAT y comparten IP. Usa persistencia por cookie. show tabledevuelveUnknown commandoPermission denied. El socket de administración no está configurado conlevel adminen la secciónglobalo no ejecutas el comando consudo.- Un usuario pierde la sesión tras un despliegue. Es el comportamiento esperado al reiniciar su backend. Pon el servidor en modo
drainantes de retirarlo (echo "set server be_app/app1 state drain" | sudo socat stdio /run/haproxy/admin.sock) para que no reciba clientes nuevos mientras terminan los existentes, o externaliza las sesiones.
Conclusión
Has configurado persistencia de sesión por cookie y por IP en HAProxy, y por IP y por cookie en Nginx, y has comprobado que los clientes vuelven a su backend y conmutan a otro cuando cae. Como siguientes pasos, termina TLS en el balanceador y añade secure a las cookies, ajusta las comprobaciones de salud a un endpoint real de tu aplicación y valora mover las sesiones a Redis para poder prescindir de la persistencia.
