Pruebas de carga distribuidas con k6 para sistemas modernos
En un mundo monolítico, las pruebas de carga eran un asunto relativamente sencillo: simplemente dirigir una herramienta hacia un único punto final y aumentar la presión. En el panorama actual de sistemas distribuidos, microservicios y funciones sin servidor, este enfoque es peligrosamente insuficiente. El rendimiento de un sistema moderno no es un único número; es una propiedad emergente de docenas de servicios discretos e interconectados que se comunican a través de una red. Un cuello de botella podría no estar en su API principal, sino en un servicio de autenticación secundario, una cola de mensajes saturada o una tabla de base de datos mal indexada a tres niveles de distancia.
Comprender este comportamiento sistémico bajo carga es fundamental. No hacerlo no solo pone en riesgo tiempos de respuesta lentos, sino que también conlleva el riesgo de fallos en cascada, agotamiento de recursos y cortes catastróficos.
Este artículo proporciona una guía técnica y práctica para directores de tecnología (CTO) e ingenieros senior, sobre cómo implementar pruebas de carga robustas para sistemas distribuidos utilizando k6. Nos enfocaremos en la implementación práctica, desde la creación de scripts para flujos de usuario complejos hasta la ejecución de pruebas a gran escala en Kubernetes, y, lo más importante, la correlación de métricas del lado del cliente con su pila de observabilidad del lado del servidor.
Equipo de Ingeniería de Software Compartido bajo Demanda, mediante Suscripción.
Acceda a un equipo flexible y compartido de ingeniería de software bajo demanda a través de una suscripción mensual predecible. Desarrolladores, diseñadores, ingenieros de QA y un gerente de proyecto gratuito le ayudan a crear MVPs, escalar productos e innovar con tecnologías modernas como React, Node.js y más.
¿Por qué k6 para sistemas distribuidos?
Mientras que herramientas como Apache JMeter han sido populares, k6 ofrece un enfoque moderno y centrado en el desarrollador, especialmente adecuado para arquitecturas distribuidas:
- Alto Rendimiento, Bajo Impacto: k6 está escrito en Go. Utiliza un único proceso y una arquitectura basada en bucles de eventos, lo que le permite generar una carga significativa desde una sola máquina con un mínimo consumo de CPU y memoria. Esto es crucial para pruebas rentables.
- Scripting Orientado a Desarrolladores (JavaScript ES6): Las pruebas se escriben en JavaScript. Esto reduce la barrera de entrada, ya que sus ingenieros no necesitan aprender un lenguaje específico del dominio o navegar por una interfaz de usuario compleja. Los scripts son código, trátelos como tal: controle de versiones, revisión de código y modúleslos.
- Métricas e Umbrales Integrados: k6 proporciona métricas cruciales (latencia p95/p99, tasas de solicitud, tasas de error) de forma inmediata. Más importante aún, le permite definir criterios de paso/fallo explícitos (Umbrales) directamente en su script, lo que lo hace ideal para la integración CI/CD.
- Extensibilidad: k6 soporta gRPC, WebSockets, Kafka y otros protocolos comunes en sistemas distribuidos, no solo HTTP.
- Integración de Observabilidad: k6 está diseñado para integrarse directamente en las modernas plataformas de observabilidad, enviando métricas a Prometheus, Grafana, Datadog y New Relic.
Fase 1: Creación de escenarios complejos para usuarios
Un sistema distribuido rara vez sirve a una única solicitud sin estado. Los usuarios siguen un flujo: se autentican (accediendo a un servicio de autenticación), consultan un catálogo (accediendo a un servicio de productos) y realizan una compra (accediendo a un servicio de pedidos, lo que puede activar un servicio de pago y un servicio de inventario). Tu script de prueba debe modelar esta realidad.
Estructura base del script k6
Un script de k6 tiene dos partes principales: el objeto de opciones, que define el perfil de carga, y la función predeterminada, que contiene la lógica ejecutada por cada Usuario Virtual (VU).
import http from 'k6/http';
import { check, sleep, group } from 'k6';
// 1. OPTIONS: Define the load profile
export const options = {
stages: [
{ duration: '1m', target: 100 }, // Ramp-up to 100 VUs over 1 minute
{ duration: '3m', target: 100 }, // Stay at 100 VUs for 3 minutes
{ duration: '1m', target: 0 }, // Ramp-down to 0 VUs
],
thresholds: {
// 95% of requests must complete below 500ms
'http_req_duration': ['p(95)<500'],
// 99% of requests must be successful
'http_req_failed': ['rate<0.01'],
// The 'login' group must have a 99.9% success rate
'checks{group:::User Login}': ['rate>0.999'],
},
};
// 2. DEFAULT FUNCTION: The VU logic
export default function () {
const BASE_URL = 'https://api.your-system.com';
let authToken;
// Group 1: User Login (Auth Service)
group('User Login', () => {
const loginPayload = JSON.stringify({
email: `user_${__VU}@example.com`, // Parameterize data per VU
password: 'supersecretpassword',
});
const loginParams = {
headers: { 'Content-Type': 'application/json' },
};
const res = http.post(`${BASE_URL}/v1/auth/login`, loginPayload, loginParams);
check(res, {
'login successful (status 200)': (r) => r.status === 200,
'auth token received': (r) => r.json('token') !== '',
});
if (res.json('token')) {
authToken = res.json('token');
}
});
// Only proceed if login was successful
if (!authToken) {
return; // Abort this iteration
}
const authParams = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
},
};
// Group 2: Browse Products (Product Service)
group('Browse Products', () => {
const res = http.get(`${BASE_URL}/v2/products?category=electronics`, authParams);
check(res, {
'get products successful (status 200)': (r) => r.status === 200,
});
sleep(1.5); // Simulate user think time
});
// Group 3: Place Order (Order Service)
group('Place Order', () => {
const orderPayload = JSON.stringify({
productId: 'abc-123',
quantity: 1,
});
const res = http.post(`${BASE_URL}/v1/orders`, orderPayload, authParams);
check(res, {
'order placement successful (status 201)': (r) => r.status === 201,
});
});
sleep(2); // Wait before starting a new session
}
Principales conclusiones de este documento:
- Grupos: Utilizamos
group()para organizar las solicitudes en transacciones lógicas (Inicio de sesión de usuario,Navegar productos). Esto proporciona métricas agregadas para cada paso, lo que le permite identificar cuál parte del flujo está fallando o es lenta. - Verificaciones:
check()valida las respuestas. Estas no son afirmaciones; no detienen la prueba. Recopilan métricas de éxito/fracaso, que luego utilizamos en nuestrosumbral. - Umbrales: Esto es su SLO/SLA como código. La prueba devolverá un código de salida no nulo (fallando su CI pipeline) si
p(95)la latencia excede los 500 ms o si la tasa de error supera el 1%. - Parametrización de datos: Utilizamos
__VU(una variable específica de k6 para el ID del Usuario Virtual) para crear nombres de usuario únicos. En una prueba real, cargarías esto de un array o archivo compartido para evitar el uso de cachés y simular la variabilidad del mundo real. - Estado: Capturamos el
authTokende la respuesta de inicio de sesión y lo pasamos en las solicitudes posteriores, simulando una sesión de usuario real y con estado.
Fase 2: Ejecución a escala distribuida
Una única instancia de k6, aunque eficiente, no puede simular la carga de millones de usuarios. Para un sistema distribuido, debe realizar una prueba distribuida. El objetivo es generar carga desde múltiples "máquinas de generación de carga", todas controladas por un único controlador.
Mientras que k6 Cloud ofrece una solución gestionada, "con un solo clic", para esto, un enfoque auto-alojado en Kubernetes proporciona el máximo control y rentabilidad para una organización técnica. Lo logramos utilizando el k6-operator.
El k6-operator introduce una Definición de Recurso Personalizada (CRD) para Kubernetes, permitiéndote definir una prueba de carga distribuida de forma declarativa, al igual que un Deployment o un Service.
Equipo de Ingeniería de Software Compartido Bajo Demanda, mediante Suscripción.
Acceda a un equipo flexible y compartido de ingeniería de software bajo demanda a través de una suscripción mensual predecible. Desarrolladores, diseñadores, ingenieros de control de calidad y un gestor de proyectos gratuito le ayudan a construir MVPs, escalar productos e innovar con tecnologías modernas como React, Node.js y más.
Paso a paso: Pruebas distribuidas con k6-Operator
1. Requisito: Instalar k6-operator
# Ensure you are on the correct K8s context
kubectl apply -f https://github.com/grafana/k6-operator/releases/latest/download/k6-operator.yaml
2. Empaquete su script de k6 en un ConfigMap
El operador necesita acceso a tu script.js. La forma más sencilla es cargarlo en una ConfigMap.
kubectl create configmap my-load-test-script --from-file=script.js
3. Defina el K6 Recurso Personalizado
Cree un archivo YAML (por ejemplo, test-run.yaml) para definir la ejecución distribuida. Aquí reside el poder.
apiVersion: k6.io/v1alpha1
kind: K6
metadata:
name: my-distributed-test
spec:
# 1. Parallelism: Number of k6 worker pods to spin up
parallelism: 10
# 2. Script: Reference the ConfigMap created in Step 2
script:
configMap:
name: my-load-test-script
file: script.js
# 3. Arguments: Pass k6 CLI flags (e.g., VUs, duration)
# This overrides the 'options' in the script, allowing for dynamic test profiles.
arguments: --vcs 1000 --duration 10m # 1000 VUs total, split across 10 pods
# 4. Observability: Send metrics to your stack
runner:
env:
# Example: Configure k6 to output to Prometheus Remote-Write
- name: K6_PROMETHEUS_RW_SERVER_URL
value: "http://prometheus-remote-write-endpoint.monitoring.svc.cluster.local/api/v1/write"
- name: K6_PROMETHEUS_RW_TREND_STATS
value: "p(95),p(99),min,max,avg,med"
4. Ejecutar la prueba
Simplemente aplique el manifiesto:
kubectl apply -f test-run.yaml
Kubernetes ahora realizará lo siguiente:
- Lea la
documentación K6. - Inicie un único
pod k6-controller. - Inicie 10
pods k6-worker(según lo definido porla paralelización: 10). - El controlador distribuye automáticamente las 1000 VUs (
--vcs 1000) entre los workers (100 VUs cada uno). - Cada pod de worker ejecuta el mismo
script.js, transmitiendo sus métricas a tu backend configurado (por ejemplo, Prometheus).
Ahora cuenta con un marco de pruebas de carga escalable, repetible y declarativo que se ejecuta de forma nativa en tu propia infraestructura.
Fase 3: El Enlace Crítico: Correlación de métricas del cliente y del servidor
Este es el paso más importante, y el que se suele pasar por alto con mayor frecuencia.
Ejecutar la prueba en la Fase 2 le indicará qué ocurrió desde la perspectiva del cliente (por ejemplo, "El /v1/orders p95 tuvo una latencia de 3000ms"). No le indicará por qué.
La razón principal se encuentra en tus servidores:
- ¿Se agotaron los recursos de CPU del servicio de
order-service? ¿Se agotó su conexión a la base de datos? - ¿Se produjo un tiempo de espera en la llamada gRPC al servicio de
- inventory-service
servicio de gestión de inventario? ¿Hubo un aumento en el retraso de los consumidores de Kafka? - ¿Hubo un aumento en el retraso en el consumo de mensajes de Kafka?
Para encontrar la razón, debe correlacionar las métricas del lado del cliente de k6 con sus datos de observabilidad del lado del servidor en una única línea de tiempo compartida.
Cómo implementar la correlación
1. Inyectar el contexto de seguimiento de k6
Su sistema de seguimiento distribuido (por ejemplo, OpenTelemetry, Jaeger, Datadog APM) se basa en la propagación de contexto, normalmente a través de encabezados HTTP como transparenciatraceparent
import http from 'k6/http';
import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js';
// ... (options and other setup) ...
export default function () {
// Generate a unique trace ID for this entire user flow
const traceId = uuidv4().replace(/-/g, '');
const spanId = uuidv4().substring(0, 16);
// W3C Trace Context header
const traceparent = `00-${traceId}-${spanId}-01`;
const authParams = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
'traceparent': traceparent, // <-- INJECT THE TRACE HEADER
},
};
group('Browse Products', () => {
// This request will now be picked up by your APM/tracing backend
const res = http.get(`${BASE_URL}/v2/products?category=electronics`, authParams);
check(res, { /* ... */ });
});
// ... (rest of script) ...
}
2. Construir el Panel de Control Unificado
Con el envío de métricas k6 a Prometheus por parte de k6-operator (Fase 2) y con la inyección de IDs de traza por parte de tu script (Fase 3), todo tus datos están ahora en un solo lugar.
En Grafana (o en la herramienta que prefieras), crea un panel que combine estas dos fuentes de datos:
- Panel superior (lado del cliente):
k6_http_reqs_total(Tasa de solicitudes)k6_http_req_duration_p95(Latencia P95)k6_http_req_failed_rate(Tasa de errores)k6_vus(Usuarios virtuales activos)
- Paneles inferiores (lado del servidor):
- Por servicio: Uso de CPU/memoria, número de pods (actividad HPA).
- Base de datos: Rendimiento de las consultas, latencia de las consultas, número de conexiones.
- Colas: Profundidad de la cola de mensajes (por ejemplo, retraso de Kafka, tamaño de la cola de RabbitMQ).
- Red: Ancho de banda de entrada/salida, errores de conexión.
Ahora, cuando ejecute su prueba de carga, podrá ver este panel. Cuando observe un pico en k6_http_req_duration_p95, deberá mirar directamente debajo de él. Verá el pico correspondiente en las conexiones de la base de datos, el punto muerto de los pods de un servicio secundario, o el aumento de la escala de un nuevo nodo mediante HPA.el correspondiente aumento en las conexiones a la base de datos, el estancamiento de los pods de un servicio dependiente, o el aumento de la capacidad (HPA) de un nuevo nodo.
Ha pasado de "el sitio es lento" a "el sitio es lento porque la latencia del servicio de pedidos (p99) es alta, lo que se correlaciona directamente con una saturación del 95% del CPU en el servicio de pagos, lo que está fallando sus comprobaciones de estado."
Equipo de Ingeniería de Software Compartido Bajo Demanda, Por Suscripción.
Acceda a un equipo flexible de ingeniería de software compartido bajo demanda a través de una suscripción mensual predecible. Desarrolladores, diseñadores, ingenieros de control de calidad y un gestor de proyectos gratuito le ayudan a construir MVPs, escalar productos e innovar con tecnologías modernas como React, Node.js y más.
Finalmente
Realizar pruebas de carga en un sistema distribuido con k6 no es un evento único; es una práctica continua. Al crear scripts con escenarios realistas, ejecutarlos a gran escala con k6-operator y, lo más importante, construir paneles de observabilidad unificados, transforma las pruebas de carga de una simple verificación de "aprobado/rechazo" en una herramienta poderosa de ingeniería y depuración del rendimiento.
Al integrar estas prácticas en su flujo de CI/CD, usted establece una línea de base de rendimiento, protege contra errores y proporciona a sus equipos de ingeniería los datos de alta fidelidad que necesitan para construir sistemas resilientes, escalables y de alto rendimiento. Esto ya no es solo "pruebas"; es un componente fundamental de la arquitectura de sistemas moderna y la excelencia operativa.
Preguntas frecuentes
¿Qué es k6 y por qué se recomienda para probar sistemas distribuidos?
k6 es una herramienta moderna de pruebas de carga de alto rendimiento, escrita en Go. Se recomienda altamente para sistemas distribuidos porque tiene un bajo consumo de recursos, lo que permite generar una carga significativa de forma rentable. Su enfoque centrado en el desarrollador utiliza JavaScript (ES6) para la programación, lo que reduce la barrera de entrada para los ingenieros. También admite varios protocolos comunes en sistemas distribuidos (como gRPC, Kafka y WebSockets) e integra directamente con los modernos stacks de observación como Prometheus y Grafana.
¿Cómo puede un script k6 simular una compleja trayectoria de usuario en un sistema distribuido?
Un script k6 simula una compleja trayectoria de usuario utilizando grupos, verificaciones, y gestión de estado.
- Grupos (
group()) se utilizan para organizar múltiples solicitudes en transacciones lógicas, como "Iniciar Sesión" o "Realizar Pedido", lo que permite visualizar métricas para cada parte específica del flujo.Verificaciones (check()) se utilizan para validar respuestas (por ejemplo, confirmando un estado HTTP 200) para medir la tasa de éxito de las solicitudes sin detener la prueba.Gestión de Estado se logra capturando datos de la respuesta de una solicitud (como un token de autenticación) y utilizándolos en solicitudes posteriores, imitando una sesión de usuario real y con estado.¿Cuál es el paso más crítico para analizar los resultados de la prueba de carga de k6?
El paso más crítico es correlacionar las métricas del lado del cliente con las métricas del lado del servidor. Ejecutar una prueba k6 le indica qué ocurrió desde la perspectiva del usuario (por ejemplo, "la latencia p95 aumentó a 3000ms"). Para descubrir por qué ocurrió, debe utilizar un panel de control unificado (por ejemplo, en Grafana) para visualizar sus métricas k6 junto con sus datos de observabilidad del lado del servidor (por ejemplo, saturación de CPU, agotamiento de la piscina de conexiones de la base de datos o retraso del consumidor de Kafka) en una sola línea de tiempo. Esta correlación proporciona información valiosa sobre la causa raíz de los cuellos de botella.