Módulo 3: Métricas — latencia, throughput y errores

4. Throughput/RPS y su relación con los VUs

Descripción

El segundo instrumento del tablero es el throughput: cuántas peticiones por segundo procesa tu sistema, el famoso RPS (requests per second). Mientras la latencia mide la experiencia de una petición, el throughput mide el caudal del sistema entero: cuánto trabajo total despacha por unidad de tiempo. Es la métrica de la capacidad —"¿cuántos usuarios puede servir esta API?"— y la que casi todo el mundo cree entender hasta que la mide, porque su relación con los usuarios virtuales es más sutil de lo que parece. Esta lección la desarma: qué es el RPS, cómo lo reporta k6 (http_reqs, iterations), y las dos verdades contraintuitivas que solo se ven midiendo.

La primera verdad: subir VUs no sube el throughput indefinidamente. Hay una intuición natural —"si pongo el doble de usuarios virtuales, proceso el doble de peticiones por segundo"— y es falsa pasado cierto punto. Cuando el sistema satura (agota su recurso más escaso: CPU, hilos, conexiones), añadir más VUs ya no le saca más RPS; lo único que consigues es alargar la cola, o sea, subir la latencia. Lo verás con un barrido real de concurrencia sobre Reservo. La segunda verdad: el think time —la pausa que un usuario real hace entre acciones— es lo que convierte "N usuarios virtuales" en "una tasa de llegada realista". Sin think time, unos pocos VUs martillan la API a máxima velocidad y no se parecen en nada a usuarios humanos; con think time, N VUs producen un RPS predecible que puedes calcular con la ley de Little. También lo verás medido.

Conexión con el módulo: la latencia (lecciones 2-3) y el throughput (esta) son las dos caras de la carga —tiempo por petición vs peticiones por tiempo—, y están atadas: cuando el sistema satura, el throughput se aplana y la latencia se dispara, y verlo en una sola tabla es entender la carga. Aquí reusamos los VUs del módulo 2. Modelar cómo varía la carga en el tiempo (rampas, picos) es el módulo 4 —el siguiente—: aquí la concurrencia es fija en cada corrida y observamos el throughput que produce.

La caja del supermercado

Piensa en un supermercado con cajas de cobro. El throughput es cuántos clientes salen pagados por minuto —el caudal de la tienda—. La latencia es cuánto espera cada cliente concreto en la fila hasta que le cobran. Son cosas distintas: la tienda puede tener un throughput altísimo (cientos de clientes por minuto) mientras un cliente concreto espera veinte minutos, si las filas son largas.

Ahora, ¿qué pasa si quieres más throughput y abres más cajas? Al principio funciona de maravilla: con una caja despachas 6 clientes por minuto; con dos, 12; con tres, 18. El throughput sube casi en proporción a las cajas. Pero llega un momento en que abres la décima caja y... el throughput ya no sube. ¿Por qué? Porque el cuello de botella dejó de ser las cajas: ahora es el único pasillo por el que todos entran, o el sistema de pago central que se saturó, o simplemente que no hay tantos clientes esperando. Has llegado a la saturación. A partir de ahí, abrir más cajas (poner más cajeros parados sin clientes) no despacha más gente por minuto; solo gastas cajeros. Y si además metes a la fuerza más clientes de los que la tienda puede procesar, lo único que consigues es que las filas se alarguen —la latencia sube— sin que salga más gente por minuto.

Un VU es exactamente un cliente en la tienda que, apenas le cobran, vuelve al final de la fila a comprar otra vez. Sin think time, ese cliente compra a velocidad inhumana, sin parar. Con think time —una pausa de, digamos, cinco segundos entre compra y compra, mientras "mira los productos"— el cliente se parece a uno real, y el número de clientes por minuto que genera baja a algo predecible. El think time es lo que hace que "20 usuarios virtuales" signifique un tráfico realista y no una ametralladora.

El throughput (RPS) es el caudal del sistema: peticiones despachadas por segundo. Sube con los VUs solo hasta la saturación; pasado ese punto, más VUs solo alargan la cola (suben la latencia), no el throughput. El think time convierte N VUs en una tasa de llegada realista.

El RPS en k6: http_reqs e iterations

En el resumen de k6, el throughput aparece en dos métricas hermanas:

Métrica de k6Qué cuentaCómo se reporta
http_reqsNúmero total de peticiones HTTP que k6 generó.Un total y una tasa: 2000 1517.6/s.
iterationsCuántas veces los VUs ejecutaron la función default (el script completo).Igual: total y tasa /s.

La diferencia importa. http_reqs cuenta peticiones; iterations cuenta ejecuciones del script. Si tu función default hace una petición (un http.post a /quote), los dos números coinciden. Pero si hace tres peticiones por iteración (un flujo /rooms/quote/book, como el del módulo 6), entonces http_reqs será tres veces iterations. La tasa que suele reportarse como "RPS" es la de http_reqs (peticiones por segundo); la de iterations es útil cuando piensas en usuarios completando un flujo por segundo. En nuestro generador de Python, como cada tarea hace una sola petición, el RPS es simplemente total_peticiones / tiempo_de_reloj.

Ejemplo trabajado 1: el barrido de concurrencia (dónde satura Reservo)

Vamos a ver la primera verdad medida: subir VUs sube el RPS solo hasta que el sistema satura. Corremos el mismo /quote, con 2000 peticiones, subiendo la concurrencia: 1, 5, 15, 30, 60 clientes. Para cada nivel medimos el RPS y el p95 de latencia, lado a lado.

import json, statistics, time, urllib.request
from concurrent.futures import ThreadPoolExecutor

def sweep(port, concurrency, total=2000):
    url = f"http://127.0.0.1:{port}/quote"
    payload = json.dumps({"room": "Focus", "tier": "basic", "hours": 3}).encode()

    def one(_):
        s = time.perf_counter()
        req = urllib.request.Request(url, data=payload,
                                     headers={"Content-Type": "application/json"})
        with urllib.request.urlopen(req, timeout=10) as r:
            r.read()
        return (time.perf_counter() - s) * 1000

    t0 = time.perf_counter()
    with ThreadPoolExecutor(max_workers=concurrency) as pool:
        latencies = sorted(pool.map(one, range(total)))
    wall = time.perf_counter() - t0
    rps = total / wall
    p95 = statistics.quantiles(latencies, n=100, method="inclusive")[94]
    print(f"concurrency {concurrency:3d}  ->  RPS {rps:8.1f}   "
          f"avg {statistics.fmean(latencies):6.2f} ms   p95 {p95:7.2f} ms")

for c in (1, 5, 15, 30, 60):
    sweep(PORT, c)

Qué esperar. El RPS sube de 1 a 5 clientes (la API tenía capacidad ociosa), pero enseguida se aplana alrededor de un techo —el throughput máximo de Reservo en esta máquina— mientras la latencia (avg y p95) sigue subiendo. Esta es la salida real:

concurrency   1  ->  RPS   4212.6   avg   0.23 ms   p95    0.29 ms
concurrency   5  ->  RPS   5770.5   avg   0.86 ms   p95    1.25 ms
concurrency  15  ->  RPS   5465.7   avg   2.73 ms   p95    3.98 ms
concurrency  30  ->  RPS   5234.2   avg   5.69 ms   p95    9.08 ms
concurrency  60  ->  RPS   5134.5   avg  11.50 ms   p95   19.17 ms

Lee la tabla como una sola historia. De 1 a 5 clientes, el RPS sube (4212 → 5770): había capacidad libre. A partir de ahí, el RPS deja de subir —se queda pegado entre 5100 y 5800, que es el techo de Reservo aquí— por mucho que sigas añadiendo clientes. Y mientras el throughput se aplana, mira la latencia: el p95 pasa de 0.29 ms (1 cliente) a 19.17 ms (60 clientes), multiplicándose por ~66. Esa es la saturación en carne viva: pasado el punto en que el sistema da todo lo que puede, cada VU extra no compra throughput; compra latencia. Los clientes de más no salen más rápido por la caja; solo hacen la fila más larga.

Esto tiene una consecuencia práctica enorme. Si alguien te dice "sube los VUs hasta llegar a 10.000 RPS" y tu sistema satura en 5.500, no hay número de VUs que lo logre: lo único que conseguirás subiendo VUs es una latencia por las nubes y (pronto) errores. El techo de throughput es una propiedad del sistema, no del test. Encontrar ese techo —el punto donde el RPS se aplana y la latencia despega— es, de hecho, el objetivo de un stress test (que viste nombrado en el módulo 1 y modelarás en el 4).

Ejemplo trabajado 2: el think time y la ley de Little

Ahora la segunda verdad: el think time convierte N VUs en una tasa de llegada predecible. Aquí cada VU hace su petición y luego duerme un tiempo fijo (el think time) antes de la siguiente, imitando a un humano que lee la pantalla entre clics. Fijamos 20 VUs durante 3 segundos y variamos el think time: 0, 0.05, 0.1 y 0.2 segundos.

La teoría que vamos a confirmar es la ley de Little aplicada a carga: el RPS que producen N VUs es, aproximadamente,

RPS ≈ VUs / (latencia_de_servicio + think_time)

La intuición: cada VU completa un ciclo cada (latencia + think_time) segundos, así que hace 1 / (latencia + think_time) peticiones por segundo, y con N VUs multiplicas por N. Si la latencia de servicio de Reservo es ~5 ms (0.005 s), el modelo predice el RPS para cada think time.

import json, time, threading, urllib.request

def think_run(port, think, vus=20, duration=3.0):
    url = f"http://127.0.0.1:{port}/quote"
    payload = json.dumps({"room": "Focus", "tier": "basic", "hours": 3}).encode()
    count = [0]; lock = threading.Lock(); stop_at = time.perf_counter() + duration

    def vu():
        while time.perf_counter() < stop_at:
            req = urllib.request.Request(url, data=payload,
                                         headers={"Content-Type": "application/json"})
            with urllib.request.urlopen(req, timeout=10) as r:
                r.read()
            with lock:
                count[0] += 1
            if think:
                time.sleep(think)   # el think time: el VU "piensa" entre peticiones

    t0 = time.perf_counter()
    threads = [threading.Thread(target=vu) for _ in range(vus)]
    for t in threads: t.start()
    for t in threads: t.join()
    wall = time.perf_counter() - t0
    rps = count[0] / wall
    model = vus / (0.005 + think)   # ley de Little, con latencia de servicio ~5 ms
    print(f"think {think:4.2f}s  ->  RPS medido {rps:8.1f}   (modelo ~{model:7.1f})")

for think in (0.0, 0.05, 0.10, 0.20):
    think_run(PORT, think)

Qué esperar. Sin think time, 20 VUs martillan la API a máxima velocidad y el RPS es enorme (miles). En cuanto añades think time, el RPS cae a algo modesto y predecible por la ley de Little: cuanto mayor el think time, menor el RPS. Esta es la salida real:

think 0.00s  ->  RPS medido   5492.8   (modelo ~ 4000.0)
think 0.05s  ->  RPS medido    342.2   (modelo ~  363.6)
think 0.10s  ->  RPS medido    182.0   (modelo ~  190.5)
think 0.20s  ->  RPS medido     94.9   (modelo ~   97.6)

Mira las tres filas con think time: el modelo predice casi bordado el RPS medido (342 vs 364, 182 vs 190, 95 vs 98). El think time domina el denominador —0.05 s de think contra ~0.005 s de latencia—, así que cada VU hace ~1 petición cada 0.055 s, y 20 VUs dan ~360 RPS. Esa es la utilidad enorme del think time: te deja razonar sobre cuántos VUs necesitas para simular un tráfico dado. ¿Quieres 1000 RPS con usuarios que piensan 1 segundo entre acciones? Necesitas ~1000 VUs (1000 × (0.005 + 1) ≈ 1005). Sin think time, esa cuenta no existe: los VUs corren a la velocidad de la máquina.

Y fíjate en la primera fila, la del think time 0: el RPS medido (5492) es mayor que el modelo (4000). ¿Por qué falla ahí el modelo? Porque sin think time los 20 VUs saturan la API, y a plena saturación la latencia de servicio real ya no es los 0.005 s que asumimos —es menor, porque el sistema está a tope y sirve en lotes—. Es un recordatorio de la primera verdad: sin think time, la carga entra en régimen de saturación y el throughput lo dicta el techo del sistema, no una fórmula limpia. El modelo de Little describe bien el régimen realista (con pausas humanas), no el de la ametralladora.

Cómo se leen juntos throughput y latencia

La lección que atraviesa las dos verdades es que el throughput no se lee solo. Un RPS alto es bueno solo si la latencia bajo ese RPS es aceptable. En el barrido, Reservo daba ~5200 RPS tanto con 30 clientes (p95 = 9 ms) como con 60 (p95 = 19 ms): el mismo throughput, pero el doble de latencia. Si alguien reporta "aguantamos 5200 RPS" sin decir el p95, oculta la mitad de la historia —a 60 clientes ese throughput viene con una latencia que quizás ya sea inaceptable—. Por eso, en cualquier reporte de carga, el RPS va siempre acompañado de la latencia (percentiles) a la que se alcanzó, y de la tasa de error (la lección que sigue). Los tres instrumentos, juntos.

Errores comunes

Creer que más VUs siempre dan más RPS. Qué pasa: el sistema satura en 5.500 RPS y alguien sube de 60 a 600 VUs esperando 10× el throughput; obtiene el mismo RPS con una latencia atroz y, pronto, errores. Por qué pasa: se confunde la carga que generas (VUs) con la que el sistema procesa (RPS). Cómo detectarlo: si al subir VUs el RPS se aplana pero la latencia se dispara, saturaste. Cómo corregirlo: entiende que el techo de throughput es del sistema; pasado ese punto, más VUs solo compran cola. Para subir el techo hay que optimizar el sistema (otra guía), no el test.

Cargar sin think time y creer que es realista. Qué pasa: alguien lanza 50 VUs sin sleep y concluye "mi API aguanta 50 usuarios", cuando en realidad simuló 50 ametralladoras que generan un tráfico que ningún humano produciría. Por qué pasa: se equipara "VU" con "usuario", pero un usuario real piensa entre clics. Cómo detectarlo: si tus VUs no tienen think time, tu RPS por VU es irrealmente alto y tu número de "usuarios soportados" es pesimista y sin sentido. Cómo corregirlo: añade think time realista (sleep) para que N VUs produzcan una tasa de llegada humana, y usa la ley de Little para dimensionar cuántos VUs necesitas.

Reportar el RPS sin la latencia a la que se alcanzó. Qué pasa: "aguantamos 5200 RPS" suena a titular de éxito, pero a esa carga el p95 era de 800 ms. Por qué pasa: el throughput es la métrica vistosa de capacidad y se reporta sola. Cómo detectarlo: si tu número de RPS no viene con un p95 al lado, falta contexto. Cómo corregirlo: reporta siempre "X RPS con p95 de Y ms y Z% de error". Un RPS sin su latencia y su tasa de error no dice si el sistema estaba sano o agonizando.

Ejercicios

Ejercicio 1 — http_reqs vs iterations. Un script de k6 tiene una función default que hace un GET /rooms, luego un POST /quote y luego un POST /book. La corrida reporta iterations: 2000. (a) ¿Cuánto vale http_reqs? (b) Si la corrida duró 4 segundos, ¿cuál es el RPS (peticiones/s) y cuál la tasa de iteraciones/s? (c) ¿Cuál de las dos usarías para decir "usuarios que completan el flujo por segundo"?

Ver solución
  • (a) Cada iteración hace 3 peticiones (/rooms, /quote, /book), así que http_reqs = 2000 × 3 = 6000.
  • (b) RPS (peticiones/s) = 6000 / 4 = 1500 req/s. Tasa de iteraciones = 2000 / 4 = 500 iteraciones/s.
  • (c) La de iteraciones (500/s): cada iteración es un usuario completando el flujo entero cotizar→reservar. http_reqs cuenta peticiones sueltas, no flujos completos.

Ejercicio 2 — ¿Saturó? Un barrido da estos resultados. Concurrencia 10 → 3000 RPS, p95 12 ms. Concurrencia 40 → 4900 RPS, p95 30 ms. Concurrencia 160 → 5000 RPS, p95 220 ms. (a) ¿En qué punto satura el sistema, aproximadamente? (b) ¿Qué compraste al pasar de 40 a 160 VUs? (c) ¿Tiene sentido subir a 640 VUs para "llegar a 8000 RPS"?

Ver solución
  • (a) Entre 40 y 160 VUs: el RPS pasa de 4900 a 5000 (prácticamente igual) mientras la concurrencia se cuadriplica. El techo de throughput está alrededor de 5000 RPS.
  • (b) Nada de throughput (4900 → 5000 es ruido) y muchísima latencia: el p95 se multiplicó por ~7 (30 → 220 ms). Pasado el punto de saturación, los VUs extra solo alargan la cola.
  • (c) No. El sistema satura en ~5000 RPS; ningún número de VUs lo llevará a 8000. Subir a 640 VUs solo dispararía la latencia (y pronto los errores). Para 8000 RPS hay que optimizar el sistema, no añadir carga.

Ejercicio 3 — Dimensiona con la ley de Little. Quieres simular un tráfico donde los usuarios piensan 2 segundos entre acciones, y la latencia de servicio de tu API es ~10 ms (0.01 s). (a) ¿Cuántas peticiones por segundo genera un VU? (b) ¿Cuántos VUs necesitas para ~300 RPS? (c) Si quitaras el think time, ¿el número de VUs de (b) daría 300 RPS?

Ver solución
  • (a) Un VU completa un ciclo cada 0.01 + 2 = 2.01 s, así que genera 1 / 2.01 ≈ 0.498 peticiones/s (casi media petición por segundo).
  • (b) VUs ≈ RPS × (latencia + think) = 300 × 2.01 ≈ 603 VUs. (O, equivalentemente, 300 / 0.498 ≈ 603.) Necesitas ~600 VUs.
  • (c) No, daría muchísimo más (o saturaría). Sin think time, cada VU haría ~1 / 0.01 = 100 peticiones/s, y 600 VUs pedirían ~60.000 RPS —muy por encima de lo que el sistema aguanta—: saturaría y la latencia explotaría. El think time es lo que hace que 600 VUs signifiquen 300 RPS realistas.

Resumen y siguiente paso

En esta lección abriste el segundo instrumento: el throughput (RPS), el caudal del sistema. Aprendiste que en k6 vive en http_reqs (peticiones totales y por segundo) y en iterations (ejecuciones del script), y que coinciden solo si cada iteración hace una petición. Y mediste las dos verdades contraintuitivas. La primera: subir VUs sube el RPS solo hasta la saturación; en el barrido, Reservo se aplanó en ~5200 RPS mientras el p95 pasaba de 0.29 ms a 19 ms —pasado el techo, cada VU compra latencia, no throughput—. La segunda: el think time convierte N VUs en una tasa realista, predecible con la ley de Little (RPS ≈ VUs / (latencia + think)), que ajustó los datos casi bordados (182 medidos vs 190 del modelo).

La lección de fondo es que el throughput no se lee solo: un RPS alto vale solo si la latencia y la tasa de error a ese RPS son aceptables. Antes de avanzar deberías poder: distinguir http_reqs de iterations; explicar qué es la saturación y por qué más VUs no la superan; y usar la ley de Little para dimensionar cuántos VUs necesitas para un RPS objetivo con un think time dado.

Lo que sigue es el tercer y último instrumento. En la lección 5 entra la tasa de error (http_req_failed): qué cuenta como fallo, y por qué una latencia bajísima con 10% de errores es un fracaso rotundo —lo verás medido con el endpoint flaky declarado, que responde rapidísimo pero devuelve 500 una de cada diez veces—.

Recursos