Arquitectura de software
Metodología de pruebas de carga: modelos de carga, tipos de prueba y análisis
Intermedio12 min de lecturaActualizado
Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés
Una prueba de carga es un experimento, y la mayoría fracasan como experimentos mucho antes de hacer fallar el software. Esta guía cubre los distintos tipos de prueba y la pregunta que responde cada uno, cómo construir un modelo de carga a partir del tráfico real, cuánta fidelidad de entorno necesitas de verdad, qué métricas recoger en ambos lados y cómo leer un resultado para encontrar el punto de saturación en lugar de un número para poner en una diapositiva.
Lo que aprenderás
- Elegir el tipo de prueba adecuado — carga, estrés, resistencia, picos, capacidad — según la pregunta que se plantea
- Construir un modelo de carga a partir de datos de producción y no de suposiciones
- Diseñar la rampa, el think time y los datos de prueba para que la prueba se parezca al tráfico real
- Recoger las métricas del lado del cliente y del servidor necesarias para interpretar una ejecución
- Identificar el punto de saturación y distinguir un cuello de botella real de un artefacto de la prueba
La mayoría de las pruebas de carga producen un número en lugar de una respuesta. Un equipo lanza un script, anota que el sistema aguantó 1200 peticiones por segundo, lo mete en un informe y no aprende nada que cambie una decisión. El problema rara vez es la herramienta; es que la prueba nunca se planteó como una pregunta. «¿Aguantaremos las rebajas del viernes?» y «¿por qué se degrada el p99 al cabo de dos horas?» requieren experimentos completamente distintos, y ninguno se responde con una única cifra de throughput.
Trata una prueba de rendimiento como un experimento: una hipótesis, condiciones controladas, mediciones a ambos lados de la red y un análisis que confirma o refuta la hipótesis. Esta guía explica cómo construir ese experimento. Diagnosticar lo que revelan los resultados —profiling, GC, acceso a la base de datos— es el tema de rendimiento de aplicaciones.
Tipos de prueba y las preguntas que responden
Los tipos se diferencian por la forma de la carga y la duración, y usar el equivocado produce una respuesta errónea pero muy convincente.
| Tipo | Forma de la carga | Pregunta que responde |
|---|---|---|
| Smoke | Muy baja, minutos | ¿Funciona el script y responde siquiera el entorno? |
| Carga | Pico esperado, estable | ¿Cumplimos nuestros objetivos con el tráfico que esperamos? |
| Estrés | Creciente hasta el fallo | ¿Dónde se rompe y cómo se rompe? |
| Capacidad | Incrementos escalonados | ¿Cuál es el throughput máximo sostenible y qué lo limita? |
| Resistencia (soak) | Moderada, de horas a días | ¿Se degrada algo con el tiempo: fugas, fragmentación, crecimiento de logs? |
| Picos (spike) | Salto brusco y caída | ¿Sobrevive a una avalancha repentina y se recupera después? |
| Escalabilidad | Carga por unidad de recurso | ¿Duplicar instancias duplica el throughput? |
Dos de ellos se infravaloran sistemáticamente. Las pruebas de resistencia encuentran los fallos que solo aparecen con el tiempo: una colección que crece despacio, fugas de conexiones, una caché que nunca expulsa nada, discos que se llenan de logs. Son los fallos que tiran producción a las 3 de la madrugada del cuarto día, y ninguna prueba corta puede encontrarlos. Las pruebas de estrés importan menos por la cifra de rotura que por el comportamiento al romperse: un sistema que descarta el exceso de carga con respuestas 429 rápidas y se recupera cuando la carga baja es muchísimo mejor que uno que encola hasta agotar la memoria, aunque ambos se rompan con el mismo throughput.
Construir el modelo de carga
El factor que más determina si una prueba predice algo es que la carga modelada se parezca a la real. Un tráfico uniforme contra un único endpoint es fácil de programar y no te dice nada.
Parte de los datos. Los logs de acceso, las analíticas del gateway y las trazas te dan la distribución real: qué operaciones, en qué proporción, a qué ritmo y con qué variedad de parámetros. Toma una hora punta representativa en lugar de una media diaria, porque la capacidad la deciden los picos.
Modela procesos de negocio, no endpoints. Los usuarios reales siguen recorridos —navegar, buscar, ver un producto, añadir a la cesta, pagar— con un abandono realista entre pasos. Un modelo en el que el 100 % de los usuarios virtuales llega al pago sobrestima enormemente la carga de escritura e infravalora la búsqueda.
Scenario mix for a retail peak hour (from 30 days of access logs):
Browse-only ........ 62% home → category → 3 × product
Search-driven ...... 24% search → 2 × product → add to basket (40% of these)
Checkout ............ 9% basket → address → payment → confirmation
Account admin ....... 4% login → orders → order detail
API partners ........ 1% steady 25 req/s, no think time
Peak measured arrival rate: 340 sessions/minute
Read:write ratio: 94:6
El think time no es opcional. Los usuarios reales hacen pausas entre acciones, lo que cambia la concurrencia para una tasa de llegada dada y, por la ley de Little, cuántas sesiones hacen falta para producir un throughput dado. Eliminar el think time convierte 5000 usuarios realistas en la carga de unos 200, y produce una prueba que machaca un único camino de código con una localidad de caché irreal. Usa una distribución, no una constante: una pausa aleatoria de entre 3 y 12 segundos es mucho mejor que una fija de 5.
Varía los datos. Si todos los usuarios virtuales piden el mismo ID de producto, estás midiendo tu caché, no tu sistema. Usa una distribución de claves realista —normalmente una cola larga con algunos elementos realmente calientes— y datos únicos donde la unicidad importe, para que la prueba no choque por accidente con restricciones de unicidad ni llene una sola partición.
Modela las llegadas, no solo la concurrencia. Las herramientas de modelo cerrado mantienen N usuarios virtuales que esperan cada uno una respuesta antes de continuar, así que la carga ofrecida baja automáticamente cuando el sistema se ralentiza: es el problema de la coordinated omission, y además te favorece. Las herramientas de modelo abierto inyectan a una tasa de llegada fijada, sea cual sea el tiempo de respuesta, que es como se comporta el tráfico real de Internet y como se forman de verdad las colas. Prefiere la inyección por tasa de llegada para todo lo que esté expuesto al público.
Entorno y datos de prueba
La fidelidad perfecta es inasumible; la pregunta útil es qué diferencias distorsionan el resultado.
| Factor | Importa | Suele poder reducirse |
|---|---|---|
| Volumen y distribución de los datos | De forma crítica: los planes cambian con la cardinalidad | Rara vez; usa datos de tamaño de producción |
| Topología de la aplicación | Sí: número de saltos, comportamiento del balanceo de carga | Misma forma, menos instancias |
| Dimensionamiento de la base de datos | Sí; suele ser el cuello de botella | Solo con una interpretación cuidadosa |
| Ruta de red y latencia | Sí en diseños con muchas idas y vueltas | Simula latencia añadida |
| Dependencias de terceros | Sí | Stub con latencia realista, incluida su cola |
| Generación de CPU, tipo de disco | Sí para cifras absolutas | Compara solo resultados relativos |
El volumen de datos merece especial atención. Un plan de ejecución que usa un índice con 10 000 filas puede pasar a un recorrido secuencial con 10 millones, así que una prueba contra una base de datos pequeña valida la funcionalidad y nada del rendimiento. Si no puedes copiar los datos de producción, genera un conjunto con el mismo volumen, cardinalidad y sesgo, incluido ese cliente con 40 000 pedidos que rompe todas las suposiciones de paginación.
Los stubs de terceros deben reproducir la distribución de latencia de la dependencia, no su media. Una API de un partner que responde en 80 ms con algún valor atípico ocasional de 3 segundos pondrá a prueba tu comportamiento de timeouts y reintentos; un stub que siempre responde en 80 ms, no. Cuando sea posible, incluye un modo de fallo en algunas ejecuciones.
Por último, reduce la escala con honestidad. Los resultados de un entorno a un cuarto de tamaño solo predicen producción si el sistema escala de forma lineal y la base de datos se reduce en la misma proporción, que es justo lo que comprueba una prueba de escalabilidad. Deja constancia del supuesto de escalado en el informe en lugar de dejar que el lector dé por hecha una fidelidad que no tenías.
Escribir la prueba
Hay tres herramientas dominantes, y la elección importa menos que el modelo que hay detrás.
JMeter es una herramienta madura, se maneja desde una interfaz gráfica con un plan de prueba en XML, tiene un ecosistema enorme de protocolos y plugins, y los equipos de pruebas la conocen bien. Sus threads corresponden a usuarios virtuales, así que una concurrencia muy alta requiere inyectores distribuidos, y versionar el plan XML es incómodo, aunque viable.
Gatling expresa los escenarios en un DSL de Scala o Java, usa un motor asíncrono con el que un solo inyector sostiene una concurrencia alta y genera buenos informes HTML. Encaja con equipos cómodos en un lenguaje de la JVM que quieren revisar las pruebas como código.
k6 usa JavaScript, es fácil de programar y genera diffs legibles, tiene soporte nativo para executors por tasa de llegada y umbrales que hacen fallar una ejecución automáticamente, y encaja bien en los pipelines.
// k6: open model — inject a fixed arrival rate regardless of response time.
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Trend } from 'k6/metrics';
const checkoutLatency = new Trend('checkout_latency', true);
export const options = {
scenarios: {
browse: {
executor: 'ramping-arrival-rate',
startRate: 20, timeUnit: '1s',
preAllocatedVUs: 400, maxVUs: 2000,
stages: [
{ target: 120, duration: '5m' }, // ramp
{ target: 120, duration: '20m' }, // steady state — this is what we measure
{ target: 0, duration: '2m' }, // ramp down
],
},
},
thresholds: {
'http_req_failed': ['rate<0.005'], // fail the run above 0.5% errors
'checkout_latency': ['p(95)<800', 'p(99)<2000'],
},
};
const products = JSON.parse(open('./product-ids.json')); // realistic key distribution
export default function () {
const id = products[Math.floor(Math.random() * products.length)];
const home = http.get(`${__ENV.BASE}/`, { tags: { name: 'home' } });
check(home, { 'home 200': (r) => r.status === 200 });
sleep(Math.random() * 9 + 3); // think time 3–12 s
const product = http.get(`${__ENV.BASE}/products/${id}`, { tags: { name: 'product' } });
check(product, { 'product 200': (r) => r.status === 200 });
sleep(Math.random() * 6 + 2);
if (Math.random() < 0.09) {
const res = http.post(`${__ENV.BASE}/orders`,
JSON.stringify({ sku: id, quantity: 1 }),
{ headers: { 'Content-Type': 'application/json', 'X-Synthetic': 'loadtest' },
tags: { name: 'checkout' } });
checkoutLatency.add(res.timings.duration);
check(res, { 'order created': (r) => r.status === 201 });
}
}
Fíjate en los tags de cada petición: sin etiquetado por operación, un p95 agregado mezcla un asset estático de 20 ms con un pago de 900 ms y no describe ninguno de los dos. Fíjate también en la cabecera X-Synthetic, que permite a la plataforma excluir el tráfico de prueba de las métricas de negocio.
Uses la herramienta que uses, guarda los scripts de prueba en el repositorio junto a la aplicación, revísalos y parametriza el entorno. Una prueba de carga que solo una persona sabe lanzar desde su portátil deja de lanzarse.
Métricas que recoger
Las cifras del lado del cliente te dicen qué experimentarían los usuarios; las del lado del servidor te dicen por qué. Necesitas ambas, sincronizadas con el mismo reloj.
Del generador: la tasa de llegada conseguida frente a la solicitada (una diferencia significa que el generador está saturado y que todas las demás cifras son sospechosas), los percentiles de tiempo de respuesta por operación, la tasa de errores desglosada por código de estado y por causa, los errores de conexión y los timeouts por separado de los errores HTTP, y los datos transferidos.
Del sistema: las señales que exponen Actuator y la observabilidad: tasa de peticiones, tasa de errores y percentiles de latencia por servicio y endpoint; CPU, memoria, disco y red por instancia; threads activos y profundidad de cola de los pools de threads; uso del pool de conexiones y tiempo de espera para obtener una conexión; duración y frecuencia de las pausas de GC; transacciones por segundo, esperas de bloqueos, número de consultas lentas y tasas de acierto de caché en la base de datos; y la profundidad de las colas de cualquier broker.
Vigila también el propio generador. Un inyector al 95 % de CPU, sin puertos efímeros libres o con sus propias pausas de GC reporta una latencia que es únicamente suya. Monitorízalo con el mismo cuidado que el sistema bajo prueba y distribúyelo en cuanto una sola máquina vaya justa.
Leer los resultados
Representa el throughput, el tiempo de respuesta y la tasa de errores frente a la carga ofrecida en el mismo eje temporal, y la forma de las curvas cuenta la historia.
A medida que aumenta la carga, el throughput crece de forma más o menos lineal mientras el tiempo de respuesta se mantiene plano: el sistema tiene capacidad de sobra. Luego el crecimiento del throughput se frena y el tiempo de respuesta empieza a subir: es el codo, donde un recurso se acerca a la saturación y empiezan a formarse colas. Si sigues empujando, el throughput se estanca o cae mientras el tiempo de respuesta se dispara y aparecen errores: es la saturación, y más allá está el colapso, donde el throughput cae porque el sistema dedica su esfuerzo a timeouts, reintentos y cambios de contexto.
La cifra que vale la pena reportar es la carga ofrecida más alta con la que el sistema todavía cumple sus objetivos, no el throughput máximo observado, que suele darse con una latencia que nadie aceptaría. Después responde a la siguiente pregunta: ¿qué lo limitó? Una CPU saturada, un pool de conexiones lleno, un bloqueo, un servicio downstream o el propio generador. Si no puedes nombrar el recurso limitante, la prueba ha producido un número en lugar de una respuesta.
Algunos patrones y lo que suelen significar:
- La latencia sube mientras la CPU sigue baja: el sistema está esperando algo, ya sea un pool, un bloqueo o una llamada downstream.
- El throughput se estanca muy por debajo de lo esperado con un sistema aparentemente sano: un límite artificial de concurrencia, como el tamaño del pool de threads, el pool de conexiones, un semáforo o el propio límite del generador.
- Los errores solo aparecen por encima de un umbral: una cola que se desborda o un timeout que vence; averigua cuál y si es el comportamiento correcto.
- Los tiempos de respuesta se degradan poco a poco durante horas con carga constante: una fuga, un crecimiento sin límite o índices hinchados. Amplía a una prueba de resistencia.
- Un cambio brusco al cabo de unos minutos: una caché que se llena, un proceso batch que arranca o la compilación JIT que termina; comprueba qué más se ejecuta con esa cadencia.
Compara con una línea base del mismo entorno en lugar de con un objetivo absoluto de otro sistema. Y repite: tres ejecuciones de la misma configuración permiten saber si una diferencia del 7 % es una regresión o ruido.
Errores habituales
Probar un único endpoint. Mide un solo camino de código con una localidad de caché perfecta y no predice nada sobre el tráfico mixto de producción.
Sin think time. Tanto la concurrencia como el comportamiento de la caché son erróneos, y en la dirección que favorece al sistema.
Reportar la media. Como en cualquier trabajo sobre latencia, la media oculta justo el comportamiento que importa.
Un conjunto de datos diminuto o poco realista. Los planes de ejecución cambian, las cachés lo guardan todo y el sesgo desaparece.
Ignorar el calentamiento. Medir durante la compilación JIT, con pools fríos y cachés vacías, da cifras pesimistas que ocultan el problema del régimen estable.
No restablecer entre ejecuciones. Los datos crecen, las cachés siguen calientes y cada ejecución mide un sistema distinto.
Ignorar el cliente. Un generador saturado hace que un sistema en mal estado parezca sano.
Probar solo el camino feliz. Ninguna dependencia lenta, ningún fallo parcial, ninguna tormenta de reintentos: el comportamiento de resiliencia, que decide si un incidente acaba en caída total, nunca se ejercita.
Ejecutarla una sola vez, antes del paso a producción. El rendimiento empeora con cada release. Una prueba automatizada corta en el pipeline de entrega, más una prueba completa programada, es lo que mantiene el resultado vigente.
Puntos clave
- Plantea cada prueba como una pregunta y elige el tipo —carga, estrés, capacidad, resistencia, picos— que la responde; la resistencia y el estrés revelan los fallos que más duelen.
- El modelo de carga decide el valor del resultado: mezcla real de operaciones, escenarios de negocio con abandonos, think time aleatorio, datos variados e inyección por tasa de llegada.
- El volumen y la distribución de los datos importan más que casi cualquier otra diferencia de entorno; simula a los terceros con una distribución de latencia realista, no con una constante.
- Etiqueta cada petición por operación, mantén los scripts bajo control de versiones y registra versión, configuración y conjunto de datos en cada ejecución.
- Recoge juntas las métricas del lado del cliente y del servidor, y verifica siempre que el generador de carga no sea él mismo el cuello de botella.
- Reporta la carga más alta que todavía cumple los objetivos y nombra el recurso limitante; una cifra de throughput sin causa no es un hallazgo.
Fuentes y lecturas recomendadas
Preguntas frecuentes
- ¿Puedo hacer pruebas de carga en producción?
- Muchas organizaciones lo hacen, y es el único entorno con una fidelidad real. Requiere salvaguardas: ejecutar en horas de poco tráfico, marcar las peticiones sintéticas con una cabecera para poder excluirlas de las métricas de negocio, limitar la tasa inyectada, tener un interruptor de parada inmediata y asegurarte de que el tráfico de prueba no pueda crear pedidos reales ni enviar correos reales. Empieza con una pequeña parte de la capacidad de producción y aumenta poco a poco.
- ¿Cuánto tiempo debe durar una prueba de carga?
- Lo suficiente para superar el calentamiento y alcanzar el régimen estable, lo que en un servicio JVM significa al menos diez o quince minutos de medición tras la rampa, para que la compilación JIT, los pools de conexiones y las cachés se hayan asentado. Algo más corto mide el arranque. Las pruebas de resistencia responden a una pregunta completamente distinta y necesitan de horas a días para revelar fugas y fragmentación.
- ¿Por qué mis resultados cambian en cada ejecución?
- Normalmente porque hay algo sin controlar. Los culpables habituales son un entorno compartido con otro tráfico, una base de datos cuyo volumen crece de una ejecución a otra, cachés que siguen calientes de una prueba anterior, vecinos ruidosos en entornos virtualizados y un generador de carga que está él mismo saturado. Restablece los datos entre ejecuciones, vigila la CPU y la red del generador y repite cada configuración al menos tres veces antes de creerte una diferencia.
- ¿Deben ejecutarse las pruebas de carga en el pipeline de CI?
- Una prueba de rendimiento corta, a nivel smoke, en cada build es muy valiosa: unos minutos con carga moderada, que falla ante un umbral claro de regresión y detecta la consulta N+1 accidental antes de que llegue al merge. Las pruebas completas de capacidad y de resistencia deben programarse contra un entorno dedicado, porque necesitan tiempo, una infraestructura estable y una persona que interprete el resultado.