En este tutorial, creamos una comparación práctica entre un sistema síncrono basado en RPC y una arquitectura asíncrona basada en eventos para comprender cómo se comportan los sistemas distribuidos reales bajo carga y falla. Simulamos servicios descendentes con latencia variable, condiciones de sobrecarga y errores transitorios, y luego impulsamos ambas arquitecturas utilizando patrones de tráfico en ráfagas. Al observar métricas como la latencia de cola, los reintentos, las fallas y las colas de mensajes no entregados, examinamos cómo el estrecho acoplamiento de RPC amplifica las fallas y cómo los diseños asíncronos impulsados por eventos intercambian consistencia inmediata por resiliencia. A lo largo del tutorial, nos centramos en mecanismos prácticos, reintentos, retrocesos exponenciales, disyuntores, mamparos y colas que los ingenieros utilizan para controlar fallas en cascada en los sistemas de producción. Consulta los CÓDIGOS COMPLETOS aquí.
volver xs2[f] + (xs2[c] – xs2[f]) * (k – f) @dataclass class Estadísticas: latencias_ms: lista = campo(default_factory=list) ok: int = 0 falla: int = 0 descartado: int = 0 reintentos: int = 0 tiempos de espera: int = 0 cb_open: int = 0 dlq: int = 0 def resumen(self, nombre): l = self.latencies_ms return { “nombre”: nombre, “ok”: self.ok, “fail”: self.fail, “dropped”: self.dropped, “retries”: self.retries, “timeouts”: self.timeouts, “cb_open”: self.cb_open, “dlq”: self.dlq, “lat_p50_ms”: round(pctl(l, 0.50), 2) si l else Ninguno, “lat_p95_ms”: round(pctl(l, 0.95), 2) if l else Ninguno, “lat_p99_ms”: round(pctl(l, 0.99), 2) if l else Ninguno, “lat_mean_ms”: round(statistics.mean(l), 2) if l else Ninguno, }
Definimos las utilidades principales y las estructuras de datos utilizadas a lo largo del tutorial. Establecemos ayudas de sincronización, cálculos de percentiles y un contenedor de métricas unificado para realizar un seguimiento de la latencia, los reintentos, las fallas y el comportamiento de cola. Nos brinda una forma consistente de medir y comparar RPC y ejecuciones basadas en eventos. Consulta los CÓDIGOS COMPLETOS aquí.
Modelamos el comportamiento de falla y las primitivas de resiliencia que dan forma a la estabilidad del sistema. Simulamos fallas y latencia sensibles a sobrecargas, e introducimos disyuntores, mamparos y retroceso exponencial para controlar los efectos en cascada. Estos componentes nos permiten experimentar con configuraciones de sistemas distribuidos seguras versus inseguras. Consulta los CÓDIGOS COMPLETOS aquí.
Implementamos la ruta RPC síncrona y su interacción con los servicios posteriores. Observamos cómo los tiempos de espera, los reintentos y la carga en vuelo afectan directamente la latencia y la propagación de fallas. También destaca cómo el acoplamiento estrecho en RPC puede amplificar los problemas transitorios en condiciones de tráfico intenso. Consulta los CÓDIGOS COMPLETOS aquí.
Construimos la canalización asincrónica basada en eventos utilizando una cola y consumidores en segundo plano. Procesamos eventos independientemente del envío de solicitudes, aplicamos lógica de reintento y enrutamos mensajes irrecuperables a una cola de mensajes no entregados. Demuestra cómo el desacoplamiento mejora la resiliencia al tiempo que introduce nuevas consideraciones operativas. Consulta los CÓDIGOS COMPLETOS aquí.
deshacerse = 0 mientras que deshacerse < total: n = min(ráfaga, total - deshacerse) para _ en el rango (n): reqs.append(rid) deshacerse += 1 await asyncio.sleep(gap_ms / 1000.0) return reqs async def main(): random.seed(7) fm = FailureModel() svc = DownstreamService(fm) ids = await generate_requests() rpc_stats = Estadísticas() cb = CircuitBreaker() masivo = Mamparo(40) espera asyncio.gather(*[ rpc_call(svc, {"id": i}, rpc_stats, max_retries=3, cb=cb, bulkhead=bulk) for i in ids ]) bus = EventBus() ev_stats = Estadísticas() parada = asyncio.Event() dlq = [] consumidores = [ asyncio.create_task(event_consumer(bus, svc, ev_stats, stop, max_retries=3, dlq=dlq)) for _ in range(16) ] para i en ids: await bus.publish(Event(i)) await bus.q.join() stop.set() para c en consumidores: c.cancel() print(rpc_stats.summary("RPC")) print(ev_stats.summary("EventDriven")) print("DLQ size:", len(dlq)) await main()
Impulsamos ambas arquitecturas con cargas de trabajo en ráfagas y organizamos el experimento completo. Recopilamos métricas, finalizamos limpiamente a los consumidores y comparamos resultados entre RPC y ejecuciones basadas en eventos. El paso final une la latencia, el rendimiento y el comportamiento de falla en una comparación coherente a nivel del sistema.
En conclusión, vimos claramente las ventajas y desventajas entre RPC y las arquitecturas basadas en eventos en sistemas distribuidos. Observamos que RPC ofrece una latencia más baja cuando las dependencias están en buen estado, pero se vuelve frágil cuando se satura, donde los reintentos y los tiempos de espera se convierten rápidamente en fallas en todo el sistema. Por el contrario, el enfoque basado en eventos desacopla a los productores de los consumidores, absorbe las ráfagas mediante el almacenamiento en búfer y localiza las fallas, pero requiere un manejo cuidadoso de los reintentos, la contrapresión y las colas de mensajes fallidos para evitar sobrecargas ocultas y colas ilimitadas. A través de este tutorial, demostramos que la resiliencia en los sistemas distribuidos no proviene de elegir una única arquitectura, sino de combinar el modelo de comunicación correcto con patrones disciplinados de manejo de fallas y un diseño consciente de la capacidad.
Consulta los CÓDIGOS COMPLETOS aquí. Además, no dude en seguirnos en Twitter y no olvide unirse a nuestro SubReddit de más de 100.000 ML y suscribirse a nuestro boletín. ¡Esperar! estas en telegrama? Ahora también puedes unirte a nosotros en Telegram.
Michal Sutter es un profesional de la ciencia de datos con una Maestría en Ciencias de Datos de la Universidad de Padua. Con una base sólida en análisis estadístico, aprendizaje automático e ingeniería de datos, Michal se destaca en transformar conjuntos de datos complejos en conocimientos prácticos.