Módulo 4: Perfiles de carga y stages
1. Presentación del módulo: dar forma a la carga
Descripción
Hasta aquí todas tus mediciones tuvieron una simplificación escondida: la carga era constante. En el módulo 2 lanzabas N usuarios virtuales y en el módulo 3 medías su p95, su throughput y su tasa de error. Funcionaba, pero cargaba una suposición que nadie dijo en voz alta: que el tráfico es plano, que siempre hay exactamente N usuarios pegando a la vez, ni uno más ni uno menos. Y eso jamás es cierto en un sistema real. El tráfico llega en olas: sube despacio por la mañana, se dispara cuando sale una campaña, se sostiene en la hora pico, y baja por la noche. Este módulo te da la herramienta para modelar esas olas: el perfil de carga.
Un perfil de carga es una forma de la carga a lo largo del tiempo: cuántos usuarios (o cuántas peticiones por segundo) hay en cada instante de la prueba. En k6 esa forma se escribe con stages —una lista de tramos que dibujan una subida (ramp-up), una meseta (steady) y una bajada (ramp-down)—. Al terminar este módulo sabrás leer y escribir esos stages, distinguir las tres formas clásicas de carga (constante, rampa, spike) y qué revela cada una, entender los executors de k6 (los motores que deciden cómo se aplica la carga, incluida la carga por tasa de llegada), y sobre todo elegir el perfil que responde tu pregunta. Y lo verás con números medidos de verdad: un generador Python que sube y baja la concurrencia por etapas contra Reservo, mostrándote cómo el p95 sigue a la carga como una sombra.
Conexión con el módulo: esta lección es el mapa. Aquí instalas la idea grande —que la carga tiene forma, no solo tamaño— y ves un adelanto ejecutado de a dónde vamos. Las lecciones siguientes la desarrollan en orden: la 2 argumenta por qué la carga constante no basta; la 3 abre la anatomía de los stages (ramp-up/steady/ramp-down); la 4 recorre las tres formas (constante/rampa/spike); la 5 y la 6 explican los executors (por VUs y por tasa de llegada); la 7 te enseña a elegir el perfil según la pregunta; y la 8 es el mini-proyecto donde escribes un perfil por etapas con tus manos. La frontera con los módulos vecinos importa: las métricas (cómo se calcula el p95) ya fueron el módulo 3 —aquí las reusamos, no las re-explicamos—; y los thresholds (poner un umbral que hace pasar o fallar la prueba) son el módulo 5, no este. Aquí el tema es uno solo: la forma de la carga.
La analogía: el gimnasio que solo entrena a un ritmo
Imagina a alguien que quiere saber si su corazón está en forma y se sube a una caminadora que va siempre a la misma velocidad, digamos ocho kilómetros por hora, durante diez minutos. Al bajar dice: "aguanté ocho km/h sin problema, estoy en forma". Y no miente: esa es información real. Pero mira todo lo que esa prueba a ritmo constante no le dijo. No le dijo cómo reacciona su corazón mientras acelera de reposo a ocho km/h —quizá ahí, en la subida, es donde se marea—. No le dijo en qué velocidad deja de aguantar —¿a diez?, ¿a doce?, ¿a quince?—. No le dijo cómo se recupera cuando baja el ritmo —¿su pulso vuelve a la normalidad en un minuto o sigue disparado cinco minutos después?—. Un ritmo plano responde una sola pregunta —"¿aguantas este ritmo?"— y calla sobre la subida, el límite y la recuperación.
Por eso las pruebas de esfuerzo de verdad —las que hace un cardiólogo— no van a ritmo constante: siguen un protocolo con forma. Empiezan lento, suben la velocidad y la inclinación por etapas cada pocos minutos, sostienen el pico, y luego bajan para observar la recuperación. Esa forma —subir, sostener, bajar— es exactamente un perfil de carga, y es precisamente lo que le vas a aplicar a tu API. No basta con preguntarle "¿aguantas 24 usuarios?"; le vas a preguntar "¿cómo te comportas mientras subo de 0 a 24?, ¿en qué punto empiezas a sufrir?, y cuando la ola baja, ¿te recuperas o quedas tocado?". Una API, como un corazón, revela cosas distintas según la forma del esfuerzo, no solo su tamaño.
Una prueba de carga tiene dos dimensiones, no una: el tamaño (cuántos usuarios) y la forma (cómo evoluciona en el tiempo). El módulo 3 midió el tamaño; este módulo le da forma. Subir, sostener y bajar la carga revela cosas —el arranque, el punto de degradación, la recuperación— que una carga plana esconde por completo.
Un adelanto ejecutado: la carga con forma, medida de verdad
Para que la idea no quede en abstracto, aquí tienes el resultado que vas a construir en la lección 8, ya ejecutado. Es un generador en Python que aplica un perfil por etapas contra la API de Reservo: sube la concurrencia (ramp-up), la sostiene en un pico (steady) y la baja (ramp-down), midiendo el p95 de cada etapa. La API es la canónica de siempre —POST /quote de Focus/basic/3h devuelve 7500 centavos—; el generador la golpea con un pool de trabajadores concurrentes (los "VUs") que crece y decrece.
Qué esperar — a más concurrencia, más peticiones compiten por el servidor, así que el p95 debe subir en el ramp-up, quedarse alto en el pico, y bajar en el ramp-down. Si la degradación es por carga (y no un daño permanente), las etapas de bajada deben espejar a las de subida. Salida real (ejecutada contra Reservo en localhost):
$ python3.14 staged_load.py http://127.0.0.1:PORT
perfil por etapas contra http://127.0.0.1:PORT/quote (Focus/basic/3h -> 7500)
etapa VUs peticiones p95 (ms) promedio (ms)
--------------------------------------------------------------
ramp-up (warm) 4 1028 22.55 12.17
ramp-up (mid) 12 1208 77.01 33.18
steady (peak) 24 1775 174.63 63.00
ramp-down (mid) 12 1187 82.56 33.65
ramp-down (cool) 4 1054 24.20 11.85
--------------------------------------------------------------
errores: 0
Lee la columna del p95 de arriba abajo y verás la ola dibujada en números: con 4 usuarios concurrentes el p95 es 22.55 ms; al subir a 12, salta a 77.01 ms; en el pico de 24, llega a 174.63 ms. Y luego, cuando la ola baja, el p95 regresa: 82.56 ms con 12 de vuelta (casi idéntico a los 77.01 de la subida), y 24.20 ms con 4 (casi los mismos 22.55 del arranque). Esa simetría —la bajada refleja a la subida— es una información valiosísima que una carga constante nunca te habría dado: te dice que la degradación fue por la carga, y que el sistema se recupera limpio cuando la carga cede. Un pico que no se recuperara (que quedara lento aun con 4 usuarios) sería otra historia —una fuga, un recurso que no se libera—, y solo el ramp-down la habría delatado.
Fíjate también en que esto reutiliza todo lo del módulo 3: el p95 se calcula igual, el throughput sigue ahí (las columnas de peticiones), y la tasa de error es 0. Lo nuevo de este módulo es la primera columna: las etapas. La carga ya no es un número, es una secuencia.
Lo mismo en k6: los stages (contenido)
Ese perfil por etapas que acabas de ver ejecutado en Python se escribe en k6 con la opción stages. Como k6 no está instalado en este entorno (es un binario de Go con su propio runtime), su script va como contenido rotulado: es correcto y fiel a la documentación oficial de k6, pero no fue ejecutado aquí. Así se ve la forma ramp-up → steady → ramp-down en k6:
// CONTENIDO (no ejecutado aquí): k6 no está instalado.
// Referencia: grafana.com/docs/k6 (options → stages).
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 20 }, // ramp-up: de 0 a 20 VUs en 30 s
{ duration: '1m', target: 20 }, // steady: sostener 20 VUs por 1 min
{ duration: '30s', target: 0 }, // ramp-down: de 20 a 0 VUs en 30 s
],
};
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);
check(res, { 'status 200': (r) => r.status === 200 });
sleep(1);
}
Mira la relación pieza por pieza. La función default y el http.post con el cuerpo JSON son la anatomía del script que ya viste en el módulo 2 —el código que cada VU ejecuta en bucle—. Lo nuevo es export const options = { stages: [...] }: esa lista de tramos {duration, target} es la forma de la carga. El primer tramo lleva los VUs de 0 a 20 en 30 segundos (ramp-up); el segundo los mantiene en 20 durante un minuto (steady); el tercero los baja a 0 en 30 segundos (ramp-down). Es exactamente la misma ola que el generador Python dibujó con sus etapas, solo que industrializada: k6 interpola los VUs de forma continua hacia cada target, y tú lees el p95 en la meseta. La lección 3 desmenuza cada campo; por ahora quédate con la correspondencia: stages en k6 = las etapas de concurrencia en el generador Python, la misma forma de carga en dos lenguajes.
El mapa: las ocho lecciones
Este módulo va de entender por qué la carga necesita forma a aprender a escribirla a elegir la correcta. Cada lección deja una pieza:
| Lección | Qué instala |
|---|---|
| 1. Presentación (esta) | La carga tiene forma, no solo tamaño. Adelanto ejecutado y de contenido, y el mapa |
| 2. Carga constante no basta | El tráfico llega en olas; qué esconde una carga plana; la idea de forma de carga |
| 3. Los stages | La anatomía de stages: ramp-up, steady, ramp-down, y cómo se leen |
| 4. Las formas | Constante, rampa y spike: qué revela cada una |
| 5. Executors: VUs | constant-vus vs ramping-vus; qué es un executor |
| 6. constant-arrival-rate | Carga por tasa de llegada (RPS): el modelo abierto |
| 7. Elegir el perfil | Pregunta → perfil: load, stress, spike, soak |
| 8. Mini-proyecto | Escribes un perfil por etapas, en k6 (contenido) y Python (ejecutado) |
La regla del entorno (recordatorio) y la frontera
La regla de honestidad de toda la guía sigue vigente y explica por qué a veces ves salida "de verdad" y a veces "así se vería". k6 no está instalado, así que sus scripts .js y su resumen van como contenido rotulado —correcto, fiel a la documentación de k6, pero no ejecutado aquí—. En cambio todo lo de Python sí se ejecuta y se cita: la API de Reservo y el generador por etapas, con sus p95 medidos. El generador es el "hermano ejecutable" de los stages de k6: hace lo mismo (aplicar una forma de carga y medir), a menor escala, para que toques los números. Y nunca se ejecuta git ni gh.
Sobre la frontera con los módulos vecinos, para que no mezcles temas: el módulo 3 fue cómo medir (el p95, el throughput, los errores) —lo damos por sabido y lo reusamos—; este módulo es con qué forma cargar (los perfiles, los stages, los executors); el módulo 5 será qué umbral exigirle al resultado (los thresholds que hacen pasar o fallar). Si en algún momento te sorprendes escribiendo thresholds: {...}, recuerda: eso es la lección del módulo 5. Aquí solo damos forma a la carga.
Errores comunes
Creer que "aguanta 24 usuarios" es una conclusión completa. Qué pasa: alguien corre una carga constante de 24 VUs, ve un p95 aceptable y cierra el tema. Por qué pasa: la carga constante es la más fácil de escribir, y da un número que suena definitivo. Cómo detectarlo: si tu prueba nunca subió ni bajó la carga, mediste una meseta, no una ola —no sabes cómo arrancas, dónde te rompes ni si te recuperas—. Cómo corregirlo: dale forma a la carga con stages (o etapas en el generador); la meseta es una de las tres fases, no toda la historia.
Pensar que este módulo mide algo nuevo. Qué pasa: alguien espera aprender aquí una métrica distinta del p95. Por qué pasa: es fácil confundir "nueva forma de carga" con "nueva cosa que medir". Cómo detectarlo: si buscas una fórmula nueva, te equivocaste de módulo; la fórmula del p95 fue el módulo 3. Cómo corregirlo: entiende que aquí la novedad es la variable independiente (la forma de la carga en el tiempo), no la dependiente (la métrica). Medimos el mismo p95, pero a lo largo de una ola.
Confundir stages con thresholds. Qué pasa: alguien mete un umbral de pass/fail dentro de la conversación de perfiles y se enreda. Por qué pasa: ambos viven en export const options y suenan parecidos. Cómo detectarlo: stages describe la forma de la carga (cuántos VUs y cuándo); thresholds describe el criterio de éxito (qué p95 es aceptable). Si estás decidiendo cuánta carga, es stages (este módulo); si estás decidiendo qué resultado aprueba, es thresholds (módulo 5). Cómo corregirlo: mantén separadas las dos preguntas —"¿qué forma de carga aplico?" y "¿qué resultado considero un aprobado?"—.
Ejercicios
Ejercicio 1 — Tamaño vs forma. Para cada frase, di si describe el tamaño de la carga o su forma. (a) "Probé con 500 usuarios concurrentes." (b) "La carga subió de 0 a 500 en dos minutos, se sostuvo cinco, y bajó a 0." (c) "Lancé un pico súbito de 1000 peticiones." (d) "Corrí 300 VUs fijos durante diez minutos."
Ver solución
- (a) Tamaño. Da un número de concurrencia, pero no dice cómo evoluciona; podría ser constante.
- (b) Forma. Describe una evolución en el tiempo —subida, meseta, bajada—: es un perfil ramp-up/steady/ramp-down.
- (c) Forma. "Súbito" es información de forma: un spike (subida abrupta), distinto de una rampa gradual del mismo tamaño.
- (d) Tamaño (con una forma implícita: constante). "Fijos durante diez minutos" es una meseta plana; el tamaño es 300, la forma es constante.
La lección: el tamaño responde cuántos; la forma, cuándo y cómo. Este módulo es sobre la forma.
Ejercicio 2 — Lee la ola. Mirando la salida ejecutada del adelanto, responde: (a) ¿Cuánto vale el p95 en el pico (24 VUs) y cuánto en el arranque (4 VUs)? (b) El p95 de la etapa "ramp-down (mid)" con 12 VUs es 82.56 ms; el de "ramp-up (mid)", también con 12 VUs, es 77.01 ms. ¿Qué te dice que sean casi iguales? (c) ¿Qué habrías perdido si solo hubieras corrido una carga constante de 24 VUs?
Ver solución
- (a) En el pico (24 VUs) el p95 es
174.63 ms; en el arranque (4 VUs),22.55 ms. Casi ocho veces más: la latencia crece con la concurrencia. - (b) Que el sistema se recupera limpio. Al mismo nivel de carga (12 VUs), el p95 es prácticamente el mismo tanto subiendo como bajando, lo que confirma que la latencia depende de la carga actual, no de un daño acumulado. Si en la bajada hubiera quedado mucho más alto, sospecharías una fuga o un recurso que no se libera.
- (c) Habrías obtenido solo el número del pico (
174.63 ms) y nada más: ni cómo se comporta subiendo, ni la confirmación de que se recupera. Un solo punto en vez de la ola completa.
Ejercicio 3 — Traduce la forma. Tu jefe describe el tráfico de un lunes así: "Empieza tranquilo a las 8, sube fuerte hasta las 10, se mantiene en pico hasta las 12, y afloja por la tarde". Descríbelo como un perfil de carga con sus fases (ramp-up/steady/ramp-down) y di qué le preguntarías a cada fase.
Ver solución
Es un perfil de tres fases:
- Ramp-up (8:00 → 10:00): la carga sube de baja a pico. Pregunta: "¿cómo se comporta la API mientras sube la demanda? ¿En qué punto de la subida empieza a degradarse el p95?".
- Steady (10:00 → 12:00): la carga se sostiene en el pico. Pregunta: "en la hora punta sostenida, ¿el p95 se mantiene dentro de lo aceptable, o se degrada con el tiempo?". Esta es la fase donde lees el número que reportas.
- Ramp-down (tarde): la carga baja. Pregunta: "cuando afloja la demanda, ¿el sistema se recupera —el p95 vuelve a niveles bajos— o queda tocado?".
El patrón: cada fase de la forma responde una pregunta distinta (arranque, sostenimiento, recuperación) que una carga plana no podría contestar.
Resumen y siguiente paso
En esta lección diste el salto conceptual del módulo: una prueba de carga tiene dos dimensiones, el tamaño (cuántos usuarios) y la forma (cómo evoluciona en el tiempo). El módulo 3 midió el tamaño con una carga constante; este módulo le da forma. Esa forma es el perfil de carga, y su expresión canónica es la ola ramp-up → steady → ramp-down: subir, sostener, bajar. La analogía del corazón lo fija: un ritmo plano solo te dice si aguantas ese ritmo; una prueba de esfuerzo con protocolo revela el arranque, el límite y la recuperación.
Lo viste con números reales: un generador Python que sube la concurrencia de 4 a 24 y la baja, con el p95 dibujando la ola (22 → 77 → 175 → 83 → 24 ms) y una simetría entre subida y bajada que delata una recuperación limpia. Y viste su equivalente en k6: la opción stages con su lista de tramos {duration, target}, presentada como contenido rotulado. La correspondencia es exacta: los stages de k6 son las etapas de concurrencia del generador Python.
Antes de avanzar deberías poder: explicar la diferencia entre el tamaño y la forma de una carga; nombrar las tres fases de un perfil (ramp-up, steady, ramp-down) y qué pregunta responde cada una; leer una tabla de p95 por etapa e interpretar la simetría subida/bajada como recuperación; y recordar la frontera (métricas fueron M3, thresholds serán M5, aquí es la forma).
Lo que sigue es el argumento a fondo. En la lección 2 defendemos con detalle por qué una carga constante no basta: qué preguntas responde una meseta plana, cuáles esconde, y cómo la idea de "forma de carga" nace precisamente de las preguntas que la carga constante deja sin contestar.
Recursos
- k6 — Opciones y
stages— la documentación oficial de la opción con la que se escribe la forma de la carga; la desmenuzamos en la lección 3. - k6 — Scenarios y executors (visión general) — el marco completo de cómo k6 modela perfiles de carga; base de las lecciones 5 y 6.
concurrent.futuresythreading— documentación de Python — las herramientas con las que el generador crea y varía la concurrencia (los "VUs") que dan forma a la carga ejecutada.- Google SRE Workbook — Implementing SLOs (carga y percentiles en el tiempo) — por qué el rendimiento se observa como una serie en el tiempo y no como un solo número; el marco detrás de mirar el p95 por etapa.