Skip to content

About

Traefik | Nodejs | Api Rest | Socket IO | Redis

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 

Repository files navigation

traefik-test

Sandbox standalone para entender, en un ejemplo minimo, dos cosas fundamentales del balanceo de carga sobre un servidor que expone una API REST y un servidor WebSocket en el mismo proceso:

  1. Como Traefik descubre y balancea automaticamente varias replicas de un mismo contenedor Node, sin tocar ninguna configuracion cada vez que escalas (docker compose up --scale server=N).
  2. Por que escalar una API REST (sin estado) es trivial, pero escalar un servidor WebSocket (con estado por conexion) no lo es: un Socket.IO multi-instancia sin adapter compartido rompe el broadcast a rooms cuando los clientes quedan conectados a instancias distintas — y como @socket.io/redis-adapter lo resuelve.

Por que escalar en instancias (y no solo verticalmente)

Node.js corre con un unico hilo para tu codigo JS (el event loop): un solo proceso node server.js, sin importar cuantos nucleos tenga la maquina, solo va a usar un nucleo de CPU. Si el servidor recibe trafico suficiente como para saturar ese nucleo (muchas requests, mucho trabajo de CPU por request, muchas conexiones de socket activas), agregar mas RAM o un CPU mas rapido (escalar verticalmente) no soluciona el cuello de botella — el proceso sigue limitado a un solo nucleo.

La forma de aprovechar el resto de los nucleos es correr varias instancias del mismo proceso (una por nucleo, tipicamente) y repartir el trafico entre todas — escalar horizontalmente. Eso es exactamente lo que hace docker compose up --scale server=N: levanta N procesos Node independientes, y Traefik se encarga de repartir las requests entre ellos sin que tengas que tocar ninguna configuracion manual cada vez que cambias N.

La contrapartida de tener N procesos independientes en vez de uno solo es que cualquier estado que antes vivia en la memoria de un unico proceso (rooms de Socket.IO, locks, cachés en memoria, etc.) deja de ser visible para las otras N-1 instancias — de ahi la segunda mitad de este sandbox.

"Escalado horizontal" es el mismo termino tanto si las N instancias corren en un solo host (este sandbox) como si corren repartidas en varios servidores/VMs — lo que lo distingue del escalado vertical es multiplicar unidades en vez de agrandar los recursos de una sola. La diferencia entre ambos casos es el techo, no el concepto:

  • Un solo host: podes escalar horizontalmente hasta agotar los nucleos o la RAM de esa maquina. Pasado ese punto, cada instancia nueva compite por el mismo pool de CPU en vez de sumar capacidad real.
  • Varios hosts: extiende el mismo escalado horizontal mas alla del limite de una sola maquina. Ahi el balanceador ya no puede descubrir instancias solo mirando el docker.sock local (como hace Traefik en este sandbox) — hace falta un orquestador que las conozca a todas (Docker Swarm, Kubernetes) o un balanceador externo a cada host.

Por que el balanceo de carga se comporta distinto en REST y en WebSocket

Con una API REST, cada request es independiente: a Traefik le alcanza con hacer round robin entre las replicas, porque cualquier instancia puede responder cualquier request sin necesitar contexto de requests anteriores.

Con un servidor WebSocket (Socket.IO), el cliente abre una conexion persistente y queda pegado a una sola instancia durante toda la duracion de esa conexion:

  • Si dos clientes en la misma "room" quedan conectados a instancias distintas, un io.to(room).emit(...) ejecutado en la instancia A no llega al cliente conectado a la instancia B — porque por defecto Socket.IO solo conoce los sockets conectados a su propio proceso.
  • La sticky session (cookie que Traefik usa para mantener a un cliente en la misma instancia) resuelve la reconexion/polling de un mismo cliente, pero no resuelve la comunicacion entre clientes que cayeron en instancias diferentes.
  • La solucion es compartir el estado de las rooms entre instancias via un medio externo (Redis pub/sub, con @socket.io/redis-adapter), para que el emit de una instancia se propague a los sockets conectados a cualquier otra instancia.

Este sandbox reproduce ambos escenarios (paso 5 sin adapter, paso 6 con adapter) para verlos funcionar en vivo.

Estructura

traefik-test/
├── README.md
└── app/                    # standalone: compose, env y app minima Express + Socket.IO
    ├── docker-compose.yml  # redis + server (N replicas) + traefik, lee .env de esta misma carpeta
    ├── .env                # variables para traefik y server (ya viene con defaults)
    ├── .dockerignore
    ├── Dockerfile
    ├── package.json
    ├── server.js
    └── public/index.html   # cliente de prueba en el navegador

1. Revisar variables de entorno

app/.env ya viene con defaults funcionales (TRAEFIK_PORT=3305, USE_REDIS_ADAPTER=false), lo lee tanto el servicio traefik (sustitución dentro de docker-compose.yml) como el servicio server (vía env_file).

2. Levantar el stack

cd traefik-test/app
docker compose up --build

Traefik publica TRAEFIK_PORT (3305 por defecto) al host — ese es el unico puerto que hay que exponer desde afuera (por ejemplo, desde Apache).

Dashboard de Traefik (solo para esta prueba, no usar asi en produccion): http://localhost:8080/dashboard/

3. Escalar a varias replicas

docker compose up --scale server=3 -d

Mirar los logs para confirmar que hay 3 instancias corriendo, cada una con su propio INSTANCE_ID (el hostname del contenedor):

docker compose logs -f server

4. Probar la API REST y el balanceo

curl http://localhost:3305/health

Repetilo varias veces: en los logs vas a ver que las requests no siempre las atiende la misma instancia (round robin de Traefik).

5. Probar el WebSocket y el problema de las rooms

Abri en el navegador: http://localhost:3305

  1. Abrila en dos contextos distintos (una pestaña normal + una de incognito, o dos navegadores) para que no compartan la cookie de sticky session y probablemente queden pegadas a instancias distintas. La pagina te muestra a que instance quedo conectada cada una.
  2. En ambas, tocá "Join room" con el mismo nombre de room (demo-room por defecto).
  3. Desde una de las dos, escribi un mensaje y tocá "Enviar".

Con USE_REDIS_ADAPTER=false: si las dos pestañas quedaron en instancias distintas, el mensaje no llega a la otra pestaña — io.to(room).emit(...) solo alcanza a los sockets conectados al mismo proceso que ejecuta el emit.

6. Activar el fix (redis-adapter) y repetir la prueba

Editá app/.env:

USE_REDIS_ADAPTER=true

Y reiniciá (no hace falta rebuildear, solo cambio una variable de entorno):

docker compose up -d

Repeti el paso 5: ahora el mensaje llega a todas las pestañas que estén en esa room, sin importar a que instancia quedo conectada cada una — porque server.js arma el adapter con @socket.io/redis-adapter, que usa Redis (pub/sub) para propagar los eventos de rooms entre todas las instancias.

Que mirar en el codigo

  • app/server.js: setupAdapter() es el unico lugar donde se decide si Socket.IO usa o no el adapter compartido. El resto del codigo (join-room, broadcast-message) es identico en ambos casos — la diferencia la hace únicamente el adapter.
  • app/docker-compose.yml: las labels de traefik.* en el servicio server son lo unico que Traefik necesita para descubrir, balancear y aplicar sticky sessions a las replicas — no hay ningun archivo de configuracion estatico que editar al escalar.

Apagar y limpiar

docker compose down -v

About

Traefik | Nodejs | Api Rest | Socket IO | Redis

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages