Buscar artículos

Escribe para buscar en todos los artículos.

Ir al archivo de artículos
Desarrollo webk6GrafanaDockerRendimiento

Pruebas de carga con k6: probando el rendimiento de una web

Cómo hacer pruebas de carga con k6 y Docker: instalación, scripts y un laboratorio con humo, estrés, picos y compras para analizar el rendimiento web.

Fuentes revisadas el 14 de noviembre de 2026

Pruebas de rendimiento web con Grafana k6 y Docker: logotipo de k6 y curva ilustrativa de tráfico

Una página web puede cargar rápido cuando la visitamos nosotros solos y comportarse de otra manera cuando empiezan a entrar muchas visitas a la vez. Lo mismo ocurre con una aplicación web. En mi caso quería comprobar qué pasaba al aumentar el tráfico: si los tiempos de respuesta se mantenían, qué recursos consumía el servidor y cómo respondía cuando, además de consultar páginas, había usuarios haciendo compras.

Para estas pruebas he utilizado Grafana k6. En este artículo voy a mostrar cómo prepararlo con Docker, cómo escribir una primera prueba y cómo he ido aumentando la carga sobre la web. Después veremos los resultados del laboratorio y algunas cosas que encontré al cruzarlos con las métricas del servidor. Si no has utilizado k6, puedes empezar por la primera prueba y seguir después con los escenarios que te interesen.

En este artículo

Qué es k6 y para qué lo voy a utilizar

k6 es una herramienta de código abierto para realizar pruebas de carga y rendimiento. Nos permite definir un comportamiento mediante un script de JavaScript y ejecutarlo con varios usuarios virtuales, durante el tiempo y con el ritmo que configuremos. Un usuario virtual, o VU, es uno de los usuarios simulados que ejecutan ese script; no es una persona ni una ventana de navegador.

Por ejemplo, podemos pedirle que un usuario consulte una página cada pocos segundos, que ese número de usuarios aumente hasta 200 o que se consulten 100 páginas por segundo. Mientras se ejecuta la prueba, k6 recoge cuánto tardan las peticiones, cuáles fallan y si se cumplen los objetivos que hemos definido.

Una prueba de carga sirve para observar cómo responde la web con un tráfico determinado. Con una prueba de estrés aumentamos ese tráfico para buscar cuándo empieza a empeorar. Y con una prueba de resistencia lo mantenemos durante más tiempo para ver si aparecen problemas que una visita rápida no deja ver.

Los scripts están escritos en JavaScript, pero se ejecutan en el entorno de k6. No es necesario instalar Node.js para utilizarlos y tampoco podemos dar por hecho que cualquier librería de Node vaya a funcionar aquí.

En este laboratorio voy a usar el módulo HTTP de k6. Una petición HTTP es la solicitud que se envía al servidor para consultar una página o enviar datos; el servidor devuelve una respuesta con un estado y su contenido. Esto quiere decir que estoy probando las peticiones y respuestas de la aplicación: no se abre un navegador, no se dibuja la página y no se ejecuta su JavaScript de cliente. Consultar el HTML tampoco descarga automáticamente sus imágenes, hojas de estilo o scripts. Para medir el comportamiento en un navegador existe k6 browser, que tiene otro alcance. Los tiempos que veremos aquí no son una medida de cuánto tarda una persona en ver la página completa.

Dónde se ejecuta k6 y qué estamos midiendo

En este laboratorio hay dos equipos, cada uno con su trabajo:

Dónde Qué hace
Mi Mac, con k6 dentro de Docker Genera el tráfico, envía las peticiones HTTP y mide las respuestas que recibe.
El servidor donde está alojada la web Atiende esas peticiones. Allí recojo, por separado, su consumo de CPU, memoria y otros recursos.

No necesito instalar k6 en el servidor de la web. Lo ejecuto desde el Mac contra la URL que quiero probar. Los tiempos incluyen el recorrido de la red; la CPU y la memoria del servidor no aparecen automáticamente en el resumen de k6.

También podemos probar una web que se esté ejecutando en el propio Mac. En ese caso k6 y la web comparten los recursos del equipo, y el resultado puede verse condicionado por esa competencia. Para este laboratorio los he separado: el Mac genera la carga y el servidor remoto la recibe.

Instalar y ejecutar k6 con Docker

Voy a trabajar desde un Mac con Docker disponible mediante OrbStack. Docker permite ejecutar k6 dentro de un contenedor: un entorno separado con lo necesario para arrancar la herramienta. Puedes utilizar Docker Desktop o un equipo Linux con Docker; los comandos de este artículo están preparados para una terminal de macOS o Linux.

La documentación oficial de instalación incluye también paquetes para Windows y macOS. En mi caso he elegido el contenedor para no instalar k6 directamente en el equipo.

Antes de seguir, Docker debe estar instalado y en ejecución. Puedes comprobarlo con docker version: deben aparecer tanto el cliente como el servidor. Si no tienes Docker, puedes instalar OrbStack para Mac o Docker Desktop. No es necesario tener ambos.

Lo primero es descargar la imagen de k6 y comprobar la versión:

docker pull grafana/k6:2.4.0
docker run --rm grafana/k6:2.4.0 version

La versión utilizada en este laboratorio es k6 v2.4.0. Dejo la etiqueta indicada en los ejemplos para que una actualización de latest no cambie la herramienta entre una prueba y otra.

Creando una primera prueba

Antes de preparar el laboratorio completo, vamos a empezar con un script pequeño. Crea una carpeta de trabajo con mkdir -p k6 && cd k6 y, dentro, un fichero llamado primer-test.js con el siguiente contenido. Este primer ejemplo es independiente: no necesita ningún otro fichero.

import http from 'k6/http';
import { check, sleep } from 'k6';

const BASE_URL = (__ENV.BASE_URL || '').replace(/\/$/, '');
if (!/^https?:\/\//.test(BASE_URL)) {
    throw new Error('Indica la URL de tu entorno de pruebas mediante BASE_URL.');
}

export const options = {
    vus: 1,
    duration: '30s',
    thresholds: {
        http_req_failed: ['rate==0'],
        http_req_duration: ['p(95)<1000'],
        checks: ['rate==1'],
    },
};

export default function () {
    const respuesta = http.get(BASE_URL);
    check(respuesta, {
        'estado 200': (r) => r.status === 200,
        'incluye el cierre del HTML': (r) =>
            typeof r.body === 'string' && r.body.includes('</html>'),
    });
    sleep(1);
}

En options he configurado un usuario virtual durante 30 segundos. La función default contiene lo que va a repetir ese usuario: hacer una petición, comprobar la respuesta y esperar un segundo antes de volver a empezar.

Los check comprueban condiciones sobre cada respuesta. Aquí verifico el estado 200, que indica una respuesta HTTP correcta, y que llegue el cierre del HTML. Es una comprobación básica: una página de error también podría devolver 200 y contener esa etiqueta. En una prueba de tu aplicación conviene comprobar además un título, un texto o algún dato que identifique la respuesta correcta.

Los thresholds son los criterios para decidir si la prueba pasa. En este ejemplo no acepto errores HTTP ni checks fallidos, y pido que el 95 % de las peticiones tarde menos de un segundo. Un check fallido, por sí solo, no hace que k6 termine con un código de error; el umbral sobre checks es lo que incorpora esa condición al resultado de la prueba.

Para ejecutarlo, sitúate en la carpeta donde has guardado el fichero y utiliza:

docker run --rm -v "$PWD:/k6" -w /k6 \
  -e BASE_URL=https://tu-web-de-pruebas.example \
  grafana/k6:2.4.0 run primer-test.js

Sustituye la URL por la de un entorno tuyo preparado para las pruebas. El parámetro -v monta la carpeta actual dentro del contenedor y -w indica el directorio de trabajo. BASE_URL llega al script a través de __ENV, por lo que no tenemos que editar el fichero cada vez que cambiamos de entorno.

La URL que termina en .example es un marcador, no una web de pruebas real. Empieza contra tu entorno de pruebas; estos ejemplos no están pensados para lanzar tráfico a una web ajena.

Si la web o aplicación se ejecuta en el mismo Mac, recuerda que localhost dentro del contenedor es el propio contenedor. Con Docker Desktop u OrbStack puedes acceder al equipo anfitrión mediante host.docker.internal, seguido del puerto donde escuche tu aplicación. Por ejemplo, si la abres en el Mac en http://localhost:8000, desde k6 utilizarías -e BASE_URL=http://host.docker.internal:8000. Esto cambia el destino de las peticiones; k6 sigue ejecutándose en el contenedor.

Cómo leer el resultado de k6

Al terminar aparecerá el resumen en la terminal. Al principio puede parecer que hay demasiados datos, pero estos son los que voy a utilizar durante el artículo:

Métrica Qué nos indica
http_reqs Número de peticiones HTTP realizadas y ritmo medio.
http_req_failed Proporción de peticiones consideradas fallidas por k6. Por defecto, los estados esperados son 200–399; los checks permiten validar también el contenido.
http_req_duration Tiempo de envío, espera y recepción de la respuesta. No incluye el establecimiento de la conexión TCP ni la negociación TLS.
http_req_waiting Tiempo hasta el primer byte de la respuesta, o TTFB, observado desde el cliente. Incluye la latencia de red.
iterations Veces que se ha ejecutado la función del escenario. Una iteración es una repetición de ese recorrido: puede contener una petición o una compra completa con varias llamadas.
vus Usuarios virtuales activos.

Dentro de los tiempos veremos la media, el mínimo, el máximo y varios percentiles. Si el p95 es de 300 ms, aproximadamente el 95 % de las muestras queda por debajo de ese tiempo; el 5 % restante tarda más. El p50 es la mediana y el p99 nos permite fijarnos en una parte más pequeña de las respuestas lentas.

Voy a mirar esos percentiles durante las pruebas. Una media aceptable puede ocultar que algunas peticiones estén tardando bastante más de lo habitual.

El escenario sobre el que voy a trabajar

La aplicación del laboratorio está desarrollada en PHP y se ejecuta en un VPS, un servidor virtual, con 4 vCPU y unos 8 GB de RAM. Dentro del servidor tengo Caddy con FrankenPHP, PostgreSQL, Redis y varios procesos de cola. También hay tareas programadas y CrowdSec analizando el tráfico.

Las pruebas se lanzan desde un MacBook Pro M1 Pro con 16 GB de RAM. El servidor está lejos del generador de carga. El ping, una referencia del tiempo de ida y vuelta por la red, es de unos 124 ms. Por tanto, incluso cuando el servidor responde rápido, el resultado lleva incorporado el tiempo de la red.

He preparado seis escenarios. En k6, un escenario define qué función se ejecuta y cómo se reparte el trabajo. Estos son los que voy a utilizar:

Prueba Qué quiero comprobar
Humo Si el script y las rutas funcionan con un solo usuario.
Carga Cómo responde la web con usuarios navegando y haciendo pausas.
Estrés Qué ocurre al subir el ritmo de peticiones por escalones.
Picos Cómo responde y se recupera después de una subida repentina.
Visitas y compras Cómo conviven las consultas con un recorrido que crea pedidos.
Resistencia Si los tiempos y los recursos se mantienen con carga sostenida.

Los valores que siguen son los de mi laboratorio. Para probar otra web, empezaría con un usuario y aumentaría la carga poco a poco; no copiaría directamente los escalones más altos. Los umbrales también dependen de lo que se considere aceptable para cada operación.

Preparando el entorno

Antes de lanzar la batería he revisado las protecciones del servidor. En una prueba anterior, CrowdSec bloqueó mi IP durante cuatro horas. El tráfico de una prueba de carga también pasa por el WAF, el cortafuegos de aplicaciones web, y por los límites de peticiones de la aplicación, y puede quedarse detenido ahí antes de llegar a lo que queremos medir.

En este laboratorio utilicé una lista permitida temporal para la IP del generador y una excepción equivalente en la aplicación. Esto cambia el escenario: los resultados corresponden a esa configuración de pruebas, no a todas las restricciones de una visita normal. Al terminar retiré ambas excepciones.

Si necesitas hacer lo mismo, comprueba primero la IP que realmente ve el servidor, especialmente si hay IPv6 o un proxy de por medio. No hace falta desactivar las protecciones para todos los visitantes. En la documentación de CrowdSec puedes consultar cómo gestionar sus listas permitidas.

Las pruebas que crean pedidos las he separado de las que solo consultan páginas. Para repetirlas hay que tener datos de prueba, stock preparado y desactivados los envíos o integraciones que puedan afectar a personas reales. El recorrido que mostraré se detiene antes de realizar ningún pago.

Organizando los scripts

La primera prueba ya funciona por sí sola. Para los siguientes escenarios voy a reutilizar algunas funciones y guardar los resultados en JSON. Todo el código necesario está en los bloques del artículo; puedes copiarlo con el botón de cada bloque.

Desde la carpeta k6 que hemos creado antes, prepara dos subcarpetas y deja configurada la URL de tu entorno en esta terminal:

mkdir -p scripts resultados
export BASE_URL=https://tu-web-de-pruebas.example

Recuerda cambiar esa URL. Los siguientes comandos se ejecutan desde la carpeta k6, no desde scripts. Si abres otra terminal, tendrás que volver a definir BASE_URL.

Voy a guardar las páginas que quiero consultar en scripts/rutas.json:

[
    { "ruta": "/", "nombre": "inicio" },
    { "ruta": "/productos", "nombre": "catalogo" },
    { "ruta": "/condiciones", "nombre": "condiciones" },
    { "ruta": "/tienda", "nombre": "tienda" },
    { "ruta": "/resultados", "nombre": "resultados" },
    { "ruta": "/consulta", "nombre": "consulta" }
]

Son rutas de ejemplo: sustitúyelas por páginas que existan en tu web. ruta es el camino después del dominio y nombre la etiqueta con la que aparecerá en las métricas. Si no tienes una tienda, elimina esa entrada y el umbral específico de tienda del escenario de carga.

El módulo compartido estará en scripts/comun.js. Lee esas rutas, hace las peticiones y guarda el resumen. Lo dejo completo para que no falte ninguna función al copiar los escenarios:

Ver el código completo de scripts/comun.js
import http from 'k6/http';
import { check } from 'k6';
import { Trend } from 'k6/metrics';
import { textSummary } from 'https://jslib.k6.io/k6-summary/0.1.0/index.js';

export const BASE_URL = (__ENV.BASE_URL || '').replace(/\/$/, '');
if (!/^https?:\/\//.test(BASE_URL)) throw new Error('Indica la URL de tu entorno mediante BASE_URL.');

export const RUTAS = JSON.parse(open(__ENV.RUTAS || './rutas.json'));
if (!Array.isArray(RUTAS) || !RUTAS.length || RUTAS.some(r => !r.nombre || typeof r.ruta !== 'string' || !r.ruta.startsWith('/') || r.ruta.startsWith('//'))) {
    throw new Error('rutas.json debe contener una lista no vacía de rutas relativas y sus nombres.');
}

export const CABECERAS = {
    'Accept': 'text/html,application/xhtml+xml',
    'Accept-Encoding': 'gzip, deflate, br, zstd',
    'Accept-Language': 'es-ES,es;q=0.9',
    'User-Agent': 'k6-laboratorio/1.0',
};

export const ttfb = new Trend('ttfb_pagina', true);

export function rutaAlAzar() {
    return RUTAS[Math.floor(Math.random() * RUTAS.length)];
}

export function visitar(ruta) {
    const r = http.get(`${BASE_URL}${ruta.ruta}`, {
        headers: CABECERAS,
        tags: { name: ruta.nombre },
    });
    ttfb.add(r.timings.waiting, { pagina: ruta.nombre });
    check(r, {
        'status 200': (x) => x.status === 200,
        'incluye el cierre del HTML': (x) => typeof x.body === 'string' && x.body.includes('</html>'),
    });
    return r;
}

export function resumen(nombre) {
    return (data) => ({
        stdout: textSummary(data, { indent: '  ', enableColors: true }),
        [`resultados/${nombre}-resumen.json`]: JSON.stringify(data, null, 2),
    });
}

export function rutasMezcladas() {
    const rutas = [...RUTAS];
    for (let i = rutas.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [rutas[i], rutas[j]] = [rutas[j], rutas[i]];
    }
    return rutas;
}

La etiqueta name agrupa las peticiones por página y permite poner objetivos distintos a cada operación. ttfb_pagina es una métrica propia de tipo Trend, que guarda tiempos para calcular estadísticas como la media y los percentiles.

open() lee el JSON antes de empezar a ejecutar las visitas. La función resumen() mantiene el resultado en la consola y escribe una copia en resultados. Para mostrarlo utiliza una pequeña librería oficial, k6-summary, que k6 obtiene de la URL indicada en el import.

Los ficheros van a quedar así; los seis escenarios los iremos creando a continuación:

k6/
├── primer-test.js
├── scripts/
│   ├── comun.js
│   ├── rutas.json
│   ├── 01-humo.js
│   ├── 02-carga.js
│   ├── 03-estres.js
│   ├── 04-picos.js
│   ├── 05-resistencia.js
│   └── 06-compras.js
└── resultados/

Recogiendo las métricas del servidor

Además de los resultados de k6, quiero saber qué ocurre dentro del VPS mientras dura la prueba. Para el laboratorio he utilizado un recolector propio que guarda CPU, memoria y carga del sistema, consumo de los contenedores y conexiones a PostgreSQL. De esos registros salen las gráficas de recursos del artículo.

Para seguir los ejemplos de k6 no hace falta montar ese recolector. Si tu web está en un servidor Linux, puedes abrir una terminal en ese servidor, por ejemplo mediante SSH, y observar su actividad con:

vmstat 1

Es un comando de procps; si tu distribución no lo incluye, tendrás que instalar ese paquete. Muestra una muestra cada segundo. En la CPU, us es el tiempo de usuario, sy el del sistema, id el tiempo inactivo y wa la espera de entrada/salida. La primera fila resume el tiempo desde el arranque; para la prueba nos interesan las siguientes.

Si utilizas contenedores, en otra terminal del servidor puedes ver el consumo de cada uno:

docker stats

Con Ctrl+C sales de cualquiera de los dos comandos sin detener la web ni sus contenedores. Si quieres registrar los datos en un fichero o añadir métricas de la base de datos, necesitarás preparar esa recogida según tu instalación. Anota también cuándo empieza y termina cada prueba para poder compararlos con los resultados de k6.

Recomiendo comprobar la recogida antes de empezar. A mí el recolector detallado se me detuvo durante la prueba de resistencia y perdí parte de las muestras. Más adelante veremos qué datos pude conservar.

Comprobando el script con una prueba de humo

Ahora que tenemos preparada la herramienta, lo primero es comprobar que las rutas funcionan. Esta prueba utiliza un usuario durante un minuto, que recorre todas las páginas del JSON con una pausa de un segundo entre ellas.

El script es parecido al primero, pero utiliza las funciones compartidas:

Fichero: scripts/01-humo.js.

import { sleep } from 'k6';
import { RUTAS, visitar, resumen } from './comun.js';

export const options = {
    vus: 1,
    duration: '1m',
    thresholds: {
        http_req_failed: ['rate==0'],
        http_req_duration: ['p(95)<1000'],
        checks: ['rate==1'],
    },
};

export default function () {
    for (const ruta of RUTAS) {
        visitar(ruta);
        sleep(1);
    }
}

export const handleSummary = resumen('01-humo');

Para ejecutarlo desde la carpeta k6:

docker run --rm --user "$(id -u):$(id -g)" \
  -v "$PWD:/k6" -w /k6 -e BASE_URL \
  grafana/k6:2.4.0 run scripts/01-humo.js

El resumen se verá en la terminal y se guardará en resultados/01-humo-resumen.json. En los siguientes ejemplos solo cambia el nombre del script. Para detener una prueba antes de que termine, utiliza Ctrl+C.

En la ejecución del laboratorio se realizaron 48 peticiones, sin errores ni checks fallidos. El p95 fue de 300 ms y la mediana de 257 ms. Es mi referencia para comparar lo que ocurra cuando entren más usuarios.

Hay un detalle importante en estos primeros tiempos: la mediana del TTFB fue de unos 133 ms y la recepción del cuerpo de la respuesta rondó los 123 ms. Con una latencia de red de 124 ms, está claro que la distancia pesa bastante en el resultado.

El TTFB incluye tanto la espera en el servidor como el recorrido de la red. Restarle el ping no nos da el tiempo exacto de ejecución de PHP: son mediciones distintas. Si quiero separar ambas cosas, necesito medir en el servidor o repetir la prueba desde un punto más cercano. Para las siguientes comparaciones voy a mantener el mismo origen y destino.

Simulando usuarios que navegan por la web

Una vez comprobado que el script hace lo que esperamos, voy a aumentar la carga. En este caso quiero llegar a 200 usuarios virtuales. Cada uno consulta cuatro páginas y espera entre dos y seis segundos después de cada respuesta, como una aproximación al tiempo de lectura.

Para ello utilizo ramping-vus. El executor es la forma en que k6 programa el trabajo; este permite subir y bajar el número de usuarios de forma progresiva:

Fichero: scripts/02-carga.js.

import { sleep, group } from 'k6';
import { rutasMezcladas, visitar, resumen } from './comun.js';

export const options = {
    scenarios: {
        navegacion: {
            executor: 'ramping-vus',
            startVUs: 0,
            stages: [
                { duration: '2m', target: 200 },
                { duration: '8m', target: 200 },
                { duration: '1m', target: 0 },
            ],
            gracefulRampDown: '30s',
        },
    },
    thresholds: {
        http_req_failed: ['rate<0.01'],
        http_req_duration: ['p(95)<800', 'p(99)<1500'],
        'http_req_duration{name:tienda}': ['p(95)<1000'],
        checks: ['rate>0.99'],
    },
};

export default function () {
    const visita = rutasMezcladas().slice(0, 4);
    group('visita', () => {
        for (const ruta of visita) {
            visitar(ruta);
            sleep(2 + Math.random() * 4);
        }
    });
}

export const handleSummary = resumen('02-carga');
docker run --rm --user "$(id -u):$(id -g)" \
  -v "$PWD:/k6" -w /k6 -e BASE_URL \
  grafana/k6:2.4.0 run scripts/02-carga.js

stages contiene las etapas: dos minutos para llegar a 200 VUs, ocho manteniéndolos y uno para bajar a cero. rutasMezcladas() baraja las páginas y slice(0, 4) selecciona hasta cuatro. Fíjate también en el umbral de tienda: no tengo por qué evaluar todas las operaciones con el mismo objetivo.

Este escenario utiliza un modelo cerrado. Un usuario no empieza su siguiente paso hasta que termina el anterior y se cumple su pausa. Si una respuesta tarda más, ese usuario también tarda más en volver a pedir otra página.

Por eso 200 usuarios virtuales no equivalen a 200 peticiones por segundo. En la parte estable de esta prueba se produjeron unas 47 peticiones por segundo; el promedio de toda la ejecución, incluyendo subida y bajada, fue de unas 40.

Usuarios virtuales, peticiones por segundo, latencias y CPU durante la prueba de carga con 200 VUs

En la gráfica podemos ver cómo suben los usuarios y el tráfico mientras los tiempos de respuesta apenas cambian. Se realizaron 27.172 peticiones, todas las comprobaciones pasaron y el p95 quedó en 273 ms, muy cerca de la referencia de la prueba de humo.

La CPU del servidor estuvo en un 21 % de media. Con este recorrido, estas pausas y las páginas seleccionadas, la aplicación atendió la carga sin una degradación apreciable de los tiempos medidos.

También he separado las respuestas por página:

Distribución de los tiempos de respuesta de las seis páginas consultadas con k6

Las distribuciones son parecidas entre sí y ninguna de estas rutas destaca claramente por ser mucho más lenta que las demás. Son páginas públicas, con su caché y sin realizar escrituras. Cuando llegue a la compra estaré probando un trabajo diferente.

Aumentando el ritmo con una prueba de estrés

En la siguiente prueba quiero ver qué ocurre cuando exijo cada vez más trabajo. Para eso cambio a ramping-arrival-rate, un executor de modelo abierto.

Aquí configuro cuántas iteraciones por segundo deben empezar. Como la función de este escenario hace una única petición, el ritmo de iteraciones se corresponde con el de peticiones, salvo redirecciones u otras peticiones añadidas. Si una iteración hiciera cinco llamadas, la interpretación sería distinta.

Los escalones van de 50 a 350, dejando 30 segundos de transición y 90 segundos mantenidos en cada nivel:

Fichero: scripts/03-estres.js.

import { rutaAlAzar, visitar, resumen } from './comun.js';

const ESCALONES = (__ENV.ESCALONES || '50,100,150,200,250,300,350').split(',').map(Number);

export const options = {
    scenarios: {
        escalones: {
            executor: 'ramping-arrival-rate',
            startRate: ESCALONES[0],
            timeUnit: '1s',
            preAllocatedVUs: 200,
            maxVUs: 2000,
            stages: ESCALONES.flatMap((ritmo) => [
                { duration: '30s', target: ritmo },
                { duration: '90s', target: ritmo },
            ]),
        },
    },
    thresholds: {
        http_req_failed: ['rate<0.05'],
        http_req_duration: ['p(95)<2000'],
        dropped_iterations: ['count<100'],
    },
};

export default function () {
    visitar(rutaAlAzar());
}

export const handleSummary = resumen('03-estres');
docker run --rm --user "$(id -u):$(id -g)" \
  -v "$PWD:/k6" -w /k6 -e BASE_URL \
  grafana/k6:2.4.0 run scripts/03-estres.js

La función del escenario solo llama a visitar(rutaAlAzar()). No añado un sleep final, porque el ritmo ya lo está marcando el executor. timeUnit: '1s' indica que ese ritmo se expresa por segundo.

En este modelo hay que prestar atención a los VUs disponibles. preAllocatedVUs indica los que se preparan al principio; maxVUs, el límite hasta el que k6 puede ampliarlos. Si no hay uno libre cuando debe comenzar una iteración, esa iteración no se inicia y se cuenta en dropped_iterations.

Prueba de estrés por escalones: ritmo objetivo y conseguido, percentiles de latencia y CPU del servidor

Hasta alrededor de 250 peticiones por segundo, los tiempos se mantienen bastante próximos a los anteriores. En los escalones de 300 y 350 aparecen picos más frecuentes y la CPU se acerca a su capacidad.

El resumen de los 14 minutos muestra:

Peticiones HTTP:         162.991
Peticiones HTTP fallidas: 0 %
p95 de respuesta:        1.279 ms
p99 de respuesta:        1.666 ms
Iteraciones descartadas: 508
Umbral incumplido:       dropped_iterations < 100

Son cifras del laboratorio, presentadas de forma abreviada. Aunque las peticiones que llegaron a ejecutarse terminaron sin errores, la prueba no pasó: no se cumplió el umbral de iteraciones descartadas.

Esto no demuestra, por sí solo, que hayamos encontrado la capacidad máxima del servidor. También puede influir una asignación insuficiente de VUs o el propio generador. En esta ejecución k6 llegó a preparar 648 VUs, por debajo del máximo configurado de 2.000. Antes de fijar un límite definitivo repetiría la parte alta con más VUs preasignados y comprobaría cuándo se producen los descartes.

Con los datos que sí tenemos, la zona de unas 200–250 peticiones por segundo es una referencia para este recorrido corto de lectura. A partir de ahí veo una degradación clara. No utilizaría ese número como una garantía para cualquier página, una carga prolongada o el proceso de compra.

Mirando qué consume CPU

Para entender la gráfica anterior, he separado el consumo de cada contenedor:

Consumo de CPU de aplicación, CrowdSec, tareas programadas, Redis, PostgreSQL y procesos de cola

La aplicación llega a rozar el 290 % de CPU, mientras que CrowdSec alcanza el 92 % en las muestras de mayor consumo. En docker stats, un 100 % equivale aproximadamente a un núcleo ocupado, por lo que una cifra puede superar 100 en un equipo con varias CPU.

Estos datos apuntan a la CPU como el recurso que primero conviene investigar. CrowdSec sigue haciendo trabajo durante la prueba, aunque haya una excepción para la IP del generador. Para saber cuánto afecta al rendimiento tendría que hacer una comparación equivalente cambiando solo ese componente; su consumo aislado no me da esa respuesta.

PostgreSQL tiene bastante menos actividad en este escenario de lectura. Eso tampoco descarta que pueda convertirse en un límite con otras consultas o con más escrituras.

Los picos que se repiten cada minuto

En las gráficas hay otro comportamiento que llama la atención: algunos picos aparecen a intervalos regulares. Al agrupar las muestras por el segundo del minuto, se ve mejor:

Comparación del p95 según el segundo del minuto durante los escalones intermedios de estrés

Durante los primeros cinco segundos, el p95 ronda los 411 ms, frente a 281 ms en el resto del minuto. Ese momento coincide con las tareas programadas y con sus picos de CPU.

Es una pista para revisar qué trabajos se están ejecutando juntos. Para comprobar cuánto contribuye cada uno, lo siguiente sería repetir el tramo cambiando su planificación. La media de toda la prueba habría hecho mucho más difícil ver esta coincidencia.

Las cookies también cambian la prueba

Otro detalle que encontré fue el crecimiento de Redis. Durante el estrés pasó de unos 18 a 92 MB y la memoria no volvió al valor inicial al terminar.

Por defecto, k6 restablece las cookies de cada VU al empezar una nueva iteración. Dentro de una iteración sí mantiene las cookies entre peticiones. En este escenario, sin embargo, cada iteración contiene una sola visita, y la aplicación puede crear una sesión nueva en cada una.

Si quieres comprobarlo por separado, guarda este ejemplo como cookies.js en la carpeta k6. Muestra las cookies enviadas en cada una de tres iteraciones:

import http from 'k6/http';
export const options = { vus: 1, iterations: 3 };

export default function () {
    const r = http.get(__ENV.BASE_URL);
    console.log(`Iteración ${__ITER}: ${Object.keys(r.request.cookies).join(',') || 'sin cookies enviadas'}`);
}

Puedes ejecutarlo con el mismo comando de primer-test.js, cambiando el nombre por cookies.js. Que aparezca «sin cookies enviadas» solo demuestra este comportamiento si la web devuelve cookies en sus respuestas.

La opción noCookiesReset: true permite conservarlas entre iteraciones. Elegir una u otra depende de lo que queramos representar: visitas nuevas, personas que vuelven o usuarios que mantienen una sesión.

En esta ejecución mantuve el comportamiento por defecto. La carga de sesiones forma parte del resultado y hay que tenerla en cuenta al compararlo con tráfico de navegadores que reutilizan sus cookies.

Comprobando la respuesta a un pico de visitas

Después del aumento progresivo, voy a probar una subida repentina: pasar de 20 a 600 VUs en diez segundos, mantenerlos un minuto y volver a 20.

Utilizo de nuevo ramping-vus, con estas etapas:

Fichero: scripts/04-picos.js.

import { sleep } from 'k6';
import { rutaAlAzar, visitar, resumen } from './comun.js';

export const options = {
    scenarios: {
        pico: {
            executor: 'ramping-vus',
            startVUs: 20,
            stages: [
                { duration: '1m', target: 20 },
                { duration: '10s', target: 600 },
                { duration: '1m', target: 600 },
                { duration: '10s', target: 20 },
                { duration: '2m', target: 20 },
            ],
            gracefulRampDown: '10s',
        },
    },
    thresholds: {
        http_req_failed: ['rate<0.02'],
        http_req_duration: ['p(95)<3000'],
    },
};

export default function () {
    visitar(rutaAlAzar());
    sleep(1 + Math.random() * 2);
}

export const handleSummary = resumen('04-picos');
docker run --rm --user "$(id -u):$(id -g)" \
  -v "$PWD:/k6" -w /k6 -e BASE_URL \
  grafana/k6:2.4.0 run scripts/04-picos.js

Cada VU consulta una página y espera entre uno y tres segundos. Quiero observar cómo suben los tiempos durante el pico y si vuelven a su nivel anterior cuando baja la carga.

Subida de 20 a 600 usuarios virtuales, latencias y recuperación después del pico de tráfico

Se realizaron 20.274 peticiones, sin errores HTTP. Durante la parte de mayor carga el p95 fue de unos 472 ms, frente a los 274 ms anteriores al pico. Al bajar el tráfico volvió a rondar los 272 ms.

La CPU llegó al 94 %. Al bajar la carga, las respuestas recuperaron tiempos similares a los iniciales. Como es un modelo cerrado, el ritmo efectivo también depende de cuánto tarde en completarse cada visita.

Probando un recorrido de compra con sesión y CSRF

Hasta ahora solo he consultado páginas. En el siguiente escenario voy a mezclar ese tráfico con usuarios que recorren el proceso de compra hasta llegar a la página de pago.

Hay dos escenarios ejecutándose a la vez: 60 iteraciones por segundo de visitas y hasta 30 compradores virtuales. En las visitas utilizo constant-arrival-rate; en las compras, ramping-vus.

El recorrido de un comprador tiene cinco peticiones: abrir la tienda, elegir un producto, obtener el formulario, enviar los datos y consultar la página de pago. Aquí ya tenemos que mantener una sesión, leer tokens CSRF y comprobar redirecciones.

Esta parte requiere más adaptación que una visita a una página. Es un ejemplo para un formulario de tienda como el del laboratorio, no un script que funcione sin cambios en cualquier tienda o en WooCommerce. Si estás empezando, puedes seguir con las pruebas de lectura y volver a este recorrido después.

Leer los datos del formulario

Un token CSRF es un valor que la web incluye en el formulario y valida al enviarlo, junto con la sesión, para proteger esa operación. El token debe obtenerse de la respuesta del servidor. No lo copio del navegador ni lo dejo fijo en el script. Para leerlo utilizo k6/html. Este fragmento es una parte del script completo que dejo más abajo; tienda es la respuesta de la petición a esa página:

import { parseHTML } from 'k6/html';

function campo(respuesta, selector) {
    return parseHTML(respuesta.body).find(selector).first().attr('value');
}

const token = campo(tienda, 'input[name="_token"]');

También se leen los campos ocultos que la aplicación necesita para continuar. Los nombres publicados son genéricos: hay que adaptar esta parte a los formularios y las rutas de cada aplicación.

Mantener la sesión y comprobar las redirecciones

Al comenzar cada compra vacío las cookies del comprador anterior:

http.cookieJar().clear(BASE_URL);

Después dejo que el mismo VU mantenga su sesión durante todo el recorrido. Si se pierde esa cookie entre el GET del formulario y el POST, es posible que el token deje de ser válido.

Por defecto, k6 sigue las redirecciones. Para comprobar la respuesta original de un POST utilizo redirects: 0:

const enviar = http.post(`${BASE_URL}/checkout`, datos, {
    headers: CABECERAS,
    redirects: 0,
    tags: { name: 'compra_datos' },
});

const destino = enviar.headers.Location || '';
check(enviar, {
    'pedido creado': (r) =>
        r.status === 302 && /\/pedido\/[0-9a-f-]{36}\/pago/.test(destino),
});

El script completo comprueba además que el destino permanece en el entorno configurado antes de seguirlo. Los emails son datos de prueba con dominio .invalid; el teléfono y las demás validaciones deben ajustarse al entorno de pruebas.

Counter sirve para contar eventos, Rate para calcular una proporción de éxitos y Trend para guardar tiempos y obtener sus percentiles. Aquí los utilizo para seguir los pedidos creados, los recorridos completados y su duración.

Ver el código completo de scripts/06-compras.js

Fichero: scripts/06-compras.js.

import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { parseHTML } from 'k6/html';
import { Counter, Rate, Trend } from 'k6/metrics';
import { BASE_URL, CABECERAS, rutaAlAzar, visitar, resumen } from './comun.js';

const compraEspera = new Trend('compra_espera', true);
const pedidosCreados = new Counter('pedidos_creados');
const compraOk = new Rate('compra_ok');

export const options = {
    scenarios: {
        visitas: {
            executor: 'constant-arrival-rate',
            rate: 60,
            timeUnit: '1s',
            duration: '10m',
            preAllocatedVUs: 60,
            maxVUs: 300,
            exec: 'visitas',
        },
        compras: {
            executor: 'ramping-vus',
            startVUs: 0,
            stages: [
                { duration: '2m', target: 10 },
                { duration: '3m', target: 20 },
                { duration: '3m', target: 30 },
                { duration: '2m', target: 0 },
            ],
            exec: 'compra',
        },
    },
    thresholds: {
        'http_req_duration{scenario:visitas}': ['p(95)<800'],
        'http_req_duration{scenario:compras}': ['p(95)<1500'],
        'http_req_failed{scenario:compras}': ['rate<0.01'],
        compra_ok: ['rate>0.98'],
        compra_espera: ['p(95)<3000'],
    },
};

if (__ENV.SOLO_COMPROBAR === 'true') {
    options.scenarios = {
        comprobar_compra: {
            executor: 'shared-iterations',
            vus: 1,
            iterations: 1,
            maxDuration: '1m',
            exec: 'compra',
        },
    };
    options.thresholds = {
        http_req_failed: ['rate==0'],
        checks: ['rate==1'],
        compra_ok: ['rate==1'],
    };
}

export function visitas() {
    visitar(rutaAlAzar());
}

function campo(respuesta, selector) {
    return parseHTML(respuesta.body).find(selector).first().attr('value');
}

export function compra() {
    let espera = 0;
    const medir = (r) => { espera += r.timings.duration; return r; };
    http.cookieJar().clear(BASE_URL);
    const sinSeguir = { headers: CABECERAS, redirects: 0 };
    let ok = false;

    try {
        group('compra', () => {
            const tienda = medir(http.get(`${BASE_URL}/tienda`, { headers: CABECERAS, tags: { name: 'compra_tienda' } }));
            const productos = parseHTML(tienda.body).find('input[name="producto"]').toArray().map((x) => x.attr('value'));
            if (!check(tienda, { 'tienda 200': (r) => r.status === 200 && productos.length > 0 })) return;
            const producto = productos[Math.floor(Math.random() * productos.length)];
            sleep(2 + Math.random() * 3);
            const elegir = medir(http.post(`${BASE_URL}/tienda`, { _token: campo(tienda, 'input[name="_token"]'), producto },
                { ...sinSeguir, tags: { name: 'compra_elegir' } }));
            if (!check(elegir, { 'elegir 302': (r) => r.status === 302 })) return;
            const checkout = medir(http.get(`${BASE_URL}/checkout`, { headers: CABECERAS, tags: { name: 'compra_checkout' } }));
            if (!check(checkout, { 'checkout 200': (r) => r.status === 200 })) return;
            const doc = parseHTML(checkout.body);
            const datos = {
                _token: campo(checkout, 'input[name="_token"]'),
                intento: campo(checkout, 'input[name="intento"]'),
                [`items[${producto}]`]: '1',
                nombre: 'Prueba',
                apellidos: `Carga ${__VU}-${__ITER}`,
                telefono: __ENV.TELEFONO_PRUEBA || '000000000',
                acepto: '1',
                comunicaciones: '0',
            };
            doc.find('input[type="hidden"][name^="precios["]').each((_, el) => { datos[el.getAttribute('name')] = el.getAttribute('value'); });
            datos.email = `k6-${__VU}-${__ITER}-${Date.now()}@ejemplo.invalid`;
            datos.email_confirmation = datos.email;
            sleep(5 + Math.random() * 5);
            const enviar = medir(http.post(`${BASE_URL}/checkout`, datos, { ...sinSeguir, tags: { name: 'compra_datos' } }));
            const destino = enviar.headers.Location || '';
            const paginaPago = destino.startsWith('/') && !destino.startsWith('//') ? `${BASE_URL}${destino}` : destino;
            const mismoOrigen = paginaPago.startsWith(`${BASE_URL}/`);
            if (!check(enviar, { 'pedido creado': (r) => r.status === 302 && mismoOrigen && /\/pedido\/[0-9a-f-]{36}\/pago/.test(destino) })) return;
            pedidosCreados.add(1);
            const pago = medir(http.get(paginaPago, { headers: CABECERAS, tags: { name: 'compra_pago' } }));
            ok = check(pago, { 'pago 200': (r) => r.status === 200 });
        });
    } finally {
        compraOk.add(ok);
        if (ok) compraEspera.add(espera);
        sleep(3);
    }
}

export const handleSummary = resumen('06-compras');

Antes de ejecutarlo adapta /tienda, /checkout, los selectores, los campos y la comprobación de la página de pago a tu aplicación. La opción exec selecciona la función de cada escenario: visitas o compra.

Primero revisa el recorrido con un comprador y una sola iteración. SOLO_COMPROBAR=true activa ese modo, sin las visitas de fondo:

docker run --rm --user "$(id -u):$(id -g)" \
  -v "$PWD:/k6" -w /k6 -e BASE_URL -e SOLO_COMPROBAR=true \
  grafana/k6:2.4.0 run scripts/06-compras.js

Cuando esa comprobación pase y el entorno esté preparado para los pedidos de prueba, puedes ejecutar el escenario mixto de diez minutos sin esa variable:

docker run --rm --user "$(id -u):$(id -g)" \
  -v "$PWD:/k6" -w /k6 -e BASE_URL \
  grafana/k6:2.4.0 run scripts/06-compras.js

Medir si el recorrido termina correctamente

Además de los tiempos HTTP, he añadido tres métricas:

  • pedidos_creados: cuenta las respuestas que cumplen la comprobación de creación del pedido.
  • compra_ok: registra qué proporción de recorridos llega correctamente a la página de pago.
  • compra_espera: suma http_req_duration de las peticiones de las compras completadas. No incluye las pausas, la apertura de conexiones ni el tiempo de renderizado de un navegador.

En la ejecución del laboratorio se completaron 640 recorridos hasta la página de pago, sin checks fallidos. No son 640 pagos realizados. Las visitas siguieron entrando durante la prueba y su p95 quedó en unos 270 ms.

TTFB de cada paso del proceso de compra y pedidos creados por minuto durante el escenario mixto

La mediana del TTFB fue de unos 131 ms en la tienda, 152 ms al obtener el formulario y 174 ms al enviar los datos. El POST que valida y crea el pedido es el paso con más espera en esta medición. Sería uno de los primeros que revisaría si el recorrido comenzara a degradarse.

La suma de duraciones de una compra completada tuvo una media de 1,13 segundos y un p95 de unos 1,21 segundos. Es una forma de seguir el comportamiento del recorrido sin confundirlo con los segundos que hemos añadido para simular la lectura y el rellenado del formulario.

Manteniendo la carga durante treinta minutos

Para terminar, voy a mantener 80 iteraciones por segundo durante 30 minutos. Como en el estrés, cada iteración realiza una petición, pero aquí el ritmo se mantiene constante.

Me interesa ver si los tiempos cambian con el paso de los minutos y si se acumulan memoria, sesiones o conexiones. Utilizo constant-arrival-rate para mantener el ritmo sin aumentar ni reducir usuarios manualmente:

Fichero: scripts/05-resistencia.js.

import { rutaAlAzar, visitar, resumen } from './comun.js';

export const options = {
    scenarios: {
        constante: {
            executor: 'constant-arrival-rate',
            rate: Number(__ENV.RITMO || 80),
            timeUnit: '1s',
            duration: __ENV.DURACION || '30m',
            preAllocatedVUs: 100,
            maxVUs: 400,
        },
    },
    thresholds: {
        http_req_failed: ['rate<0.001'],
        http_req_duration: ['p(95)<600'],
        dropped_iterations: ['count==0'],
    },
};

export default function () {
    visitar(rutaAlAzar());
}

export const handleSummary = resumen('05-resistencia');
docker run --rm --user "$(id -u):$(id -g)" \
  -v "$PWD:/k6" -w /k6 -e BASE_URL \
  grafana/k6:2.4.0 run scripts/05-resistencia.js

Se realizaron 144.000 peticiones, sin errores HTTP ni iteraciones descartadas. El p95 quedó en 270 ms, próximo al de las pruebas anteriores de lectura.

Memoria, tiempos de respuesta y CPU durante la prueba de resistencia de treinta minutos con k6

La memoria del contenedor de la aplicación se mantuvo entre unos 157 y 171 MB en las muestras disponibles. La del servidor terminó en torno a 1,8 GB. En este intervalo no aparece un crecimiento sostenido de la memoria de la aplicación.

Redis sí siguió creciendo, de unos 119 a 150 MB en el tramo detallado, de aproximadamente trece minutos. El patrón encaja con las sesiones nuevas que provocan las iteraciones sin cookies. Prolongar esa pendiente hasta el límite de memoria sería una estimación, no un resultado que haya medido en esta ejecución.

También hubo un episodio de respuestas lentas alrededor del minuto 14. En esas muestras aumentó sobre todo el tiempo de recepción, mientras que el TTFB cambió menos. El siguiente paso sería revisar qué ocurrió durante la transferencia y repetir el tramo para ver si se reproduce.

Antes comentaba que el recolector me dio un problema. Dejó de escribir aproximadamente a los doce minutos, de modo que no tengo el mismo detalle de recursos para los treinta minutos completos. Para el resto utilicé las muestras que el propio servidor guardaba cada minuto. Los resultados de k6 sí cubren toda la prueba. Esta diferencia de resolución hay que recordarla al comparar los máximos de CPU con los de los escenarios anteriores.

Qué resultados he obtenido

En esta tabla he reunido los tiempos de las seis ejecuciones. Los percentiles corresponden a las peticiones HTTP del conjunto de cada prueba; no son el tiempo total de un recorrido de compra.

Prueba Peticiones HTTP Fallos HTTP p50 p95 p99
Humo: 1 VU, 1 min 48 0 % 257 ms 300 ms 350 ms
Carga: hasta 200 VUs, 11 min 27.172 0 % 256 ms 273 ms 282 ms
Estrés: 50–350 iteraciones/s, 14 min 162.991 0 % 259 ms 1.279 ms 1.666 ms
Pico: 20–600 VUs, 4 min 20 s 20.274 0 % 262 ms 456 ms 571 ms
Visitas y compras: 10 min 39.201 0 % 253 ms 272 ms 302 ms
Resistencia: 80 iteraciones/s, 30 min 144.000 0 % 253 ms 270 ms 303 ms

En total son 393.686 peticiones HTTP y 640 recorridos de compra hasta la página de pago. Todas las comprobaciones registradas pasaron, aunque el estrés incumplió el umbral de iteraciones descartadas. Por tanto, tener una columna de errores HTTP a cero no significa que todas las pruebas hayan pasado.

También he revisado el generador. En el estrés, el contenedor de k6 llegó aproximadamente al consumo de un núcleo y a unos 792 MB de memoria. No se observa una saturación global de CPU en las muestras del Mac. En la siguiente ejecución vigilaría también los momentos de ampliación de VUs y su coincidencia con las iteraciones descartadas.

La conclusión práctica que saco es que el recorrido de lectura se comporta bien en los escalones intermedios y empeora en los altos. Para dar un número de capacidad con más confianza, repetiría esos tramos durante más tiempo, ajustaría los VUs preasignados y probaría desde un punto con menor latencia. Las compras necesitarían su propia prueba de capacidad, porque hacen un trabajo diferente.

Ver los resultados en el panel web de k6

Además de la consola, k6 incluye un panel web. No hace falta instalar Grafana ni crear una cuenta para utilizarlo. Se activa con variables de entorno y permite exportar un informe HTML al terminar. Este comando vuelve a ejecutar la prueba de estrés; no abre los resultados de una ejecución anterior.

Con Docker, para verlo en directo desde el equipo anfitrión hay que publicar el puerto e indicar que el panel escuche dentro del contenedor en 0.0.0.0:

mkdir -p resultados

docker run --rm --user "$(id -u):$(id -g)" \
  -v "$PWD:/k6" -w /k6 -p 127.0.0.1:5665:5665 \
  -e BASE_URL=https://tu-web-de-pruebas.example \
  -e K6_WEB_DASHBOARD=true -e K6_WEB_DASHBOARD_HOST=0.0.0.0 \
  -e K6_WEB_DASHBOARD_EXPORT=resultados/03-estres-informe.html \
  grafana/k6:2.4.0 run scripts/03-estres.js

Durante la prueba puedes abrir el panel en http://127.0.0.1:5665. El puerto se publica únicamente en la interfaz local del equipo. Al finalizar se guarda resultados/03-estres-informe.html; ese es el fichero que puedes abrir después para revisar la ejecución.

Este es el informe de estrés del laboratorio:

Informe HTML del panel de k6 con el ritmo de peticiones, el p95 y los errores de la prueba de estrés

Y este, el de la prueba de carga:

Informe del panel web de k6 correspondiente a la navegación con hasta doscientos usuarios virtuales

Las pruebas muy cortas pueden terminar sin suficientes muestras para generar el informe; hay que tenerlo en cuenta si se reduce su duración.

El panel facilita consultar las métricas de k6. Para compararlas con las del VPS sigo utilizando los CSV y las marcas de inicio y fin de cada prueba. No incorpora automáticamente la CPU o la memoria del servidor remoto.

Si un navegador no muestra el HTML al abrirlo desde disco, se puede servir desde la carpeta de resultados con python3 -m http.server 8080 --bind 127.0.0.1 y consultarlo por HTTP.

Dejando el entorno como estaba

Una vez terminadas las pruebas, he parado el recolector y retirado las excepciones temporales de la aplicación y de CrowdSec. Después he comprobado que la limitación de peticiones volviera a estar activa.

Si has seguido la actividad con vmstat y docker stats, basta con salir de esos comandos con Ctrl+C. No hay que apagar el servidor ni detener los contenedores de la aplicación.

Los pedidos del entorno de pruebas quedaban pendientes y caducaban a los quince minutos, liberando las reservas. Comprobé esa caducidad y que el stock volviera al valor anterior. Este comportamiento pertenece a la aplicación del laboratorio: en otro sistema puede ser necesario limpiar esos datos de otra forma.

Los CSV y los informes también pueden contener URLs y datos del entorno. Por eso he conservado aquí las gráficas y los resultados agregados, y los ejemplos del artículo utilizan rutas y nombres genéricos.

Conclusión

Hasta aquí las pruebas con k6. Empezar con un usuario y una petición es sencillo; lo que requiere más atención es decidir qué queremos representar y cómo vamos a interpretar lo que salga.

En este laboratorio, ir aumentando la carga me ha permitido encontrar diferencias que no veía al navegar a mano: los picos que coinciden con las tareas programadas, el consumo de los distintos contenedores y las sesiones que crea una prueba sin cookies. También me he encontrado con una limitación de mi propia medición al perder parte del recolector.

Me quedo con esa forma de trabajar: comprobar primero el script, añadir carga de manera progresiva y mirar el servidor mientras se ejecuta. Después, repetir la parte que deja dudas antes de utilizar una cifra como referencia de capacidad.

¿Has utilizado k6 para probar alguna de tus webs o aplicaciones? Si tienes dudas sobre los ejemplos o quieres compartir cómo planteas tus pruebas de carga, déjame un comentario.

Logotipo de k6 obtenido del repositorio oficial de Grafana. Las marcas pertenecen a Grafana Labs y se utilizan para explicar su herramienta de código abierto. Este artículo es independiente y no está afiliado, patrocinado ni avalado por Grafana Labs.

Compartir artículo

Conversamos

Los comentarios se cargan desde Disqus cuando los abres.