Módulo 4: Perfiles de carga y stages

3. Los stages: ramp-up, steady, ramp-down

Descripción

Ya sabes por qué la carga necesita forma. Ahora aprendes a escribirla. La herramienta de k6 para dibujar un perfil de carga es la opción stages: una lista de tramos, cada uno un par {duration, target}, que le dice a k6 cuántos usuarios virtuales (VUs) debe haber activos y cuándo. Con esa lista, k6 va subiendo y bajando el número de VUs de forma continua, dibujando la ola que quieras. En esta lección desarmamos esa lista pieza por pieza: qué significa cada campo, cómo k6 interpola los VUs para llegar a cada target, y —lo más importante— las tres fases que casi siempre componen un perfil: ramp-up (la subida), steady (la meseta donde lees el resultado) y ramp-down (la bajada donde confirmas la recuperación). Como k6 no está instalado, su script va como contenido rotulado; y como siempre, ejecutamos el equivalente en Python contra Reservo para que veas las tres fases en números reales.

Conexión con el módulo: la lección 2 te convenció de que la forma importa; esta te da la sintaxis para expresarla. Es la lección más "de anatomía" del módulo, la que te deja leer cualquier stages que encuentres. Reutiliza la anatomía del script k6 del módulo 2 (la función default, http.post, check, sleep) —eso ya lo sabes— y le agrega la única pieza nueva: export const options = { stages: [...] }. Las formas concretas que puedes dibujar con stages (constante, rampa, spike) son la lección 4; los executors que hay debajo (que stages usa ramping-vus por defecto) son la lección 5. Aquí nos concentramos en la mecánica de la lista de tramos y en sus tres fases.

La analogía: la receta del control de crucero

Imagina el control de crucero de un coche, pero uno programable por tramos. En vez de fijar una sola velocidad, le das una receta: "durante los próximos 30 segundos, sube gradualmente hasta 100 km/h; mantén 100 durante un minuto; en los últimos 30 segundos, baja gradualmente hasta 0". El control de crucero no salta de golpe a cada velocidad: interpola, acelera o frena suave para llegar al objetivo de cada tramo justo cuando termina su duración. La receta tiene tres partes claras —acelerar, crucero, frenar— y cada tramo se define por dos cosas: cuánto dura y a qué velocidad quieres llegar al final.

Los stages de k6 son exactamente esa receta, pero la "velocidad" es el número de VUs (usuarios concurrentes). Cada tramo es un {duration, target}: duration es cuánto dura el tramo, target es el número de VUs al que quieres llegar al final de ese tramo. Y k6, como el control de crucero, interpola: no salta de golpe, sino que ajusta el número de VUs de forma continua desde donde estaba hasta el target, repartido a lo largo de la duration. Esa palabra —interpolar— es la clave para leer bien un stages: el target no es "cuántos VUs durante el tramo", es "cuántos VUs al terminar el tramo".

La anatomía de stages (contenido)

Aquí está el perfil canónico de tres fases en k6. Recuerda: contenido rotulado, correcto y fiel a la documentación de k6, no ejecutado aquí.

// 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 }, // tramo 1 — ramp-up:   de 0 a 20 VUs
    { duration: '1m',  target: 20 }, // tramo 2 — steady:    quedarse en 20 VUs
    { duration: '30s', target: 0 },  // tramo 3 — ramp-down: de 20 a 0 VUs
  ],
};

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);
}

Lee la lista stages de arriba abajo, recordando que cada target es "a cuántos VUs llegar al final del tramo", partiendo de donde te dejó el tramo anterior:

  • Tramo 1 — { duration: '30s', target: 20 }. Al empezar la prueba hay 0 VUs. Este tramo lleva los VUs de 0 a 20 a lo largo de 30 segundos, interpolando: a los 15 segundos habrá unos 10, a los 30 justos habrá 20. Es el ramp-up, la subida.
  • Tramo 2 — { duration: '1m', target: 20 }. Venimos de 20 VUs, y el target es otra vez 20. Como el punto de partida y el objetivo coinciden, k6 no sube ni baja: mantiene 20 VUs durante un minuto. Es el steady, la meseta.
  • Tramo 3 — { duration: '30s', target: 0 }. Venimos de 20, y el target es 0. Este tramo baja los VUs de 20 a 0 en 30 segundos. Es el ramp-down, la bajada.

Fíjate en el truco de la meseta: un tramo es "plano" (steady) cuando su target es igual al target del tramo anterior. No hay una palabra clave "steady" en k6; la meseta emerge de poner dos tramos seguidos con el mismo target. Y fíjate en la duración total: 30s + 1m + 30s = 2 minutos. La suma de las duration es cuánto dura toda la prueba.

Las tres fases y qué hace cada una

Un perfil ramp-up → steady → ramp-down no es una convención arbitraria: cada fase tiene un trabajo, y entender ese trabajo es lo que te deja leer los resultados sin equivocarte.

Ramp-up (la subida): calentar y recorrer los niveles. La subida gradual hace dos cosas. Primero, calienta el sistema: en el mundo real hay cachés que se llenan, conexiones que se abren, código que se compila justo a tiempo (JIT), pools que se inicializan. Arrancar de golpe en el pico mide ese arranque en frío, que no representa el tráfico real (que sube gradual). Segundo, recorre los niveles intermedios: al pasar por 5, 10, 15, 20 VUs, el ramp-up te deja ver en qué nivel el p95 empieza a torcerse —la rodilla de degradación—. Por eso, normalmente, el ramp-up no es donde lees tu número final: es una transición, útil para observar la subida pero contaminada por el calentamiento.

Steady (la meseta): el estado estable donde lees el resultado. Aquí la carga es constante en el target, el sistema ya está caliente y estabilizado, y las métricas se asientan. Esta es la fase de la que reportas el p95. Cuando alguien dice "el p95 bajo 20 usuarios es 175 ms", se refiere al p95 medido en la meseta, no durante la subida. La meseta debe durar lo suficiente para que las métricas se estabilicen —unos minutos en una prueba real, no unos segundos—; si es demasiado corta, el número aún trae el ruido del calentamiento.

Ramp-down (la bajada): confirmar la recuperación. La bajada gradual devuelve el sistema al reposo y te deja observar la recuperación. Comparas la latencia de la bajada con la de la subida al mismo nivel de carga: si regresan a valores parecidos, el sistema se recupera limpio; si en la bajada queda mucho más alto, algo no se liberó (una fuga, una cola que no se vacía). Muchas pruebas descuidan el ramp-down —"total, ya pasó el pico"—, y con eso se pierden justo la señal de las fugas. En k6, además, el ramp-down permite que las iteraciones en curso terminen ordenadamente en vez de cortarlas de golpe.

El equivalente ejecutado: las tres fases en Python

El generador Python dibuja esta misma forma con etapas de concurrencia: sube el número de trabajadores (VUs) hasta un pico y lo baja, midiendo el p95 de cada etapa. No interpola de forma continua como k6 —usa niveles discretos, que es más fácil de leer—, pero el mapeo es directo: cada etapa es un tramo de stages, y su número de VUs es el target.

Qué esperar — el p95 debe subir a lo largo del ramp-up, ser máximo en el steady, y bajar en el ramp-down espejando a la subida. Salida real contra Reservo:

$ 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

Mapea cada fila a su fase:

  • Las dos primeras filas (4 y 12 VUs) son el ramp-up: la carga sube y el p95 con ella (22.55 → 77.01). Aquí observas la subida, pero no reportas: es una transición.
  • La fila del medio (24 VUs) es el steady: el pico sostenido, 174.63 ms. Este es el número que reportarías como "p95 en la carga de pico".
  • Las dos últimas filas (12 y 4 VUs de vuelta) son el ramp-down: la carga baja y el p95 regresa (82.56 → 24.20). Aquí confirmas la recuperación comparando con la subida: 82.56 de bajada contra 77.01 de subida a 12 VUs; 24.20 de bajada contra 22.55 de subida a 4 VUs. Casi idénticos: recuperación limpia.

Así se corresponden el stages de k6 (contenido) y las etapas del generador (ejecutado): la misma ola de tres fases, escrita en dos lenguajes. El target de cada tramo de k6 es el número de VUs de cada etapa del generador.

Cómo se ve el resumen de k6 (contenido)

Cuando corres un script con stages, el resumen de k6 reporta las métricas agregadas de toda la prueba (subida, meseta y bajada juntas), más el rango de VUs. Así se ve —contenido rotulado, fiel a la forma del resumen de k6, no ejecutado aquí—:

// CONTENIDO (no ejecutado aquí): forma del resumen de k6. Ref: grafana.com/docs/k6
     scenarios: (100.00%) 1 scenario, 20 max VUs, 2m0s max duration
              * default: Up to 20 looping VUs for 2m0s over 3 stages

     http_req_duration...: avg=110ms min=8ms med=95ms max=480ms p(90)=180ms p(95)=210ms
     http_req_failed.....: 0.00%   ✓ 0     ✗ 2400
     http_reqs...........: 2400    20/s
     vus.................: 1       min=0   max=20
     vus_max.............: 20      min=20  max=20

Dos cosas importan aquí. Primero, la línea vus muestra max=20: k6 confirma que la forma llegó al pico de 20 VUs que pediste en el stages. Segundo —y es una trampa clásica—: el p(95) de ese resumen (210ms) es el p95 de toda la prueba mezclada, incluyendo la subida y la bajada, no solo el de la meseta. Como durante la subida y la bajada la carga fue menor, ese p95 agregado suele ser algo más bajo que el p95 real de la meseta. Si quieres el p95 limpio del estado estable, en k6 se aísla la meseta (con un umbral por escenario, o mirando la métrica en el tiempo con --out, que es material del módulo 7). Por eso el generador Python reporta el p95 por etapa: para que veas cada fase por separado, sin que la subida y la bajada diluyan el número del pico.

Errores comunes

Leer target como "VUs durante el tramo" en vez de "VUs al final del tramo". Qué pasa: alguien ve { duration: '1m', target: 20 } como segundo tramo y cree que "durante ese minuto hay 20 VUs", sin darse cuenta de que eso solo es cierto porque el tramo anterior también terminó en 20. Por qué pasa: la palabra target es ambigua si no recuerdas que k6 interpola. Cómo detectarlo: si un tramo tiene un target distinto al anterior, dentro de ese tramo el número de VUs está cambiando, no fijo. Cómo corregirlo: lee cada tramo como "desde los VUs actuales, llega a target en duration"; un tramo es plano solo si su target iguala al anterior.

Reportar el p95 agregado de toda la prueba como el p95 del pico. Qué pasa: alguien toma el p(95) del resumen de k6 (que mezcla subida, meseta y bajada) y lo presenta como "el p95 bajo carga de pico". Por qué pasa: es el número más visible del resumen. Cómo detectarlo: si tu perfil tiene ramp-up y ramp-down, el p95 agregado incluye instantes de carga baja que lo abaratan; no es el del estado estable. Cómo corregirlo: aísla la meseta —el p95 por etapa del generador, o un umbral/exportación por escenario en k6— para reportar el pico real.

Poner una meseta demasiado corta. Qué pasa: un stages con una meseta de pocos segundos y se reporta ese p95. Por qué pasa: se quiere que la prueba termine rápido. Cómo detectarlo: si la meseta no dura lo suficiente para que el sistema se caliente y las métricas se estabilicen, el p95 aún trae ruido de arranque. Cómo corregirlo: dale a la meseta una duración razonable (minutos en una prueba real) para que el estado estable sea de verdad estable; la subida rápida está bien, la meseta apurada no.

Ejercicios

Ejercicio 1 — Traduce el stages a palabras. Dado este perfil, describe qué hace cada tramo y cuánto dura la prueba en total:

stages: [
  { duration: '10s', target: 50 },
  { duration: '30s', target: 50 },
  { duration: '10s', target: 100 },
  { duration: '20s', target: 0 },
]
Ver solución
  • Tramo 1 (10s, 50): ramp-up de 0 a 50 VUs en 10 segundos.
  • Tramo 2 (30s, 50): steady en 50 VUs por 30 segundos (target igual al anterior → meseta).
  • Tramo 3 (10s, 100): ramp-up de nuevo, de 50 a 100 VUs en 10 segundos (una segunda subida, más abrupta).
  • Tramo 4 (20s, 0): ramp-down de 100 a 0 VUs en 20 segundos.

Duración total: 10 + 30 + 10 + 20 = 70 segundos. Es un perfil de dos escalones (sube a 50, sostiene, sube a 100, baja): útil para ver el comportamiento a 50 y luego empujar a 100.

Ejercicio 2 — ¿Cuál es la meseta? Para cada stages, di si tiene una fase steady (meseta) y en qué VUs. (a) [{duration:'30s',target:20},{duration:'30s',target:0}]. (b) [{duration:'20s',target:30},{duration:'1m',target:30},{duration:'20s',target:0}]. (c) [{duration:'1m',target:100}].

Ver solución
  • (a) No tiene meseta. Sube de 0 a 20 y baja de 20 a 0; es un triángulo (rampa arriba, rampa abajo) sin tramo plano. Nunca sostiene una carga.
  • (b) Sí, meseta en 30 VUs durante 1 minuto (el segundo tramo tiene el mismo target que el primero). Es el perfil canónico ramp-up/steady/ramp-down.
  • (c) Es toda meseta... con una subida implícita. Un solo tramo que va de 0 a 100 en 1 minuto: es una rampa continua hacia arriba, sin un tramo plano explícito. Si quisieras sostener 100, necesitarías un segundo tramo {duration:'...', target:100}.

Ejercicio 3 — Diseña las tres fases. Quieres probar Reservo con una carga de pico de 40 VUs: una subida de medio minuto, una meseta de dos minutos (donde leerás el p95) y una bajada de medio minuto. Escribe el stages y di de qué tramo reportarías el número.

Ver solución
stages: [
  { duration: '30s', target: 40 }, // ramp-up:   0 -> 40 VUs
  { duration: '2m',  target: 40 }, // steady:    sostener 40 VUs (aquí lees)
  { duration: '30s', target: 0 },  // ramp-down: 40 -> 0 VUs
]

Reportarías el p95 del segundo tramo (la meseta de 2 minutos a 40 VUs): es el estado estable, ya caliente y estabilizado. La subida y la bajada las miras para la rodilla y la recuperación, pero el número "oficial" del rendimiento a la carga de pico sale de la meseta. Duración total: 3 minutos.

Resumen y siguiente paso

En esta lección aprendiste a escribir la forma de la carga. La opción stages de k6 es una lista de tramos {duration, target}, donde duration es cuánto dura el tramo y target es el número de VUs al que llegar al final de él —partiendo de donde te dejó el tramo anterior—. k6 interpola los VUs de forma continua hacia cada target, como un control de crucero programable por tramos. Una meseta (steady) no es una palabra clave: emerge de poner dos tramos seguidos con el mismo target.

Sobre todo, entendiste las tres fases y su trabajo: el ramp-up calienta el sistema y recorre los niveles intermedios (observas la subida, pero no reportas de ahí); el steady es el estado estable donde lees el p95 que reportas; y el ramp-down confirma la recuperación al comparar la bajada con la subida al mismo nivel. Lo viste ejecutado en Python —el p95 por etapa dibujando la ola 22 → 77 → 175 → 83 → 24 ms— y como contenido en k6, con la advertencia de que el p(95) del resumen agregado no es el del pico (mezcla subida y bajada).

Antes de avanzar deberías poder: leer un stages y decir qué hace cada tramo y cuánto dura la prueba; identificar cuál es la fase de meseta; explicar por qué se reporta el p95 del steady y no el del ramp-up ni el agregado; y mapear cada fase a su fila en la salida del generador.

Lo que sigue es ampliar el repertorio de formas. En la lección 4 recorremos las tres formas canónicas que puedes dibujar con stages —constante, rampa gradual y spike (subida abrupta)— y qué revela cada una, con un spike ejecutado de verdad contra Reservo para que veas la diferencia entre subir despacio y subir de golpe.

Recursos