Simulación de Eventos Discretos · Tema 49

Aplicaciones en servidores y redes

Requests, workers y latencia — Little dimensiona hilos, Erlang el pool y el timeout doma la cola del p99.

01 · Punto de partida

El banco corre en un data center

Tu API recibe 200 req/s que tardan 50 ms: hay 10 requests adentro (Little) y con 12 workers vas al 83%. El p95 pide 100 ms de cola; un downstream lento mete gigantes; un reintento agresivo multiplica λ justo en el pico. Misma teoría, otras unidades: rps, ms y workers.

Aplicarla acá es medir por endpoint, dimensionar pools con Little+Erlang, balancear al menos cargado y poner timeout con backoff antes que escalar a ciegas.

  • ¿Cuántos workers/hilos/conexiones para mi rps y latencia?
  • ¿Media o p95: qué SLA firmo?
  • ¿Cómo balanceo y cómo escalo (vertical vs horizontal)?
  • ¿Qué hacen timeout y reintentos con λ?

02 · Definición

Throughput, latencia y concurrencia

λ

Throughput

req/s que llegan (rps) por endpoint y ventana. Con picos 10×: se dimensiona por pico +aiman? por pico.

W

Latencia

W = Wq (cola) + S (cómputo/DB). El SLA se firma en p95/p99, no en media.

L

Concurrencia

L = λW requests adentro = workers ocupados. El número que dimensiona pools e hilos.

Tres backends con su cuenta (M/M/c).
Servicioλ × Sc y ρLectura
API 200 r/s × 50 msa = 1012 → 0.83Wq ≈ 11 ms, p95 de cola ≈ 100 ms
DB 500 r/s × 20 msa = 10pool 16 → 0.63Conexiones = L + margen
Worker 10 r/s × 2 sa = 2024 → 0.83Cola de mensajes con 24 consumers

03 · El corazón: la receta backend

Medir, dimensionar, balancear, domar, escalar

medir p95→Little + Erlang→balancear→timeout+retry→autoscaling
  1. 1
    Medir por endpoint y ventana.

    rps, p50/p95/p99 de S y W, por minuto. La media esconde el endpoint gigante que traba el pool.

  2. 2
    Dimensionar pools con Little+Erlang.

    Conexiones e hilos = λW + margen; c con P(Wq ≤ t) del SLA. El pool chico es K finita con fuga (Tema 27).

  3. 3
    Balancear al menos cargado.

    Least-connections / primero libre = fila única (pooling, Tema 29). Round-robin a ciegas parte el pooling.

  4. 4
    Timeout + backoff con jitter.

    Timeout acota S (corta gigantes, baja Cs²); reintento suma λextra = λ·p/(1−p): con p = 20% es +25% de carga fantasma.

  5. 5
    Autoscaling por ρ/latencia.

    Escalar al acercarse al codo (Tema 44), no en la pared; con headroom N+1. Escalar por CPU sola ignora la cola.

Tail latency
El p99 manda en cascada: con 10 llamadas en serie, P(alguna lenta) ≈ 10%. Doma gigantes (timeout) antes que promedios.
Retry storm
Timeouts sincronizados reintentan a la vez: λ se multiplica en el peor momento (ρ 0.83 → 1.25). Backoff + jitter la desarman.
Vertical vs horizontal
Subir μ (más CPU/rápido) baja W directo y Wq doble (Tema 36); sumar c (réplicas) aplana la curva. Se combinan por costo.
Health-checks
Sacar nodos enfermos baja c justo en el pico: el autoscaling debe reponer antes de declarar victoria.

Escalar workers (c)

Aplana

Más manos absorben rachas (pooling). Cuesta réplicas; rinde con retornos decrecientes.

Recortar latencia (S)

Acelera doble

Caché, índice, timeout: baja W directo y derrite Wq (doble dividendo). Suele rendir antes que el puesto.

04 · Ejemplos

Cuatro backends con número

  1. 1
    API 200 r/s × 50 ms, 12 workers.

    a = 10, ρ = 0.83: Wq ≈ 11 ms, P(cola ≤ 100 ms) ≈ 99%. Headroom justo.

  2. 2
    Pool DB 500 r/s × 20 ms.

    L = 10 conexiones ocupadas: pool 16 (ρ = 0.63) con margen para picos ×1.5.

  3. 3
    Consumers 10 r/s × 2 s.

    a = 20: 24 consumers (ρ = 0.83). El lag de la cola es Lq visible.

  4. 4
    Retry storm del downstream.

    p = 30% timeouts: λef ≈ 1.39×λ (0.83 → 1.15): de tenso a volteado. Backoff ya.

05 · Representación en Python

Pool por Little y carga fantasma por reintentos

Unidades rps y segundos: los mismos números del curso con otros nombres.

Python en tu navegador. El primero dimensiona el pool; el segundo multiplica λ por reintentos. Cambiá p y mirá la tormenta.

# Pool por Little + margen: lam rps, S segundos
def pool(lam, S, margen=1.5):
    return {"ocupados": lam*S, "pool": round(lam*S*margen)}

print(pool(200, 0.05))   # 10 ocupados -> pool 15
print(pool(500, 0.02))   # 10 ocupados -> pool 15
# Carga fantasma: p timeout, hasta 3 intentos
def carga_fantasma(p, k=3):
    return sum(p**i for i in range(k))

for p in [0.05, 0.2, 0.3]:
    print(f"p={p:.0%} -> x{carga_fantasma(p):.2f} (rho 0.83 -> {0.83*carga_fantasma(p):.2f})")

06 · Exploración

Laboratorio: planificar capacidad + tormenta

Elegí λ (rps), S (ms) y workers c. La curva es Wq(λ) exacta (Erlang) con tu c; el punto es tu operación; la recta es el SLA de cola (100 ms). El interruptor de tormenta multiplica λ ×1.5 (reintentos): mirá cómo el punto se muda a la pared.

EXPERIMENTO 49

Capacidad y tormenta de reintentos

Wq(λ) Erlang · SLA cola ≤100 ms

Los resultados numéricos aparecen debajo.
ρ (sin tormenta)0
Concurrencia L0
Wq operativa0 ms
P(cola ≤ 100 ms)0%

Activá la tormenta para voltearlo.

Curva lima = Wq(λ); punto = tu λ (naranja con tormenta ×1.5); recta = SLA 100 ms. La tormenta corre el punto, no la curva.

Preguntas para explorar

  1. Default (200 rps, 50 ms, 12): ¿ρ? ¿L? ¿Wq y P(≤100 ms)?
  2. Con tormenta (λ × 1.5 = 300): ¿ρ? ¿Qué pasa con Wq?
  3. Subí a c=16 sin tormenta: ¿ρ y Wq? ¿Y con tormenta?
Ver respuestas sugeridas
  1. ρ ≈ 0.83, L ≈ 12, Wq ≈ 11 ms, P ≈ 99%: tenso pero dentro del SLA.
  2. ρ ≈ 1.25: avalancha (sin punto). La tormenta voltea lo que andaba.
  3. Sin tormenta ρ ≈ 0.63, Wq ≈ 0.5 ms; con tormenta ρ ≈ 0.94, Wq ≈ 37 ms (P ≈ 90%): el +4 absorbe la tormenta al borde del SLA.

07 · Comprensión

Confusiones frecuentes

«La latencia media alcanza para el SLA»

El SLA se firma en p95/p99: media 20 ms con p99 800 ms rompe contratos. Medir percentiles por endpoint.

«Hilos = rps pico»

Hilos = rps × latencia (Little): 200 r/s × 0.05 s = 10, no 200. Sin ×W se sobredimensiona ×20.

«Reintentar es gratis»

Cada reintento es λ extra con el sistema ya lento: p = 30% suma +43% de carga. Backoff + jitter o nada.

«Escalo por CPU»

CPU 60% con Wq disparada es cola, no cómputo: escalar por latencia/ρ (o por L = λW), no por CPU sola.

08 · Práctica guiada

Ejercicios con Python

Ejercicio 1: pool DB

λ=500 r/s, S=20 ms, margen 1.5. ¿Ocupadas y pool?

# 10 ocupadas -> pool 15 (ρ=0.63 con 16)
print(pool(500, 0.02))
Ver solución razonada

L = 10, pool 15–16: el margen absorbe picos ×1.5 sin encolar conexiones.

Ejercicio 2: tormenta numérica

ρ=0.83 con p=30% y 3 intentos. ¿ρ efectiva?

# x1.39 -> rho 1.15: voltea (el lab muestra 1.25 con p≈33%)
print(f"{0.83*sum(0.3**i for i in range(3)):.2f}")
Ver solución

≈ 1.15: la tormenta convierte lo tenso en avalancha. De ahí timeout + backoff primero.

Ejercicio 3: vertical vs horizontal

S=50→25 ms (caché) vs c=12→24 con λ=200. ¿ρ en cada caso?

Ver una posible respuesta
# caché: a=5, rho=0.42; workers: rho=0.42 también. Empate en rho...
print("...pero caché además baja W a la mitad: doble dividendo")

Empate en ρ (0.42), gana caché en W: recortar S paga dos veces (Tema 36).

09 · Síntesis

Ideas para recordar

  • L = λW dimensiona hilos/pools; ρ = λS/c el margen; p95 el SLA.
  • Balanceador = fila única; timeout acota S; retry suma λ (×1+p+…).
  • En Python: pool() + carga_fantasma() antes de pedir máquinas.
  • Autoscaling por latencia/ρ al codo, con N+1.
  • Tail latency manda en cascada: domar gigantes primero.

En el próximo tema bajamos a planta: producción y logística.