| Tipo | Descripción | VUs Pico | Duración | Req. Totales | Errores | P95 | Resultado |
|---|---|---|---|---|---|---|---|
| smoke | Validación de scripts | 2 | 1 min | 118 | 0 (0%) | 89ms | PASS |
| load | Carga normal + pico | 300 | 8 min | 142.840 | 328 (0.23%) | 312ms | PASS |
| stress | Límite de capacidad | 1.000 | 12 min | 318.420 | 2.547 (0.8%) | 892ms | WARN |
| spike | Simulación lanzamiento | 2.000 | 4 min | 89.230 | 4.461 (5%) | 3.2s | FAIL |
| soak | Estabilidad prolongada | 150 | 4 h | 2.156.000 | 4.312 (0.2%) | 341ms | PASS |
// nutriflow-load-test.js — k6 Load Test para NutriFlow API // Simula carga normal (150 VUs) y pico (300 VUs) en endpoints críticos. // Lanzar: k6 run -e BASE_URL=https://api.nutriflow.io/v2 tests/nutriflow-load-test.js import http from 'k6/http'; import { check, sleep, group } from 'k6'; import { Trend, Counter, Rate } from 'k6/metrics'; import { SharedArray } from 'k6/data'; // ── Métricas personalizadas de negocio ────────────────────────────── const aiRecommendDuration = new Trend('ai_recommend_duration'); const loginSuccessRate = new Rate('login_success_rate'); const planLoads = new Counter('nutrition_plan_loads'); // ── Datos de prueba (empleados ficticios) ─────────────────────────── const employees = new SharedArray('employees', function() { return JSON.parse(open('./data/employees.json')); }); // ── Opciones de ejecución ─────────────────────────────────────────── export const options = { stages: [ { duration: '1m', target: 50 }, // ramp-up gradual { duration: '2m', target: 150 }, // carga sostenida normal { duration: '1m', target: 300 }, // pico de tráfico { duration: '3m', target: 300 }, // mantener pico { duration: '1m', target: 0 }, // ramp-down ], thresholds: { 'http_req_duration': ['p(95)<400'], 'http_req_duration{name:ai_recommend}': ['p(95)<2000'], 'http_req_failed': ['rate<0.005'], 'http_reqs': ['rate>200'], 'checks': ['rate>0.99'], 'login_success_rate': ['rate>0.98'], 'ai_recommend_duration': ['p(95)<2000'], }, }; // ── Función principal ─────────────────────────────────────────────── export default function () { const BASE = __ENV.BASE_URL || 'https://api.nutriflow.io/v2'; const emp = employees[Math.floor(Math.random() * employees.length)]; let token; group('01 - Autenticación', () => { const res = http.post( `${BASE}/auth/login`, JSON.stringify({ username: emp.email, password: emp.password }), { headers: { 'Content-Type': 'application/json' }, tags: { name: 'login' } } ); const ok = check(res, { 'login 200': r => r.status === 200, 'token presente': r => r.json('token') !== undefined, 'login < 500ms': r => r.timings.duration < 500, }); loginSuccessRate.add(ok); if (ok) token = res.json('token'); }); if (!token) { sleep(1); return; } const hdrs = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }; group('02 - Plan Nutricional', () => { const res = http.get(`${BASE}/employees/${emp.id}/nutrition-plan`, { headers: hdrs, tags: { name: 'nutrition_plan' } }); check(res, { 'plan 200': r => r.status === 200, 'plan < 400ms': r => r.timings.duration < 400 }); planLoads.add(1); }); group('03 - Registro de Comida', () => { const payload = JSON.stringify({ employee_id: emp.id, meal: 'lunch', calories: 650, timestamp: new Date().toISOString() }); const res = http.post(`${BASE}/meals/log`, payload, { headers: hdrs, tags: { name: 'meal_log' } }); check(res, { 'log 201': r => r.status === 201, 'log < 300ms': r => r.timings.duration < 300 }); }); group('04 - Dashboard Empresa', () => { const res = http.get(`${BASE}/dashboard/company/${emp.company_id}/stats`, { headers: hdrs, tags: { name: 'dashboard' } }); check(res, { 'dash 200': r => r.status === 200 }); }); // Solo 20% de usuarios solicitan recomendación IA (endpoint costoso) if (Math.random() < 0.2) { group('05 - Recomendación IA', () => { const start = Date.now(); const res = http.post(`${BASE}/ai/recommend`, JSON.stringify({ employee_id: emp.id, context: 'post_lunch' }), { headers: hdrs, tags: { name: 'ai_recommend' }, timeout: '10s' }); aiRecommendDuration.add(Date.now() - start); check(res, { 'ai 200': r => r.status === 200, 'ai < 2s': r => r.timings.duration < 2000 }); }); } sleep(1); }
// Simula el lanzamiento: 0 → 2000 VUs en 30s export const options = { stages: [ { duration: '30s', target: 2000 }, // spike brutal { duration: '1m', target: 2000 }, // mantener { duration: '30s', target: 0 }, // caída ], thresholds: { 'http_req_failed': ['rate<0.10'], // 10% tolerancia 'http_req_duration': ['p(95)<5000'], }, }; export default function () { const res = http.get( 'https://api.nutriflow.io/v2/health', { tags: { name: 'health_check' } } ); check(res, { 'alive': r => r.status < 500 }); sleep(0.5); }
// 4 horas a 150 VUs — detecta memory leaks export const options = { stages: [ { duration: '5m', target: 150 }, // warm-up { duration: '3h50m', target: 150 }, // soak { duration: '5m', target: 0 }, // cool-down ], thresholds: { 'http_req_duration': ['p(95)<500'], 'http_req_failed': ['rate<0.005'], }, }; export default function () { // flujo completo igual que load test performUserJourney(); sleep(2); }
| http_req_duration | p(95) < 400ms | PASS |
| http_req_duration{ai} | p(95) < 2000ms | WARN |
| http_req_failed | rate < 0.5% | PASS |
| http_reqs | rate > 200/s | PASS |
| checks | rate > 99% | PASS |
| login_success_rate | rate > 98% | PASS |
| ai_recommend_duration | p(95) < 2000ms | WARN |
name: NutriFlow Load Tests on: push: { branches: [main, staging] } schedule: [{ cron: '0 6 * * 1-5' }] jobs: load-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: grafana/k6-action@v0.3.1 with: filename: tests/nutriflow-load-test.js flags: > -e BASE_URL=${{ secrets.STAGING_URL }} --out influxdb=${{ secrets.GRAFANA_CLOUD }} - name: Upload results uses: actions/upload-artifact@v4 if: always() with: name: k6-results path: results/
El test de spike (2.000 VUs) falla con 5% de errores. Implementar auto-scaling ECS con target tracking y pre-warming 30 min antes del lanzamiento. Mínimo 8 instancias activas en día 0.
El endpoint /dashboard/stats tiene P99 de 891ms. Añadir Redis cache con TTL 60s para agregados por empresa. Reducción esperada: 70% latencia.
P95 de 1.84s en /ai/recommend está muy cerca del threshold de 2s. Implementar cola asíncrona con SSE para recomendaciones largas y cache semántico de respuestas similares.