Cada vez aparecen en el mercado más herramientas que nos ayudan a realizar pruebas de carga. He tenido la suerte de trabajar con Apache Jmeter y con otras posiblemente menos conocidas, como Google Lighthouse, pero en este caso he querido investigar un poco sobre Grafana k6, ya que me han hablado muy bien de ella y haciendo pruebas resulta muy interesante.

Grafana k6 es una herramienta open-source para lanzar cargas en aplicaciones web y medir su rendimiento. Pese a tener parte de pago, nos ofrece muchas opciones gratuitas, las cuales nos ayudan a realizar pruebas de rendimiento en nuestros proyectos de una forma fácil.

Preparando el entorno de pruebas

Antes de nada asegúrate de tener Node.js instalado en tu ordenador. Puedes descargarlo e instalarlo desde el sitio web oficial de Node.js.

Abre una terminal y ejecuta el siguiente comando para instalar k6 en tu sistema.

npm install -g k6

Una vez completada la instalación, puedes verificar que k6 está instalado correctamente ejecutando el siguiente comando, debería mostrar la versión de k6 instalada.

k6 version

Para lanzar los scripts que veremos a continuación tendrás que guardar el código en un archivo con extensión .js (por ejemplo, load_test.js) y ejecutar el siguiente comando en la terminal:

k6 run load_test.js

Ahora ya estás listo para utilizar k6.

Tipos de pruebas de carga

Muchas veces se tiende a confundir los distintos tipos de pruebas de carga, en este artículo me gustaría ver cada uno de los que podemos encontrar y como los implementamos con Grafana k6:

Smoke tests

Comenzamos con los famosos smoke tests. Estos son un tipo de pruebas de carga para verificar de manera rápida las funcionalidades más importantes de tu aplicación. De esta manera, podemos ver de manera sencilla si tiene fallos que interrumpan el buen funcionamiento de la misma o si con una carga baja devuelve algún tipo de error.

Vamos a hacer un escenario de una prueba de humo no funcional. Un único usuario realiza diferentes peticiones (GET, POST, PATCH, DELETE) a los diferentes endpoints que ofrece el API de Grafana k6.

import { describe } from 'https://jslib.k6.io/functional/0.0.3/index.js';
import { Httpx, Request, Get, Post } from 'https://jslib.k6.io/httpx/0.0.2/index.js';
import { randomIntBetween, randomItem } from "https://jslib.k6.io/k6-utils/1.1.0/index.js";

export let options = {
  thresholds: {
    checks: [{threshold: 'rate == 1.00', abortOnFail: true}],
  },
  vus: 1,
  iterations: 1
};

const USERNAME = `user${randomIntBetween(1, 100000)}@example.com`; 
const PASSWORD = 'superCroc2023';

let session = new Httpx({baseURL: 'https://test-api.k6.io'});

export default function testSuite() {

  describe('01. Fetch public crocs', (t) => {
    let responses = session.batch([
      new Get('/public/crocodiles/1/'),
      new Get('/public/crocodiles/2/'),
      new Get('/public/crocodiles/3/'),
      new Get('/public/crocodiles/4/'),
    ], {
      tags: {name: 'PublicCrocs'},
    });

    responses.forEach(response => {
      t.expect(response.status).as("response status").toEqual(200)
        .and(response).toHaveValidJson()
        .and(response.json('age')).as('croc age').toBeGreaterThan(7);
    });
  })

  describe(`02. Create a test user ${USERNAME}`, (t) => {

    let resp = session.post(`/user/register/`, {
      first_name: 'Crocodile',
      last_name: 'Owner',
      username: USERNAME,
      password: PASSWORD,
    });

    t.expect(resp.status).as("status").toEqual(201)
      .and(resp).toHaveValidJson();
  })

  describe(`03. Authenticate the new user ${USERNAME}`, (t) => {

    let resp = session.post(`/auth/token/login/`, {
      username: USERNAME,
      password: PASSWORD
    });

    t.expect(resp.status).as("Auth status").toBeBetween(200, 204)
      .and(resp).toHaveValidJson()
      .and(resp.json('access')).as("auth token").toBeTruthy();

    let authToken = resp.json('access');
    session.addHeader('Authorization', `Bearer ${authToken}`);

  })

  describe('04. Create a new crocodile', (t) => {
    let payload = {
      name: `Croc Name`,
      sex: randomItem(["M", "F"]),
      date_of_birth: '2023-01-25',
    };

    let resp = session.post(`/my/crocodiles/`, payload);

    t.expect(resp.status).as("Croc creation status").toEqual(201)
      .and(resp).toHaveValidJson();

    session.newCrocId=resp.json('id');
  })

  describe('05. Update the croc', (t) => {
    let payload = {
      name: `New name`,
    };

    let resp = session.patch(`/my/crocodiles/${session.newCrocId}/`, payload);

    t.expect(resp.status).as("Croc patch status").toEqual(200)
      .and(resp).toHaveValidJson()
      .and(resp.json('name')).as('name').toEqual('New name');

    let resp1 = session.get(`/my/crocodiles/${session.newCrocId}/`);

  })

  describe('06. Delete the croc', (t) => {

    let resp = session.delete(`/my/crocodiles/${session.newCrocId}/`);

    t.expect(resp.status).as("Croc delete status").toEqual(204);
  });

}

Esta herramienta ofrece una serie de información por terminal una vez terminada la ejecución donde se pueden ver ordenadas todas las pruebas realizadas a los diferentes endpoints, comprobando así que el API funciona correctamente.

Comprobamos que el API funciona correctamente.

Si por alguna razón la aplicación no estuviera funcionando correctamente y empezasen a dar error algunos endpoints, Grafana k6 marcará en rojo los resultados y añadirá en qué validaciones han fallado.

Me recuerda a las herramientas como Postman o Newman en la manera de devolver y visualizar estos resultados por terminal.

Visualizar estos resultados por terminal.

Si analizamos la gráfica de rendimiento de nuestro sistema una vez realizada la prueba comprobamos que la carga del sistema es muy baja. Como decíamos anteriormente, este tipo de pruebas en realidad no son para medir la capacidad de carga de un sistema, sino para comprobar que ciertas funcionalidades básicas son correctas.

Comprobar que ciertas funcionalidades básicas son correctas.

Stress tests

Este es un tipo de prueba muy interesante que sirve para evaluar el rendimiento de tu aplicación con los objetivos de evaluar la estabilidad y disponibilidad en condiciones extremas durante un corto periodo de tiempo y determinar los límites de la misma.

En este caso, incluiremos usuarios concurrentes para generar un rendimiento más alto. Hay que tener en cuenta que este número de usuarios debe moverse entre el número de usuarios que tu aplicación normalmente soporta y los que crees que es capaz de soportar, sin sobrepasar el límite.

Una vez más vamos a ver un ejemplo sencillo con Grafana K6 y la gráfica de carga para entender mejor el concepto de este tipo de pruebas.

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

export const options = {
  stages: [
    { duration: '2m', target: 100 }, 
    { duration: '5m', target: 100 },
    { duration: '2m', target: 200 }, 
    { duration: '5m', target: 200 },
    { duration: '2m', target: 300 }, 
    { duration: '5m', target: 300 },
    { duration: '2m', target: 600 }, 
    { duration: '5m', target: 600 },
    { duration: '10m', target: 0 }, 
  ],
};

export default function () {
  const BASE_URL = 'https://test-api.k6.io';
  const responses = http.batch([
    ['GET', `${BASE_URL}/public/crocodiles/1/`, null, { tags: { name: 'PublicCrocs' } }],
    ['GET', `${BASE_URL}/public/crocodiles/2/`, null, { tags: { name: 'PublicCrocs' } }],
    ['GET', `${BASE_URL}/public/crocodiles/3/`, null, { tags: { name: 'PublicCrocs' } }],
    ['GET', `${BASE_URL}/public/crocodiles/4/`, null, { tags: { name: 'PublicCrocs' } }],
  ]);

  sleep(1);
}

En este ejemplo comenzamos a cargar nuestro sistema con 100 usuarios y vamos aumentando sucesivamente el número de usuarios hasta llegar a 600, creando una especie de gráfico en escalera. Si nos fijamos la carga máxima de usuarios previstos está por encima de 750, por lo que los resultados deben ser satisfactorios y soportar esta carga sin problemas.

Ejemplo con 100 usuarios y vamos aumentando hasta 600.

Spike tests

Como variante de las pruebas de estrés encontramos los spike tests o pruebas de picos. En este caso no aumenta gradualmente la carga, sino que intenta saturar de manera inmediata la aplicación con un aumento extremo de la misma.

Te recomiendo realizar este tipo de pruebas si vas a realizar una campaña de publicidad y esperas un gran número de visitas, pero antes quieres comprobar la manera de comportarse tu aplicación ante un aumento inesperado de tráfico o si será capaz de estabilizarse y recuperarse una vez que este tráfico haya disminuido.

Podemos definir la reacción de tu aplicación de diferentes maneras:

Si no se degrada el rendimiento y el tiempo de respuesta es similar al período de tiempo con poco tráfico podemos decir que ha reaccionado de manera excelente.

Podemos decir que ha reaccionado bien cuando el tiempo de respuesta ha sido más lento cuando había más carga de tráfico, pero no se producen errores pudiendo resolver todas las solicitudes.

Si, por el contrario, empieza a devolver errores ante tantas solicitudes, pero se recupera una vez disminuido el tráfico, podemos decir que tu aplicación ha reaccionado de manera mejorable.

Finalmente, ha reaccionado mal si no responde a las solicitudes quedando bloqueada incluso una vez pasado el pico de tráfico.

Una vez más vamos a utilizar el API que proporciona esta herramienta para ver un ejemplo de los mismos. Comenzamos con una carga baja de 1 minuto con 100 usuarios concurrentes, después, suponiendo que la carga máxima que soporta la aplicación fuese de 1400 usuarios, realizamos una sobrecarga durante 3 minutos con 1500 usuarios y finalmente volvemos a un período de recuperación con una carga baja.

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

export const options = {
  stages: [
    { duration: '10s', target: 100 }, 
    { duration: '1m', target: 100 },
    { duration: '10s', target: 1500 }, 
    { duration: '3m', target: 1500 }, 
    { duration: '10s', target: 100 }, 
    { duration: '3m', target: 100 },
    { duration: '10s', target: 0 },
  ],
};
export default function () {
  const BASE_URL = 'https://test-api.k6.io';

  const responses = http.batch([
    ['GET', `${BASE_URL}/public/crocodiles/1/`, null, { tags: { name: 'PublicCrocs' } }],
    ['GET', `${BASE_URL}/public/crocodiles/2/`, null, { tags: { name: 'PublicCrocs' } }],
    ['GET', `${BASE_URL}/public/crocodiles/3/`, null, { tags: { name: 'PublicCrocs' } }],
    ['GET', `${BASE_URL}/public/crocodiles/4/`, null, { tags: { name: 'PublicCrocs' } }],
  ]);

  sleep(1);
}

Así se vería una gráfica de este tipo de tests:

Gráfica de este tipo de tests con más usuarios.

Soak tests

Si quieres descubrir los problemas de rendimiento y fiabilidad relacionados con problemas de memoria o fallos en la infraestructura, los soak tests o pruebas de durabilidad, son tu aliado. Estos se centran principalmente en la fiabilidad y los problemas de rendimiento que tiene el sistema durante un largo período de tiempo. Simulan en tan solo unas horas el tráfico que puede tener la aplicación en varios días.

Con estas pruebas hay que tener cuidado por los costes que pueden generarse de infraestructura así que si tienes un presupuesto ajustado primero calcula el coste antes de realizarlas.

Te recomiendo lanzar estas pruebas después de que el resto de pruebas de carga hayan terminado correctamente y que tu sistema se encuentre estable.

Son el último paso para dar por concluido un sistema fiable así que vamos a ver un ejemplo básico para entenderlas mejor:

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

export const options = {
  stages: [
    { duration: '2m', target: 400 }, 
    { duration: '3h56m', target: 400 }, 
    { duration: '2m', target: 0 },
  ],
};

const API_BASE_URL = 'https://test-api.k6.io';

export default function () {
  http.batch([
    ['GET', `${API_BASE_URL}/public/crocodiles/1/`],
    ['GET', `${API_BASE_URL}/public/crocodiles/2/`],
    ['GET', `${API_BASE_URL}/public/crocodiles/3/`],
    ['GET', `${API_BASE_URL}/public/crocodiles/4/`],
  ]);

  sleep(1);
}

Durante un periodo de casi 4 horas, 400 usuarios concurrentes realizaron peticiones a nuestra aplicación, debiendo soportarlas sin problemas. Como vemos en la gráfica, en este caso no nos acercamos a la carga máxima prevista, ya que nuestro objetivo es la fiabilidad y no comprobar la estabilidad como mencionamos en las pruebas de estrés.

Ejemplo de objetivo fiabilidad.

¿Qué métricas se pueden obtener?

Grafana K6 ofrece una amplia variedad de métricas para medir el rendimiento de tus pruebas de carga, algunas de las más comunes que encontrarás en los logs de la terminal son:

Amplia variedad de métricas.

Para conocer más en detalle estas métricas relacionadas con los tiempos de respuesta puedes visitar la documentación oficial donde vienen detalladas.

Además, puedes personalizar las métricas para adaptarlas a tus necesidades específicas, y ofrece la posibilidad de exportar las métricas a una base de datos para su visualización y análisis.

¿Cómo recopilar esas métricas?

Para recopilar las métricas del punto anterior, se pueden realizar de dos maneras. En primer lugar, guardarlas en un fichero con un formato concreto. Los formatos que nos permite Grafana k6 son los siguientes:

k6 run --out csv=metrics.csv smoke_test.js

k6 run --out csv=metrics.gz smoke_test.js

k6 run --out json=metrics.json smoke_test.js

En segundo lugar, también se pueden guardar en tiempo real, en este caso no entraré en detalles porque este punto podría dar para otro artículo, pero es interesante saber que podemos integrar la herramienta Grafana k6 con Amazon CloudWatch o Grafana Cloud, entre otros.

Análisis de sus fortalezas y debilidades

Después de haber probado la herramienta, hablar con compañeros del sector y conocer las diferentes opiniones y experiencias con Grafana k6 voy a hacer un resumen de lo que más gusta y lo que menos.

Todos coincidimos en que destaca la manera tan fácil de instalar y poner en marcha, no hace falta lidiar con requisitos adicionales. Al estar escritas las pruebas en JS, hay bastante flexibilidad para crear módulos o importar bibliotecas. Además, tiene una documentación muy completa para cuando estás dando los primeros pasos.

¡Qué sería de una herramienta sin comunidad!, hoy en día existen blogs muy buenos que sirven de apoyo si quieres profundizar en las pruebas de carga con Grafana k6 o si necesitas integrar con otras herramientas. Grafana k6 puede integrarse con herramientas como Jenkins, Gitlab, TeamCity, Azure Pipelines, CircleCI o Github Actions.

El rendimiento de la herramienta al lanzar las pruebas es muy bueno, esto es gracias a que está basado en Go.

Soporta casi cualquier herramienta de monitorización en tiempo real, se nota que está bajo el techo de Grafana.

Otro punto positivo es que tiene un soporte técnico que responde muy rápido.

Como punto negativo, con esta herramienta se pueden ver dashboards muy interesantes de las métricas con los resultados que se obtienen de las pruebas, pero son de pago.

Conclusiones

Cuando pienso en implementar algo nuevo en un proyecto yo siempre me pregunto qué valor aporta al mismo y en mi opinión considero fundamental tener implementados en nuestro proyecto este tipo de pruebas, en este caso con la herramienta k6 porque es una herramienta open-source como veíamos anteriormente que se pueden implementar de manera sencilla, rápida y aporta bastante información.

Es una ayuda para nosotros para detectar de manera rápida y eficiente los problemas que puede haber en el ciclo de desarrollo y tener una primera base fiable de pruebas en cada despliegue. Por otro lado, ayuda a los desarrolladores a corregir los errores encontrados antes de que causen mayores problemas en entornos productivos.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.