Testes de carga com k6: encontre o gargalo antes do seu usuário

1. Introdução aos Testes de Carga e ao k6

Testes de carga são uma prática essencial para garantir que sistemas web suportem o volume esperado de usuários sem degradação de performance. Eles simulam tráfego real para identificar gargalos antes que afetem clientes reais. Os principais tipos incluem:

  • Testes de carga: verificam comportamento sob tráfego esperado
  • Testes de estresse: levam o sistema além dos limites normais
  • Testes de pico: simulam aumentos repentinos de tráfego
  • Testes de resistência: avaliam desempenho ao longo do tempo

O k6 (da Grafana Labs) destaca-se por sua leveza (escrito em Go), scripts em JavaScript e integração nativa com CI/CD. Diferente de ferramentas como JMeter, o k6 é otimizado para execução em containers e pipelines automatizadas.

2. Configuração do Ambiente e Primeiro Script

Instalação (Linux/macOS):

# macOS via Homebrew
brew install k6

# Linux via script
curl -s https://dl.k6.io/install.sh | sudo bash

# Windows via Chocolatey
choco install k6

# Verificar instalação
k6 version

Estrutura básica de um script:

import http from 'k6/http';
import { sleep } from 'k6';

export default function () {
  http.get('https://test-api.example.com/health');
  sleep(1);
}

Execução:

k6 run script.js

O relatório inicial mostra métricas como http_req_duration (tempo médio de resposta), http_req_failed (taxa de erro) e vus (usuários virtuais simultâneos).

3. Criando Cenários de Teste Realistas

Usando stages para simular tráfego gradual:

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 50 },  // ramp-up para 50 usuários
    { duration: '5m', target: 50 },  // carga constante
    { duration: '2m', target: 0 },   // ramp-down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'], // 95% das requisições < 500ms
    http_req_failed: ['rate<0.01'],   // erro < 1%
  },
};

export default function () {
  const payload = JSON.stringify({
    username: `user_${__VU}`,
    password: 'test123',
  });

  const params = {
    headers: { 'Content-Type': 'application/json' },
  };

  const loginRes = http.post('https://api.example.com/login', payload, params);
  check(loginRes, {
    'login bem-sucedido': (r) => r.status === 200,
    'token recebido': (r) => r.json().token !== undefined,
  });

  const token = loginRes.json().token;
  const authHeaders = { Authorization: `Bearer ${token}` };

  const dashboardRes = http.get('https://api.example.com/dashboard', {
    headers: authHeaders,
  });
  check(dashboardRes, {
    'dashboard carregado': (r) => r.status === 200,
  });

  sleep(Math.random() * 3 + 1); // pausa realista entre 1-4s
}

4. Métricas Essenciais e Análise de Resultados

Métricas principais:
- http_req_duration: tempo total da requisição (DNS, conexão, TTFB, download)
- http_req_waiting: tempo de espera no servidor (TTFB)
- http_req_failed: taxa de falhas (status >= 400)
- vus_max: pico de usuários simultâneos
- iterations: total de execuções completas

Interpretação do relatório:

     http_req_duration..............: avg=342ms   min=120ms   med=310ms   max=890ms   p(90)=520ms   p(95)=680ms
     http_req_failed................: 0.5%  ✓ 5   ✗ 995
     vus............................: 50    min=0   max=50
     vus_max........................: 50

Quando p(95) ultrapassa 500ms, indica que 5% dos usuários têm experiência ruim. Se p(99) > 2s, há gargalos sérios.

Exportando para análise externa:

k6 run --out json=results.json script.js

5. Identificando Gargalos e Pontos de Falha

Métricas por URL:

import http from 'k6/http';
import { Trend } from 'k6/metrics';

const slowEndpoint = new Trend('slow_endpoint_duration');

export default function () {
  const res1 = http.get('https://api.example.com/endpoint1');
  slowEndpoint.add(res1.timings.duration);

  const res2 = http.get('https://api.example.com/endpoint2');
  // Comparar métricas individuais
}

Uso de checks para depuração:

check(res, {
  'status code 200': (r) => r.status === 200,
  'response body não vazio': (r) => r.body.length > 0,
  'tempo de resposta < 2s': (r) => r.timings.duration < 2000,
});

Isolamento de endpoints problemáticos:

# Testar apenas um endpoint específico
k6 run --vus 10 --duration 30s script.js --env TARGET_ENDPOINT=/reports

6. Integração Contínua e Automação com k6

GitHub Actions:

name: Performance Tests
on: [push, pull_request]

jobs:
  k6-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run k6 test
        uses: grafana/k6-action@v0.2.0
        with:
          filename: tests/load-test.js
          flags: --out json=results.json
      - name: Upload results
        uses: actions/upload-artifact@v3
        with:
          name: k6-results
          path: results.json

Thresholds para quebrar o build:

export const options = {
  thresholds: {
    http_req_duration: ['p(95)<1000'], // Falha se p95 > 1s
    http_req_failed: ['rate<0.05'],    // Falha se erro > 5%
    checks: ['rate>0.95'],             // Falha se menos de 95% dos checks passam
  },
};

7. Boas Práticas e Cenários Avançados

Evitando "thundering herd":

import { sleep } from 'k6';

export default function () {
  // Distribuir requisições uniformemente no tempo
  sleep(Math.random() * 5); // 0-5s aleatório

  // Usar __ITER para comportamento baseado em iteração
  if (__ITER % 10 === 0) {
    http.get('https://api.example.com/heavy-report');
  }
}

Testes com WebSockets:

import ws from 'k6/ws';

export default function () {
  const url = 'wss://api.example.com/socket';
  const response = ws.connect(url, null, function (socket) {
    socket.on('open', () => socket.send('ping'));
    socket.on('message', (data) => console.log('received:', data));
    socket.setTimeout(() => socket.close(), 5000);
  });
}

Testes gRPC:

import grpc from 'k6/net/grpc';
import { check } from 'k6';

const client = new grpc.Client();
client.load(['definitions'], 'user.proto');

export default function () {
  client.connect('localhost:50051', { plaintext: true });
  const response = client.invoke('UserService/GetUser', { id: 1 });
  check(response, { 'status OK': (r) => r.status === grpc.StatusOK });
  client.close();
}

Dicas finais:
- Versionar scripts de teste junto com o código da aplicação
- Documentar resultados com screenshots dos dashboards do Grafana
- Estabelecer SLIs (Service Level Indicators) e SLOs (Service Level Objectives) baseados nos thresholds
- Realizar testes de carga regularmente, não apenas antes de releases
- Monitorar métricas de infraestrutura (CPU, memória, I/O) durante os testes

Referências