Módulo 4: Perfiles de carga y stages
5. Los executors: constant-vus vs ramping-vus
Descripción
Hasta ahora dibujaste formas de carga con stages sin preguntarte qué motor las ejecuta por debajo. Ese motor tiene nombre en k6: el executor. Un executor es el componente que decide cómo se aplica la carga —cuántos VUs hay, cuándo cambian, cómo se lanzan las iteraciones—. Cuando escribes stages, k6 usa un executor por defecto sin que lo notes; en esta lección lo sacamos a la luz y conocemos los dos executors que trabajan con VUs: constant-vus (mantiene un número fijo de usuarios virtuales durante una duración) y ramping-vus (sube y baja los VUs siguiendo unos stages —es exactamente lo que hace stages por debajo—). También conoces el bloque scenarios, la forma explícita y más poderosa de configurar un executor. Como siempre, k6 va como contenido rotulado y ejecutamos el equivalente en Python: una carga constante contra una por etapas, para que veas la diferencia de motor en números.
Conexión con el módulo: la lección 4 te dio las formas; esta te dice qué motor produce cada forma. Es un cambio de nivel: pasas de "qué forma quiero" a "cómo se lo pido a k6". Los dos executors de esta lección trabajan con VUs (usuarios concurrentes), que es el modelo que ya conoces desde el módulo 2. En la lección 6 viene el executor que cambia la pregunta entera —constant-arrival-rate, que fija la tasa de llegada en vez del número de usuarios—, así que esta lección es la mitad "por VUs" del tema executors, y la 6 es la mitad "por tasa de llegada". No tocamos thresholds (módulo 5): seguimos solo en cómo se aplica la carga.
La analogía: el director de orquesta
Piensa en una orquesta. La partitura dice qué notas tocar —eso es tu función default, el código que cada VU ejecuta—. Pero alguien tiene que decidir cuántos músicos tocan y cuándo entran o salen: ese es el director. Un director puede pedir "que toquen los 20 violinistas todo el movimiento, sin cambios" —un número fijo de ejecutantes—; o puede pedir "empiecen 5, que vayan entrando más hasta llegar a 20 en el clímax, y luego que salgan poco a poco" —un número que sube y baja—. Misma partitura, dos maneras de dirigir, dos sonidos distintos.
El executor es el director de tu prueba de carga. La partitura (el default) no cambia: sigue siendo "golpea /quote, verifica el status, espera". Lo que el executor decide es la coreografía de los VUs: cuántos hay en cada instante y cómo cambian. constant-vus es el director que pide un número fijo de músicos todo el rato. ramping-vus es el director que los hace entrar y salir siguiendo la partitura de los stages. Entender que el executor es una capa separada del código del VU es la idea clave: puedes correr exactamente el mismo default con distintos executors y obtener perfiles de carga completamente distintos.
Qué es un executor y dónde vive
En k6, la configuración de carga vive en export const options. Hay dos maneras de expresarla:
La forma corta (atajo): poner vus + duration, o stages, directamente en options. Es lo que has visto hasta ahora. Por debajo, k6 elige un executor por ti:
- Si pones
vus+duration→ usaconstant-vus. - Si pones
stages→ usaramping-vus.
La forma explícita: el bloque scenarios, donde nombras el executor y sus parámetros a mano. Es más verbosa pero más poderosa: te deja controlar cada detalle, correr varios escenarios a la vez, y usar executors que el atajo no expone (como el constant-arrival-rate de la lección 6). Un scenario es una carga con nombre; su campo executor dice qué motor la mueve.
Verlo con los dos executors de VUs, uno al lado del otro, deja claro el patrón.
constant-vus: un número fijo de usuarios
El executor constant-vus mantiene un número constante de VUs activos durante una duration. Es el motor de la forma constante (la meseta) de la lección 4. Así se ve en su forma corta y en su forma explícita —contenido rotulado, no ejecutado aquí—:
// CONTENIDO (no ejecutado aquí). Forma corta: k6 usa constant-vus por debajo.
export const options = {
vus: 24,
duration: '1m',
};
// CONTENIDO (no ejecutado aquí). Forma explícita, con scenarios:
export const options = {
scenarios: {
steady_load: {
executor: 'constant-vus',
vus: 24,
duration: '1m',
},
},
};
Las dos hacen lo mismo: 24 VUs fijos golpeando la API durante un minuto. Cada VU corre el default en bucle —petición, check, sleep, repetir— y el número de VUs nunca cambia: no hay subida ni bajada, es una meseta plana. Es la forma correcta para un smoke test (pocos VUs), para verificar un SLO en la carga esperada, o para un soak (una duration larga).
Su equivalente ejecutado es correr el generador Python a concurrencia fija. La etapa de pico de la corrida por etapas es exactamente eso —24 trabajadores constantes—, así que su número es el de un constant-vus de 24:
Qué esperar — con 24 VUs fijos, el p95 se asienta en un valor estable; es la latencia del estado estacionario. Salida real (la etapa constante de 24):
etapa VUs peticiones p95 (ms) promedio (ms)
steady (peak) 24 1775 174.63 63.00
Ese 174.63 ms es lo que un constant-vus: 24 te reportaría en su meseta. Un solo número honesto para el estado estable, sin subida ni bajada.
ramping-vus: los VUs que suben y bajan
El executor ramping-vus varía el número de VUs siguiendo una lista de stages —exactamente los tramos {duration, target} de la lección 3—. Es el motor que produce las formas rampa y spike, y el que stages usa por debajo cuando lo pones en la forma corta. Así se ve, corto y explícito:
// CONTENIDO (no ejecutado aquí). Forma corta: k6 usa ramping-vus por debajo.
export const options = {
stages: [
{ duration: '30s', target: 24 }, // ramp-up
{ duration: '1m', target: 24 }, // steady
{ duration: '30s', target: 0 }, // ramp-down
],
};
// CONTENIDO (no ejecutado aquí). Forma explícita, con scenarios:
export const options = {
scenarios: {
ramping_load: {
executor: 'ramping-vus',
startVUs: 0, // con cuántos VUs arranca
stages: [
{ duration: '30s', target: 24 },
{ duration: '1m', target: 24 },
{ duration: '30s', target: 0 },
],
gracefulRampDown: '30s', // deja terminar las iteraciones en curso al bajar
},
},
};
La forma explícita revela dos parámetros que el atajo esconde: startVUs (con cuántos VUs empieza; aquí lo ponemos en 0 para arrancar desde cero) y gracefulRampDown (cuánto tiempo da k6 para que las iteraciones en curso terminen ordenadamente cuando baja la carga, en vez de cortarlas de golpe). Por lo demás, es la misma ola de tres fases: ramping-vus interpola los VUs hacia cada target, como viste en la lección 3.
Su equivalente ejecutado es la corrida por etapas completa —el pool de trabajadores que crece y decrece—:
Qué esperar — a diferencia del constant-vus (un solo número), el ramping-vus da la ola entera: p95 subiendo en el ramp-up y bajando en el ramp-down. Salida real:
$ 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
Fíjate en la relación entre los dos executors: la fila steady (peak) de esta corrida (174.63 ms) es idéntica a lo que daría un constant-vus: 24. Tiene sentido: en la meseta, el ramping-vus es un constant-vus momentáneamente. La diferencia es que el ramping-vus además te da el antes (la subida) y el después (la bajada). Un constant-vus es como quedarte solo con la fila del medio.
Cuándo cada uno
La elección es directa una vez que entiendes qué produce cada motor:
constant-vuscuando tu pregunta es sobre un estado estable: un smoke test (pocos VUs, poca duración), verificar un SLO en la carga de pico conocida, o un soak (constante largo para buscar fugas). La forma es una meseta; el motor,constant-vus.ramping-vuscuando tu pregunta involucra la evolución: cómo se comporta el sistema mientras la carga sube (la rodilla), dónde se rompe (stress, subiendo hasta que cede), o cómo reacciona a un pico súbito (spike). La forma tiene subida y/o bajada; el motor,ramping-vus.
Y un detalle práctico: si empiezas con la forma corta (vus+duration o stages) y luego necesitas más control —varios escenarios a la vez, startVUs, gracefulRampDown, o mezclar executors—, migras al bloque scenarios explícito. La forma corta es un atajo cómodo; scenarios es el volante completo.
El executor es el "director": decide cuántos VUs hay y cuándo, separado del código que cada VU ejecuta.
constant-vusmantiene un número fijo (la meseta);ramping-vuslos sube y baja siguiendostages(la ola). El atajovus+durationusa constant-vus; el atajostagesusa ramping-vus. El bloquescenarioses la forma explícita, con todo el control.
Errores comunes
Creer que stages y ramping-vus son cosas distintas. Qué pasa: alguien ve un script con stages y otro con executor: 'ramping-vus' y piensa que son dos mecanismos separados. Por qué pasa: se ven diferentes en la página. Cómo detectarlo: si tu options tiene stages en la forma corta, ya estás usando ramping-vus —es el executor por defecto de stages—. Cómo corregirlo: entiende stages (corto) como un atajo de ramping-vus (explícito); son la misma coreografía, una con menos escritura.
Usar ramping-vus para lo que pide constant-vus (o al revés). Qué pasa: alguien monta un perfil con rampa para verificar un SLO en carga fija (metiendo ruido de subida/bajada en el número), o usa una constante para buscar la rodilla de degradación (que la constante no puede encontrar). Por qué pasa: no se conecta el motor con la pregunta. Cómo detectarlo: si tu pregunta es sobre un estado y usas rampa, tu número agregado sale contaminado; si es sobre la evolución y usas constante, te falta la subida/bajada. Cómo corregirlo: motor según la pregunta —estado estable → constant-vus; evolución → ramping-vus—.
Olvidar que el default es el mismo con cualquier executor. Qué pasa: alguien cree que cambiar de perfil exige reescribir la lógica del VU. Por qué pasa: se mezcla la capa del código (la partitura) con la del executor (el director). Cómo detectarlo: si estás tocando tu función default para cambiar la forma de la carga, te equivocaste de capa. Cómo corregirlo: deja el default quieto y cambia solo el executor/stages en options; la separación entre "qué hace cada VU" y "cuántos VUs y cuándo" es justo lo que te deja reusar el mismo script con muchos perfiles.
Ejercicios
Ejercicio 1 — ¿Qué executor usa este atajo? Para cada options en forma corta, di qué executor usa k6 por debajo. (a) { vus: 10, duration: '30s' }. (b) { stages: [{duration:'1m', target:50}] }. (c) { vus: 50, duration: '2h' }.
Ver solución
- (a)
constant-vus.vus+duration→ 10 VUs fijos por 30 s (un smoke test). - (b)
ramping-vus. La presencia destagesactivaramping-vus(aquí una rampa de 0 a 50 en 1 min). - (c)
constant-vus.vus+durationde nuevo, pero con una duración larguísima (2 horas): es un soak, una constante sostenida para buscar fugas.
Ejercicio 2 — Traduce corto a explícito. Reescribe este atajo como un bloque scenarios explícito con el executor correcto:
export const options = {
stages: [
{ duration: '20s', target: 30 },
{ duration: '40s', target: 30 },
{ duration: '20s', target: 0 },
],
};
Ver solución
export const options = {
scenarios: {
my_load: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '20s', target: 30 },
{ duration: '40s', target: 30 },
{ duration: '20s', target: 0 },
],
gracefulRampDown: '30s',
},
},
};
Es el mismo perfil (ramp-up a 30, steady 40 s, ramp-down a 0), ahora nombrado (my_load) y con el executor y sus extras (startVUs, gracefulRampDown) a la vista. El atajo stages producía exactamente esto.
Ejercicio 3 — Motor según la pregunta. Para cada objetivo, elige constant-vus o ramping-vus y justifica en una frase. (a) "Confirmar que Reservo cumple p95 < 500 ms con sus 24 usuarios de pico sostenidos." (b) "Ver en qué nivel de carga el p95 se dispara." (c) "Correr 40 VUs durante 3 horas para detectar una fuga de memoria." (d) "Probar un pico súbito de 300 usuarios y su recuperación."
Ver solución
- (a)
constant-vus. La pregunta es sobre el estado estable a una carga fija conocida (24 VUs); una meseta la responde directo. - (b)
ramping-vus. Encontrar la rodilla exige subir la carga y recorrer niveles; un ramp-up. - (c)
constant-vus. Un soak: carga constante, pero larga (3 horas). El motor es constante; la clave es la duración. - (d)
ramping-vus. Un spike (baseline → salto → baseline) es un caso deramping-vuscon stages abruptos; necesitas la subida de golpe y la bajada para la recuperación.
Resumen y siguiente paso
En esta lección subiste un nivel: del qué forma al qué motor. Un executor es el componente de k6 que decide cómo se aplica la carga —cuántos VUs y cuándo—, separado del default que cada VU ejecuta (el director frente a la partitura). Conociste los dos executors basados en VUs: constant-vus, que mantiene un número fijo de usuarios (la meseta; el motor del smoke, el SLO en carga fija y el soak), y ramping-vus, que sube y baja los VUs siguiendo stages (la ola; el motor de la rampa y el spike).
Viste que los atajos que ya usabas eligen el executor por ti —vus+duration → constant-vus; stages → ramping-vus— y que el bloque scenarios es la forma explícita, con control total (startVUs, gracefulRampDown, varios escenarios a la vez). Y lo conectaste con números: la meseta de un constant-vus: 24 (174.63 ms) es idéntica a la fila steady de un ramping-vus, porque en la meseta el ramping es un constant momentáneo; la diferencia es que el ramping además te da el antes y el después.
Antes de avanzar deberías poder: explicar qué es un executor y por qué está separado del default; decir qué executor usa cada atajo; traducir una forma corta a un scenarios explícito; y elegir constant-vus o ramping-vus según si la pregunta es sobre un estado o sobre una evolución.
Lo que sigue es el executor que cambia la pregunta de raíz. Los dos de esta lección fijan cuántos usuarios hay. En la lección 6 conocemos constant-arrival-rate, que fija en cambio cuántas peticiones por segundo llegan —la tasa de llegada—, un modelo distinto (abierto) que revela sobrecargas que el modelo por VUs esconde. Y lo verás ejecutado: una tasa por debajo y otra por encima de la capacidad de Reservo.
Recursos
- k6 — Scenarios y executors (visión general) — el marco de
scenariosy la lista de executors; la referencia central de esta lección y la 6. - k6 — Executor
constant-vus— el motor de la carga constante por VUs, con todos sus parámetros. - k6 — Executor
ramping-vus— el motor que hay debajo destages, constartVUsygracefulRampDown. threading— documentación de Python — cómo el generador implementa "el director": crea y ajusta los trabajadores concurrentes (los VUs) que producen cada forma.