Módulo 7: Analizar resultados y CI

2. Leer el resumen y la métrica de tendencia

Descripción

Cuando una prueba de carga termina, escupe un resumen: un bloque de números que condensa miles de peticiones en unas pocas líneas. Saber leerlo es la primera habilidad de este módulo, porque un resumen mal leído lleva a conclusiones falsas —"el promedio fue 10 ms, ¡vuela!"— que esconden un problema real en la cola. En esta lección aprendes a leer el resumen con las tres preguntas que importan (¿el p95 está dentro del SLO? ¿la tasa de error cruzó el límite? ¿el RPS que aguantó alcanza para el tráfico esperado?) y conoces el tipo de métrica que hace posible ese resumen: la métrica de tendencia (Trend), que toma una serie entera de valores —una latencia por petición— y la resume en percentiles (avg, min, med, max, p90, p95). Del lado ejecutable, un pequeño analizador en Python lee las métricas de una corrida real y las juzga contra un SLO, mostrando en la práctica qué significa "leer el resumen con criterio".

Conexión con el módulo: esta es la primera pieza de analizar. Usa las métricas que aprendiste a medir en el módulo 3 (p95, RPS, tasa de error) y ahora las lee e interpreta contra un estándar. No re-explica qué es un percentil —eso fue M3—; enseña a leer el retrato que forman todos juntos. Lo que sigue es exportar ese resumen a un archivo (lección 3) para poder analizarlo fuera y compararlo con otras corridas. La métrica de tendencia que se presenta aquí es la que después exportarás y compararás.

El boletín de calificaciones, no la lista de todas las tareas

Imagina que al final del semestre te entregaran, en vez de un boletín, la lista completa de las 4.000 respuestas que diste en todos los exámenes del año. Técnicamente está toda la información ahí, pero es inservible: nadie puede leer 4.000 respuestas y sacar una conclusión. Por eso existe el boletín: resume esas 4.000 respuestas en unas pocas cifras con significado —el promedio, la nota más baja, la más alta, en qué percentil quedaste—. El boletín no te da menos información útil que la lista cruda; te da más, porque la vuelve legible.

El resumen de una prueba de carga es ese boletín. Detrás de él hay miles de latencias individuales —una por cada petición—, imposibles de leer una por una. La métrica de tendencia es la que hace el boletín: agarra la serie completa de latencias y la resume en avg/min/med/max/p90/p95. Y como en un boletín escolar, la cifra que más importa no es siempre el promedio: un alumno con promedio 8 pero que reprobó el examen final (su "p95") tiene un problema que el promedio esconde. Leer el resumen es saber a qué cifra mirar.

Qué es una métrica de tendencia (Trend)

k6 clasifica sus métricas en cuatro tipos, y conviene conocerlos porque cada uno responde una pregunta distinta:

  • Trend (tendencia): resume una serie de valores en estadísticos —avg, min, med, max, y percentiles como p(90), p(95), p(99)—. Es el tipo de http_req_duration (la latencia). Cada petición aporta un valor a la serie; el Trend los resume. Es el tipo que te importa para latencia.
  • Counter (contador): suma. Cuenta cuántas veces pasó algo. Es el tipo de http_reqs (total de peticiones).
  • Rate (tasa): el porcentaje de veces que algo fue verdadero. Es el tipo de http_req_failed (fracción de peticiones que fallaron) y de checks (fracción de checks que pasaron).
  • Gauge (medidor): el último valor, o el mínimo/máximo. Sirve para cosas que suben y bajan, como vus (usuarios virtuales activos ahora mismo).

La estrella para analizar rendimiento es el Trend, porque la latencia es una distribución, no un número: mil peticiones dan mil latencias distintas, y necesitas los percentiles para describir esa distribución (por qué el promedio miente y el p95 no, lo viste en el módulo 3). El resumen de k6 muestra http_req_duration como un Trend justamente por eso.

k6 también deja crear un Trend custom: una serie propia que tú alimentas, para medir algo que http_req_duration no separa —por ejemplo, la latencia solo del paso "cotizar", aislada del paso "reservar"—. Así se declara (esto es contenido de k6, rotulado; no se ejecuta aquí):

// CONTENIDO (no ejecutado aqui): un Trend custom en k6. Ver grafana.com/docs/k6.
import http from "k6/http";
import { Trend } from "k6/metrics";

// Una metrica de tendencia propia: la latencia solo del paso "cotizar".
const quoteLatency = new Trend("quote_latency", true); // true = en milisegundos

export const options = { vus: 50, duration: "30s" };

export default function () {
  const url = "http://127.0.0.1:8000/quote";
  const payload = JSON.stringify({ room: "Focus", tier: "basic", hours: 3 });
  const params = { headers: { "Content-Type": "application/json" } };

  const res = http.post(url, payload, params);
  quoteLatency.add(res.timings.duration); // alimenta la serie con esta latencia
}

En el resumen, ese Trend custom aparece con la misma forma que http_req_duration: avg/min/med/max/p(90)/p(95). Es la herramienta para responder "¿cuál de mis pasos es el lento?" cuando un flujo tiene varios (lo veremos al buscar el cuello de botella, lección 5).

Leer el resumen con tres preguntas

Así se ve el resumen de fin de test de k6 (contenido rotulado, fiel a la documentación de k6; los números son coherentes con lo que el generador Python mide de verdad). No se lee de arriba abajo como un texto; se lee buscando tres respuestas:

// CONTENIDO (no ejecutado aqui): forma del resumen de k6 run. Ver grafana.com/docs/k6
     ✓ status es 200
     ✓ price_cents es 7500

     checks.........................: 100.00%  ✓ 30000     ✗ 0
     data_received..................: 4.2 MB   140 kB/s
     data_sent......................: 3.6 MB   120 kB/s
     http_req_duration..............: avg=5.3ms  min=2.7ms  med=4.7ms  max=22.6ms  p(90)=6.1ms  p(95)=6.7ms
     http_req_failed................: 0.00%    ✓ 0         ✗ 30000
     http_reqs......................: 30000    1000/s
     iteration_duration.............: avg=5.8ms  min=2.9ms  max=24.1ms
     iterations.....................: 30000    1000/s
     vus............................: 30       min=30      max=30

Las tres preguntas, con la línea que responde cada una:

  • ¿La latencia está dentro del SLO? → mira http_req_duration, y dentro de él el percentil que fija tu SLO (casi siempre p(95)). Aquí p(95)=6.7ms. Si tu SLO es p(95) < 200 ms, estás holgadísimo. Ignora el avg para juzgar el SLO: el promedio esconde la cola. El p95 es tu boletín.
  • ¿La tasa de error cruzó el límite? → mira http_req_failed. Aquí 0.00%. Si tu SLO de fiabilidad es < 1%, perfecto. Una tasa de error alta invalida todo lo demás: un p95 bello no vale nada si el 20% de las peticiones falló (mediste la latencia de las que respondieron, un sesgo peligroso).
  • ¿El throughput alcanza? → mira http_reqs (el total y el /s). Aquí 1000/s. La pregunta es si ese RPS sostenido cubre el tráfico que esperas en producción. Si tu pico real son 300 req/s y aguantó 1000, sobra; si esperas 3000, este sistema no llega.

Con esas tres respuestas —p95 dentro del SLO, error bajo el límite, RPS suficiente— tienes un veredicto. Sin las tres, tienes números sueltos. Fíjate también en checks: 100.00%: la corrección bajo carga (M6) también vive en el resumen, como una Rate.

El lado ejecutable: un analizador que lee y juzga

Ahora el que sí corre. El resumen de k6 es contenido, pero el generador de Python produce las mismas métricas de verdad, y podemos escribir un pequeño analizador que las lea y las juzgue contra un SLO —exactamente el "leer el resumen con criterio" que acabamos de describir, hecho código—. El analizador toma un results.json exportado (la lección 3 lo produce) y responde las tres preguntas:

"""Analiza un results.json exportado: lo interpreta contra un SLO.

Leer el JSON fuera de la corrida es el punto de exportar: se puede analizar,
comparar y juzgar despues, sin volver a lanzar carga. Aqui evaluamos el p95 y la
tasa de error contra un SLO y emitimos un veredicto legible.

Uso: python3.14 analyze_results.py results.json P95_SLO_MS ERROR_SLO_PCT
"""
import json
import sys

with open(sys.argv[1]) as f:
    r = json.load(f)
p95_slo = float(sys.argv[2])
err_slo = float(sys.argv[3])

p95 = r["latency_ms"]["p95"]
err_pct = r["error_rate"] * 100

print(f"Corrida '{r['label']}'  ({r['endpoint']}, {r['requests']} req)")
print(f"  RPS sostenido : {r['rps']}")
print(f"  p50 / p95 / p99 : {r['latency_ms']['p50']} / {p95} / "
      f"{r['latency_ms']['p99']} ms")
p95_ok = p95 < p95_slo
err_ok = err_pct < err_slo
print(f"  p95 {p95} ms  vs SLO < {p95_slo} ms  -> "
      f"{'DENTRO del SLO' if p95_ok else 'FUERA del SLO'}")
print(f"  error {err_pct:.2f}%  vs SLO < {err_slo}%  -> "
      f"{'DENTRO del SLO' if err_ok else 'FUERA del SLO'}")
print(f"  VEREDICTO: {'PASS' if (p95_ok and err_ok) else 'FAIL'}")

Lo corremos contra dos corridas reales exportadas: la baseline (endpoint rápido /quote) y la degradada (endpoint lento /quote_slow), las dos contra un SLO de p95 < 200 ms y error < 1%. Todo esto es salida real.

Qué esperar — la baseline tiene un p95 de milisegundos: dentro del SLO, veredicto PASS:

$ python3.14 analyze_results.py results_baseline.json 200 1
Corrida 'baseline'  (/quote, 600 req)
  RPS sostenido : 5449.6
  p50 / p95 / p99 : 4.7 / 6.66 / 19.91 ms
  p95 6.66 ms  vs SLO < 200.0 ms  -> DENTRO del SLO
  error 0.00%  vs SLO < 1.0%  -> DENTRO del SLO
  VEREDICTO: PASS

Qué esperar — la degradada tiene un p95 mucho mayor (61 ms), pero aún dentro del SLO de 200 ms: veredicto PASS también:

$ python3.14 analyze_results.py results_actual.json 200 1
Corrida 'actual'  (/quote_slow, 600 req)
  RPS sostenido : 538.1
  p50 / p95 / p99 : 54.74 / 61.27 / 73.62 ms
  p95 61.27 ms  vs SLO < 200.0 ms  -> DENTRO del SLO
  error 0.00%  vs SLO < 1.0%  -> DENTRO del SLO
  VEREDICTO: PASS

Detente en esto, porque es una lección dentro de la lección. Las dos corridas pasan el SLO, aunque la degradada es casi diez veces más lenta (61 ms vs 6.66 ms). Leer el resumen contra un SLO absoluto te dice "ambas están bien" —y por el SLO de hoy, lo están—. Pero tu instinto grita que algo cambió: el p95 se multiplicó por nueve. Esa es la limitación de analizar una corrida aislada: ves si cumple el estándar, pero no ves si empeoró. Para eso hace falta comparar con una corrida anterior, que es exactamente la regresión de la lección 4. Leer el resumen es el primer paso; comparar resúmenes es el siguiente.

Errores comunes

Juzgar el SLO por el promedio. Qué pasa: alguien mira avg=5.3ms, concluye "rapidísimo", y no ve que el p99 es 20 ms o que hay una cola. Por qué pasa: el promedio es la cifra más visible y la más intuitiva. Cómo detectarlo: si tu veredicto se basa en el avg y no en el percentil de tu SLO, lo estás leyendo mal. Cómo corregirlo: juzga el SLO por el percentil que fijaste (p95 o p99); el promedio es contexto, no veredicto (módulo 3).

Leer la latencia sin mirar la tasa de error. Qué pasa: se celebra un p95 bajo sin notar que http_req_failed es 20%. Por qué pasa: la latencia es lo primero que se mira. Cómo detectarlo: un p95 sospechosamente bueno junto a una tasa de error alta —mediste la latencia solo de las peticiones que respondieron, ignorando las que fallaron—. Cómo corregirlo: lee siempre las tres cifras juntas; una tasa de error alta invalida el resto del resumen.

Confundir el Trend con un Counter o un Rate. Qué pasa: alguien busca el percentil en http_reqs (que es un total, un Counter) o el total en http_req_duration (que es un Trend). Por qué pasa: no se distingue el tipo de métrica. Cómo detectarlo: si buscas un p95 y la métrica solo tiene un total y un /s, es un Counter, no un Trend. Cómo corregirlo: recuerda los tipos —latencia = Trend (tiene percentiles), total = Counter (suma), fracción = Rate (porcentaje), valor actual = Gauge—.

Ejercicios

Ejercicio 1 — Clasifica la métrica. Para cada una, di de qué tipo es (Trend, Counter, Rate o Gauge) y qué pregunta responde. (a) http_req_duration. (b) http_reqs. (c) http_req_failed. (d) vus. (e) checks.

Ver solución
  • (a) Trend — resume la serie de latencias en percentiles. Responde "¿cuánto tarda?".
  • (b) Counter — suma el total de peticiones (y su /s). Responde "¿cuánto throughput?".
  • (c) Rate — la fracción de peticiones que fallaron. Responde "¿qué tan fiable?".
  • (d) Gauge — los usuarios virtuales activos ahora mismo (min/max). Responde "¿cuánta carga había?".
  • (e) Rate — la fracción de checks que pasaron. Responde "¿la respuesta fue correcta bajo carga?".

Ejercicio 2 — Lee este resumen. SLO: p(95) < 300 ms, error < 1%, y necesitas al menos 500 req/s. El resumen dice: http_req_duration: avg=80ms med=70ms p(95)=280ms; http_req_failed: 0.30%; http_reqs: ... 640/s. ¿Pasa el SLO? Justifica las tres cifras.

Ver solución

Pasa las tres. (i) Latencia: p(95)=280ms < 300ms → dentro del SLO (aunque por poco margen; conviene vigilarlo). (ii) Error: 0.30% < 1% → dentro. (iii) Throughput: 640/s ≥ 500/s → suficiente. Nota: el avg=80ms es mucho menor que el p(95)=280ms, señal de una cola larga —el usuario típico ve 70 ms, pero el peor 5% ve 280—; el SLO se cumple, pero esa brecha p50↔p95 conviene tenerla en el radar.

Ejercicio 3 — ¿Por qué un Trend custom? Un flujo de k6 hace dos peticiones por iteración: POST /quote y luego POST /book. El http_req_duration del resumen mezcla las latencias de las dos. (a) ¿Qué problema tiene eso si quieres saber cuál de los dos pasos es el lento? (b) ¿Cómo lo resuelve un Trend custom?

Ver solución
  • (a) http_req_duration agrega todas las peticiones HTTP de la corrida en una sola serie. Si /quote tarda 5 ms y /book tarda 100 ms, el p95 combinado te dice "algo tarda", pero no cuál: no puedes separar el paso rápido del lento mirando esa métrica agregada.
  • (b) Declaras dos Trend custom —quoteLatency y bookLatency— y en el código alimentas cada uno con la latencia de su paso (quoteLatency.add(resQuote.timings.duration) y lo mismo para book). En el resumen aparecen por separado, cada uno con su p95, y ahí ves de inmediato que /book es el cuello de botella. Es la herramienta para localizar el paso lento dentro de un flujo (lección 5).

Resumen y siguiente paso

En esta lección aprendiste a leer el resumen de una prueba de carga como un boletín de calificaciones, no como una lista cruda: buscando tres respuestas —¿el p95 está dentro del SLO? ¿la tasa de error cruzó el límite? ¿el RPS alcanza para el tráfico esperado?— y mirando la cifra correcta para cada una (el percentil de tu SLO para la latencia, no el promedio). Conociste los cuatro tipos de métrica de k6 y por qué la latencia es un Trend (una serie resumida en percentiles), incluido el Trend custom para aislar la latencia de un paso concreto. Y con el analizador ejecutable viste algo clave: una corrida leída aislada te dice si cumple el estándar, pero no si empeoró —las dos corridas pasaron el SLO de 200 ms aunque una era 9x más lenta—.

Antes de avanzar deberías poder: nombrar las tres preguntas para leer un resumen y la línea que responde cada una; distinguir Trend/Counter/Rate/Gauge; y explicar por qué analizar una corrida aislada no atrapa una regresión. Lo que sigue, en la lección 3, es exportar ese resumen a un archivo —justo lo que el analizador leyó— para poder analizarlo fuera y, sobre todo, guardarlo como el baseline contra el cual comparar corridas futuras.

Recursos

  • k6 — Metric types — la referencia oficial de los cuatro tipos de métrica (Trend, Counter, Rate, Gauge) y qué reporta cada uno en el resumen. La fuente del contenido de esta lección.
  • k6 — End-of-test summary — cómo se estructura el resumen de fin de test que aprendiste a leer aquí, línea por línea.
  • k6 — Trend (custom metric) — cómo se declara y alimenta un Trend custom para aislar la latencia de un paso concreto de un flujo.
  • Google SRE Book — Service Level Objectives — el criterio para juzgar el resumen: por qué el p95/p99 (y no el promedio) es lo que se compara contra un SLO.