Sessão 3Load & Stress Testing · 3h

Pôr a aplicação à prova antes que os clientes o façam

Hoje vais simular centenas de utilizadores a usar a tua aplicação ao mesmo tempo, perceber quanto ela aguenta, onde parte e porquê. Vais aprender a ler percentis, a escrever testes com Gatling, JMeter e k6, a afinar o Spring Boot e a proteger a aplicação quando a carga passa do limite.

  • Distinguir testes de carga, stress, pico e resistência
  • Ler percentis e aplicar a Lei de Little
  • Escrever simulações em Gatling, JMeter e k6
  • Encontrar o ponto de saturação e afinar Tomcat, Hikari e JVM
  • Correr testes de performance no pipeline de CI
  • Proteger a app com rate limiting e circuit breakers

Porquê testar sob carga?

Uma aplicação que responde em 50 ms com um utilizador pode demorar 8 segundos com mil. Ou simplesmente cair. Os testes normais (unitários, de integração) verificam se o código está certo. Os testes de performance verificam se continua certo e rápido quando muita gente o usa ao mesmo tempo.

A ponte nova

Antes de abrir uma ponte ao público, os engenheiros enchem-na de camiões carregados e medem quanto ela verga. Não esperam pela primeira hora de ponta para descobrir. Um teste de carga é exatamente isso: camiões simulados em cima da tua aplicação, num ambiente controlado.

Black FridayPicos previsíveisCampanhas, saldos, fim do prazo de entrega do IRS. Sabes que vem aí: testa antes.
CapacidadeQuantos servidores?Saber quantos pedidos uma instância aguenta permite planear custos e escalar com números, não com palpites.
RegressõesFicou mais lento?Uma alteração inocente (um N+1 novo) pode duplicar o tempo de resposta. Um teste automático apanha-a.

O que vais ver nesta sessão

0:00–0:35Módulo 3.1 · Fundamentos de testes de performance
0:35–1:35Módulo 3.2 · Ferramentas: Gatling, JMeter e k6 (+ laboratório)
1:35–1:45Intervalo ☕
1:45–2:25Módulo 3.3 · Execução, análise e tuning (+ laboratório)
2:25–2:50Módulo 3.4 · CI/CD e resiliência
2:50–3:00Conclusão do curso

Os tipos de teste: cada um responde a uma pergunta

Todos usam as mesmas ferramentas. O que muda é a forma da carga ao longo do tempo e a pergunta que queres ver respondida. Clica em cada tipo:

TipoPerguntaDuração típica
Load (carga)Com a carga normal esperada, cumprimos os objetivos de tempo de resposta?15–60 min
StressOnde é que parte? E quando parte, como parte (devagar ou de repente)?Até partir
Spike (pico)Aguentamos um salto súbito? Recuperamos sozinhos depois?10–20 min
Soak (resistência)Ao fim de horas há memory leaks, ligações perdidas, disco a encher?4–24 h
ScalabilitySe duplicarmos as instâncias, duplicamos a capacidade?Vários testes
Smoke test primeiro

Antes de qualquer teste grande, corre um smoke test: 1 ou 2 utilizadores durante 1 minuto. Serve só para confirmar que o script funciona e que o ambiente está de pé. Poupa horas de testes inválidos.

As métricas que importam (e porque a média engana)

ThroughputPedidos por segundo (RPS)Quanto trabalho a aplicação faz. Também se diz vazão.
LatênciaTempo de respostaQuanto espera cada utilizador. Medida em percentis.
ErrosTaxa de falhasPercentagem de respostas 5xx, timeouts e ligações recusadas.
RecursosCPU, memória, poolsDo lado do servidor: explicam porquê os outros números são o que são.

Percentis: o que os utilizadores realmente sentem

O percentil 95 (p95) é o tempo abaixo do qual ficam 95% dos pedidos. Se o p95 é 400 ms, 95 em cada 100 utilizadores esperaram menos de 400 ms, e 5 esperaram mais.

A média das alturas com um gigante na sala

Se numa sala estão 9 pessoas com 1,70 m e entra alguém com 3 metros, a média sobe pouco e não descreve ninguém. Com tempos de resposta é pior: a média esconde os utilizadores que esperaram 10 segundos. E esses são os que desistem da compra.

Gera 1000 pedidos simulados. Aumenta a percentagem de pedidos lentos e repara como a média quase não mexe enquanto o p99 dispara.

PercentilSignificadoPara que serve
p50 (mediana)Metade dos pedidos é mais rápida.A experiência «típica».
p95Só 5% são mais lentos.O objetivo mais usado em SLOs.
p99Só 1% é mais lento.A «cauda». Num site com 1 milhão de pedidos por dia, são 10 000 pessoas.
máximoO pior caso.Útil para detetar timeouts e pausas de GC.
Nunca faças a média de percentis

A média dos p95 de 3 servidores não é o p95 do sistema. Para juntar percentis precisas dos dados originais ou de histogramas. É por isso que no Micrometer se usa percentiles-histogram: true e o cálculo é feito no Prometheus.

A Lei de Little: a fórmula que liga tudo

Uma única fórmula, válida para qualquer sistema estável (um restaurante, uma fila do supermercado, um servidor):

L = λ × WConcorrência = débito × tempoL: pedidos em curso ao mesmo tempo. λ: pedidos por segundo. W: tempo médio de cada um, em segundos.

Exemplo: 200 pedidos/s × 0,25 s = 50 pedidos em curso em cada instante. Precisas de pelo menos 50 threads (ou virtual threads) e as ligações à base de dados que esses pedidos usem.

A mesma lei serve para os testes. Num modelo fechado (N utilizadores virtuais que esperam pela resposta e depois «pensam» Z segundos), o débito máximo é N / (W + Z). Se a aplicação ficar lenta, os utilizadores virtuais enviam menos pedidos e o teste esconde o problema. Num modelo aberto (chegam X utilizadores por segundo, haja o que houver), a fila cresce como na vida real. Para sites públicos, prefere o modelo aberto.

Planear antes de disparar

SLI, SLO e SLA

SLI
Service Level Indicator: o que medes. Ex.: «p95 do tempo de resposta de POST /checkout».
SLO
Service Level Objective: o objetivo interno. Ex.: «p95 < 500 ms e erros < 0,1%, com 300 pedidos/s».
SLA
Service Level Agreement: o compromisso contratual com o cliente, com penalizações. Normalmente menos exigente que o SLO, para haver margem.

Sem um SLO, um teste de carga produz números mas não produz respostas. «400 ms é bom?» Depende do objetivo.

Modelar cenários realistas

Os utilizadores reais não carregam todos no mesmo botão. Olha para os logs de produção ou para o Google Analytics e reproduz a mistura:

Jornada% dos utilizadoresPassos
Explorar70%Página inicial → pesquisar → ver 3 produtos
Comprar20%Pesquisar → produto → carrinho → checkout → pagar
Conta10%Login → ver encomendas → detalhe de uma encomenda
Think timePausa entre passos, como uma pessoa real a ler a página (2–8 s, com variação aleatória). Sem ela, 100 utilizadores virtuais parecem 2000.
Dados variadosUsa muitos produtos e clientes diferentes (ficheiros CSV). Se todos pedirem o produto 1, só testas a cache.
AquecimentoA JVM otimiza o código enquanto corre (o compilador JIT). Os primeiros minutos são mais lentos: descarta-os ou faz uma fase de aquecimento.

O ambiente

  • O mais parecido possível com produção: mesmo tamanho de máquinas, mesma versão da base de dados, volume de dados realista. Uma base de dados com 100 linhas nunca revela um índice em falta.
  • O gerador de carga noutra máquina. Se correr no mesmo servidor, rouba-lhe CPU e falseia tudo.
  • Serviços externos simulados (WireMock) com latências realistas. Não faças testes de carga contra o banco ou a transportadora de verdade!
  • Nunca em produção sem autorização expressa, janela combinada e plano de interrupção.

Gatling: testes de carga escritos em Java

O Gatling descreve os testes como código (em Java, Kotlin ou Scala), o que permite guardá-los no Git, revê-los e corrê-los no pipeline. Usa um modelo assíncrono muito eficiente: uma máquina normal simula milhares de utilizadores. No fim gera um relatório HTML com gráficos.

Instalar no projeto Maven

A primeira simulação

PeçaO que é
HttpProtocolBuilderConfiguração comum: URL base, cabeçalhos.
ScenarioBuilderA jornada de um utilizador virtual, passo a passo.
feed(...)Lê dados de um CSV para variar os pedidos. #{id} insere o valor.
check(...)Valida a resposta. Sem checks, um 500 contaria como sucesso!
injectOpen / injectClosedModelo aberto (chegadas por segundo) ou fechado (número fixo de utilizadores concorrentes).
assertionsCritérios de aprovação. Se falharem, o build falha.

Perfis de injeção mais comuns

Corre a simulação e vê o resumo que aparece no terminal. Muda a carga e o número de threads do servidor.

O relatório HTML

No fim, o Gatling escreve target/gatling/compra-…/index.html. Os gráficos mais úteis: «Response Time Percentiles over Time» (ver se o p95 sobe ao longo do teste) e «Number of Requests per Second» comparado com «Active Users».

JMeter e k6: as alternativas

O JMeter é o veterano (desde 1998), com interface gráfica e uma enorme comunidade. Suporta HTTP, JDBC, JMS, FTP e muito mais. Os testes são ficheiros XML (.jmx) construídos na interface.

ElementoPara que serve
Thread GroupGrupo de utilizadores virtuais: quantos, em quanto tempo arrancam (ramp-up), quantas repetições.
SamplerUm pedido (ex.: HTTP Request).
Config ElementConfiguração partilhada: HTTP Request Defaults, CSV Data Set Config.
TimerThink time (Uniform Random Timer).
AssertionValidar respostas (código, JSON).
ListenerMostrar resultados. Desliga-os durante o teste a sério: consomem imensa memória.

-n = sem interface; -t = plano de teste; -l = ficheiro de resultados; -e -o = gerar o painel HTML; -J = passar propriedades (lidas no plano com ${__P(utilizadores)}).

O k6 (da Grafana Labs) usa scripts em JavaScript, é leve, rápido e muito amigável para CI. Os thresholds fazem o mesmo papel das assertions do Gatling.

Qual escolher?

GatlingJMeterk6
Linguagem dos testesJava / Kotlin / ScalaXML via interface gráficaJavaScript
Eficiência do geradorMuito altaMédia (1 thread por utilizador)Muito alta
Testes como código / GitExcelenteFraco (XML difícil de rever)Excelente
ProtocolosHTTP, WebSocket, JMS, gRPC…O mais vastoHTTP, WebSocket, gRPC, browser
RelatórioHTML rico incluídoPainel HTML incluídoResumo no terminal; gráficos via Grafana
Ideal paraEquipas Java, integrar no Maven/GradleEquipas de QA sem programação, protocolos exóticosEquipas poliglotas, CI, ecossistema Grafana

Laboratório 4: uma simulação Gatling para a loja

Objetivo laboratório · 25 min

Criar LojaSimulation com duas jornadas (70% explorar, 30% comprar) contra a aplicação do curso, com SLO de p95 < 300 ms e menos de 1% de erros.

  1. Cria src/test/resources/produtos.csv com 200 ids reais da tua base de dados.
  2. Escreve os dois cenários, com check em todos os pedidos e think time.
  3. Faz um smoke test: 1 utilizador/s durante 30 s. Corrige até ter 0% de erros.
  4. Corre um load test: subir de 1 a 30 utilizadores/s em 1 minuto e manter 3 minutos.
  5. Abre o relatório HTML e anota: RPS máximo, p50, p95, p99 e erros.
Ver solução

Cada cenário tem a sua própria taxa de chegada: 21 + 9 = 30 utilizadores/s, na proporção 70/30. Alternativa: um único cenário com randomSwitch().on(percent(70.0).then(…), percent(30.0).then(…)).

Executar e encontrar o ponto de saturação

Durante um teste, olha para dois ecrãs ao mesmo tempo: o relatório do gerador de carga (o que o utilizador sente) e o painel Grafana da sessão 2 (o que o servidor sente). A explicação está sempre na ligação entre os dois.

Gerador de cargaRPS, p95, erros
⇄
Grafana / PrometheusCPU, heap, GC, threads Tomcat, Hikari pending
⇄
JFR / tracesQue método? Que consulta?
No gerador vês…No servidor procura…
p95 a subir com RPS estávelhikaricp.connections.pending > 0, threads Tomcat todas ocupadas: há uma fila.
Picos de latência periódicosjvm.gc.pause: pausas longas de GC.
Erros 500 de repenteLogs com CannotGetJdbcConnectionException, timeouts de serviços externos.
Erros de ligação recusadaaccept-count do Tomcat cheio, ficheiros abertos (ulimit) esgotados.
CPU a 100% e RPS a não subirGrava um JFR: o flame graph diz quem está a gastar CPU.

Simulador de stress test

Este simulador sobe a carga de 0 até ao máximo em 2 minutos (com 1 s de think time) e mostra o que acontece. Experimenta: começa com os valores por defeito, encontra o ponto em que o p95 dispara e depois afina a configuração para o empurrar para a direita.

Ler a curva

1. Zona linearMais utilizadores → mais RPS, latência estável. O servidor tem folga.
2. JoelhoO RPS deixa de subir. É o ponto de saturação: algum recurso chegou a 100%. A latência começa a subir.
3. SaturaçãoOs pedidos extra só fazem fila. A latência cresce em linha reta (Lei de Little) e chegam os timeouts.
4. ColapsoÀs vezes o RPS até desce: o servidor gasta tempo com pedidos que já expiraram, retries, GC. Isto é o que os mecanismos de resiliência evitam.
A capacidade utilizável não é o pico

Planeia para operar a cerca de 60–70% do ponto de saturação. A margem absorve picos, deploys, uma instância em baixo e o crescimento do próximo trimestre.

Degradação e recuperação

Num spike test, a pergunta mais importante é: depois do pico, a aplicação volta ao normal sozinha? Se a latência continua alta minutos depois, algo ficou «entupido»: filas internas cheias, ligações perdidas, uma cache que foi esvaziada, um circuit breaker preso. Mede sempre os 5 minutos depois do pico.

Tuning de uma aplicação Spring Boot

Regra de ouro: muda uma coisa de cada vez, repete o mesmo teste e compara. Se mudares três parâmetros e melhorar, não sabes qual ajudou (nem se algum piorou).

Servidor web (Tomcat)

Base de dados (HikariCP)

JVM em contentores

ParâmetroSintoma de que está malDireção
tomcat.threads.maxThreads todas ocupadas e CPU baixo.Subir, ou virtual threads. Mas confirma primeiro se a fila não é na BD.
hikari.maximum-pool-sizepending > 0 e a BD com CPU folgado.Subir aos poucos. Se a BD já estiver a 100%, subir piora.
-Xmx / MaxRAMPercentageGC muito frequente, Full GCs.Mais heap, ou reduzir alocações (JFR → Allocations).
GCPicos de p99 que coincidem com pausas.ZGC para pausas curtas.

Rever as sessões 1 e 2 sob carga

É agora que se vê o verdadeiro valor das técnicas anteriores. Repete o teste com e sem cada melhoria:

CacheCom hit ratio de 90%, a BD recebe 10 vezes menos consultas: o joelho afasta-se muito. Mas atenção aos dados de teste: se forem todos iguais, a cache parece milagrosa e mente.
ParalelismoReduz a latência de cada pedido, mas pode aumentar a pressão sobre os recursos partilhados (mais ligações em simultâneo).
N+1 corrigidoMenos idas à BD = ligações devolvidas mais cedo = mais pedidos por ligação.

Laboratório 5: stress, tuning e comparação laboratório · 20 min

  1. Corre um stress test em escada (20, 40, 60… utilizadores/s, 1 minuto cada) e anota o nível em que o p95 passa os 300 ms.
  2. No Grafana, identifica o recurso que saturou primeiro (CPU, threads, Hikari, GC).
  3. Muda um parâmetro relacionado com esse recurso e repete.
  4. Preenche a tabela: configuração · RPS no joelho · p95 · p99 · erros. Compara com os números que guardaste na sessão 1.

Testes de performance no pipeline

Um teste de carga que se corre uma vez por ano não apanha regressões. A ideia é ter um teste curto e estável que corre automaticamente (por exemplo, todas as noites ou antes de cada release) e falha o build se a performance piorar.

Ambientes efémeros com Testcontainers

O Testcontainers arranca bases de dados, Redis ou qualquer imagem Docker a partir dos testes, e desliga tudo no fim. Assim cada execução tem um ambiente limpo e igual.

Exemplo com GitHub Actions

Detetar regressões

  • Assertions absolutas: p95 < 300 ms. Simples, mas só falham quando já é tarde.
  • Comparação com a referência: guarda os resultados da última versão boa e falha se o p95 piorar mais de 15%. O Gatling Enterprise e o k6 Cloud fazem isto; também se faz com um script que lê o stats.json do relatório.
  • Ruído: runners de CI partilhados variam muito. Usa máquinas dedicadas para testes de performance, repete 3 vezes e compara a mediana, e tolera alguma variação.

Resiliência: falhar com elegância

Por mais que afines, haverá sempre um dia com mais carga do que a capacidade, ou um serviço externo que fica lento. Uma aplicação resiliente não cai: degrada-se graciosamente, servindo parte dos pedidos bem em vez de todos mal.

O disjuntor lá de casa

Quando há um curto-circuito, o disjuntor desliga a corrente para proteger a instalação. Não fica a tentar sem parar. Mais tarde voltas a ligá-lo para ver se o problema passou. Um circuit breaker faz o mesmo com chamadas a serviços que estão a falhar.

⏱ TimeoutNunca esperes para sempre. Uma chamada sem timeout segura uma thread e uma ligação indefinidamente.
🔌 Circuit breakerSe um serviço falha muito, deixa de lhe ligar durante algum tempo e responde logo com um plano B.
🚦 Rate limiterLimita quantos pedidos por segundo são aceites (por utilizador ou no total). O excesso recebe 429 Too Many Requests.
🚢 BulkheadLimita quantas chamadas simultâneas vão a cada dependência, para que uma lenta não consuma todas as threads.
🔁 RetryTenta outra vez falhas temporárias, com espera crescente. Cuidado: retries sem limite multiplicam a carga num serviço que já está em sofrimento.

Resilience4j no Spring Boot

Os três estados do circuit breaker

CLOSED 🟢Normal. As chamadas passam e conta-se a taxa de falhas.
→falhas ≥ limite
OPEN 🔴Não chama o serviço. Responde logo com o fallback. Poupa threads e dá tempo ao serviço para recuperar.
→após espera
HALF_OPEN 🟡Deixa passar algumas chamadas de teste. Se correrem bem, fecha; se falharem, volta a abrir.

Experimenta: faz alguns pagamentos, liga as falhas do banco, continua a pagar e vê o circuito abrir. Depois desliga as falhas e espera que recupere.

Proteger a entrada: rate limiting

Onde pôr o rate limiting

O Resilience4j limita por instância. Para limites globais ou por cliente (por API key), faz-se normalmente à entrada: num API gateway (Spring Cloud Gateway com Redis), no Nginx ou no balanceador da cloud.

Degradação graciosa e encerramento gracioso

  • Degradação graciosa: sob pressão, desliga o que é acessório. As recomendações personalizadas podem ser substituídas por uma lista fixa; o checkout nunca.
  • Encerramento gracioso: com server.shutdown=graceful, num deploy o Spring deixa de aceitar pedidos novos, termina os que estão em curso e só depois desliga. Combinado com o readiness probe do Actuator, os deploys deixam de gerar erros 502.

Conclusão: o ciclo completo

Ao longo das três sessões construíste um método, não apenas um conjunto de truques. É um ciclo que se repete:

1. MedirTeste de carga + métricas. Sessão 3.
→
2. AnalisarTraces, JFR, dumps. Encontrar O bottleneck. Sessão 2.
→
3. OtimizarCache, paralelismo, consultas, tuning. Sessões 1 e 3.
→
4. ValidarRepetir o mesmo teste. Melhorou? Não piorou nada?
↺

Pára quando o SLO for cumprido com margem. Otimização sem objetivo nunca acaba.

Recursos recomendados

TemaOnde aprofundar
ConcorrênciaLivro «Java Concurrency in Practice» (Goetz) · JEP 444 (Virtual Threads)
Performance da JVMLivro «Optimizing Cloud Native Java» (Evans, Gough) · blog de Aleksey Shipilёv
SpringDocumentação Spring Boot: «Production-ready Features», «Caching», «Task Execution»
HibernateBlog e livro «High-Performance Java Persistence» (Vlad Mihalcea)
Testes de cargadocs.gatling.io · grafana.com/docs/k6 · «Site Reliability Engineering» (Google), capítulos sobre SLOs

Exercícios

1. Ler um resultado fácil

Um teste dá: média 120 ms, p50 80 ms, p95 310 ms, p99 4200 ms, erros 0,2%. O SLO é p95 < 400 ms e p99 < 1000 ms. Passa? O que investigas primeiro?

Ver solução

Falha no p99 (4200 ms > 1000 ms), embora passe no p95. 1% dos pedidos está a ser muito lento. Uma cauda tão longa com p95 normal sugere algo esporádico: pausas de GC (ver jvm.gc.pause), espera por ligações do pool em momentos de pico, timeouts de um serviço externo ou um endpoint específico lento. Começa por separar o p99 por endpoint no relatório.

2. Lei de Little médio

Queres servir 500 pedidos/s com tempo médio de 80 ms. Cada pedido usa a base de dados durante 15 ms. Quantas threads e quantas ligações precisas, no mínimo?

Ver solução

Threads: 500 × 0,080 = 40 pedidos em curso. Ligações: 500 × 0,015 = 7,5 ligações ocupadas em média. Como os pedidos não chegam certinhos, dá margem: por exemplo 15–20 ligações e o pool padrão do Tomcat (200) chega com folga.

3. Spike test em k6 médio

Escreve as stages de um spike: 2 minutos a 20 utilizadores, salto para 500 em 10 segundos, 1 minuto a 500, volta a 20 e mantém 5 minutos para observar a recuperação.

Ver solução

Resumo da sessão

O essencial em 10 pontos

  • Load, stress, spike, soak e scalability respondem a perguntas diferentes. Começa sempre com um smoke test.
  • Usa percentis (p95, p99), nunca só a média. Não faças médias de percentis.
  • Lei de Little: pedidos em curso = pedidos/s × tempo médio.
  • Define SLOs antes de testar; sem objetivo não há conclusão.
  • Cenários realistas: mistura de jornadas, think time, dados variados, aquecimento.
  • Gatling (Java), JMeter (GUI, protocolos) e k6 (JavaScript): todos com critérios de aprovação.
  • O ponto de saturação é onde o RPS deixa de subir e a latência dispara. Opera a 60–70% dele.
  • Muda um parâmetro de cada vez: Tomcat, Hikari, heap, GC.
  • Testes curtos e estáveis no pipeline apanham regressões.
  • Timeouts, circuit breakers, bulkheads e rate limiting evitam o colapso.

Glossário

RPS
Pedidos por segundo: medida de throughput.
Percentil
Valor abaixo do qual fica uma dada percentagem das medições.
SLO
Objetivo de nível de serviço, ex.: p95 < 500 ms.
Think time
Pausa simulada entre ações de um utilizador.
Modelo aberto
Carga definida por chegadas por segundo, independentes da resposta.
Ponto de saturação
Carga a partir da qual o débito deixa de crescer.
Circuit breaker
Mecanismo que corta chamadas a um serviço em falha durante algum tempo.
Bulkhead
Isolamento de recursos por dependência.
Rate limiting
Limite de pedidos aceites por unidade de tempo.