Teste de carga na prática: k6 + dashboard

Teste de carga na prática: k6 + dashboard

Performance
Repositório de exemplos: todo o setup desse post está em github.com/wander4747/grafana-k6

Código funciona no ambiente de dev. Passa nos testes unitários. Passa na revisão. E ainda assim cai na primeira sexta-feira de alto tráfego. Falta uma pergunta que teste nenhum desses cobre: o sistema aguenta carga real?

É pra isso que existe teste de carga. Neste post vou explicar o que é, listar os principais tipos de teste que existem, e mostrar como rodar todos eles com k6, usando um projeto que montei com Docker Compose e dashboard em tempo real.

O que é teste de carga

Teste de carga (load testing) é a prática de simular tráfego real (ou maior que o real) contra um sistema, pra observar como ele se comporta. Não é sobre achar bug de lógica, é sobre responder perguntas de capacidade:

  • Quantos usuários simultâneos o sistema aguenta antes de degradar?
  • O que acontece quando o tráfego triplica de repente?
  • O sistema se recupera depois de um pico, ou fica degradado?
  • Rodando por horas, alguma coisa vaza memória ou conexão?
  • Qual é o teto absoluto antes de quebrar de vez?

Cada uma dessas perguntas tem um tipo de teste específico pra respondê-la. Não existe “o” teste de carga: existe uma família de testes, cada um com um objetivo diferente.

Por que k6

k6 é uma ferramenta de teste de carga open-source da Grafana Labs, feita pra ser confortável pra quem já programa. Os scripts são JavaScript puro, o binário é uma única ferramenta de linha de comando (escrita em Go, então roda leve), e ela se integra nativamente com o ecossistema Grafana pra visualização de métricas.

Pontos que fazem diferença no dia a dia:

  • Script como código: o teste é um arquivo .js versionado junto com o resto do projeto, não uma configuração de GUI.
  • Métricas nativas: latência, taxa de erro, throughput. Tudo já vem medido, sem instrumentar nada.
  • Thresholds: você define limites (“p95 abaixo de 500ms”, “taxa de erro abaixo de 1%”) e o k6 falha o teste automaticamente se passar do limite. Dá pra plugar isso em CI.
  • Dashboard: o k6 tem um web dashboard embutido pra acompanhar as métricas em tempo real. É o que aparece nas imagens abaixo.

Um script mínimo:

import http from 'k6/http';

export default () => {
    http.get('http://localhost:1234');
};

Isso já é um teste válido. O que muda entre os tipos de teste é a seção options: quantos usuários virtuais (VUs), por quanto tempo, e em que padrão.

Os tipos de teste de carga

A documentação oficial do k6 organiza os testes de performance em seis categorias. Cada uma responde uma pergunta diferente.

Smoke Test

Pergunta: o script funciona e o sistema responde numa carga mínima?

Carga: 1 usuário. Duração: ~1 minuto.

É o primeiro teste que você roda, sempre. Não é sobre carga, é sobre validação: confirma que o script está correto, os endpoints respondem, os thresholds estão configurados direito. Roda a cada mudança de código ou de script, antes de qualquer teste mais pesado.

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

export const options = {
    vus: 3,
    duration: '1m',
};

export default function () {
    const res = http.get('http://localhost:1234');

    check(res, {
        'status code 200': (r) => r.status === 200,
    });
}

Load Test (Average-Load)

Pergunta: o sistema aguenta a carga esperada do dia a dia?

Carga: volume equivalente à produção (no exemplo abaixo, 100 usuários simultâneos). Duração: 5 a 60 minutos.

É o teste de referência, o que você roda com regularidade pra garantir que o desempenho continua estável nas condições normais de uso. Geralmente segue um padrão de três fases: ramp-up (sobe gradual até a carga alvo), sustentação (mantém a carga), ramp-down (desce gradual).

export const options = {
    stages: [
        { duration: '5m', target: 100 },  // ramp-up: 1 -> 100 usuários
        { duration: '30m', target: 100 }, // sustenta 100 usuários
        { duration: '5m', target: 0 },    // ramp-down: volta a 0
    ],
};

Stress Test

Pergunta: o que acontece quando a carga passa do esperado?

Carga: acima do normal, crescente (no exemplo, 100 a 200 usuários progressivamente). Duração: 5 a 60 minutos.

Aqui o objetivo é forçar o sistema além da zona de conforto pra encontrar o ponto onde ele degrada: latência sobe, erros aparecem, throughput cai. Roda antes de períodos de demanda esperada acima da média: Black Friday, lançamento de campanha, etc.

export const options = {
    stages: [
        { duration: '1m', target: 200 },  // ramp-up: 1 -> 200 usuários
        { duration: '30m', target: 200 }, // sustenta 200 usuários
        { duration: '5m', target: 0 },    // ramp-down: volta a 0
    ],
};

Spike Test

Pergunta: o sistema sobrevive a um pico repentino, e se recupera depois?

Carga: salto abrupto e muito alto (no exemplo, de 100 pra 2000 usuários em 30 segundos). Duração: poucos minutos.

Diferente do stress test, aqui não importa o caminho gradual: importa a transição brusca. Simula o efeito de ficar em destaque de repente, um link viral, uma campanha que estourou. O que se observa não é só se o sistema aguenta o pico, mas se ele volta ao normal depois que o pico passa.

export const options = {
    stages: [
        { duration: '2m', target: 2000 }, // salto rápido a 2000 usuários, sem plateau
        { duration: '1m', target: 0 },    // ramp-down rápido de volta a 0
    ],
};

Soak Test (Endurance)

Pergunta: o sistema se mantém estável ao longo de horas?

Carga: volume normal e constante (no exemplo, 100 usuários). Duração: horas (no exemplo, 8 horas).

É o único teste onde a variável principal é tempo, não volume. Vazamento de memória, conexões de banco que não fecham direito, cache que cresce sem limite: esse tipo de problema só aparece depois de rodar por um bom tempo. Um teste de 10 minutos não pega isso.

export const options = {
    stages: [
        { duration: '5m', target: 100 }, // ramp-up: 1 -> 100 usuários
        { duration: '8h', target: 100 }, // sustenta 100 usuários por 8 horas
        { duration: '5m', target: 0 },   // ramp-down: volta a 0
    ],
};

Breakpoint Test

Pergunta: qual é o limite absoluto do sistema antes de quebrar?

Carga: crescente sem teto definido, até a falha (no exemplo, até 20.000 usuários). Duração: variável, até algumas horas.

É o teste mais agressivo da lista. Não tem meta de carga: sobe até o sistema quebrar de vez, e o ponto de quebra é o resultado. Faz sentido rodar depois de mudança grande de infraestrutura, ou pra saber, de fato, qual é o teto de capacidade atual.

export const options = {
    executor: 'ramping-arrival-rate',
    stages: [
        { duration: '2h', target: 20000 }, // sobe até 20.000 requisições/s em 2h
    ],
};

Repara que aqui muda o executor pra ramping-arrival-rate: em vez de controlar número de usuários virtuais (VUs), controla diretamente a taxa de requisições por segundo, deixando o k6 alocar quantos VUs forem necessários pra sustentar essa taxa.

Resumo

Tipo Carga Duração Frequência Pergunta que responde
Smoke Mínima < 1 min A cada mudança O script funciona?
Load Normal 5-60 min Regular Aguenta o dia a dia?
Stress Alta, crescente 5-60 min Antes de picos esperados Onde degrada?
Spike Muito alta, súbita Minutos Antes de eventos Sobrevive e se recupera?
Soak Normal, prolongada Horas Periódica Fica estável com o tempo?
Breakpoint Crescente, sem teto Variável Ocasional Qual o limite absoluto?

O projeto: grafana-k6

Montei um repositório com os seis tipos de teste prontos pra rodar, mais exemplos de funcionalidades específicas do k6 (checks, groups, métricas customizadas, thresholds, tags). Tudo sobe com um único docker compose up.

Estrutura

grafana-k6/
├── docker-compose.yaml
├── go-server/              # aplicação alvo, em Go
│   ├── Dockerfile
│   └── main.go
└── k6/
    ├── example/             # exemplos de funcionalidades do k6
    │   ├── check.js
    │   ├── group.js
    │   ├── metrics.js
    │   ├── request.js
    │   ├── tags.js
    │   └── thresholds.js
    ├── scenarios/           # padrões de carga (executors)
    │   ├── constant-arrival-rate.js
    │   ├── constant-vus.js
    │   ├── per-vu-iterations.js
    │   └── shared-iterations.js
    └── test-types/          # os seis tipos de teste
        ├── smoke_test.js
        ├── load_test.js
        ├── stress_test.js
        ├── spike_test.js
        ├── soak_test.js
        └── breakpoint_test.js

O alvo dos testes é um servidor Go minimalista: só precisa responder HTTP, não precisa fazer nada de especial. Em qualquer projeto real você aponta os scripts pro seu próprio serviço.

Rodando

# sobe o ambiente
docker compose up -d

# roda o teste desejado
docker exec -it k6_run k6 run /scripts/test-types/smoke_test.js

Substitui smoke_test.js por qualquer outro arquivo de test-types/: todos seguem o mesmo padrão de execução. Enquanto o teste roda, o dashboard fica disponível em http://localhost:5665.

O dashboard em tempo real

Essa é a tela que abre em localhost:5665 durante a execução: taxa de requisição, duração, taxa de falha, VUs ativos e taxa de transferência, atualizando a cada poucos segundos:

Dashboard do k6 mostrando Iteration Rate, HTTP Request Rate, Request Duration, gráfico de performance ao longo do tempo, VUs e Transfer Rate

A aba Timings quebra a duração da requisição HTTP em fases. É onde se acha onde o tempo está sendo gasto, não só quanto: Request Duration (avg/p90/p95/p99), Request Failed Rate, Request Rate, Request Waiting (TTFB), além de TLS handshaking e Request Sending mais abaixo. Numa investigação de lentidão, é essa aba que diz se o gargalo é conexão, espera do servidor, ou envio de dados:

Aba Timings do k6 com painéis de Request Duration, Request Failed Rate, Request Rate, Request Waiting, TLS handshaking e Request Sending

Na aba Summary, depois que o teste termina (ou em qualquer ponto durante a execução), aparece o resumo estatístico completo: percentis (p90, p95, p99) de cada métrica de tempo, contadores totais de dados trafegados e requisições, e os gauges de VUs:

Aba Summary do k6 com tabela de trends (avg, max, med, min, p90, p95, p99) e painéis de Counters, Rates e Gauges

Além dos tipos de teste: recursos que valem conhecer

O repositório também tem exemplos isolados de features do k6 que aparecem em qualquer script real, independente do tipo de teste:

  • thresholds.js: define critérios de sucesso/falha automáticos (ex: http_req_duration: ['p(95)<500']). Sem isso, teste de carga vira só “olhar gráfico e achar que está bom”.
  • check.js: assertions dentro do teste (check(res, { 'status 200': (r) => r.status === 200 })), sem parar a execução quando falham, diferente de um assert tradicional.
  • group.js: agrupa requisições relacionadas pra organizar métricas por fluxo (ex: “login”, “checkout”).
  • tags.js: etiqueta requisições e métricas pra filtrar depois, útil quando o mesmo script cobre múltiplos endpoints.
  • metrics.js: métricas customizadas (Counter, Trend, Gauge, Rate) além das que o k6 já coleta por padrão.

E na pasta scenarios/, os diferentes executors: constant-vus, per-vu-iterations, shared-iterations, constant-arrival-rate, que controlam como a carga é gerada (número fixo de VUs vs. taxa fixa de requisições por segundo, entre outras estratégias). Vale explorar quando o padrão simples de stages não é suficiente pro cenário que você precisa simular.

Fechando

Teste de carga não é uma etapa a mais no checklist antes de ir pra produção: é a única forma confiável de responder “isso aguenta?” antes que o tráfego real responda por você. k6 torna essa prática acessível: script em JavaScript, execução via linha de comando, métricas prontas, dashboard sem esforço extra.

Começa pequeno: um smoke test já revela se o script e o alvo estão configurados direito. Depois disso, o tipo de teste que você precisa depende da pergunta que você está tentando responder, e agora você tem os seis principais mapeados.

O projeto completo, com os seis tipos de teste prontos pra rodar via Docker Compose, está em github.com/wander4747/grafana-k6.

Tags: k6 grafana load-test performance testes devops docker