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étodoCómo identifica al clienteVentajasInconvenientes
Cookie insertada por el balanceadorCookie propia (SERVERID) con el nombre del backendPrecisa, independiente de la IPRequiere HTTP (o terminar TLS en el balanceador)
Hash de IP de origenIP del clienteFunciona sin cookies, también en TCPMuchos 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 balanceadorFlexible, con caducidad configurableSe 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

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 cookie SERVERID en 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ñade Cache-Control: private a 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én secure.
  • cookie app1 en cada server: el valor que se guarda en la cookie para ese servidor.
  • option httpchk y check: 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.

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_target vale app_pool y Nginx reparte con el balanceo normal del upstream.
  • Con srv=app1, proxy_pass va directamente a 127.0.0.1:8081.
  • add_header Set-Cookie $srv_cookie fija la cookie según el backend que respondió. Si Nginx probó varios servidores, $upstream_addr contiene la lista completa, por eso el map usa 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_page pasa la petición a @fallback, que usa el grupo completo y reescribe la cookie hacia el servidor que sí funciona.

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 curl usa -b y -c; en el navegador revisa en las herramientas de desarrollo que la cookie se guarda. Si terminas TLS en el balanceador y la cookie tiene secure, 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 table devuelve Unknown command o Permission denied. El socket de administración no está configurado con level admin en la sección global o no ejecutas el comando con sudo.
  • Un usuario pierde la sesión tras un despliegue. Es el comportamiento esperado al reiniciar su backend. Pon el servidor en modo drain antes 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.