Entrega 1 Pruebas de carga - JuanVargas0325/Prueba GitHub Wiki

Entrega 1 – Pruebas de carga y análisis de capacidad

Grupo N° completar · Integrantes

Línea base de rendimiento del despliegue local (Docker Compose). Sigue la metodología del tutorial Pruebas de rendimiento: análisis de capacidad del curso: planeación, ejecución con niveles crecientes de concurrencia, repeticiones y análisis. Los scripts, los datos y los resultados crudos están en la carpeta pruebas-carga/ del repositorio.

1. Plan de pruebas

1.1 Objetivo

Establecer la línea base de capacidad de la API desplegada localmente: cuántas peticiones por segundo y cuántos usuarios concurrentes soporta cada operación representativa antes de degradarse, qué recurso se satura primero y qué decisiones de arquitectura explican ese comportamiento. Esta línea base se comparará con el despliegue en la nube de la Entrega 2.

Objetivos específicos

  1. Medir throughput, tiempos de respuesta (media y percentiles) y tasa de errores por escenario y nivel de concurrencia.
  2. Identificar el punto de saturación de cada escenario y el componente que lo limita (CPU de la API o base de datos).
  3. Determinar la capacidad máxima que cumple los criterios de aceptación.

1.2 Escenarios

Se eligieron tres operaciones que representan los tres tipos de carga del sistema:

Escenario Operación Tipo de carga Por qué
E1 POST /api/v1/auth/login CPU (verificación bcrypt) Toda sesión empieza aquí; es la operación más costosa por diseño de seguridad.
E2 GET /api/v1/seguimiento/personas (profesora con 40 personas) Lectura y agregación Vista principal del profesor (RF-10.1): consulta vinculaciones y tareas de dos semanas y calcula el estado de cada persona.
E3 POST /api/v1/tareas Escritura Operación más frecuente de los monitores: valida reglas y persiste la tarea.

La generación de reportes con IA no se incluye: es asíncrona y su duración depende del proveedor del modelo, todavía no definido. Se evaluará cuando el modelo esté configurado.

1.3 Parámetros de ejecución

Parámetro Valor
Niveles de concurrencia 10, 50, 100, 150 y 200 usuarios simultáneos
Duración por corrida 15 segundos (ab -t 15), con keep-alive
Repeticiones por nivel 3
Total 3 escenarios × 5 niveles × 3 repeticiones = 45 corridas
Pausa entre corridas 3 segundos
Punto de entrada Nginx (http://nginx), igual que un cliente real

1.4 Datos de prueba

pruebas-carga/datos_carga.sql crea una profesora con un curso de 40 monitores, cada uno con 2 tareas por semana durante 10 semanas (800 tareas). Representa a un profesor con un curso grande a mitad del semestre. Antes de cada ejecución el script elimina las tareas creadas por ejecuciones anteriores, para que todas partan del mismo estado.

1.5 Ambiente de prueba

Elemento Detalle
Equipo anfitrión Intel Core 7 240H (10 núcleos, 16 hilos), 15,6 GB de RAM, Windows 11
Virtualización Docker Desktop 29.8 sobre WSL2, con 16 vCPU y 7,6 GB de RAM para los contenedores
Sistema Despliegue de la Entrega 1 sin modificaciones: Nginx 1.27, API y worker en Go 1.26, PostgreSQL 16 y Redis 7, todos sobre Debian 12
Generador de carga Apache Benchmark 2.3 (httpd:2.4-bookworm) en un contenedor de la misma red de Docker
Monitoreo docker stats, una muestra por segundo de CPU y memoria de la API y de PostgreSQL

1.6 Criterios de aceptación

Escenario Criterio
E1 · Inicio de sesión p95 ≤ 1.000 ms y 0 % de errores
E2 · Vista del profesor p95 ≤ 500 ms y 0 % de errores
E3 · Registro de tareas p95 ≤ 300 ms y 0 % de errores

Son valores de referencia para una aplicación interactiva interna. El inicio de sesión admite más tiempo porque ocurre una vez por sesión.

1.7 Métricas

Por corrida: peticiones completadas, errores (respuestas no 2xx y fallos de conexión), peticiones por segundo, tiempo medio, percentiles 50, 90, 95 y 99, tiempo máximo, CPU máxima de la API y de la base de datos, y memoria máxima. Las respuestas que ab marca como fallidas solo por variar de tamaño (las respuestas JSON cambian de longitud) no se cuentan como errores.

1.8 Riesgos y limitaciones

  • El generador de carga corre en el mismo equipo que el sistema y compite por CPU. Los valores absolutos son conservadores, sobre todo en E1.
  • El entorno local (Docker Desktop) no representa la infraestructura de la nube; por eso esta medición es una línea base para comparar, no una predicción de producción.
  • No se mide el tráfico de red entre contenedores ni el disco de forma separada.
  • El reporte con IA queda fuera hasta definir el proveedor del modelo.

2. Resultados

Ejecutado el 12 de octubre de 2026, con el código final de la entrega (incluida la optimización de DT-08, sección 3). Cada fila promedia las 3 corridas del nivel (± desviación estándar). Los datos crudos están en pruebas-carga/resultados/ (resumen.csv, salidas de ab y muestras de docker stats).

Ninguna de las 45 corridas registró errores (casi 2 millones de peticiones en total).

E1 · Inicio de sesión (POST /api/v1/auth/login)

Concurrencia Peticiones/s Media (ms) p50 (ms) p95 (ms) p99 (ms) Máx (ms) Errores (%) CPU API máx (%) CPU BD máx (%) Mem API máx (MB)
10 181.1 ± 4.4 55.2 ± 1.3 55 59 ± 1 64 79 0.00 971 9 18
50 234.7 ± 15.2 213.7 ± 14.3 207 289 ± 26 339 896 0.00 1603 11 21
100 238.7 ± 0.2 419.0 ± 0.3 400 601 ± 12 809 1281 0.00 1583 10 28
150 239.1 ± 0.6 627.2 ± 1.6 588 911 ± 16 1171 1626 0.00 1581 10 29
200 237.2 ± 1.1 843.1 ± 4.0 783 1292 ± 145 1864 3219 0.00 1591 9 33
%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#1d4ed8"}}}}%%
xychart-beta
    title "login: throughput"
    x-axis "Usuarios concurrentes" [10, 50, 100, 150, 200]
    y-axis "Peticiones/s" 0 --> 275
    line [181.1, 234.7, 238.7, 239.1, 237.2]
%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#1d4ed8"}}}}%%
xychart-beta
    title "login: tiempo de respuesta p95"
    x-axis "Usuarios concurrentes" [10, 50, 100, 150, 200]
    y-axis "ms" 0 --> 1485
    line [59.0, 288.7, 600.7, 910.7, 1291.7]

E2 · Vista principal del profesor (GET /api/v1/seguimiento/personas)

Concurrencia Peticiones/s Media (ms) p50 (ms) p95 (ms) p99 (ms) Máx (ms) Errores (%) CPU API máx (%) CPU BD máx (%) Mem API máx (MB)
10 3173.5 ± 60.1 3.2 ± 0.1 3 4 ± 0 5 11 0.00 558 222 35
50 3627.0 ± 32.1 13.8 ± 0.1 13 20 ± 1 23 50 0.00 660 268 45
100 3781.3 ± 7.1 26.4 ± 0.0 26 33 ± 1 37 57 0.00 670 282 48
150 3917.7 ± 18.1 38.3 ± 0.2 38 45 ± 1 50 65 0.00 679 299 53
200 4062.9 ± 9.8 49.2 ± 0.1 48 57 ± 1 63 80 0.00 693 316 58
%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#1d4ed8"}}}}%%
xychart-beta
    title "seguimiento: throughput"
    x-axis "Usuarios concurrentes" [10, 50, 100, 150, 200]
    y-axis "Peticiones/s" 0 --> 4672
    line [3173.5, 3627.0, 3781.3, 3917.7, 4062.9]
%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#1d4ed8"}}}}%%
xychart-beta
    title "seguimiento: tiempo de respuesta p95"
    x-axis "Usuarios concurrentes" [10, 50, 100, 150, 200]
    y-axis "ms" 0 --> 66
    line [4.0, 19.7, 32.7, 45.3, 57.3]

E3 · Registro de tareas (POST /api/v1/tareas)

Concurrencia Peticiones/s Media (ms) p50 (ms) p95 (ms) p99 (ms) Máx (ms) Errores (%) CPU API máx (%) CPU BD máx (%) Mem API máx (MB)
10 3540.1 ± 27.6 2.8 ± 0.0 3 4 ± 0 4 16 0.00 281 210 34
50 5237.0 ± 41.8 9.5 ± 0.1 9 12 ± 1 13 27 0.00 409 336 31
100 5242.2 ± 15.2 19.1 ± 0.1 19 22 ± 0 26 41 0.00 413 346 36
150 5169.9 ± 42.3 29.0 ± 0.2 28 33 ± 1 40 94 0.00 410 349 41
200 5153.3 ± 74.9 38.8 ± 0.6 38 44 ± 2 50 88 0.00 419 349 40
%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#1d4ed8"}}}}%%
xychart-beta
    title "tareas: throughput"
    x-axis "Usuarios concurrentes" [10, 50, 100, 150, 200]
    y-axis "Peticiones/s" 0 --> 6029
    line [3540.1, 5237.0, 5242.2, 5169.9, 5153.3]
%%{init: {"themeVariables": {"xyChart": {"plotColorPalette": "#1d4ed8"}}}}%%
xychart-beta
    title "tareas: tiempo de respuesta p95"
    x-axis "Usuarios concurrentes" [10, 50, 100, 150, 200]
    y-axis "ms" 0 --> 51
    line [4.0, 11.7, 22.0, 33.3, 44.3]

3. Análisis

E1 · Inicio de sesión: limitado por CPU

El throughput sube de 181 a unas 239 peticiones/s con 50 usuarios y no crece más: desde ese nivel la API usa toda la CPU disponible (~1.600 %, los 16 vCPU). Cada inicio de sesión verifica la contraseña con bcrypt, un algoritmo diseñado para ser costoso y frenar ataques de fuerza bruta. Por eso la operación está limitada por CPU y no por la base de datos, que no supera el 11 % de CPU.

Una vez saturada la CPU, los tiempos crecen linealmente con la concurrencia, como predice la ley de Little: con 200 usuarios y 237 peticiones/s, cada petición espera en promedio ≈ 200 / 237 ≈ 0,84 s, que coincide con la media medida (843 ms). La desviación entre repeticiones es baja, así que el comportamiento es consistente.

E2 · Vista del profesor: optimizada durante la entrega (DT-08)

La primera medición de este escenario mostró que se aplanaba en ~765 peticiones/s sin que ningún recurso llegara al máximo, y que con ~3.000 tareas adicionales en la semana caía a 51 peticiones/s. Al revisar el código se encontraron dos causas, registradas como deuda técnica DT-08:

  1. Cada persona reagrupaba las tareas de todas las demás. El servicio entregaba al cálculo semanal de cada persona la lista completa de tareas del profesor. Con 40 personas y 3.000 tareas eran unas 120.000 operaciones por petición. Ahora las tareas se reparten por persona una sola vez (~3.000 operaciones).
  2. La consulta traía datos que no se usaban. Cada tarea venía con su usuario, su espacio y el conteo de sus adjuntos (tres JOIN y una subconsulta por fila). Ahora el seguimiento usa una consulta que lee solo la tabla tareas.

Ambos cambios son pequeños y no cambiaron el diseño: el estado "sin reporte" se sigue calculando en cada consulta (RN-36).

Comparación en las mismas condiciones. Como el equipo no tiene la misma carga de un día a otro, se midió el escenario E2 con ambas versiones del código el mismo día, con los mismos parámetros:

Usuarios Antes: peticiones/s Después: peticiones/s Antes: p95 Después: p95
10 984 ± 84 3.174 ± 60 13 ms 4 ms
50 1.249 ± 69 3.627 ± 32 73 ms 20 ms
100 1.307 ± 9 3.781 ± 7 105 ms 33 ms
150 1.348 ± 6 3.918 ± 18 139 ms 45 ms
200 1.378 ± 5 4.063 ± 10 173 ms 57 ms
  • Unas 3 veces más throughput y un tercio del tiempo de respuesta en todos los niveles.
  • Con 3.000 tareas extra en la semana consultada (un profesor con mucha actividad): de 46 a 423 peticiones/s con 10 usuarios, y de 216 a 24 ms por petición.
  • Por separado, el reparto por persona aportó la mayor parte (con volumen alto, de 46 a 217 peticiones/s) y la consulta liviana duplicó lo restante (de 217 a 423).
  • Tras la optimización, el throughput sigue creciendo hasta 200 usuarios y la API usa ~690 % de CPU: el escenario ya no está cerca de su límite con esta carga.

E3 · Registro de tareas: el escenario más eficiente

Alcanza unas 5.240 peticiones/s desde 50 usuarios, con un p95 de 22 ms con 100 usuarios y de 44 ms con 200. Es una escritura simple: valida la vinculación, aplica las reglas y hace un INSERT. El throughput se mantiene estable hasta 200 usuarios; el recurso más exigido es PostgreSQL (~350 % de CPU), por las escrituras.

Uso de memoria

La API usa menos de 60 MB en todos los escenarios y PostgreSQL menos de 280 MB (este último crece en E3 por las inserciones). La memoria no es una restricción en ningún caso.

Comparación con la primera medición

La primera versión de estos resultados se tomó el 11 de octubre con el equipo más cargado. El inicio de sesión, cuyo código no cambió, daba ~190 peticiones/s; hoy da ~239. Por eso no se comparan directamente los números viejos con los nuevos: la comparación válida de DT-08 es la de la tabla anterior, medida en las mismas condiciones. Es la misma regla que exigen las Entregas 2 a 4: misma herramienta, mismos parámetros y mismo entorno.

4. Capacidad máxima y conclusiones

Escenario Throughput máximo Concurrencia máxima que cumple el criterio Recurso limitante
E1 · Inicio de sesión ~239 peticiones/s 150 usuarios (p95 = 911 ms ≤ 1.000 ms; con 200 ya son 1.292 ms) CPU de la API (bcrypt)
E2 · Vista del profesor ~4.060 peticiones/s ≥ 200 usuarios (p95 = 57 ms ≤ 500 ms) No se alcanzó con 200 usuarios
E3 · Registro de tareas ~5.240 peticiones/s ≥ 200 usuarios (p95 = 44 ms ≤ 300 ms) Escrituras en PostgreSQL

Ninguna de las corridas registró errores. Para la escala esperada (profesores con decenas de personas que registran tareas varias veces por semana), el despliegue local cumple holgadamente.

Relación con las decisiones de arquitectura y siguientes pasos

  1. bcrypt (E1): el costo es intencional y no se reducirá. Es el verdadero límite del sistema y el mejor escenario para demostrar el autoscaling por CPU de la Entrega 3: como la API no guarda estado en memoria (sesiones con JWT), varias instancias detrás de un balanceador multiplican su capacidad casi linealmente. El límite de intentos de inicio de sesión (DT-05) además protegería este recurso.
  2. Vista del profesor (E2): DT-08 quedó resuelta en esta entrega. Si en la nube volviera a ser un cuello de botella, el siguiente paso sería agregar en SQL las horas y los conteos por persona y semana. Además, en una VM pequeña el pool de conexiones de pgx será de 4 (lo calcula según los núcleos); se ajusta con pool_max_conns en DATABASE_URL.
  3. Escrituras (E3): no son una restricción. En la Entrega 2, con Cloud SQL, habrá que observar la latencia de red entre la máquina virtual y la base de datos, que hoy es prácticamente cero.
  4. Línea base: la Entrega 2 repetirá estos escenarios en GCP, ya con la herramienta elegida para el resto del curso (sección 6).

5. Cómo reproducir

docker compose up -d --wait
docker compose exec -T postgres psql -U seguimiento -d seguimiento < pruebas-carga/datos_carga.sql
bash pruebas-carga/ejecutar.sh        # ~17 minutos
python pruebas-carga/analizar.py      # tablas y gráficas en pruebas-carga/resultados/resumen.md

Se pueden ajustar NIVELES, REPETICIONES, DURACION y ESCENARIOS con variables de entorno (ver el encabezado de ejecutar.sh).

6. Herramienta para las Entregas 2 a 4: k6 o JMeter

Esta línea base usa Apache Benchmark (ab), como el tutorial de análisis de capacidad de la semana 2. Desde la Entrega 2, el enunciado exige una de estas herramientas: k6, Locust, Artillery o Apache JMeter, con la elección justificada en el informe. Además, la misma herramienta debe usarse en las Entregas 2, 3 y 4 para que las comparaciones sean válidas. La elección vale para el resto del curso.

ab deja de servir por dos razones: no está en la lista y no permite rampas de usuarios (subir la carga poco a poco, mantenerla y bajarla). La Entrega 3 necesita rampas para observar el autoscaling, y la Entrega 4 para el arranque en frío.

6.1 Las dos opciones más adecuadas

k6 (Grafana Labs) Apache JMeter
Material del curso No tiene tutorial Tutorial Pruebas de estrés con JMeter (semana 3)
Cómo se define una prueba Un script corto de JavaScript, versionado en el repositorio Un plan .jmx (XML) que se arma con interfaz gráfica
Rampas de usuarios (E3, E4) Una lista de etapas (stages): { duration: '2m', target: 100 } Grupos de hilos con ramp-up; las rampas con varias etapas requieren un plugin (Ultimate Thread Group)
Métricas obligatorias (media, p95, p99, errores, throughput) Todas en el resumen, configurando summaryTrendStats Todas en el reporte HTML (-e -o) y en el Summary Report
Reporte con gráficas Panel web integrado, exportable a HTML (K6_WEB_DASHBOARD=true) Panel HTML completo generado al final de la prueba
Criterios automáticos de aceptación thresholds: la prueba falla si, por ejemplo, p95 > 500 ms Aserciones de duración o plugins
Ejecución Un binario o la imagen grafana/k6; consume pocos recursos Requiere Java; más consumo de memoria por usuario virtual
Uso típico Desde la terminal, en scripts y en CI Interfaz gráfica para diseñar y terminal para ejecutar

Locust (Python) y Artillery (Node.js) también son válidos, pero no aportan nada que no cubran estas dos opciones, y JMeter tiene la ventaja de ser el que enseña el curso.

6.2 Recomendación: k6

  1. Las pruebas quedan como código en pruebas-carga/, igual que los scripts actuales. Se revisan en un pull request, se versionan con cada entrega y cualquiera las reproduce con un comando. Un .jmx es un XML largo que solo se edita cómodamente desde la interfaz de JMeter.
  2. Las rampas de la Entrega 3 son triviales. El escenario de autoscaling (subir hasta superar el umbral, sostener y bajar) es una lista de tres etapas.
  3. Reproduce la metodología de la Entrega 1 sin esfuerzo. Los niveles de concurrencia (10, 50, 100, 150, 200) y las tres repeticiones se programan en un ciclo, como hoy hace ejecutar.sh. Los datos de carga (datos_carga.sql) y los tres escenarios se reutilizan tal cual.
  4. Generador liviano. k6 está escrito en Go y genera mucha carga con poca CPU. Importa porque en la nube el generador corre en una VM aparte, idealmente pequeña para no gastar créditos.
  5. Criterios de aceptación automáticos con thresholds: los mismos de la sección 1.6 quedan escritos en el script.

Cuándo elegir JMeter: si el grupo prefiere seguir exactamente el tutorial del curso, si alguien ya lo conoce, o si se valora diseñar las pruebas de forma visual. Es igual de válido. Lo importante es elegir uno, justificarlo en el informe de la Entrega 2 y no cambiarlo después.

6.3 Ejemplo: escenario E2 en k6

// pruebas-carga/k6/e2-vista-profesor.js
import http from 'k6/http';
import { check } from 'k6';

export const options = {
  stages: [
    { duration: '1m', target: 50 },  // sube a 50 usuarios en 1 minuto
    { duration: '3m', target: 50 },  // los sostiene 3 minutos
    { duration: '1m', target: 0 },   // baja a 0
  ],
  summaryTrendStats: ['avg', 'p(50)', 'p(95)', 'p(99)', 'max'],
  thresholds: { http_req_duration: ['p(95)<500'], http_req_failed: ['rate==0'] },
};

// setup se ejecuta una vez: inicia sesión y entrega el token a todos los usuarios virtuales.
export function setup() {
  const r = http.post(`${__ENV.API}/api/v1/auth/login`,
    JSON.stringify({ correo: '[email protected]', password: 'Carga123*' }),
    { headers: { 'Content-Type': 'application/json' } });
  return { token: r.json('token') };
}

// Cada usuario virtual repite esta función mientras dura la prueba.
export default function (datos) {
  const r = http.get(`${__ENV.API}/api/v1/seguimiento/personas`,
    { headers: { Authorization: `Bearer ${datos.token}` } });
  check(r, { 'responde 200': (x) => x.status === 200 });
}

Ejecución desde la VM generadora de carga, contra la IP del sistema en GCP:

docker run --rm -i -e API=http://IP_DEL_SISTEMA grafana/k6 run - < pruebas-carga/k6/e2-vista-profesor.js

Para las Entregas 2 a 4 conviene medir también el detalle semanal de una persona (GET /api/v1/seguimiento/personas/{id}/semanas) dentro del escenario 3, porque el enunciado de la Entrega 2 lo incluye en la "vista del profesor". El escenario 2 debe repartir el registro de tareas entre varios monitores (los datos de carga traen 40).