Módulo 7: Analizar resultados y CI
1. Presentación del módulo: de medir a analizar y automatizar
Descripción
Hasta aquí has aprendido a producir una prueba de carga de principio a fin. Sabes escribir el script y lanzar usuarios virtuales (módulo 2), medir la latencia p95, el throughput y la tasa de error (módulo 3), moldear la carga con perfiles y etapas (módulo 4), convertir una métrica en un veredicto pasa/falla con thresholds (módulo 5) y verificar la corrección de las respuestas bajo carga con check() en escenarios realistas (módulo 6). Cada corrida que lanzas deja algo atrás: un resultado. Este módulo trata de las dos cosas que todavía no has hecho con ese resultado: analizarlo con criterio y automatizar la prueba para que corra sola.
La primera mitad es analizar. Vas a leer el resumen y entender qué te dice de verdad —¿el p95 quedó dentro del SLO? ¿cuál fue la tasa de error? ¿qué RPS aguantó el sistema?—, a conocer la métrica de tendencia (el Trend de k6, que resume una serie entera de latencias en percentiles), a exportar el resultado a un archivo (--out json / csv) para poder analizarlo fuera y comparar corridas, a detectar una regresión de rendimiento comparando una corrida anterior (baseline) contra la actual, y a saber cómo se investiga un cuello de botella —sin optimizar aquí—. La segunda mitad es automatizar: correr k6 en integración continua con un .github/workflows/load.yml donde los thresholds actúan como gate que bloquea el deploy, y decidir cuándo conviene correr la prueba.
Conexión con el módulo: esta lección es el mapa. Explica el giro —de medir a analizar y automatizar—, declara el endpoint /quote_slow que este módulo añade a la API canónica para poder modelar una regresión, fija las reglas del entorno (qué se ejecuta y qué va como contenido) y traza las fronteras. Reúsa todo lo anterior: el script y los VUs (M2), las métricas que vas a analizar (M3), los perfiles que generan la carga (M4), los thresholds que se vuelven el gate del pipeline (M5) y los checks/escenarios (M6). Lo que viene después es el capstone (M8), donde harás una prueba de carga completa. Aquí cerramos el ciclo de una prueba: producir → analizar → automatizar.
El chef que prueba el plato y el que escribe la receta
Imagina una cocina. Ya sabes cocinar el plato: mediste los ingredientes, controlaste el fuego, emplataste. Pero cocinar el plato una vez no es lo que hace bueno a un restaurante. Faltan dos cosas.
La primera es probar el plato con criterio. No basta con mirar que "se ve bien"; el chef prueba una cucharada y la juzga contra un estándar: ¿le falta sal? ¿está más flojo que el de ayer? Y lo apunta en una libreta, para poder comparar el plato de hoy con el de la semana pasada. Sin esa libreta, cada plato es un evento aislado y nadie nota que la sopa lleva tres días saliendo más aguada. Eso es analizar el resultado: leer el resumen, exportarlo a una libreta (el archivo de resultados) y compararlo con el de antes para atrapar una regresión.
La segunda es escribir la receta y colgarla en la cocina para que el plato salga igual sin que el chef esté presente. La receta dice los pasos, las cantidades y —lo más importante— la regla de aceptación: "si la sopa no cuaja, no sale a la mesa". Cualquier cocinero la sigue, y el plato malo se detiene antes de llegar al cliente. Eso es automatizar en CI: el load.yml es la receta colgada, y el threshold es la regla de "no sale a la mesa" que detiene el deploy sola. Este módulo te enseña las dos cosas: probar el plato con libreta y colgar la receta con su regla.
Qué haces cuando la corrida termina
Un error muy común es tratar la prueba de carga como un evento que termina cuando aparece el resumen en la pantalla. Corres k6, miras el p95, dices "ajá, 200 ms" y cierras la terminal. Ese resultado se evaporó: no quedó guardado, no se comparó con nada, y la próxima vez que alguien pregunte "¿el rendimiento empeoró con el último cambio?" no habrá con qué responder. La corrida fue trabajo tirado a la basura en cuanto se cerró la ventana.
Lo que este módulo te enseña es que una corrida es el principio de algo, no el final. Cuando termina, hay cuatro movimientos que le dan valor:
- Leerla con criterio. No "¿se ve bien?" sino "¿el p95 está dentro del SLO que prometimos? ¿la tasa de error cruzó el límite? ¿el RPS que aguantó es suficiente para el tráfico esperado?". Un número sin un estándar contra el cual compararlo no dice nada.
- Exportarla. Guardar el resultado en un archivo (
results.json) para poder analizarlo con otras herramientas, adjuntarlo a un reporte, y —sobre todo— compararlo con corridas futuras. La libreta del chef. - Compararla con el pasado. ¿El p95 de hoy es peor que el de la semana pasada? Eso es una regresión de rendimiento, y atraparla exige tener guardada la corrida anterior (el baseline).
- Automatizarla. Que todo lo anterior corra solo, en un pipeline, con un gate que bloquee el deploy si el rendimiento se degradó —sin que nadie tenga que acordarse de correr la prueba a mano—.
Los cuatro movimientos son este módulo. Las lecciones 2 a 5 cubren analizar (leer, exportar, comparar, investigar); las lecciones 6 y 7, automatizar (el CI y cuándo correrlo).
El endpoint que este módulo declara: /quote_slow
Para enseñar a detectar una regresión hace falta poder provocar una: un estado en el que el sistema es medible y correcto, pero más lento que antes. La API canónica de Reservo es rapidísima en localhost (POST /quote responde en pocos milisegundos), así que por sí sola no sirve para mostrar "esto empeoró". Necesitamos un blanco que represente un camino de código que se degradó.
Como establece el diseño de la guía, un módulo que necesita un comportamiento extra lo añade y lo declara. Este módulo declara /quote_slow: hace exactamente lo mismo que /quote (recibe {room, tier, hours}, devuelve {price_cents} con los mismos números-ancla 7500 y 6000) pero antes de responder espera un retardo fijo (time.sleep). Modela una situación muy real: alguien hizo un cambio —añadió una llamada a otro servicio, quitó un índice de la base de datos, metió una consulta N+1— y el endpoint que antes respondía en 5 ms ahora tarda 50. La lógica sigue correcta (el precio es el mismo), pero el rendimiento se cayó. La corrida contra /quote es el baseline (el rendimiento de antes); la corrida contra /quote_slow es la actual (después del cambio malo). Comparar sus p95 es detectar la regresión.
Este es el añadido al servidor canónico. El resto del servidor (el de la lección 6 del módulo 1) no cambia; solo se agrega la constante SLOW_DELAY_S, el import time y la rama de /quote_slow en do_POST:
# --- AÑADIDO por el modulo 7 al servidor canonico de Reservo ---
import time
# Segundos de trabajo extra por peticion en /quote_slow. Modela una consulta que
# se volvio lenta tras un cambio de codigo: la REGRESION que la prueba debe atrapar.
SLOW_DELAY_S = 0.045
# ...dentro de do_POST, despues de validar y ANTES de calcular el precio:
# if self.path == "/quote_slow":
# time.sleep(SLOW_DELAY_S) # el camino que se degrado
# cents = price_cents(room, tier, hours)
#
# if self.path in ("/quote", "/quote_slow"):
# self._send_json(200, {"price_cents": cents})
Una nota honesta sobre por qué usamos un sleep fijo aquí, y no el trabajo de CPU serializado por el GIL que declaró el módulo 5. Para enseñar thresholds (M5) queríamos una latencia que dependiera de la carga (barata con poca concurrencia, cara con mucha), y para eso el trabajo de CPU es ideal. Para enseñar regresiones (M7) queremos lo contrario: una diferencia estable y reproducible entre dos versiones del código, para que la comparación baseline-vs-actual sea limpia y no dependa del ruido del momento. Un retardo fijo da justo eso: /quote responde en milisegundos, /quote_slow en ~45 ms más, corrida tras corrida. La regresión salta a la vista.
La regla del entorno (otra vez, porque importa)
Este módulo tiene dos protagonistas que van en categorías distintas, y confundirlos arruinaría el aprendizaje:
- Lo que se ejecuta de verdad y se cita es Python. El generador de carga que exporta sus métricas a un
results.jsonreal, el chequeo de regresión que compara dos corridas y devuelve un código de salida real (sys.exit), el analizador que juzga una corrida contra un SLO, el CSV que acumula corridas: todo corre de verdad contralocalhosty su salida se pega tal cual. Cuando veas un bloque con un comandopython3.14 ...y unexit code, eso pasó. - k6 y el CI van como contenido rotulado. k6 no está instalado en este entorno (es un binario de Go con su propio runtime JavaScript; node no lo corre). El
Trendcustom,k6 run --out json, el resumen, y —la estrella de la segunda mitad— el.github/workflows/load.yml, son contenido, fieles a la documentación oficial de k6 y de GitHub Actions, nunca una salida fabricada presentada como ejecutada. Cuando veas un bloque de k6 o un YAML de CI, estará rotulado como contenido. Nunca se ejecutagitnighaquí.
Esta separación es la que hace sólido el aprendizaje. Ves el mecanismo real —exportar a un archivo, comparar dos corridas, fallar con un exit code— con tus métricas de verdad en Python; y ves la forma industrial de ese mismo mecanismo en k6 y en el YAML de CI. Son la misma idea a dos escalas.
Las fronteras de este módulo
- El script de k6 y los VUs son del módulo 2. Aquí se dan por sabidos: analizamos y automatizamos un script que ya existe.
- Las métricas (qué es el p95, el throughput, la tasa de error) son del módulo 3. Aquí no las re-explicamos; las leemos, exportamos y comparamos.
- Los perfiles de carga son del módulo 4. Aquí generamos la carga con el generador de siempre; el análisis vale sea cual sea el perfil.
- Los thresholds son del módulo 5. Aquí los reutilizamos como el gate del pipeline de CI. M5 construyó el veredicto; M7 lo instala en el pipeline entero y muestra el YAML.
- Los
check()y los escenarios son del módulo 6. Aquí la tasa de checks es una métrica más que aparece en el resumen exportado. - Optimizar la app o la base de datos (índices, caché, arreglar la consulta lenta) está fuera de esta guía: es el "después". Aquí mencionamos cómo se localiza el cuello de botella (lección 5), pero no lo arreglamos. Enlazamos a dónde sigue.
- El capstone —una prueba de carga completa de Reservo, de smoke a stress con thresholds y checks— es el módulo 8. Este módulo aporta el análisis y el CI que ese capstone usará.
En una frase: este módulo es sobre qué haces con el resultado y cómo automatizas la prueba. Leerlo, exportarlo, comparar corridas para atrapar una regresión, y ponerlo en un pipeline con un gate.
Cómo se ve el destino (un adelanto ejecutado)
Para que el mapa no sea solo palabras, aquí está el final del camino, ejecutado de verdad. Primero, el generador exporta las métricas de dos corridas a JSON —una baseline contra /quote (rápido) y una actual contra /quote_slow (el camino que se degradó)—:
Qué esperar — dos corridas de 600 peticiones; la baseline con un p95 de milisegundos, la degradada con un p95 mucho mayor por el retardo fijo:
$ python3.14 load_and_export.py http://127.0.0.1:PORT /quote 600 30 results_baseline.json baseline
[baseline] /quote 600 req concurrencia 30
rps=5449.6 error_rate=0.00% checks=100.00%
p50=4.7ms p95=6.66ms p99=19.91ms max=22.63ms
-> escrito results_baseline.json
$ python3.14 load_and_export.py http://127.0.0.1:PORT /quote_slow 600 30 results_actual.json actual
[actual] /quote_slow 600 req concurrencia 30
rps=538.1 error_rate=0.00% checks=100.00%
p50=54.74ms p95=61.27ms p99=73.62ms max=75.82ms
-> escrito results_actual.json
Y ahora el chequeo de regresión compara el p95 de las dos corridas y falla con un código de salida real porque empeoró muchísimo más que el umbral permitido (+20%):
Qué esperar — el p95 pasó de 6.66 ms a 61.27 ms; el chequeo lo declara REGRESIÓN y sale con código 1:
$ python3.14 check_regression.py results_baseline.json results_actual.json 20
CHEQUEO DE REGRESION DE RENDIMIENTO (p95)
--------------------------------------------------------
baseline (baseline) : p95 = 6.66 ms
actual ( actual) : p95 = 61.27 ms
cambio : +54.61 ms (+820.0%)
umbral permitido : +20.0%
--------------------------------------------------------
REGRESION: el p95 subio de 6.66 ms a 61.27 ms (+820.0%, supera +20.0%)
RESULTADO: FAIL (exit code 1)
$ echo $?
1
Ese es el módulo entero en tres comandos: exportas dos corridas, las comparas, y un chequeo automático atrapa que el rendimiento empeoró y falla —con el exit code de verdad que un pipeline de CI usaría para bloquear el deploy—. (Los números exactos varían un poco en cada corrida, porque dependen de cómo el sistema operativo reparte el tiempo; lo que no varía es la historia: la degradada es mucho más lenta que la baseline, y el chequeo lo atrapa.) El resto de las lecciones desarma esto pieza por pieza.
Errores comunes
Tratar la corrida como el final. Qué pasa: se corre la prueba, se mira el p95 en pantalla, y se cierra la terminal sin guardar nada. Por qué pasa: se confunde "ver el número" con "tener el resultado". Cómo detectarlo: si no puedes responder "¿el p95 empeoró respecto a la semana pasada?", es que no guardaste el baseline. Cómo corregirlo: exporta cada corrida a un archivo y guárdala; una corrida sin exportar es trabajo perdido (lección 3).
Confundir un threshold absoluto con un chequeo de regresión. Qué pasa: se cree que si el p95 está por debajo del SLO (p(95)<200), no hay nada que vigilar. Por qué pasa: un threshold absoluto solo mira "¿es rápido?", no "¿es más lento que antes?". Cómo detectarlo: un p95 que saltó de 6 ms a 61 ms sigue pasando un umbral de 200 ms —el threshold no lo ve, pero es una regresión de 10x—. Cómo corregirlo: usa las dos herramientas —el threshold absoluto (M5) para el SLO, y el chequeo de regresión (lección 4) para el "¿empeoró?"—. Atrapan cosas distintas.
Creer que k6 o el CI corrieron aquí. Qué pasa: alguien ve el load.yml o un resumen de k6 y lo cita como "lo que hizo esta guía". Por qué pasa: el contenido de k6 y de GitHub Actions se ve muy real. Cómo detectarlo: k6 no está instalado y aquí nunca se ejecuta git/gh; lo ejecutado siempre viene con un comando python3.14 .... Cómo corregirlo: recuerda la regla —Python se ejecuta y se cita; k6 y el YAML de CI son contenido rotulado, fieles a la doc—.
Ejercicios
Ejercicio 1 — Analizar vs automatizar. Clasifica cada tarea como parte de analizar el resultado o de automatizar la prueba. (a) Leer el resumen y comprobar que el p95 está bajo el SLO. (b) Escribir el load.yml que corre k6 cada noche. (c) Exportar la corrida a results.json. (d) Poner un threshold como gate que bloquea el deploy. (e) Comparar el p95 de hoy con el de la semana pasada.
Ver solución
- (a) Analizar (leer el resumen con criterio, lección 2).
- (b) Automatizar (el CI, lección 6).
- (c) Analizar (exportar para poder analizar y comparar, lección 3).
- (d) Automatizar (el gate en el pipeline, lección 6).
- (e) Analizar (comparar corridas, detectar regresión, lección 4).
Las dos mitades del módulo: (a), (c) y (e) son analizar; (b) y (d) son automatizar.
Ejercicio 2 — ¿Por qué /quote_slow con sleep fijo? El módulo 5 usó un endpoint con trabajo de CPU (latencia que depende de la carga) y este módulo usa uno con un sleep fijo. (a) ¿Por qué un retardo fijo es mejor para enseñar regresiones? (b) ¿Qué situación real modela /quote_slow?
Ver solución
- (a) Para detectar una regresión hay que comparar dos corridas y atribuir la diferencia al cambio de código, no al ruido del momento. Un retardo fijo hace que
/quote_slowsea siempre ~45 ms más lento que/quote, corrida tras corrida, así que la diferencia baseline-vs-actual es estable y reproducible. El trabajo de CPU del módulo 5 varía con la concurrencia, lo cual es perfecto para ver un threshold cruzar bajo carga, pero mete ruido en una comparación de dos corridas. - (b) Modela un camino de código que se degradó tras un cambio: alguien añadió una llamada a otro servicio, quitó un índice, o metió una consulta lenta. La lógica sigue correcta (el precio es el mismo), pero el endpoint que respondía en 5 ms ahora tarda 50. Es exactamente el tipo de regresión que una prueba de carga en CI existe para atrapar antes de desplegar.
Ejercicio 3 — La regresión que un threshold no ve. En el adelanto, el p95 pasó de 6.66 ms a 61.27 ms. Un SLO de la casa dice p(95) < 200 ms. (a) ¿La corrida degradada pasa ese threshold absoluto? (b) ¿Por qué, entonces, sí es un problema? (c) ¿Qué herramienta lo atrapa?
Ver solución
- (a) Sí. 61.27 ms < 200 ms, así que un threshold
p(95)<200pasa con la corrida degradada. Por el SLO absoluto, todo está "bien". - (b) Porque el p95 se multiplicó por más de 9 respecto al baseline (6.66 → 61.27 ms). Aunque siga bajo el límite hoy, es una degradación enorme: si el equipo la ignora, la próxima regresión encima de esta sí cruzará el SLO, y para entonces será más difícil saber qué cambio la causó. Una regresión temprana es una alerta barata.
- (c) El chequeo de regresión (lección 4), que compara el p95 contra el de una corrida anterior y falla si empeoró más de un umbral relativo (+20%, digamos). No mira "¿es rápido?" sino "¿es más lento que antes?" —y por eso atrapa lo que el threshold absoluto deja pasar—.
Resumen y siguiente paso
Este módulo cierra el ciclo de una prueba de carga: después de producirla (M2–M6), toca analizarla y automatizarla. Analizar es leer el resumen con criterio (¿p95 dentro del SLO? ¿tasa de error? ¿RPS?), exportar el resultado a un archivo para poder estudiarlo fuera y comparar corridas, detectar una regresión comparando baseline vs actual, y saber cómo se investiga un cuello de botella —sin optimizar aquí—. Automatizar es correr k6 en CI con un load.yml (contenido) donde el threshold es el gate que bloquea el deploy, y decidir cuándo correr la prueba.
Viste la analogía del chef (probar el plato con libreta = analizar; colgar la receta con su regla = automatizar en CI), el endpoint que este módulo declara —/quote_slow, con retardo fijo para modelar una regresión reproducible—, la regla del entorno (Python se ejecuta; k6 y el CI son contenido), y el destino ejecutado: dos corridas exportadas a JSON y un chequeo de regresión que atrapa el p95 empeorado y falla con exit code.
Antes de avanzar deberías poder: nombrar las dos mitades del módulo y qué lección cubre cada una; explicar por qué /quote_slow usa un sleep fijo y qué modela; y explicar por qué un threshold absoluto no ve una regresión que un chequeo relativo sí atrapa. Lo que sigue, en la lección 2, es la primera pieza de analizar: leer el resumen de una corrida y entender la métrica de tendencia que resume una serie de latencias en percentiles.
Recursos
- k6 — Results output — la referencia oficial de cómo k6 presenta y exporta los resultados de una corrida: el resumen de fin de test y las salidas a archivo. La fuente del contenido de k6 de este módulo.
- k6 — Running k6 in CI — cómo se integra k6 en un pipeline de integración continua; el fundamento del
load.ymlde la lección 6. - Google SRE Book — Service Level Objectives — por qué un resultado se juzga contra un SLO y no en abstracto; el criterio para leer el resumen (lección 2) y elegir umbrales.
sys.exit— documentación de Python — el mecanismo con el que el chequeo de regresión devuelve su código de salida (0 = sin regresión, 1 = regresión), el espejo ejecutable del exit code que un CI usa para bloquear el deploy.