Módulo 8: Proyecto — Prueba de carga de la API de Reservo
1. Presentación del módulo: ensamblar la prueba completa
Descripción
Llegaste al final. Durante siete módulos aprendiste a producir una prueba de carga pieza por pieza: por qué existe (módulo 1), cómo se escribe un script k6 y qué son los usuarios virtuales (módulo 2), cómo se miden la latencia p95, el throughput y la tasa de error (módulo 3), cómo se moldea la carga con perfiles y stages (módulo 4), cómo un threshold convierte una métrica en un veredicto pasa/falla (módulo 5), cómo un check() verifica la corrección bajo carga (módulo 6), y cómo se analiza el resultado y se automatiza en CI (módulo 7). Cada módulo dejó una pieza sobre la mesa. Este módulo es donde todas las piezas se ensamblan en una sola prueba completa de la API de Reservo, y la entregas.
Aquí no hay tema nuevo. Es un capstone de integración: el trabajo de reunir lo que ya sabes en un solo artefacto de principio a fin, el que escribirías el primer día que alguien te dice "ponle una prueba de carga a esta API". Vas a construir cuatro cosas que encajan entre sí —el script k6 (contenido), la API canónica como blanco, la corrida ejecutable en Python con sus métricas y su gate, y el CI (contenido)— y a cerrarlas con una rúbrica de lo que hace buena a una prueba de carga.
Conexión con el módulo: esta lección es el mapa del capstone. No explica un concepto nuevo; explica cómo se juntan los siete módulos anteriores, qué aporta cada uno, y qué vas a entregar. Fija —por última vez— la regla del entorno (qué se ejecuta y qué va como contenido), reusa el endpoint /quote_cpu que declaró el módulo 5 para poder provocar la degradación, y traza la frontera con lo que queda fuera de la guía. Al terminar el módulo habrás hecho, con tus manos, una prueba de carga completa: el arco entero de la guía en un solo entregable.
La orquesta que toca la sinfonía completa
Piensa en una orquesta. Durante meses, cada sección ensayó por separado: los violines afinaron su parte, los metales la suya, la percusión marcó su compás, el director estudió la partitura. Cada ensayo por separado fue necesario —sin él, nadie dominaría su instrumento—, pero ninguno era la sinfonía. La sinfonía es la noche del concierto: todas las secciones tocando a la vez, en el orden correcto, cada una entrando cuando le toca, formando una sola pieza que el público escucha completa.
Los siete módulos anteriores fueron los ensayos por sección. Aprendiste el script (los violines), las métricas (los metales), los perfiles (la percusión), los thresholds (el director marcando "de aquí no se pasa"). Cada uno lo practicaste aislado, contra un pedazo del problema. Este módulo es el concierto: el escenario cotizar→reservar (M2, M6) tocando bajo un perfil smoke→load→stress (M4), midiéndose con p95/RPS/error (M3), juzgado por thresholds ligados a un SLO (M5), analizado y exportado (M7), y todo puesto en un pipeline (M7). Ninguna sección es nueva; lo nuevo es que suenan juntas, y que al final tienes una prueba de carga entera —no siete ejercicios sueltos—. Dominar cada instrumento era el trabajo de los módulos; tocar la sinfonía es el trabajo de este.
Las cuatro piezas que vas a ensamblar
Una prueba de carga completa, entregable, tiene cuatro piezas. Cada una viene de módulos anteriores; el capstone las une.
- El script de k6 (contenido). El artefacto que un equipo con k6 instalado correría con
k6 run. Junta la funcióndefaultcon el escenario cotizar→reservar y suscheck()(M2, M6), elsleep()con jitter (M6), yexport const optionsconstagessmoke→load→stress (M4) ythresholdsligados al SLO (M5). Como k6 no está instalado, va rotulado como contenido, fiel a la documentación oficial. - La API de Reservo canónica (el blanco). El servidor local que ya conoces desde el módulo 1:
GET /rooms,POST /quote→{price_cents},POST /book→{booking_id, confirmed}, con los números-ancla 7500 y 6000. Corre enlocalhosten puerto 0. Es lo que la prueba martilla. - La corrida ejecutable en Python. El generador que corre el escenario cotizar→reservar por etapas (smoke→load→stress) contra la API, mide p50/p95/p99, RPS y tasa de error reales, evalúa los thresholds sobre la corrida entera —como k6—, y sale con exit code (0 = PASS, 1 = FAIL), exportando sus métricas a un
results.json. Esto se ejecuta de verdad y su salida se cita. - El CI (contenido). El
.github/workflows/load.ymldonde el threshold actúa como gate que bloquea el deploy si el rendimiento no cumple. Va como contenido, fiel a GitHub Actions y a la integración oficial de k6.
Las lecciones 2 a 7 construyen estas piezas una por una; la lección 8 las junta en la entrega y les pone una rúbrica.
El endpoint que reusamos: /quote_cpu (de M5)
Una prueba de carga que siempre pasa no enseña nada. Para ver el gate ponerse rojo hace falta un blanco que, bajo carga, degrade su p95 sobre el umbral. La API canónica de Reservo es rapidísima en localhost (POST /quote responde en menos de un milisegundo), así que por sí sola nunca cruzaría un SLO razonable. Necesitamos un blanco cuyo tiempo de respuesta dependa de la carga.
Como el módulo 5 declaró y este capstone reusa, ese blanco es /quote_cpu: 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 hace trabajo de CPU —un bucle que quema ciclos, modelando un motor de precios "realista" que calcula de verdad—. La clave está en el GIL de Python: el trabajo de CPU no corre en paralelo entre hilos, se serializa. Con pocos clientes a la vez (smoke, load) hay poca cola y el p95 se mantiene bajo; con muchos (stress), la cola de trabajo de CPU crece y el p95 se dispara. Es la firma de un endpoint cuyo cuello de botella es el cómputo: barato cuando nadie lo usa, caro cuando todos lo usan a la vez.
Con esto tenemos las dos caras que el capstone necesita: la carga sana contra /quote (el build rápido, que pasa el SLO) y la degradación contra /quote_cpu (el motor de precios pesado, cuyo p95 cruza el umbral bajo el stress). El mismo test, el mismo threshold, dos blancos: uno verde, otro rojo.
La regla del entorno (por última vez, porque importa)
Este capstone tiene dos protagonistas en categorías distintas, y confundirlos arruinaría lo aprendido en toda la guía:
- Lo que se ejecuta de verdad y se cita es Python. La API de Reservo corriendo en
localhost; el generador que corre el escenario por etapas y mide p50/p95/p99, RPS y tasa de error reales; la evaluación de los thresholds que sale con exit code; elresults.jsonque se exporta. Cuando veas un bloque con un comandopython3.14 ...y unexit code, eso pasó de verdad en este entorno con Python 3.14.0. - k6 y el CI van como contenido rotulado. k6 no está instalado (es un binario de Go con su propio runtime JavaScript; node no lo corre). El script
.jscompleto, su resumen con la sección de thresholds (✓/✗), y el.github/workflows/load.ymlson 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. Aquí nunca se ejecutagitnigh.
Esta separación es la que hace sólido el aprendizaje del capstone. Ves el mecanismo real —correr el escenario por etapas, medir el p95, evaluar el threshold, fallar con un exit code, exportar a un archivo— con tus métricas de verdad en Python; y ves la forma industrial de ese mismo mecanismo en el script de k6 y en el YAML de CI. Son la misma prueba a dos escalas.
Las fronteras de este módulo
- Cada tema individual (el script, las métricas, los perfiles, los thresholds, los checks, el análisis, el CI) es de su módulo. Aquí no se re-explican; se integran. Si dudas de una pieza, el módulo que la enseñó está a un enlace.
- Probar la corrección por el navegador (que un usuario que elige Focus/basic/3h ve
$75.00en la pantalla) es E2E, y vive en la guía hermanae2e-testing-with-playwright-guide. Aquí probamos la carga de la API, no la corrección de la UI. Loscheck()del capstone verifican la corrección de la respuesta HTTP bajo carga, que es otra cosa. - Optimizar la app o la base de datos cuando un threshold falla (arreglar la consulta lenta, poner un índice, meter caché) está fuera de esta guía: es el "después". El capstone te deja parado justo ahí —con un gate rojo y un p95 que cruzó el SLO— y te dice a dónde seguir, pero no optimiza.
- Este es el último módulo. No hay un M9 que "termine" el capstone: la lección 8 es la entrega completa y el cierre de la guía.
En una frase: este módulo es donde ensamblas y entregas la prueba de carga completa. Todo lo que ves ya lo aprendiste; lo nuevo es juntarlo.
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. La misma prueba de carga (el escenario cotizar→reservar por etapas smoke→load→stress, con los mismos thresholds) corre contra dos blancos. Primero, contra el build sano /quote:
Qué esperar — con el endpoint rápido, el p95 de la corrida entera queda muy por debajo del SLO de 200 ms; el gate pasa y sale con código 0:
$ python3.14 loadtest.py http://127.0.0.1:PORT /quote green.json
...
THRESHOLD MEDIDO RESULTADO
http_req_duration: p(95) < 200ms p(95) = 21.72ms PASS
http_req_failed: rate < 1.00% rate = 0.00% PASS
checks: rate > 99.00% rate = 100.00% PASS
--------------------------------------------------------------------------
metricas exportadas -> green.json
GATE: PASS (exit code 0)
$ echo $?
0
Y ahora, la misma prueba contra el motor de precios pesado /quote_cpu, cuyo p95 se dispara bajo el stress:
Qué esperar — el trabajo de CPU serializado por el GIL hace que el p95 cruce el SLO bajo la carga de pico; el gate falla y sale con código 1:
$ python3.14 loadtest.py http://127.0.0.1:PORT /quote_cpu red.json
...
THRESHOLD MEDIDO RESULTADO
http_req_duration: p(95) < 200ms p(95) = 218.51ms FAIL
http_req_failed: rate < 1.00% rate = 0.00% PASS
checks: rate > 99.00% rate = 100.00% PASS
--------------------------------------------------------------------------
metricas exportadas -> red.json
GATE: FAIL (exit code 1)
$ echo $?
1
Ese es el capstone entero en dos comandos: la misma prueba, verde con la carga sana y roja cuando el motor de precios se pone pesado y el p95 cruza el umbral bajo el stress —cada una con su exit code de verdad, el que un pipeline de CI usaría para autorizar o bloquear el deploy—. (Los números exactos varían un poco entre corridas, porque dependen de cómo el sistema operativo reparte el tiempo; lo que no varía es la historia: /quote pasa el SLO holgado, /quote_cpu lo cruza bajo el stress, y el gate lo atrapa.) El resto de las lecciones arma esto pieza por pieza hasta la entrega final.
Errores comunes
Creer que el capstone enseña algo nuevo. Qué pasa: alguien llega aquí esperando un concepto más y se frustra porque "solo" se juntan cosas. Por qué pasa: se confunde aprender una técnica con saber aplicarla completa. Cómo detectarlo: si crees que integrar es "menos" que aprender, no has hecho el ejercicio de ensamblar. Cómo corregirlo: entiende que juntar las siete piezas en una prueba que corre, mide, juzga y se automatiza es una habilidad en sí —la que de verdad se usa en el trabajo—. La sinfonía no es "menos" que los ensayos; es para lo que sirvieron.
Esperar que la prueba pase siempre. Qué pasa: alguien diseña la prueba para que dé verde y se sorprende (o se asusta) cuando ve el gate rojo. Por qué pasa: se confunde "prueba que pasa" con "prueba buena". Cómo detectarlo: si tu prueba nunca puede fallar, no está midiendo un límite real. Cómo corregirlo: una buena prueba de carga puede fallar —ese es el punto—. El gate rojo contra /quote_cpu no es un error: es la prueba haciendo su trabajo, atrapando que el p95 cruzó el SLO bajo carga. Un gate que nunca se pone rojo no protege nada.
Creer que k6 o el CI corrieron aquí. Qué pasa: alguien ve el script .js o el load.yml 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 son contenido rotulado, fieles a la doc—.
Ejercicios
Ejercicio 1 — De la pieza al módulo. Para cada pieza del capstone, di de qué módulo viene lo que necesitas para construirla. (a) El escenario POST /quote → POST /book con check(). (b) El stages: [...] smoke→load→stress. (c) El thresholds: { http_req_duration: ['p(95)<200'] }. (d) Exportar las métricas a results.json y el load.yml de CI.
Ver solución
- (a) Módulos 2 y 6: la función
defaultyhttp.post(M2), loscheck()de corrección, la correlación cotizar→reservar y elsleepcon jitter (M6). - (b) Módulo 4: los perfiles de carga y la opción
stages(ramp-up/steady/ramp-down, y las formas smoke/load/stress). - (c) Módulo 5: los thresholds que convierten una métrica en pasa/falla y el exit code que gatea el CI.
- (d) Módulo 7: exportar el resultado (
--out json) y correr k6 en CI con elload.yml.
Cada pieza del capstone es un módulo anterior. Integrar es saber cuál aporta qué.
Ejercicio 2 — ¿Por qué /quote_cpu y no /quote para el rojo? El capstone corre la misma prueba contra dos blancos. (a) ¿Por qué el gate contra /quote da verde aun bajo la etapa de stress? (b) ¿Por qué /quote_cpu da rojo? (c) ¿Qué situación real modela /quote_cpu?
Ver solución
- (a)
/quotesolo calcula un precio (una multiplicación y una división entera) y responde: es tan rápido que, aun con 80 VUs concurrentes enlocalhost, su p95 se queda en decenas de milisegundos, muy por debajo del SLO de 200 ms. El sistema está sano, así que pasa. - (b)
/quote_cpuhace trabajo de CPU por petición, y el GIL de Python serializa ese trabajo entre hilos: con muchos clientes a la vez (stress), la cola de cómputo crece y el p95 se dispara sobre los 200 ms. La latencia depende de la carga, así que el pico la rompe. - (c) Modela un endpoint cuyo cuello de botella es el cómputo: un motor de precios que hace un cálculo pesado, una función que se volvió costosa tras un cambio, una operación que no escala con la concurrencia. Barato con poca carga, caro con mucha —justo lo que una prueba de stress existe para descubrir—.
Ejercicio 3 — Verde no es "bien", rojo no es "mal". En el adelanto, el gate contra /quote salió con $? = 0 y contra /quote_cpu con $? = 1. (a) ¿Qué le comunica cada número a un pipeline de CI? (b) ¿Por qué el gate rojo es un éxito de la prueba, no un fracaso? (c) ¿Qué harías, como equipo, ante el gate rojo?
Ver solución
- (a)
0significa "todo bien, el pipeline puede continuar (y desplegar)";1(o cualquier ≠ 0) significa "algo falló, el pipeline se detiene y el deploy se bloquea". Es el idioma universal del exit code (M5). - (b) Porque la prueba hizo su trabajo: detectó que el p95 cruzó el SLO bajo carga y lo comunicó con un exit code que bloquea el deploy —antes de que un usuario real viviera esa lentitud—. Una prueba que atrapa un problema es un éxito; una que nunca puede fallar no protege nada.
- (c) Investigar por qué el p95 cruzó (¿la app? ¿la base de datos? ¿la red?) y optimizar el cuello de botella —lo cual está fuera de esta guía; es el "después"—. La prueba de carga te dice que hay un problema y dónde mirar; arreglarlo es el siguiente trabajo. El gate rojo es el principio de esa conversación, no el final.
Resumen y siguiente paso
Este módulo es el capstone: donde las siete piezas que aprendiste por separado se ensamblan en una prueba de carga completa de la API de Reservo y la entregas. No hay tema nuevo; lo nuevo es juntarlo todo. Viste la analogía de la orquesta (los ensayos por sección = los módulos; la sinfonía = el capstone), las cuatro piezas que vas a construir (el script k6 como contenido, la API canónica como blanco, la corrida ejecutable en Python con su gate, el CI como contenido), el endpoint /quote_cpu que reusamos de M5 para provocar la degradación, la regla del entorno (Python se ejecuta; k6 y el CI son contenido), y el destino ejecutado: el mismo test verde contra /quote (exit 0) y rojo contra /quote_cpu (exit 1).
Antes de avanzar deberías poder: nombrar las cuatro piezas del capstone y de qué módulo viene cada una; explicar por qué /quote_cpu degrada bajo carga y /quote no; y entender por qué un gate rojo es la prueba funcionando, no fallando. Lo que sigue, en la lección 2, es la primera pieza: el escenario cotizar→reservar con checks —la función default que es el corazón de la prueba—, escrito en k6 (contenido) y corrido de verdad en Python con sus checks al 100%.
Recursos
- k6 — Get started (escribir tu primer test) — el recorrido oficial que arma un script completo con
http,check,sleep,optionsythresholds; el mapa de todo lo que este capstone ensambla. La fuente del contenido de k6. - k6 — Test types (smoke, load, stress) — la referencia oficial de los tipos de prueba que el perfil del capstone recorre (smoke → load → stress); el fundamento de la lección 3.
- Google SRE Book — Service Level Objectives — por qué una prueba se juzga contra un SLO y no en abstracto; el criterio detrás de los thresholds del capstone (lección 4).
concurrent.futures.ThreadPoolExecutor— documentación de Python — el pool de hilos con el que el generador modela los VUs (un worker por VU) en la corrida ejecutable. El motor de lo que se ejecuta de verdad.