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.
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.
O que vais ver nesta sessão
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:
| Tipo | Pergunta | Duração típica |
|---|---|---|
| Load (carga) | Com a carga normal esperada, cumprimos os objetivos de tempo de resposta? | 15–60 min |
| Stress | Onde é 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 |
| Scalability | Se duplicarmos as instâncias, duplicamos a capacidade? | Vários testes |
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)
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.
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.
| Percentil | Significado | Para que serve |
|---|---|---|
| p50 (mediana) | Metade dos pedidos é mais rápida. | A experiência «típica». |
| p95 | Só 5% são mais lentos. | O objetivo mais usado em SLOs. |
| p99 | Só 1% é mais lento. | A «cauda». Num site com 1 milhão de pedidos por dia, são 10 000 pessoas. |
| máximo | O pior caso. | Útil para detetar timeouts e pausas de GC. |
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):
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 utilizadores | Passos |
|---|---|---|
| Explorar | 70% | Página inicial → pesquisar → ver 3 produtos |
| Comprar | 20% | Pesquisar → produto → carrinho → checkout → pagar |
| Conta | 10% | Login → ver encomendas → detalhe de uma encomenda |
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ça | O que é |
|---|---|
HttpProtocolBuilder | Configuração comum: URL base, cabeçalhos. |
ScenarioBuilder | A 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 / injectClosed | Modelo aberto (chegadas por segundo) ou fechado (número fixo de utilizadores concorrentes). |
assertions | Crité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.
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.
| Elemento | Para que serve |
|---|---|
| Thread Group | Grupo de utilizadores virtuais: quantos, em quanto tempo arrancam (ramp-up), quantas repetições. |
| Sampler | Um pedido (ex.: HTTP Request). |
| Config Element | Configuração partilhada: HTTP Request Defaults, CSV Data Set Config. |
| Timer | Think time (Uniform Random Timer). |
| Assertion | Validar respostas (código, JSON). |
| Listener | Mostrar 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?
| Gatling | JMeter | k6 | |
|---|---|---|---|
| Linguagem dos testes | Java / Kotlin / Scala | XML via interface gráfica | JavaScript |
| Eficiência do gerador | Muito alta | Média (1 thread por utilizador) | Muito alta |
| Testes como código / Git | Excelente | Fraco (XML difícil de rever) | Excelente |
| Protocolos | HTTP, WebSocket, JMS, gRPC… | O mais vasto | HTTP, WebSocket, gRPC, browser |
| Relatório | HTML rico incluído | Painel HTML incluído | Resumo no terminal; gráficos via Grafana |
| Ideal para | Equipas Java, integrar no Maven/Gradle | Equipas de QA sem programação, protocolos exóticos | Equipas 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.
- Cria
src/test/resources/produtos.csvcom 200 ids reais da tua base de dados. - Escreve os dois cenários, com
checkem todos os pedidos e think time. - Faz um smoke test: 1 utilizador/s durante 30 s. Corrige até ter 0% de erros.
- Corre um load test: subir de 1 a 30 utilizadores/s em 1 minuto e manter 3 minutos.
- 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.
| No gerador vês… | No servidor procura… |
|---|---|
| p95 a subir com RPS estável | hikaricp.connections.pending > 0, threads Tomcat todas ocupadas: há uma fila. |
| Picos de latência periódicos | jvm.gc.pause: pausas longas de GC. |
| Erros 500 de repente | Logs com CannotGetJdbcConnectionException, timeouts de serviços externos. |
| Erros de ligação recusada | accept-count do Tomcat cheio, ficheiros abertos (ulimit) esgotados. |
| CPU a 100% e RPS a não subir | Grava 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
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âmetro | Sintoma de que está mal | Direção |
|---|---|---|
tomcat.threads.max | Threads todas ocupadas e CPU baixo. | Subir, ou virtual threads. Mas confirma primeiro se a fila não é na BD. |
hikari.maximum-pool-size | pending > 0 e a BD com CPU folgado. | Subir aos poucos. Se a BD já estiver a 100%, subir piora. |
-Xmx / MaxRAMPercentage | GC muito frequente, Full GCs. | Mais heap, ou reduzir alocações (JFR → Allocations). |
| GC | Picos 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:
Laboratório 5: stress, tuning e comparação laboratório · 20 min
- 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.
- No Grafana, identifica o recurso que saturou primeiro (CPU, threads, Hikari, GC).
- Muda um parâmetro relacionado com esse recurso e repete.
- 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.jsondo 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.
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.
429 Too Many Requests.Resilience4j no Spring Boot
Os três estados do circuit breaker
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
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:
Pára quando o SLO for cumprido com margem. Otimização sem objetivo nunca acaba.
Recursos recomendados
| Tema | Onde aprofundar |
|---|---|
| Concorrência | Livro «Java Concurrency in Practice» (Goetz) · JEP 444 (Virtual Threads) |
| Performance da JVM | Livro «Optimizing Cloud Native Java» (Evans, Gough) · blog de Aleksey Shipilёv |
| Spring | Documentação Spring Boot: «Production-ready Features», «Caching», «Task Execution» |
| Hibernate | Blog e livro «High-Performance Java Persistence» (Vlad Mihalcea) |
| Testes de carga | docs.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.
Projeto final do curso
Relatório de performance da loja. Com a aplicação do curso, entrega um relatório de 2 páginas com: o SLO definido; os resultados de um load test e de um stress test (RPS, p50, p95, p99, erros) antes de qualquer otimização; o bottleneck encontrado (com evidência: gráfico do Grafana, flame graph ou trace); as otimizações aplicadas (cache, paralelismo, correção de consultas, tuning) com o ganho medido de cada uma; e a capacidade final recomendada por instância.
Parabéns por chegares ao fim! Tens agora as ferramentas para fazer o que as equipas de performance fazem: medir, perceber, melhorar e provar que melhorou.