Capítulo 02 · La puerta

Load balancer

Una sola API no aguanta Black Friday. Varias cajas detrás de un solo hostname. Salud, L4 vs L7, sticky.

16 min intro

Una caja no alcanza

Imaginate que seguimos en la misma tienda. Ya tenemos base de datos: el pedido 456 nace en Postgres. Ana pulsa Comprar y la API responde. Un martes cualquiera, una sola instancia de la API alcanza. En Black Friday, no.

Si todo el tráfico entra a api-1, esa caja es el cuello: CPU al tope, conexiones agotadas, timeouts. Si se reinicia en un deploy, la tienda entera se cae. No es un problema de Postgres todavía. Es que hay un solo mostrador atendiendo.

La respuesta obvia es poner más APIs: api-1, api-2, api-3. El problema nuevo: Ana no puede memorizar tres hostnames. Quiere shop.example.com y listo. Hace falta alguien delante que reciba el request y lo mande a una caja sana.

Eso es un load balancer (balanceador de carga): la puerta. Un hostname, muchas instancias detrás.

Qué hace, en la práctica

Ana abre la ficha del 123. El DNS de shop.example.com apunta al load balancer (o a una IP anycast, o a un cloud LB). El balanceador elige una API, le pasa el request, y le devuelve la respuesta a Ana. Ella no sabe si la atendió la 1 o la 3.

Ana → shop.example.com → load balancer
                              ├─ api-1
                              ├─ api-2
                              └─ api-3
                                   │
                                   ▼
                              Postgres (verdad)

Ganás tres cosas de una:

  1. Capacidad. Tres cajas atienden en paralelo. Si una se satura, las otras siguen.
  2. Disponibilidad. Si api-2 se muere, el balanceador deja de mandarle tráfico. La tienda no se apaga entera.
  3. Deploys más humanos. Sacás una instancia del pool, actualizás, la devolvés. Sin “apagamos todo el checkout cinco minutos”.

En una entrevista de system design, dibujar el hexágono de la API y parar ahí es incomplete. En cuanto hablás de “varias instancias”, el load balancer tiene que aparecer.

Cómo elige a quién mandar

No hace falta memorizar diez algoritmos. Con tres ideas alcanzás casi cualquier pizarra.

Round robin. Uno a la 1, el siguiente a la 2, el siguiente a la 3, y otra vez. Simple. Funciona si las cajas son parecidas y los requests duran más o menos lo mismo.

Least connections. Mandá a la que tiene menos requests abiertos. Útil si algunos requests son largos (un checkout lento, un upload) y no querés empujar más carga a la caja ya ocupada.

Weighted. La caja grande recibe más. La chica, menos. Tiene sentido cuando el hardware no es uniforme.

Lo que casi nunca querés como default mágico: “el algoritmo perfecto”. Querés algo aburrido, predecible, y un pool de instancias sanas. El resto del diseño (caché, colas, base de datos) importa más que pelear por micro-optimizar el round robin.

L4 y L7: dos oficios distintos

Acá es donde mucha gente se enreda en la entrevista. Hay dos maneras de balancear, y no son lo mismo.

Capa 4 (L4) mira la conexión TCP: IP, puerto. No lee el HTTP. Es rápido, barato, y le importa poco si adentro va un GET de ficha o un WebSocket. Encaja cuando querés pasar el tráfico casi transparente, o cuando el protocolo no es HTTP clásico.

Capa 7 (L7) entiende HTTP. Puede rutar por path (/checkout a un pool, /static a otro), por host, por headers. Puede terminar TLS, mirar cookies, devolver un 503 de mantenimiento. Es más listo y un poco más caro.

En la tienda:

  • Ficha, login, Comprar: casi siempre L7. Querés HTTP, certificados, a veces reglas por path.
  • Tiempo real con WebSocket largo: a menudo pensás L4 (o un L7 que no cierre el upgrade a los 60 segundos). Mezclar “proxy HTTP agresivo” con sockets vivos es un clásico de incidente.

No digas “uso un load balancer” y listo. Decí qué capa y por qué para este tráfico.

Salud: no mandes a un muerto

Si api-2 está colgada pero el balanceador sigue mandándole Anas, empeorás el Black Friday. Hace falta un health check: el LB pregunta cada X segundos “¿estás vivo?”. Si falla N veces, saca la instancia del pool.

El check tiene que significar algo. Un TCP open no basta: el proceso puede estar up y Postgres inalcanzable. Mejor un endpoint /health que verifique lo mínimo (proceso ok, dependencia crítica ok) sin hacer un SELECT pesado en cada probe. Si el health check es más caro que atender clientes, te convertís en tu propio ataque.

Cuando la instancia vuelve, el LB la reincorpora. En deploys: drain (dejar de mandar tráfico nuevo, esperar a que terminen los requests abiertos) y después matar el proceso. Menos 502 en la cara de Ana.

Sticky sessions: el atajo y la trampa

A veces querés que Ana caiga siempre en la misma API. Sesión en memoria local, un carrito que “vive” en esa caja, un WebSocket ya abierto. Eso es sticky session (affinity): el LB recuerda y la manda al mismo backend.

Sirve como parche. No es la estrategia de escala. Si esa caja se cae, Ana pierde lo que solo existía ahí. Si hacés deploy, igual. En esta tienda, la regla que ya vas a ver en caché es la correcta: lo compartido vive en Redis o en Postgres, no en la RAM de api-2. Sticky puede ayudar a WebSockets o a un L1 caliente; no reemplaza estado compartido.

En una entrevista técnica: sticky sessions help; they are not the scaling plan.

TLS, timeouts y lo que no es el LB

El load balancer suele terminar TLS: el certificado vive en la puerta, detrás a veces va HTTP en red privada. Simplifica renovar certs y quita CPU de crypto a las APIs. También concentra un sitio donde mirar latencia de entrada.

Cuidado con los timeouts. Si el LB corta a los 60 segundos y el checkout habla con Stripe más tiempo, Ana ve un error aunque el cobro siga. Los timeouts del LB, de la API y del cliente tienen que contar la misma historia.

Y lo que el LB no es:

  • No es caché. Repartir requests no hace más barata la ficha del 123.
  • No es rate limiting, aunque muchos L7 pueden hacer un límite tosco. El portero fino (login por IP + email) vive cerca de la app.
  • No arregla un Postgres saturado. Solo reparte el golpe entre más APIs; el disco sigue siendo uno (hasta que replicas o shards entren al diseño).

Si en la pizarra el único movimiento es “ponemos un load balancer”, te falta el resto de la tienda.

La puerta, entera

Ana entra a shop.example.com. DNS la lleva al load balancer. Health checks: api-1 y api-3 sanas; api-2 en deploy, fuera del pool. Round robin (o least connections) manda el GET de la ficha a api-1. La API lee Postgres (y pronto, el caché). Respuesta 200.

Un pico: sumás api-4 al pool. Una caja se muere: el health check la saca. Ana no cambia de URL.

Ana llega por DNS al load balancer; reparte a api-1 y api-3; api-2 está fuera por health check; las APIs hablan con Postgres.

En la pizarra: un hostname, N instancias sin estado (o con poco estado), checks que importan, L7 para HTTP y cuidado con los sockets largos. El balanceador reparte. No piensa por vos. Las fotos del 123, en cambio, no deberían pasar por estas APIs: eso es el CDN.

Qué espera cada nivel

Un mid dibuja un load balancer delante de varias APIs y nombra health checks.

Un senior elige L4 vs L7 según el tráfico, no usa sticky como plan de escala, alinea timeouts, y no pretende que el LB sustituya caché ni base de datos.

Si te lo preguntan

¿Para qué un load balancer? One hostname in front of many API instances so we can scale horizontally and survive a single box dying.

¿Cómo reparte? Round robin or least connections is enough for most designs. Weighted if the hardware differs.

¿L4 o L7? L7 for HTTP APIs — path routing, TLS termination. L4 when we need raw TCP or long-lived connections that a chatty HTTP proxy would break.

¿Sticky? Only if we must. Shared state belongs in Redis or Postgres; sticky is not the scaling strategy.

¿Cómo sabés que una API está viva? Health checks. Pull unhealthy instances out of the pool; drain before deploy.