Otimização de Desempenho
Otimização de desempenho é um trabalho de engenharia de sistemas — envolve três dimensões: a JVM, o banco de dados e o pool de conexões. Somente identificando gargalos com precisão podemos tratar as causas raiz.
1. O Que Você Vai Aprender
- Ajuste de Parâmetros da JVM: Tamanho do Heap / Seleção do Algoritmo GC (G1GC / ZGC)
- Otimização de Parâmetros do Pool de Conexões HikariCP e Detecção de Vazamento de Conexão
- Otimização de Consultas JPA: O Problema N+1 e Soluções
@EntityGraph/fetch join - Estratégias de Indexação do Banco de Dados e Análise de Log de Queries Lentas
- Alice otimizou a latência P99 da API de lista de pedidos do OrderFlow de 500 ms para 50 ms
2. Uma História Real de um Engenheiro de Desempenho
(1) Dor: A API é lenta como uma lesma
Charlie relatou que a latência P99 da API de lista de pedidos do OrderFlow era de 500 ms, resultando em uma experiência de usuário muito ruim. A análise de Alice revelou o seguinte: 1) Pausas frequentes de Full GC na JVM durando 200 ms; 2) Um problema N+1 ocorrendo quando JPA consulta a lista de pedidos (100 pedidos = 101 queries SQL); 3) O tamanho do pool de conexões HikariCP é insuficiente, causando timeout nas requisições enquanto aguardam uma conexão.
(2) Métodos de Solução para Otimização Sistêmica
Otimizar Camada por Camada para Abordar Gargalos com Precisão:
| Nível | Gargalo | Solução de Otimização |
|---|---|---|
| JVM | Pausa de Full GC | G1GC + Ajuste do Tamanho do Heap |
| Pool de Conexões | Conexões Insuficientes | Otimização de Parâmetros HikariCP |
| ORM | Queries N+1 | JOIN FETCH / EntityGraph |
| Banco de Dados | Scan Completo da Tabela | Adicionar Índice |
(3) Resultado
Depois que Alice otimizou cada componente um a um, a latência P99 caiu de 500 ms para 50 ms e a throughput aumentou dez vezes.
3. Ajuste de Parâmetros da JVM
(1) Comparação de Algoritmos GC
| Algoritmo GC | Pausa Máxima | Cenários Adequados | Parâmetros JVM |
|---|---|---|---|
| G1GC | 10-200 ms | Geral (padrão JDK 17) | -XX:+UseG1GC |
| ZGC | < 1 ms | Baixa Latência (JDK 17+) | -XX:+UseZGC |
| SerialGC | Longa | Heap Pequena (< 200MB) | -XX:+UseSerialGC |
| ParallelGC | Média | Prioridade de Throughput | -XX:+UseParallelGC |
graph LR
A["Tuning JVM"] --> B["Tamanho do Heap<br/>-Xms = -Xmx"]
A --> C["Algoritmo GC<br/>G1 / ZGC"]
A --> D["Log do GC<br/>-Xlog:gc*"]
B --> B1["Contêiner: MaxRAMPercentage"]
C --> C1["Padrão: G1GC"]
C --> C2["Baixa latência: ZGC"]
(1) ▶ Exemplo: Configuração JVM em Ambiente Docker
ENTRYPOINT ["java", \
"-XX:+UseG1GC", \
"-XX:MaxRAMPercentage=75.0", \
"-XX:InitialRAMPercentage=50.0", \
"-XX:+UseStringDeduplication", \
"-Xlog:gc*:file=/app/logs/gc.log:time,uptime,level,tags", \
"-jar", "app.jar"]
Saída:
// Execução bem-sucedida
| Parâmetro | Significado | Valor Recomendado |
|---|---|---|
MaxRAMPercentage |
Heap máximo como percentual da memória do contêiner | 75.0 |
InitialRAMPercentage |
Percentual da memória do contêiner ocupado pelo heap inicial | 50.0 |
+UseG1GC |
Usar o coletor de lixo G1 | Habilitado por padrão |
+UseStringDeduplication |
Desduplicação de Strings Economiza Memória | Recomendado |
MaxGCPauseMillis |
Tempo Alvo de Pausa do GC | 200 (G1) / Nenhum (ZGC) |
(2) ▶ Exemplo: Configuração ZGC de Baixa Latência
java -XX:+UseZGC \
-XX:MaxRAMPercentage=75.0 \
-XX:+ZGenerational \
-Xlog:gc*:file=gc.log \
-jar app.jar
Saída:
// Comando executado com sucesso
4. Otimização do Pool de Conexões HikariCP
(1) Parâmetros do Pool de Conexões
| Parâmetro | Valor Padrão | Descrição | Valor Recomendado |
|---|---|---|---|
maximumPoolSize |
10 | Número máximo de conexões | Núcleos de CPU x 2 + número de discos |
minimumIdle |
= maxPool | Conexões ociosas mínimas | = maxPoolSize |
connectionTimeout |
30.000 ms | Timeout de conexão | 3.000 ms |
idleTimeout |
600.000 ms | Timeout de conexão ociosa | 600.000 ms |
maxLifetime |
1.800.000 ms | Tempo de vida máximo da conexão | 1.800.000 ms |
leakDetectionThreshold |
0 (Desabilitado) | Detecção de Vazamento de Conexão | 60.000 ms |
(1) ▶ Exemplo: Configuração de Produção do HikariCP
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 20
connection-timeout: 3000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 60000
pool-name: OrderFlowHikariCP
Saída:
A configuração entrou em vigor.
(2) ▶ Exemplo: Detecção de Vazamento de Conexão
# Habilitar detecção de vazamento (aviso no log se conexão mantida > 60s)
spring:
datasource:
hikari:
leak-detection-threshold: 60000
WARN - Connection leak detection triggered for {conn-12345}
Stack trace of the call site that acquired the connection:
at com.orderflow.service.OrderService.getOrder(OrderService.java:45)
...
5. Otimização de Consultas JPA
(1) O Problema N+1
(1) ▶ Exemplo: Demonstração do Problema N+1
// RUIM: problema N+1
// 1 SQL para buscar 100 pedidos + 100 SQLs para buscar items de cada pedido = 101 SQLs!
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
order.getItems().size(); // Dispara carregamento lazy para cada pedido
}
Saída:
// Execução bem-sucedida
(2) ▶ Exemplo: Usando JOIN FETCH para Resolver o Problema N+1
// BOM: Query única com JOIN FETCH
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items i JOIN FETCH i.product WHERE o.status = :status")
List<Order> findByStatusWithItems(@Param("status") String status);
Saída:
// Execução bem-sucedida
(3) ▶ Exemplo: Carregamento Declarativo com @EntityGraph
@Entity
@NamedEntityGraph(
name = "Order.withItemsAndProduct",
attributeNodes = {
@NamedAttributeNode("items"),
@NamedAttributeNode(value = "items", subgraph = "item-product")
},
subgraphs = {
@NamedSubgraph(name = "item-product", attributeNodes = @NamedAttributeNode("product"))
}
)
public class Order { /* ... */ }
// Uso no Repository
@EntityGraph(value = "Order.withItemsAndProduct", type = EntityGraphType.LOAD)
List<Order> findByStatus(String status);
Saída:
// Execução bem-sucedida
| Solução | Número de Statements SQL | Cenários Aplicáveis | Manutenibilidade |
|---|---|---|---|
| Lazy Loading Padrão | N+1 | Query de Objeto Único | Alta |
| JOIN FETCH | 1 | Query Específica | Média |
| @EntityGraph | 1 | Plano de carregamento reutilizável | Alta |
| @BatchSize | N/batchSize | Carregamento em Lote | Alta |
6. Otimização de Índices do Banco de Dados
(1) Estratégia de Indexação
(1) ▶ Exemplo: Adicionando Índice no Banco de Dados
-- Índice para consultas por status de pedido
CREATE INDEX idx_order_status ON orders(status);
-- Índice composto para padrão de consulta comum
CREATE INDEX idx_order_status_created ON orders(status, created_at DESC);
-- Índice para busca de produtos
CREATE INDEX idx_product_name ON products(name);
-- Índice para lookup de produto nos items de pedido
CREATE INDEX idx_order_item_product ON order_items(product_id);
Saída:
CREATE TABLE
| Tipo de Índice | Caso de Uso | Exemplo |
|---|---|---|
| Índice de coluna única | Query de condição única | WHERE status = 'PENDING' |
| Índice Composto | Query de Múltiplas Condições | WHERE status = ? AND created_at > ? |
| Índice Único | Restrição de Unicidade | UNIQUE(sku) |
| Índice de Cobertura | Índice contém todas as colunas da query | SELECT status FROM orders WHERE id = ? |
(2) ▶ Exemplo: Análise de Query Lenta
-- Configuração do log de queries lentas do MySQL
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- Log queries > 1 segundo
SET GLOBAL log_queries_not_using_indexes = ON;
-- Analisar plano de execução da query
EXPLAIN SELECT o.*, i.*
FROM orders o
JOIN order_items i ON o.id = i.order_id
WHERE o.status = 'PENDING'
AND o.created_at > '2024-01-01'
ORDER BY o.created_at DESC
LIMIT 20;
Saída:
CREATE TABLE
| Campo EXPLAIN | Significado | Pontos Chave |
|---|---|---|
type |
Tipo de Acesso | ALL (Scan Completo da Tabela) Precisa de Otimização |
key |
Índice usado | NULL indica que nenhum índice foi usado |
rows |
Número de Linhas Escaneadas | Quanto menor, melhor |
Extra |
Informação Adicional | Using filesort requer otimização |
7. Exemplo Completo: P99 do OrderFlow Reduzido de 500 ms para 50 ms
# application-prod.yml (otimizado)
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 20
connection-timeout: 3000
leak-detection-threshold: 60000
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
default_batch_fetch_size: 100
jdbc:
batch_size: 50
order_inserts: true
order_updates: true
# Parâmetros JVM (Docker ENTRYPOINT)
java -XX:+UseG1GC \
-XX:MaxRAMPercentage=75.0 \
-XX:InitialRAMPercentage=50.0 \
-XX:+UseStringDeduplication \
-XX:MaxGCPauseMillis=100 \
-Xlog:gc*:file=/app/logs/gc.log:time,uptime,level,tags \
-jar app.jar
// OrderRepository otimizado com EntityGraph e busca em lote
public interface OrderRepository extends JpaRepository<Order, Long> {
@EntityGraph(value = "Order.withItemsAndProduct", type = EntityGraphType.LOAD)
@Query("SELECT o FROM Order o WHERE o.status = :status ORDER BY o.createdAt DESC")
Page<Order> findByStatusPaged(@Param("status") String status, Pageable pageable);
@EntityGraph(value = "Order.withItemsAndProduct", type = EntityGraphType.LOAD)
Optional<Order> findById(Long id);
}
// Configuração de batch size do Hibernate elimina N+1 para coleções
// application.yml: hibernate.default_batch_fetch_size=100
| Antes da Otimização | Depois da Otimização | Fator de Melhoria |
|---|---|---|
| P99: 500 ms | P99: 50 ms | 10x |
| SQL/Requisição: 101 | SQL/Requisição: 1 | 100x |
| Pausa GC: 200 ms | Pausa GC: 50 ms | 4x |
| Espera de conexão: 100 ms | Espera de conexão: 2 ms | 50x |
❓ Perguntas Frequentes
batch_size do Hibernate faz?hibernate.jdbc.batch_size=50 é configurado, statements INSERT/UPDATE são executados em lotes (50 registros por lote), reduzindo o número de round trips ao banco de dados. Funciona ainda melhor quando usado em conjunto com order_inserts=true./metrics para verificar durações de requisições HTTP; 2) Analise logs de GC para identificar pausas de GC; 3) Métricas HikariCP para verificar o status do pool de conexões; 4) Log de queries lentas do MySQL para identificar queries lentas; 5) Ferramentas APM (SkyWalking/Zipkin) para rastreamento end-to-end.📖 Resumo
- Tuning JVM: G1GC é suficiente por padrão; ZGC é projetado para ultra baixa latência; MaxRAMPercentage controla a memória do contêiner
- HikariCP: Tamanho do pool de conexões = núcleos de CPU x 2 + número de discos; habilitar leakDetection para detectar vazamentos de memória
- O Problema N+1: JOIN FETCH em uma única SQL, carregamento declarativo com @EntityGraph e carregamento em lote com @BatchSize
- Índices do banco de dados: Adicionar índices para condições de consulta frequentes; usar EXPLAIN para analisar o plano de execução
- Processamento em lote: hibernate.jdbc.batch_size + order_inserts reduz round trips ao banco de dados
- Otimização Sistêmica: Identificar gargalos → Quantificar baselines → Otimizar camada por camada → Verificar resultados
📝 Exercícios
-
Problema Básico (Dificuldade: ⭐): Configure os parâmetros G1GC da JVM e do pool de conexões HikariCP para o OrderFlow, e habilite o log de GC e a detecção de vazamento de conexão.
-
Problema Avançado (Dificuldade ⭐⭐): Use
@EntityGrapheJOIN FETCHpara resolver o problema N+1 na consulta da lista de pedidos e compare o número de statements SQL e tempos de resposta antes e depois da otimização. -
Desafio (Dificuldade: ⭐⭐⭐): Use JMeter para realizar testes de carga na API de lista de pedidos do OrderFlow, estabeleça uma baseline de desempenho e otimize gradualmente (JVM + pool de conexões + N+1 + índices). Registre as mudanças em P50 e P99 para cada etapa de otimização e, por fim, otimize o P99 para abaixo de 50 ms.



