Loading...

Rendimiento · 12 min de lectura

Qué es un balanceador de carga: cómo se reparte, se comprueba y se conmuta el tráfico

Un balanceador de carga acepta tráfico en una sola dirección y lo reparte entre varios servidores que pueden responderlo todos, saltándose cualquier servidor que falle sus health checks. Esta guía cubre el balanceo en capa 4 y capa 7, los algoritmos, los health checks, las sesiones persistentes y la terminación TLS, configuraciones reales de HAProxy y nginx, el balanceo de carga global por DNS, y los errores que convierten un balanceador de carga en la causa de una caída.

Actualizado

Qué es un balanceador de carga: cómo se reparte, se comprueba y se conmuta el tráfico

Qué hace un balanceador de carga

Un balanceador de carga se sitúa entre los clientes y un grupo de servidores que pueden responder todos a la misma petición, y decide cuál recibe cada una. Los clientes se conectan a una sola dirección; el balanceador elige un backend saludable, reenvía la petición y devuelve la respuesta.

Hace tres trabajos. Reparte la carga, así que tres servidores pueden soportar aproximadamente el triple de tráfico que uno. Retira los servidores caídos: los health checks detectan un backend que dejó de responder y no le envían nadie hasta que se recupera. Y hace invisibles los cambios: puedes sacar un servidor, parchearlo, desplegar en él y devolverlo sin que los visitantes vean un error.

El segundo trabajo suele ser el más importante: dos servidores detrás de un balanceador de carga tienen menos que ver con la capacidad y más con que uno de los dos pueda morir sin problema. Un balanceador de carga de capa 7 es un tipo de proxy inverso cuyo trabajo es elegir entre backends idénticos; HAProxy y nginx hacen las dos cosas.

Balanceo de carga en capa 4 frente a capa 7

La capa es cuánto del tráfico lee el balanceador de carga antes de decidir.

Un balanceador de capa 4 trabaja con conexiones TCP y paquetes UDP. Ve direcciones y puertos de origen y destino, elige un backend cuando se abre la conexión, y copia bytes en los dos sentidos. No puede ver una URL, una cabecera ni una cookie, así que todas las peticiones de esa conexión van al mismo servidor. A cambio es rápido, agnóstico al protocolo (bases de datos, MQTT, SMTP, servidores de juegos) y puede dejar pasar TLS directamente, así que el certificado se queda en los backends. Aquí trabajan el mode tcp de HAProxy y el bloque stream {} de nginx.

Un balanceador de capa 7 habla HTTP. Termina la conexión, lee cada petición y puede enrutar por host, ruta, cabecera o cookie: /api a un pool, / a otro, una cookie de canary a la versión nueva. Puede reintentar una petición fallida en otro servidor y repartir las peticiones de una sola conexión HTTP/2 entre varios backends (mode http en HAProxy, http {} en nginx).

Para leer HTTP, un balanceador de capa 7 tiene que descifrarlo, así que la terminación TLS viene de serie. El certificado vive en el balanceador y los backends reciben HTTP normal en una red privada, o una segunda conexión TLS si la red no es de confianza. El coste es que el backend ya no ve al cliente: ve al balanceador. Los balanceadores de capa 7 arreglan esto con X-Forwarded-For y X-Forwarded-Proto; los de capa 4 usan el protocolo PROXY, que el backend debe estar configurado para aceptar.

Algoritmos de balanceo: round robin, least connections, hashing, pesos

Round robin entrega las peticiones a cada servidor por turnos. Es el valor por defecto en casi todas partes y es correcto cuando las peticiones cuestan más o menos lo mismo y los servidores son idénticos.

Round robin con pesos da una porción mayor a un servidor más grande: con pesos 2, 2 y 1, el tercer servidor recibe una quinta parte del tráfico. Los pesos también son la forma de hacer un canary: pon la versión nueva en el pool con un peso pequeño, vigila su tasa de errores, y luego súbelo.

Least connections envía la siguiente petición al servidor con menos conexiones activas. Úsalo cuando el coste de las peticiones varía mucho (informes junto a vistas de página, subidas junto a llamadas a la API) o las conexiones son de larga duración, como con WebSockets.

IP hash (balance source en HAProxy, ip_hash en nginx) mapea cada dirección de cliente siempre al mismo servidor. Da afinidad sin cookies, pero la distribución sigue a la población de clientes: una oficina o un operador móvil detrás de una dirección compartida puede hacer que miles de usuarios caigan en un mismo backend.

El hashing consistente sobre una clave (la URI, un ID de usuario) mantiene la misma clave en el mismo servidor, y cuando se añade o se quita un servidor solo se mueve una pequeña parte de las claves. Eso es lo que quieres delante de cachés: hashea por URI y cada objeto vive en una caché en lugar de en todas. nginx lo escribe como hash $request_uri consistent;, HAProxy como balance uri con hash-type consistent.

Sea cual sea tu elección, el algoritmo importa menos que unos health checks que digan la verdad.

Health checks y failover

Un balanceador de carga es tan bueno como su idea de qué servidores están vivos.

Los health checks activos sondean cada backend con un temporizador: abren una conexión TCP, o piden una URL como /healthz y esperan un 200. Tras fall fallos consecutivos, el servidor se marca como caído y no recibe nada; tras rise éxitos, vuelve. HAProxy hace esto en su versión de código abierto. nginx de código abierto no: max_fails y fail_timeout son comprobaciones pasivas, que cuentan peticiones reales fallidas y dejan descansar al servidor un rato. Las comprobaciones activas en nginx son parte del NGINX Plus comercial. El tiempo es una decisión de compromiso: una comprobación cada dos segundos con fall 3 retira un servidor muerto en unos seis segundos; cada 30 segundos significa minuto y medio de errores.

El failover es lo que pasa después. El tráfico se traslada a los servidores que quedan, así que tienen que tener sitio para él: tres servidores funcionando al 80% cada uno no pueden absorber la pérdida de uno. Un servidor backup recibe tráfico solo cuando todos los primarios están caídos, lo que va bien para una reserva fría o una página de mantenimiento.

Haz que el endpoint de salud responda a la pregunta que hace el balanceador: *¿debería este servidor recibir tráfico ahora mismo?* Eso significa comprobar lo que es local al proceso (ha arrancado, puede alcanzar sus propias dependencias, no se está apagando) y responder rápido. Durante un despliegue, falla primero la comprobación, deja que terminen las peticiones en vuelo (connection draining), y luego detén el proceso.

Persistencia de sesión (sticky sessions)

Si una aplicación guarda la sesión de un usuario conectado en la memoria de un servidor, la siguiente petición tiene que llegar a ese servidor o el usuario pierde la sesión. La persistencia de sesión hace que el balanceador recuerde quién va a dónde.

Las formas habituales son una cookie insertada (el balanceador añade una cookie que nombra al servidor, como hace HAProxy con cookie SRV insert indirect nocache más un valor cookie en cada línea server), una cookie de aplicación de la que aprende (PHPSESSID, JSESSIONID), o el hashing por IP de origen. nginx de código abierto solo tiene los métodos de hashing (ip_hash o hash $cookie_name); la directiva sticky pertenece a NGINX Plus.

La persistencia tiene un precio. La carga deja de estar repartida de forma pareja, porque unos pocos usuarios intensivos se quedan clavados en un servidor. Drenar un servidor tarda tanto como su sesión más larga, y cuando un servidor muere, sus usuarios pierden la sesión igualmente.

La solución duradera es hacer intercambiables a los servidores: guardar las sesiones en un almacén compartido como Redis o la base de datos, o en una cookie firmada, y dejar que cualquier servidor responda a cualquier petición. Entonces un servidor caído no le cuesta a nadie su sesión.

Una configuración de HAProxy que funciona

Una configuración de HAProxy se lee de arriba abajo: global para el proceso, defaults que heredan todas las secciones, un frontend que acepta conexiones y un backend que contiene el pool.

El ejemplo termina TLS en el 443 (el archivo .pem guarda la cadena del certificado y la clave privada juntas), redirige el HTTP normal, añade las cabeceras de reenvío y balancea con least connections. El health check es una petición HTTP real con una cabecera Host, porque muchas aplicaciones responden 404 o una redirección a una petición sin ella, y una comprobación que espera 200 entonces falla en un servidor sano. http-check send necesita HAProxy 2.2 o más reciente; las versiones antiguas ponen la petición en la línea option httpchk.

default-server fija el ritmo de comprobación una sola vez: una sonda cada dos segundos, tres fallos para marcar un servidor como caído, dos éxitos para que vuelva. app3 es una máquina más pequeña y recibe la mitad de la porción que las otras; spare recibe tráfico solo cuando los tres están caídos. Para hacer sticky las sesiones, añade cookie SRV insert indirect nocache al backend y cookie app1 (y así sucesivamente) a cada línea de servidor.

Comprueba el archivo con haproxy -c -f /etc/haproxy/haproxy.cfg antes de cada reload.

/etc/haproxy/haproxy.cfg: terminación TLS, least connections, health checks HTTP

global
    log /dev/log local0
    maxconn 20000

defaults
    mode http
    log global
    option httplog
    timeout connect 5s
    timeout client  60s
    timeout server  60s

frontend fe_web
    bind :80
    bind :443 ssl crt /etc/haproxy/certs/example.com.pem alpn h2,http/1.1
    http-request redirect scheme https unless { ssl_fc }
    option forwardfor
    http-request set-header X-Forwarded-Proto https
    default_backend be_app

backend be_app
    balance leastconn
    option httpchk
    http-check send meth GET uri /healthz ver HTTP/1.1 hdr Host app.example.com
    http-check expect status 200
    default-server inter 2s fall 3 rise 2
    server app1 10.0.0.11:8080 check weight 100
    server app2 10.0.0.12:8080 check weight 100
    server app3 10.0.0.13:8080 check weight 50
    server spare 10.0.0.20:8080 check backup

Balanceo de carga con upstream de nginx

En nginx, el pool es un bloque upstream y proxy_pass apunta a él por nombre. Sin una línea de algoritmo, nginx usa round robin con pesos; least_conn, ip_hash, hash … consistent y random cambian eso.

Algunos detalles del ejemplo son fáciles de pasar por alto. keepalive 32 mantiene abiertas para reutilizar las conexiones inactivas hacia los backends, pero solo funciona junto con proxy_http_version 1.1 y una cabecera Connection vacía; sin esas dos líneas, nginx abre una conexión nueva en cada petición. max_fails=3 fail_timeout=10s retira a un servidor durante diez segundos tras tres peticiones fallidas en diez segundos: es el health check pasivo de nginx. backup solo funciona con los métodos round robin, con pesos y least_conn, no con los de hashing.

proxy_next_upstream decide cuándo se reintenta una petición fallida en el siguiente servidor. Por defecto, nginx reintenta en errores de conexión y timeouts, y desde la versión 1.9.13 nunca reintenta un POST, LOCK ni PATCH a menos que añadas non_idempotent. Déjalo así: reintentar un pago porque el primer servidor hizo timeout después de cobrar la tarjeta es peor que un error. proxy_next_upstream_tries 2 evita que una petición lenta recorra todo el pool. Las cabeceras son las mismas que necesita cualquier proxy inverso; la guía de nginx cubre el resto.

nginx: un pool upstream con pesos, comprobaciones pasivas, un backup y keep-alive

upstream app {
    least_conn;
    server 10.0.0.11:8080 weight=2 max_fails=3 fail_timeout=10s;
    server 10.0.0.12:8080 weight=2 max_fails=3 fail_timeout=10s;
    server 10.0.0.13:8080          max_fails=3 fail_timeout=10s;
    server 10.0.0.20:8080 backup;
    keepalive 32;
}

server {
    listen 443 ssl;
    server_name example.com;
    ssl_certificate     /etc/ssl/example.com/fullchain.pem;
    ssl_certificate_key /etc/ssl/example.com/privkey.pem;

    location / {
        proxy_pass http://app;
        proxy_http_version 1.1;
        proxy_set_header Connection        "";
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_next_upstream error timeout http_502 http_503;
        proxy_next_upstream_tries 2;
        proxy_connect_timeout 3s;
    }
}

Global frente a local: DNS, GeoDNS, anycast y balanceadores de carga en la nube

Todo lo anterior es balanceo de carga local: un sitio, un pool, un proxy en el camino de la petición. El balanceo global decide a qué sitio, región o centro de datos llega un visitante en primer lugar, y normalmente se hace antes de que ningún proxy vea la petición.

El round robin de DNS es la forma más antigua: publica varios registros A y los resolutores los entregan en orden rotatorio. No cuesta nada y reparte el tráfico de forma aproximada, pero el DNS no sabe nada de salud. La dirección de un servidor muerto se sigue entregando hasta que alguien la retira, y los resolutores y navegadores cachean la respuesta antigua durante el TTL del registro, y a veces más.

GeoDNS responde de forma distinta según de dónde venga la consulta, así que los visitantes de un país reciben la dirección del sitio más cercano. Combinado con health checks en el lado del DNS, se convierte en failover por DNS: una región que falla sus comprobaciones se retira de las respuestas. Los límites son el mismo cacheo por TTL y el hecho de que la ubicación es la del resolutor, no la del visitante, a menos que el resolutor transmita parte de la dirección del cliente (EDNS Client Subnet). La guía de DNS cubre los TTL y los resolutores en detalle.

Anycast anuncia la misma dirección IP desde muchas ubicaciones por BGP, y el enrutamiento de internet entrega cada paquete a la más cercana. No hay TTL que esperar: cuando una ubicación deja de anunciar, las rutas se mueven en segundos o minutos. Así alcanzan sus nodos los grandes servicios de DNS y las CDN. Las capas se apilan: el DNS o el anycast elige la ubicación, un proxy dentro de ella elige el servidor.

Los balanceadores de carga en la nube empaquetan las mismas ideas como servicio gestionado. AWS tiene el Application Load Balancer (capa 7) y el Network Load Balancer (capa 4); Google Cloud Load Balancing ofrece variantes globales y regionales, de aplicación y de red; Azure separa Azure Load Balancer (capa 4) de Application Gateway (capa 7). En Kubernetes, un Service reparte conexiones entre pods y un controlador de Ingress hace el enrutamiento de capa 7. Los algoritmos, health checks y errores habituales de esta guía se aplican a ellos sin cambios.

Errores habituales que causan caídas

El balanceador de carga es el punto único de fallo. Tres servidores de aplicación detrás de una sola máquina HAProxy significa que ahora esa máquina es lo que puede tumbarte. Ejecuta dos y mueve una IP flotante entre ellas con VRRP (keepalived es la herramienta habitual), o pon delante una capa gestionada o a nivel de DNS. Luego pruébalo apagando la que está activa.

El health check miente, en cualquier dirección. Un /healthz que siempre devuelve 200 mantiene en rotación a un servidor cuyo pool de conexiones a la base de datos está agotado, así que una parte de las peticiones falla mientras el panel muestra todo en verde. Lo contrario es igual de malo: una comprobación que consulta la base de datos compartida marca *todos* los servidores como caídos durante un fallo de cinco segundos de la base de datos, y una ralentización breve se convierte en una caída completa. Comprueba el servidor, no los sistemas que todos los servidores comparten.

Timeouts descoordinados. Si el backend cierra las conexiones keep-alive inactivas antes de lo que espera el balanceador, este envía de vez en cuando una petición por una conexión que el backend acaba de cerrar, y el cliente recibe un 502 esporádico. Mantén el timeout de keep-alive del backend más largo que el timeout de inactividad del balanceador. Más causas en la guía de 502 Bad Gateway.

La dirección del cliente desaparece. Sin X-Forwarded-For (capa 7) o el protocolo PROXY (capa 4), la aplicación registra, limita por tasa y geolocaliza la IP del balanceador.

Servidores que no son idénticos. Builds distintos, configuración distinta, o marcas de tiempo de archivo distintas que cambian el ETag por defecto, así que la revalidación del navegador devuelve un 200 completo en lugar de un 304 cada vez que responde el otro servidor. Despliega un solo artefacto en todas partes.

Probar esto es sencillo. Expón el nombre del backend en una cabecera de respuesta durante las pruebas y repite peticiones en un bucle; observa la distribución, luego detén un backend y mira cómo se mueven las peticiones.

Ver la distribución, leer el estado de los servidores, drenar un servidor
# which backend answered? (expose the server name in a debug header first)
for i in $(seq 1 10); do
  curl -s -o /dev/null -D - https://example.com/ | grep -i '^x-served-by'
done

# HAProxy: live state of every server through the runtime socket
# (needs "stats socket /run/haproxy/admin.sock mode 660 level admin" in global)
echo "show servers state be_app" | socat stdio /run/haproxy/admin.sock

# take one server out gracefully before a deploy, then put it back
echo "set server be_app/app1 state drain" | socat stdio /run/haproxy/admin.sock
echo "set server be_app/app1 state ready" | socat stdio /run/haproxy/admin.sock

Dónde encaja una CDN, y qué hace CDN.com.tr

Una CDN es balanceo de carga global que no tienes que construir. Los visitantes se enrutan a un servidor de edge cercano, los edges responden desde caché todo lo que pueden, y solo los fallos de caché viajan hasta tu origen. CDN.com.tr ejecuta servidores de edge en Turquía y en el extranjero y reparte a los visitantes entre ellos, así que repartir visitantes entre ubicaciones ya está hecho antes de que una petición llegue a nada que tú operes.

Lo que sigue siendo tuyo es el origen. Para un sitio Pull CDN, el edge se abastece de la dirección de origen que fijas en el panel, una IP o un dominio. Si ejecutas varios servidores de origen, el balanceador de carga que elige entre ellos se sitúa detrás de esa dirección, y todo lo de esta guía se aplica a él. Protégelo para que solo acepte conexiones del edge: la propia guía del panel recomienda mantener la dirección del origen fuera del DNS público para que el único camino pase por el edge.

El edge también suaviza los fallos del origen. El contenido que ya está en la caché del edge sigue sirviéndose durante su TTL mientras el origen está caído, y el informe de calidad del tráfico marca como STALE una copia cacheada servida mientras el origen estaba lento o fallando. El mismo informe muestra la salud del origen: peticiones al origen, porcentaje de reintentos, errores de conexión (502/504) y el tiempo medio de primer byte del origen. Los objetos sin caché y los caducados todavía necesitan un origen que funcione, por lo que el origen sigue necesitando su propia redundancia para las páginas dinámicas.

Si prefieres no operar esa capa en absoluto, las aplicaciones en contenedor de CDN.com.tr toman un número de réplicas y un health check (ruta HTTP, TCP o ninguno) por aplicación. El tráfico entra por el edge; con varias réplicas, si un contenedor se está reiniciando o no está saludable, las demás siguen sirviendo, y los despliegues están sujetos a health checks, así que desplegar una versión nueva no abre una ventana en la que la aplicación sea inalcanzable. Las sesiones van a un Redis gestionado, así que un usuario sigue conectado sin importar qué réplica responda a la siguiente petición: el diseño sin estado recomendado antes, sin la cookie persistente.

Preguntas frecuentes sobre balanceadores de carga

¿HAProxy o nginx para balanceo de carga?

Los dos son rápidos y fiables. HAProxy tiene health checks activos, persistencia por cookie, una API en tiempo de ejecución para drenar servidores y estadísticas muy detalladas en la versión gratuita, lo que lo hace el balanceador de carga puro más completo. nginx encaja mejor cuando la misma máquina también sirve archivos, cachea o hace el enrutamiento de varios sitios, pero su versión gratuita solo tiene health checks pasivos.

¿Cuál es la diferencia entre balanceo de carga en capa 4 y en capa 7?

La capa 4 elige un servidor por conexión TCP o flujo UDP y nunca lee el contenido, así que funciona con cualquier protocolo y puede dejar pasar TLS sin tocarlo. La capa 7 lee cada petición HTTP, así que puede enrutar por URL, cabecera o cookie, reintentar en otro servidor y añadir cabeceras de reenvío, a costa de terminar TLS.

¿Qué algoritmo de balanceo debería usar?

Empieza con round robin para servidores idénticos y peticiones parecidas. Cambia a least connections cuando la duración de las peticiones varía mucho o las conexiones se mantienen abiertas, como con WebSockets. Usa hashing consistente cuando la misma clave deba llegar al mismo servidor, como las URL delante de una capa de caché.

¿Es el round robin de DNS un balanceador de carga de verdad?

Reparte el tráfico, pero no comprueba la salud y sus respuestas se cachean durante el TTL del registro, así que los clientes siguen intentando una dirección muerta. Está bien para una distribución aproximada entre sitios que ya tienen su propio balanceador de carga, y es pobre como único mecanismo de failover.

¿Necesito un balanceador de carga si uso una CDN?

La CDN reparte a los visitantes entre sus propios servidores de edge y absorbe la mayor parte del tráfico desde caché. Si tu origen es un solo servidor, no necesitas un balanceador por capacidad, aunque el origen sigue siendo un punto único de fallo para todo lo que no esté en caché. En cuanto ejecutes dos o más servidores de origen, pon un balanceador de carga detrás de la dirección de origen de la que se abastece la CDN.