Módulo 2: El script de k6 y los usuarios virtuales

1. Presentación del módulo: el script y el usuario virtual

Descripción

En el módulo 1 miraste una prueba de carga desde arriba: entendiste la diferencia entre "¿funciona?" y "¿aguanta?", conociste los tipos de prueba (smoke, load, stress, spike, soak) y supiste qué es k6 y cuál es su lugar. Incluso viste, de pasada, un primer script. Este módulo hace zoom sobre ese script: lo abre, lo desarma y te enseña a leer y escribir cada una de sus piezas, porque ese pequeño archivo .js es el plano de toda prueba de carga que harás con k6.

La buena noticia es que el script es corto: cabe en una pantalla. La idea que hay que entender de verdad no es la sintaxis, sino el modelo de ejecución que ese script pone en marcha —el VU, o Virtual User, un usuario virtual que repite tu código una y otra vez, en paralelo con muchos otros—. Cuando ese modelo hace clic, todo lo demás encaja: entiendes por qué existe la función default, por qué hay un sleep, qué significan las cifras del resumen, y por qué "10 VUs" no es lo mismo que "10 peticiones".

Conexión con el módulo: esta lección es el mapa. Aquí ves el módulo entero de un vistazo —las piezas del script y el modelo del VU— y entiendes la regla de juego que gobierna toda la guía: k6 se muestra como contenido, el generador Python se ejecuta de verdad. Todo lo que en las próximas lecciones aparezca rotulado como salida de Python fue medido en este entorno con Python 3.14.0 contra la API de Reservo; todo lo que aparezca como script o resumen de k6 es contenido de referencia, correcto pero no ejecutado aquí. Las lecciones 2 a 6 abren cada pieza del script; la 7 desarma el malentendido VUs-vs-iteraciones y te enseña a leer el resumen; la 8 lo junta todo en un mini-proyecto.

El guion de teatro y los actores

Piénsalo así. Un director de teatro monta una obra. Escribe un solo guion —la secuencia de acciones y frases que un personaje ejecuta— y luego contrata actores que representan ese mismo guion. Si contrata a diez actores, no escribe diez guiones: escribe uno y lo reparte. Los diez actores entran al escenario a la vez, cada uno recorre el guion a su ritmo, y cuando uno termina su parte, vuelve a empezar desde el principio. El director no controla a cada actor línea por línea; controla dos cosas: el guion (qué hacen) y el reparto (cuántos actores y por cuánto tiempo).

Un script de k6 es exactamente eso. La función default es el guion: la secuencia de acciones que un usuario simulado ejecuta —cotizar una sala, verificar el precio, esperar un momento—. Los VUs son los actores: usuarios virtuales que representan ese mismo guion, todos a la vez, cada uno en su propio bucle. Y options es la hoja de reparto: vus: 10 contrata diez actores, duration: '30s' los mantiene en escena treinta segundos. Tú escribes un guion y una hoja de reparto; k6 se encarga de poner a los diez actores a actuar en paralelo y de contar lo que pasó.

Un script de k6 tiene un solo guion (la función default) que muchos actores (los VUs) representan en paralelo, cada uno en un bucle. Tú describes qué hace un usuario y cuántos usuarios hay; k6 los ejecuta a todos a la vez y mide el resultado. No escribes diez guiones para diez usuarios: escribes uno.

La anatomía de un script de k6

Aquí está el script completo que vamos a desarmar a lo largo del módulo. Léelo entero una vez —no para entender cada detalle todavía, sino para ver la forma— y luego revisamos las piezas.

// quote_test.js — prueba de carga del endpoint /quote de Reservo.
// MOSTRADO COMO CONTENIDO: k6 no esta instalado en este entorno.
import http from 'k6/http';
import { check, sleep } from 'k6';

// options: la hoja de reparto. 10 usuarios virtuales durante 30 segundos.
export const options = {
  vus: 10,
  duration: '30s',
};

const BASE_URL = 'http://localhost:8000';

// default: el guion. Esto es lo que cada VU ejecuta una y otra vez en bucle.
export default function () {
  const payload = JSON.stringify({ room: 'Focus', tier: 'basic', hours: 3 });
  const params = { headers: { 'Content-Type': 'application/json' } };

  const res = http.post(`${BASE_URL}/quote`, payload, params);

  check(res, {
    'status is 200': (r) => r.status === 200,
    'price is 7500': (r) => r.json('price_cents') === 7500,
  });

  sleep(1); // think time: una pausa de un segundo, como un usuario real.
}

Son apenas veinte líneas, y cada bloque es una lección de este módulo:

  • import http from 'k6/http' y import { check, sleep } from 'k6' — traes las herramientas: el módulo HTTP (para hablar con la API) y las funciones check y sleep. k6 no usa require de Node ni instalas paquetes con npm: los módulos vienen dentro del propio k6. (Lección 3.)
  • export const options = {vus: 10, duration: '30s'} — la configuración de la corrida. Cuántos VUs (vus: 10) y por cuánto tiempo (duration: '30s'). Es la hoja de reparto, separada del guion. (Lección 6.)
  • export default function () { ... } — el guion del VU. Cada usuario virtual ejecuta este cuerpo, en bucle, hasta que se acaba la duración. Es el corazón del script. (Lección 2.)
  • http.post(...) — la petición HTTP: manda {room, tier, hours} a /quote con su header JSON. (Lección 3.)
  • check(res, {...}) — verifica que la respuesta fue correcta: status 200 y price_cents igual a 7500. Es cómo sabes que la API no solo respondió, sino que respondió bien, incluso bajo carga. (Lección 4.)
  • sleep(1) — el think time: la pausa que imita a un humano que lee la pantalla antes de la siguiente acción. Sin esto, tu VU es un robot enloquecido y tu prueba mide una tormenta irreal. (Lección 5.)

Fíjate en lo que no está: no hay un bucle for, no hay while, no hay nada que diga "repite esto diez veces". El bucle lo pone k6, no tú. Tú escribes lo que pasa una vez; k6 lo repite en cada VU, en cada iteración, durante toda la duración. Esa inversión —tú describes una vuelta, k6 gestiona las miles de vueltas— es la idea que hace de k6 una herramienta de carga y no un simple cliente HTTP.

El VU: el actor que repite el guion

El VU (Virtual User) es la unidad de concurrencia de k6. Un VU es un "usuario virtual": un hilo de ejecución independiente que corre tu función default en un bucle. Cuando pones vus: 10, k6 arranca diez de estos hilos a la vez. Cada uno:

  1. Ejecuta la función default de arriba abajo (una iteración).
  2. Al llegar al final, vuelve al principio y la ejecuta de nuevo.
  3. Repite hasta que se cumple la duration.

Diez VUs corriendo en paralelo simulan diez personas usando Reservo simultáneamente. Cada una cotiza, verifica, espera un segundo, y vuelve a cotizar —igual que un usuario real que hace una reserva tras otra—. La carga sobre tu servidor es la suma de todos esos VUs golpeando a la vez.

Y aquí está el malentendido más común, que desarmaremos con números en la lección 7: un VU no es una petición, y una iteración no es un usuario. Un solo VU, en treinta segundos, puede hacer decenas de iteraciones (una por cada vuelta al guion). Diez VUs durante treinta segundos, con un sleep(1) de por medio, hacen alrededor de trescientas iteraciones en total —no diez, no treinta, sino la suma de todas las vueltas de todos los actores—. Contar bien esa aritmética es la mitad de saber leer una prueba de carga.

El puente k6 ↔ Python: por qué verás dos caras de cada cosa

Como k6 no está instalado aquí, cada concepto lo verás en dos versiones, y es importante que entiendas la regla desde ya:

  • La cara de k6 (contenido). El script .js y su resumen. Son reales y correctos —verificados contra la documentación de k6—, pero van rotulados como contenido de referencia, nunca presentados como si los hubiéramos corrido en esta máquina.
  • La cara de Python (ejecutado). Un mini-generador de carga que modela los VUs de k6 con concurrent.futures.ThreadPoolExecutor: N workers (hilos) = N VUs, cada hilo repite un "guion" en bucle contra la API de Reservo, hace el equivalente de un check(), y cuenta iteraciones y errores. Esto sí se ejecuta, y su salida es real, medida en este entorno.

El mapeo entre las dos caras es casi uno a uno, y esa es la razón de usar Python: el modelo del VU deja de ser una metáfora y se vuelve algo que puedes contar.

IdeaEn k6 (contenido)En Python (ejecutado)
Un VUun virtual user, hilo interno de k6un worker del ThreadPoolExecutor
El guionexport default function () {...}una función default_fn() en un bucle while
Una peticiónhttp.post(url, body, params)urllib.request.urlopen(req)
Verificarcheck(res, {...})comparar status y price_cents y contar
Think timesleep(1)time.sleep(1)
Repartooptions = {vus, duration}argumentos vus y duration_s
Resumenel bloque checks/iterations/vusun resumen impreso con los mismos conteos

Para que veas que el puente es real desde ya, esta es la API canónica de Reservo respondiendo de verdad —el blanco de carga que golpearemos en todo el módulo—:

# GET /rooms — la lista de salas
$ curl -s http://127.0.0.1:PORT/rooms

Qué esperar. Salida real de la API canónica en este entorno:

{"rooms": [{"room": "Focus", "rate_cents": 2500}, {"room": "Studio", "rate_cents": 4000}, {"room": "Boardroom", "rate_cents": 8000}]}
# POST /quote — cotizar Focus/basic/3h (el ancla de la guia)
$ curl -s -X POST http://127.0.0.1:PORT/quote \
    -H 'Content-Type: application/json' \
    -d '{"room":"Focus","tier":"basic","hours":3}'

Qué esperar. Salida real:

{"price_cents": 7500}

Ese 7500 (Focus a 2500 centavos por hora, tres horas) es el ancla que ya conoces del módulo 1, y es exactamente el valor que el check('price is 7500') del script de k6 verifica. Las dos caras miran el mismo número.

La frontera de este módulo

Este módulo enseña la anatomía del script y el modelo del VU. Deliberadamente se detiene antes de varios temas, para no adelantar lo que otros módulos tratan a fondo:

  • Las métricas a fondo —los percentiles (p90/p95/p99), el throughput/RPS, la tasa de error, por qué el promedio miente— son el módulo 3. Aquí verás el bloque http_req_duration en el resumen y sabrás que existe, pero no lo diseccionaremos: cuando en la lección 7 leamos el resumen, nos concentramos en checks, iterations y vus.
  • Los perfiles de cargastages, rampas, spikes, los executors— son el módulo 4. Aquí la carga es plana: un número fijo de VUs durante una duración fija.
  • Los thresholds —los umbrales que hacen PASAR o FALLAR la prueba, el quality gate— son el módulo 5. Aquí check() te dice si una respuesta fue correcta, pero no hace fallar la corrida; esa es justo la diferencia que veremos en la lección 4.
  • Los checks y escenarios a fondogroup(), parametrizar datos, la correlación cotizar→reservar— son el módulo 6. Aquí check() se presenta en su forma básica: status 200 y precio correcto.

Y, como toda la guía, construir la API de Reservo no es el tema: es un blanco de carga que ya está dado (el servidor Python canónico). Nosotros la golpeamos y la medimos, no la construimos.

Errores comunes

Creer que hay que escribir el bucle a mano. Qué pasa: alguien que viene de escribir clientes HTTP mete un for dentro de default para "repetir la petición muchas veces". Por qué pasa: no ha interiorizado que el bucle lo pone k6. Cómo detectarlo: tu función default tiene un for/while que repite la petición, y de pronto cada iteración hace cientos de peticiones en vez de una. Cómo corregirlo: escribe en default lo que pasa una sola vez (una vuelta del guion); k6 lo repetirá por ti, en cada VU, en cada iteración. El bucle del VU es de k6, no tuyo.

Confundir "VU" con "petición" o con "iteración". Qué pasa: alguien lee vus: 10 y anota "10 peticiones", o ve 300 iterations y cree que hubo 300 usuarios. Por qué pasa: los tres conceptos suenan parecidos pero miden cosas distintas —VUs son actores concurrentes, iteraciones son vueltas totales al guion, peticiones son llamadas HTTP—. Cómo detectarlo: tus cuentas no cuadran (esperabas 10 y ves 300). Cómo corregirlo: recuerda la cadena —N VUs, cada uno hace muchas iteraciones, y cada iteración hace una o más peticiones—. La lección 7 lo mide con números reales.

Esperar que k6 run corra en Node. Qué pasa: alguien intenta node script.js o npm install k6 y nada funciona. Por qué pasa: aunque el script se escribe en JavaScript, k6 no es Node: es un binario de Go con su propio motor de JS, y sus módulos (k6/http, k6) solo existen dentro de k6. Cómo detectarlo: node se queja de que no encuentra 'k6/http'. Cómo corregirlo: los scripts de k6 se corren con k6 run script.js, no con Node. En este entorno k6 no está instalado, así que el script es contenido y su equivalente ejecutable es el generador Python.

Ejercicios

Ejercicio 1 — Nombra las piezas. Mira el script quote_test.js de esta lección y responde: (a) ¿Qué línea es el "guion" que cada VU repite? (b) ¿Qué línea es la "hoja de reparto" que dice cuántos usuarios y por cuánto tiempo? (c) ¿Qué línea imita la pausa de un humano entre acciones?

Ver solución
  • (a) El guion es export default function () { ... }: todo su cuerpo es lo que cada VU ejecuta en cada iteración (cotizar, verificar, esperar).
  • (b) La hoja de reparto es export const options = { vus: 10, duration: '30s' }: vus: 10 son los diez actores, duration: '30s' es cuánto están en escena.
  • (c) La pausa es sleep(1): un segundo de think time que imita a un usuario que lee la pantalla antes de la siguiente cotización.

Ejercicio 2 — Traduce el concepto. En el puente k6↔Python, ¿con qué se modela cada una de estas piezas de k6 en el generador de Python? (a) un VU; (b) sleep(1); (c) options = {vus: 10}.

Ver solución
  • (a) Un VU se modela con un worker (hilo) del ThreadPoolExecutor: cada worker repite el guion en bucle, igual que un VU.
  • (b) sleep(1) se modela con time.sleep(1) de la biblioteca estándar de Python: la misma pausa dentro del bucle.
  • (c) options = {vus: 10} se modela con un argumento vus que fija cuántos workers (max_workers) arranca el pool. Diez workers = diez VUs.

Ejercicio 3 — Ubica la frontera. Para cada pregunta, di si este módulo (M2) la responde o si pertenece a otro módulo (y a cuál): (a) "¿Cuál fue el p95 de latencia?" (b) "¿Cómo escribo la función que cada VU ejecuta?" (c) "¿Cómo hago que la prueba falle si la latencia pasa de 500 ms?" (d) "¿Cómo subo la carga en rampa de 0 a 50 VUs?"

Ver solución
  • (a) El p95 (percentiles de latencia) es M3 (métricas). Aquí solo verás que el bloque http_req_duration existe.
  • (b) Escribir la función default del VU es este módulo (M2) —es justo la lección 2—.
  • (c) Hacer que la prueba falle contra un umbral es M5 (thresholds). En M2, check() verifica pero no hace fallar la corrida.
  • (d) Subir la carga en rampa (stages) es M4 (perfiles de carga). En M2 la carga es plana: vus y duration fijos.

Resumen y siguiente paso

En esta lección viste el módulo entero de un vistazo. Un script de k6 es un guion (la función default, lo que cada usuario hace una vez) más una hoja de reparto (options con vus y duration), y k6 se encarga de poner a muchos VUs —usuarios virtuales— a representar ese guion en paralelo, cada uno en su propio bucle. Tú describes una vuelta; k6 gestiona las miles de vueltas y las mide. El malentendido a vencer —VU no es petición, iteración no es usuario— quedó planteado y lo desarmaremos con números en la lección 7.

También quedó clara la regla de juego de la guía: k6 se muestra como contenido rotulado (correcto, no ejecutado aquí) y el generador Python se ejecuta de verdad (salida real contra la API canónica de Reservo). Para cada pieza del script verás las dos caras, y el puente entre ellas es casi uno a uno —un VU es un hilo, sleep es time.sleep, check es una comparación que se cuenta—.

Antes de avanzar deberías poder: señalar en un script de k6 cuál es el guion, cuál el reparto y cuál el think time; explicar en una frase qué es un VU; y decir a qué módulo pertenece una pregunta sobre percentiles, rampas o thresholds. Lo que sigue, la lección 2, abre la primera pieza: la función default y el bucle del VU —y por primera vez pondremos un VU (un hilo de Python) a repetir el guion contra /quote y contaremos sus iteraciones reales—.

Recursos