Un balanceador de carga: turnos, pesos, menos conexiones, hash de IP y caídas
Simula un balanceador de carga como nginx o HAProxy: round robin, round robin ponderado suave, menos conexiones y hash de la IP, con conexiones que terminan, servidores que se caen y vuelven, 503 cuando no queda ninguno y el resumen del reparto.
- Balanceo de carga y alta disponibilidad
- Algoritmos de reparto
- Afinidad de sesión (hash de IP)
- Simulación por sucesos
- Clases y diccionarios
- Comprobaciones de salud
Enunciado
Cuando una aplicación web no cabe en un servidor, se ponen varios iguales detrás de un balanceador de carga (nginx, HAProxy o el de la nube) que reparte las peticiones. Así se escala (más servidores, más capacidad) y se gana disponibilidad (si uno cae, los demás siguen). La gracia está en cómo se reparte: por turnos, según la potencia de cada servidor, según cuántas conexiones tiene abiertas o, para que cada usuario caiga siempre en el mismo, según su IP.
La entrada tiene la configuración (algoritmo X y una línea servidor nombre peso por servidor), una línea --- y los sucesos en orden de tiempo (en segundos): t peticion IP duración, t caido servidor y t recuperado servidor. Tu programa dice a qué servidor va cada petición y, al final, cómo ha quedado el reparto.
Qué tiene que hacer el programa
- Configuración:
algoritmoesturno,ponderado,menos-conexionesohash-ip; cadaservidortiene un nombre que no se repite y un peso entero mayor que 0. Cualquier otra línea:Línea N: no se entiende «línea». Sin algoritmo o sin servidores:Hace falta un algoritmo y al menos un servidory nada más. Si no,Algoritmo: X · servidores: web1 (peso 3), web2 (peso 1). - Sucesos: una línea mal escrita (tiempo no entero, IP no válida, duración no entera mayor que 0, número de datos incorrecto) da
Línea N: no se entiende «línea»; un tiempo menor que el anterior,Línea N: el tiempo no puede ir hacia atrás; un servidor que no existe,Línea N: servidor desconocido «x». Antes de cada suceso, terminan las conexiones cuyo fin (t + duración) es igual o anterior a su tiempo. - Reparto, siempre entre los servidores disponibles:
turnosigue el orden de la configuración desde el siguiente al último elegido (saltando los caídos);ponderadoes el round robin ponderado suave de nginx (a cada disponible se le suma su peso a su contador, se elige el de contador mayor —el primero si empatan— y se le resta la suma de los pesos de los disponibles; los contadores vuelven a 0 cuando un servidor cae o vuelve);menos-conexioneselige el de menos conexiones abiertas (el primero si empatan); yhash-ip, el disponible númeroIP como entero % disponibles. - Cada petición escribe
t=T IP → servidor [web1:N web2:caído …]con las conexiones abiertas de todos tras asignarla, ot=T IP → 503 Service Unavailable (ningún servidor disponible).caidoyrecuperadoescribent=T x deja de responderot=T x vuelve a responder(t=T x ya estaba caídooya estaba disponiblesi no cambia nada); las conexiones abiertas de un servidor que cae terminan normalmente. - Al final,
Resumen:y, por servidor,nombre: N peticiones (P %), máximo M a la vez(1 petición; P sobre las peticiones atendidas, redondeado) ySin servicio (503): K.
Entrada
algoritmo X y una línea servidor nombre peso por servidor.
Una línea ---.
Los sucesos: t peticion IP duración, t caido servidor o t recuperado servidor.
Datos de referencia
| Algoritmo | Cómo elige | Para qué |
|---|---|---|
| turno | uno detrás de otro | servidores iguales y peticiones parecidas |
| ponderado | proporcional al peso, sin rachas | servidores de distinta potencia |
| menos-conexiones | el que tiene menos conexiones abiertas | peticiones de duración muy distinta |
| hash-ip | siempre el mismo para la misma IP | sesiones guardadas en el servidor |
Ejemplos de ejecución
Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.
Ponderado con una caída
Entrada
algoritmo ponderado servidor web1 3 servidor web2 1 servidor web3 1 --- 0 peticion 10.0.0.1 4 1 peticion 10.0.0.2 4 2 peticion 10.0.0.3 4 3 peticion 10.0.0.4 4 4 peticion 10.0.0.5 4 5 caido web1 5 peticion 10.0.0.6 2 6 peticion 10.0.0.7 2 7 recuperado web1 8 peticion 10.0.0.8 2
Salida por consola
Algoritmo: ponderado · servidores: web1 (peso 3), web2 (peso 1), web3 (peso 1) t=0 10.0.0.1 → web1 [web1:1 web2:0 web3:0] t=1 10.0.0.2 → web2 [web1:1 web2:1 web3:0] t=2 10.0.0.3 → web1 [web1:2 web2:1 web3:0] t=3 10.0.0.4 → web3 [web1:2 web2:1 web3:1] t=4 10.0.0.5 → web1 [web1:2 web2:1 web3:1] t=5 web1 deja de responder t=5 10.0.0.6 → web2 [web1:caído web2:1 web3:1] t=6 10.0.0.7 → web3 [web1:caído web2:1 web3:2] t=7 web1 vuelve a responder t=8 10.0.0.8 → web1 [web1:1 web2:0 web3:0] Resumen: web1: 4 peticiones (50 %), máximo 2 a la vez web2: 2 peticiones (25 %), máximo 1 a la vez web3: 2 peticiones (25 %), máximo 2 a la vez Sin servicio (503): 0
Menos conexiones, errores y sin servicio
Entrada
algoritmo menos-conexiones servidor app1 1 servidor app2 1 servidor app1 2 balanceo rapido --- 0 peticion 192.168.1.10 10 0 peticion 192.168.1.11 2 1 peticion 192.168.1.12 3 3 peticion 192.168.1.13 1 2 peticion 192.168.1.14 1 4 caido app1 4 caido app2 4 peticion 192.168.1.15 1 5 caido app3 6 recuperado app2 6 recuperado app2 6 peticion 192.168.1.300 1 7 peticion 192.168.1.16 8 peticion 192.168.1.16 1
Salida por consola
Línea 4: no se entiende «servidor app1 2» Línea 5: no se entiende «balanceo rapido» Algoritmo: menos-conexiones · servidores: app1 (peso 1), app2 (peso 1) t=0 192.168.1.10 → app1 [app1:1 app2:0] t=0 192.168.1.11 → app2 [app1:1 app2:1] t=1 192.168.1.12 → app1 [app1:2 app2:1] t=3 192.168.1.13 → app2 [app1:2 app2:1] Línea 11: el tiempo no puede ir hacia atrás t=4 app1 deja de responder t=4 app2 deja de responder t=4 192.168.1.15 → 503 Service Unavailable (ningún servidor disponible) Línea 15: servidor desconocido «app3» t=6 app2 vuelve a responder t=6 app2 ya estaba disponible Línea 18: no se entiende «6 peticion 192.168.1.300 1» Línea 19: no se entiende «7 peticion 192.168.1.16» t=8 192.168.1.16 → app2 [app1:caído app2:1] Resumen: app1: 2 peticiones (40 %), máximo 2 a la vez app2: 3 peticiones (60 %), máximo 1 a la vez Sin servicio (503): 1
Guía paso a paso
Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.
1. El estado del balanceador
Una clase con, por servidor, la lista de finales de sus conexiones abiertas: liberar es filtrar los que ya pasaron, y las conexiones abiertas son la longitud de la lista.
2. El ponderado suave
Con pesos 3, 1 y 1, el round robin ponderado ingenuo daría web1, web1, web1, web2, web3; el suave intercala: web1, web2, web1, web3, web1. Son tres líneas por elección.
for s, p in vivos:
self.actual[s] += p
elegido = max(vivos, key=lambda sp: self.actual[sp[0]])[0]
self.actual[elegido] -= total3. Hash de la IP
Convierte la IP en un número (cada octeto es una cifra en base 256) y quédate con el resto entre el número de disponibles: la misma IP cae siempre en el mismo servidor... mientras no cambien los disponibles.
4. Las caídas
Un servidor caído no recibe peticiones, pero lo que ya tenía termina. Si no queda ninguno, el balanceador responde 503 él mismo.
Resuélvelo aquí
El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 3 casos ocultos que buscan los errores típicos.
Ejemplo
algoritmo ponderado servidor web1 3 servidor web2 1 servidor web3 1 --- 0 peticion 10.0.0.1 4 1 peticion 10.0.0.2 4 2 peticion 10.0.0.3 4 3 peticion 10.0.0.4 4 4 peticion 10.0.0.5 4 5 caido web1 5 peticion 10.0.0.6 2 6 peticion 10.0.0.7 2 7 recuperado web1 8 peticion 10.0.0.8 2
Algoritmo: ponderado · servidores: web1 (peso 3), web2 (peso 1), web3 (peso 1) t=0 10.0.0.1 → web1 [web1:1 web2:0 web3:0] t=1 10.0.0.2 → web2 [web1:1 web2:1 web3:0] t=2 10.0.0.3 → web1 [web1:2 web2:1 web3:0] t=3 10.0.0.4 → web3 [web1:2 web2:1 web3:1] t=4 10.0.0.5 → web1 [web1:2 web2:1 web3:1] t=5 web1 deja de responder t=5 10.0.0.6 → web2 [web1:caído web2:1 web3:1] t=6 10.0.0.7 → web3 [web1:caído web2:1 web3:2] t=7 web1 vuelve a responder t=8 10.0.0.8 → web1 [web1:1 web2:0 web3:0] Resumen: web1: 4 peticiones (50 %), máximo 2 a la vez web2: 2 peticiones (25 %), máximo 1 a la vez web3: 2 peticiones (25 %), máximo 2 a la vez Sin servicio (503): 0
Solución explicada
Ver la solución completa
1import sys
2
3ALGORITMOS = ("turno", "ponderado", "menos-conexiones", "hash-ip")
4
5
6def ip_a_entero(ip):
7 """'10.0.0.5' → 167772165, o None si no es una IPv4 válida."""
8 partes = ip.split(".")
9 if len(partes) != 4 or not all(p.isdigit() and int(p) <= 255 for p in partes):
10 return None
11 n = 0
12 for p in partes:
13 n = n * 256 + int(p)
14 return n
15
16class Balanceador:
17 def __init__(self, algoritmo, servidores):
18 self.algoritmo = algoritmo
19 self.servidores = servidores # [(nombre, peso)] en el orden de la configuración
20 self.caidos = set()
21 self.activas = {s: [] for s, _ in servidores} # fin de cada conexión abierta
22 self.servidas = {s: 0 for s, _ in servidores}
23 self.maximo = {s: 0 for s, _ in servidores}
24 self.turno = 0
25 self.actual = {s: 0 for s, _ in servidores} # contadores del ponderado suave
26 self.sin_servicio = 0
27
28 def disponibles(self):
29 return [(s, p) for s, p in self.servidores if s not in self.caidos]
30
31 def liberar(self, t):
32 for s in self.activas:
33 self.activas[s] = [f for f in self.activas[s] if f > t]
34
35 def elegir(self, ip):
36 vivos = self.disponibles()
37 if not vivos:
38 return None
39 if self.algoritmo == "turno":
40 nombres = [s for s, _ in self.servidores]
41 for k in range(len(nombres)):
42 s = nombres[(self.turno + k) % len(nombres)]
43 if s not in self.caidos:
44 self.turno = (nombres.index(s) + 1) % len(nombres)
45 return s
46 if self.algoritmo == "ponderado":
47 # round robin ponderado suave (el de nginx): reparte sin rachas largas del mismo servidor
48 total = sum(p for _, p in vivos)
49 for s, p in vivos:
50 self.actual[s] += p
51 elegido = max(vivos, key=lambda sp: self.actual[sp[0]])[0]
52 self.actual[elegido] -= total
53 return elegido
54 if self.algoritmo == "menos-conexiones":
55 return min(vivos, key=lambda sp: len(self.activas[sp[0]]))[0]
56 return vivos[ip_a_entero(ip) % len(vivos)][0]
57
58 def estado(self):
59 return " ".join(f"{s}:{'caído' if s in self.caidos else len(self.activas[s])}" for s, _ in self.servidores)
60
61 def peticion(self, t, ip, duracion):
62 s = self.elegir(ip)
63 if s is None:
64 self.sin_servicio += 1
65 return f"t={t} {ip} → 503 Service Unavailable (ningún servidor disponible)"
66 self.activas[s].append(t + duracion)
67 self.servidas[s] += 1
68 self.maximo[s] = max(self.maximo[s], len(self.activas[s]))
69 return f"t={t} {ip} → {s} [{self.estado()}]"
70
71 def cambiar(self, t, nombre, caido):
72 if (nombre in self.caidos) == caido:
73 return f"t={t} {nombre} ya estaba {'caído' if caido else 'disponible'}"
74 if caido:
75 self.caidos.add(nombre)
76 else:
77 self.caidos.discard(nombre)
78 self.actual = {s: 0 for s, _ in self.servidores} # cambia el reparto: los contadores empiezan de cero
79 return f"t={t} {nombre} {'deja de responder' if caido else 'vuelve a responder'}"
80
81
82def main():
83 lineas = sys.stdin.read().splitlines()
84 algoritmo, servidores, i = None, [], 0
85 while i < len(lineas) and lineas[i].strip() != "---":
86 p = lineas[i].split()
87 if len(p) == 2 and p[0] == "algoritmo" and p[1] in ALGORITMOS:
88 algoritmo = p[1]
89 elif len(p) == 3 and p[0] == "servidor" and p[2].isdigit() and int(p[2]) > 0 and p[1] not in [s for s, _ in servidores]:
90 servidores.append((p[1], int(p[2])))
91 elif p:
92 print(f"Línea {i + 1}: no se entiende «{lineas[i].strip()}»")
93 i += 1
94 if algoritmo is None or not servidores:
95 print("Hace falta un algoritmo y al menos un servidor")
96 return
97 print(f"Algoritmo: {algoritmo} · servidores: " + ", ".join(f"{s} (peso {p})" for s, p in servidores))
98 b = Balanceador(algoritmo, servidores)
99 ultimo = 0
100 for num in range(i + 1, len(lineas)):
101 p = lineas[num].split()
102 if not p:
103 continue
104 valido = len(p) >= 3 and p[0].isdigit()
105 if valido and p[1] == "peticion":
106 valido = len(p) == 4 and ip_a_entero(p[2]) is not None and p[3].isdigit() and int(p[3]) > 0
107 elif valido and p[1] in ("caido", "recuperado"):
108 valido = len(p) == 3
109 else:
110 valido = False
111 if not valido:
112 print(f"Línea {num + 1}: no se entiende «{lineas[num].strip()}»")
113 continue
114 t = int(p[0])
115 if t < ultimo:
116 print(f"Línea {num + 1}: el tiempo no puede ir hacia atrás")
117 continue
118 ultimo = t
119 b.liberar(t)
120 if p[1] == "peticion":
121 print(b.peticion(t, p[2], int(p[3])))
122 elif p[2] not in b.activas:
123 print(f"Línea {num + 1}: servidor desconocido «{p[2]}»")
124 else:
125 print(b.cambiar(t, p[2], p[1] == "caido"))
126 total = sum(b.servidas.values())
127 print("Resumen:")
128 for s, _ in servidores:
129 n = b.servidas[s]
130 print(f" {s}: {n} {'petición' if n == 1 else 'peticiones'} ({round(n * 100 / total) if total else 0} %), máximo {b.maximo[s]} a la vez")
131 print(f" Sin servicio (503): {b.sin_servicio}")
132
133
134main()Round robin funciona bien si todas las peticiones cuestan lo mismo; si unas duran mucho más que otras, un servidor puede acumular trabajo mientras otro está libre, y ahí gana menos conexiones.
El hash de IP da afinidad de sesión: útil si la sesión se guarda en la memoria del servidor. Pero cuando un servidor cae, el módulo cambia y casi todos los usuarios cambian de servidor (pierden la sesión); el hash consistente reduce ese baile, y la solución de verdad es guardar las sesiones fuera (Redis, base de datos).
Las comprobaciones de salud (health checks) son las que marcan un servidor como caído: el balanceador pide una URL cada pocos segundos y, tras varios fallos seguidos, lo saca del reparto. Sin ellas, la alta disponibilidad no existe.
El balanceador es a su vez un punto único de fallo: en producción se ponen dos, con una IP flotante que pasa de uno a otro (keepalived), o se usa el de la nube, que ya es redundante.
Para ir más allá
- Haz el ponderado de menos conexiones (conexiones divididas entre el peso).
- Implementa hash consistente con un anillo y compara cuántas IP cambian de servidor al caer uno.
- Simula los health checks: cada 5 segundos, un servidor que falla 3 veces seguidas queda caído.