Guía: cómo planificar y ejecutar pruebas de carga en aplicaciones PHP monolito usando k6 y Docker Compose — paso a paso

Por · Publicado el · 9 min de lectura

Pantalla de portátil con código desenfocado y reflejo de un peluche
Portátil con código desenfocado y peluche — aspecto técnico sin datos legibles. Foto: Daniil Komov / Pexels

Guía: cómo planificar y ejecutar pruebas de carga en aplicaciones PHP monolito usando k6 y Docker Compose. Definirás objetivos y SLOs, modelarás recorridos y RPS, montarás un entorno aislado con Compose, escribirás scripts k6 con checks y thresholds y analizarás resultados para priorizar correcciones y automatizar ejecuciones.

Por qué las pruebas de carga en monolitos PHP requieren cuidado

En monolitos PHP, el cuello de botella raramente es único. PHP‑FPM, base de datos, cache y I/O compiten por recursos. La prueba debe estresar el flujo completo, no solo una ruta. Mide p95 y tasa de errores, pero observa también la saturación de workers y colas internas.

PHP‑FPM tiene fila por proceso. Si el pool es corto verás latencias altas y timeouts. Si es muy grande, puede faltar memoria y aparecer swap. Planea hipótesis de fallo y señales de saturación para orientar la prueba.

Monitorea servidor web, PHP‑FPM y la base de datos durante la prueba. Tener métricas de base evita diagnósticos erróneos tras un pico de latencia. Mira cómo exponer métricas de PHP‑FPM para Prometheus.

  • Atención al pool de PHP‑FPM y a las conexiones a la base de datos.
  • Evita probar solo GET estáticos; cubre autenticación y escrituras.
  • Recopila métricas antes, durante y después de la prueba.

Planificación: objetivos, riesgos y criterios de éxito (SLOs)

Define el objetivo de la prueba antes del primer script. Busca criterios observables que guíen decisiones. Sin un objetivo claro, cualquier resultado parece aceptable, pero no orienta mejoras. Enfócate en flujos críticos del negocio y en eventos con picos previstos.

Crea SLOs simples y verificables. No prometas números rígidos; usa rangos y metas realistas, considerando la carga histórica y picos esperados.

  • Alcance: rutas, operaciones de lectura/escritura e integraciones.
  • Señales: p95 < objetivo, errores HTTP < x%, throughput mínimo.
  • Riesgos: caídas de cache, migraciones, locks en tablas, GC de PHP.
  • Duración: warm‑up, fase estable, ramp‑down.
  • Salida: informe con métricas y próximas acciones.

Modelado de carga realista: recorridos, RPS y VUs

Traduce el comportamiento de usuarios en escenarios. Evita distribuir el tráfico de forma uniforme entre rutas. Usa mezcla ponderada e incluye tiempos de think time. En e‑commerce, búsqueda y navegación dominan; el checkout es minoritario pero crítico.

Elige entre modelar por usuarios virtuales (VUs) o por tasa de llegada (RPS). Para backends, ramping‑arrival‑rate suele representar mejor accesos concurrentes impredecibles.

  • Identifica recorridos: login, navegación, búsqueda, checkout.
  • Estima la mezcla: 70% lectura, 25% navegación, 5% escrituras sensibles.
  • Aplica think time para evitar bucles irreales.
  • Incluye calentamiento para estabilizar caches y JITs.
  • Varía payloads y parámetros para cubrir cache misses.

Entorno de prueba con Docker Compose: aislamiento y reproducibilidad

Usa Docker Compose para aislar la aplicación y reproducir pruebas. Configura una red dedicada, variables de entorno y dependencias. Evita compartir la base de datos con desarrollo para no contaminar datos.

Para limitar CPU, memoria e I/O del host, complementa con cgroup v2 y systemd cuando sea necesario. Esto simula máquinas más pequeñas y revela degradación temprano. Revisa el tutorial sobre cgroup v2 y systemd para Docker.

  • Separa la red del Compose para DNS estable (app → http://app:8080).
  • Replica cache y base de datos en contenedores propios.
  • Versiona docker‑compose.yml junto con los scripts k6.
  • Evita datos sensibles; prefiere fixtures dedicadas.
version: "3.9"
services:
  app:
    image: nginx:alpine
    ports:
      - "8080:8080"
    volumes:
      - ./app:/var/www/html:ro
    depends_on:
      - php
      - db
      - cache
  php:
    image: php:8.2-fpm
    volumes:
      - ./app:/var/www/html:ro
  db:
    image: postgres:16
    environment:
      - POSTGRES_DB=app
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=app
    volumes:
      - db:/var/lib/postgresql/data
  cache:
    image: redis:7-alpine
  k6:
    image: grafana/k6:latest
    volumes:
      - ./k6:/scripts:ro
    depends_on:
      - app
    command: ["run", "/scripts/script.js"]
volumes:
  db: {}

Escribiendo scripts con k6: escenarios, checks y thresholds

Estructura el script en módulos por recorrido. Usa tags para diferenciar transacciones y rutas. Los checks validan respuestas; los thresholds hacen fallar la prueba cuando se violan las metas. Prefiere ramping‑arrival‑rate para controlar RPS y mantén think time explícito.

Parametriza la BASE_URL vía variable de entorno. Así cambias el objetivo sin modificar el código. Usa datasets en JSON para variedad de entradas y cubre cache misses de forma predecible.

  • Usa tags: { journey: "checkout", route: "/orders" }.
  • Aplica checks para estado y contenido clave.
  • Define thresholds en p95/p99 y tasa de errores.
  • Separar escenarios por recorrido facilita el análisis.
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  scenarios: {
    browse: {
      executor: 'ramping-arrival-rate',
      startRate: 10, timeUnit: '1s',
      preAllocatedVUs: 50, maxVUs: 200,
      stages: [
        { target: 50, duration: '2m' },
        { target: 100, duration: '5m' },
        { target: 0, duration: '1m' }
      ],
      exec: 'browse',
    },
    checkout: {
      executor: 'constant-vus', vus: 20, duration: '5m',
      exec: 'checkout', startTime: '2m'
    }
  },
  thresholds: {
    http_req_duration: ['p(95)<400'],
    http_req_failed: ['rate<0.02']
  }
};

const BASE_URL = __ENV.BASE_URL || 'http://app:8080';

export function browse() {
  const res = http.get(`${BASE_URL}/products?query=test`);
  check(res, { 'status 200': (r) => r.status === 200 });
  sleep(1);
}

export function checkout() {
  const login = http.post(`${BASE_URL}/login`, { u: 'user', p: 'pass' });
  check(login, { 'login ok': (r) => r.status === 200 });
  const order = http.post(`${BASE_URL}/orders`, JSON.stringify({ sku: 'ABC', qty: 1 }), { headers: { 'Content-Type': 'application/json' }});
  check(order, { 'order 201': (r) => r.status === 201 });
  sleep(0.5);
}

Datos, autenticación y logs: sin esto la prueba engaña

Sin datos realistas, mides al cache feliz, no al sistema real. Prepara fixtures, usuarios y tokens con expiración adecuada. Genera payloads variados para ejercitar índices y cardinalidad en la base de datos. Registra correlaciones para unir peticiones de k6 con los logs del servidor.

Estandariza un header de correlación e incluye el ID en todas las peticiones de k6. Esto acelera el diagnóstico. Para rastreo punta a punta, implementa trazado distribuido con OpenTelemetry en PHP.

  • Crea usuarios y datos sintéticos representativos.
  • Renueva tokens para evitar fallos falsos por expiración.
  • Incluye X‑Request‑ID en cada llamada.
  • Parametriza por ambiente vía .env.

Ejecución, observabilidad y automatización en CI

Ejecuta k6 con Docker Compose y mantén todo versionado. Durante la prueba, sigue CPU, memoria, I/O, conexiones y colas. Planea warm‑up, fase estable y ramp‑down. Exporta el resumen en JSON para adjuntarlo al informe y comparar rondas.

Recopila métricas de infraestructura y aplicación en paralelo. Usa ventanas temporales para relacionar picos de llegada con latencia. Revisa cómo configurar Prometheus y Grafana vía Docker Compose.

Automatiza: ejecuta escenarios cortos en cada PR y escenarios largos en jobs nocturnos. Conserva artefactos por ejecución. Esto evita regresiones y completa el flujo descrito en esta guía.

  • Sigue p95/p99, throughput y errores por ruta.
  • Observa la saturación de PHP‑FPM y las conexiones a BD.
  • Guarda el resumen de k6 y dashboards como evidencia.
docker compose run --rm \
  -e BASE_URL=http://app:8080 \
  -v "$PWD/results:/results" \
  k6 run /scripts/script.js --summary-export=/results/summary.json

Interpretando resultados: del síntoma a la acción

Traduce métricas en hipótesis y próximas acciones. Enfócate en lo que impacta la disponibilidad: errores del servidor y latencias extremas. Después, ataca los hotspots que limitan el throughput. Valida cambios con un nuevo ciclo corto de prueba, repitiendo el mismo escenario.

Profundiza diagnósticos cuando haya picos irregulares de latencia. El artículo sobre diagnóstico de picos de latencia en APIs PHP puede ayudar a interpretar síntomas comunes.

Métrica Señal observada Posibles acciones
p95 http_req_duration alto Cola en PHP‑FPM o BD lenta Aumentar workers, optimizar queries, introducir cache por ruta
http_req_failed > 2% Errores 5xx en picos Circuit‑breaker, límites por cliente, revisar timeouts
CPU del app > 85% sostenido Saturación de CPU Optimizar código, JIT, opcache, ampliar CPU
Espera en BD alta Locks y scans costosos Índices, plan de consultas, reducir N+1
Bajo cache hit ratio Latencia por misses Claves eficientes, TTLs y warming de cache

Preguntas frecuentes

¿Cuántos usuarios virtuales (VUs) debo usar en k6?

Depende del objetivo. Para simular tasa de llegada, prefiere ejecutores por RPS, como ramping‑arrival‑rate, ajustando la tasa y el número máximo de VUs según tu meta. Para escenarios por sesión, VUs constantes funcionan.

¿Debo probar en producción?

Evita ejecutar pruebas de carga en producción. Compiten por recursos con usuarios reales y pueden causar incidentes. Prefiere un entorno dedicado y representativo, con datos sintéticos y límites de recursos similares. Si es inevitable, limita la tasa.

¿Cuál es la diferencia entre load, stress y soak test?

Load prueba metas realistas de tráfico. Stress empuja más allá de lo normal para encontrar el punto de ruptura y recuperación. Soak mantiene carga prolongada para revelar fugas de memoria, colas crecientes y efectos a largo plazo.

¿Cómo elegir metas de p95 y throughput?

Usa el histórico y las expectativas del negocio como referencia. Define rangos por recorrido crítico y considera dependencias externas. Establece metas graduadas y revísalas tras cambios arquitectónicos. Los thresholds deben reflejar la realidad de tu sistema.

¿Puedo ejecutar k6 vía Docker Compose en CI?

Sí. Versiona docker‑compose.yml y scripts k6, y ejecútalos en el job del CI. Exporta el resumen para análisis y haz que el job falle si se violan thresholds. Usa variables de entorno para apuntar a objetivos de prueba y parametrizar datos.

¿Cómo generar datos realistas para la prueba?

Crea fixtures y datasets que cubran variedad de parámetros, tamaños y combinaciones. Genera usuarios y pedidos sintéticos, con claves, índices y patrones de acceso cercanos a lo real. Evita datos sensibles. Renueva tokens y limpia estados cuando sea necesario.

¿Quieres estructurar tus pruebas de carga con k6 y Docker Compose?

Si necesitas revisar el modelado, montar el entorno y automatizar ejecuciones, hablemos del siguiente paso para tu escenario.

Hablar sobre pruebas de carga

Ver también