Performance testing: pgbench e simulação de carga real

1. Introdução ao Teste de Performance em Bancos de Dados

1.1. Por que testar performance: gargalos, escalabilidade e SLA

Testar a performance de um banco de dados não é um luxo — é uma necessidade para qualquer aplicação que precise crescer sem perder qualidade. Gargalos como consultas lentas, contenção de locks ou I/O de disco insuficiente podem transformar uma aplicação responsiva em uma experiência frustrante. Testes de performance ajudam a identificar esses pontos antes que eles afetem usuários reais, garantindo que o banco atenda aos SLAs (Acordos de Nível de Serviço) estabelecidos.

1.2. Diferença entre teste de carga, teste de estresse e teste de volume

  • Teste de carga: mede o comportamento do sistema sob cargas esperadas (número típico de usuários simultâneos).
  • Teste de estresse: aumenta a carga além do normal para descobrir o ponto de ruptura.
  • Teste de volume: foca na quantidade de dados armazenados, verificando como a performance se degrada com tabelas grandes.

1.3. Ferramentas populares: pgbench, sysbench, HammerDB

  • pgbench: nativo do PostgreSQL, simples e eficaz para testes padronizados e customizados.
  • sysbench: multi-banco (MySQL, PostgreSQL), flexível para cenários de CPU, memória e I/O.
  • HammerDB: interface gráfica, suporta TPC-C e TPC-H, ideal para testes mais elaborados.

Para este artigo, focaremos exclusivamente no pgbench.

2. Instalação e Configuração Básica do pgbench

2.1. Pré-requisitos

O pgbench acompanha a instalação padrão do PostgreSQL. Você precisa de um banco de dados com permissões de superusuário.

# Verificar se o pgbench está disponível
pgbench --version

# Criar um banco de teste (como superusuário)
createdb teste_performance

2.2. Inicialização do ambiente de teste

O comando pgbench -i cria as tabelas padrão: pgbench_accounts, pgbench_branches, pgbench_history e pgbench_tellers.

# Inicializar com escala 100 (10 milhões de linhas em pgbench_accounts)
pgbench -i -s 100 teste_performance

O parâmetro -s define o fator de escala. Cada unidade representa 100.000 linhas em pgbench_accounts. Escolha um valor que se aproxime do volume real da sua aplicação.

2.3. Parâmetros essenciais

  • -c: número de clientes simultâneos (simula usuários concorrentes)
  • -t: número de transações por cliente
  • -T: duração do teste em segundos (substitui -t)
# Exemplo: 10 clientes, cada um executando 1000 transações
pgbench -c 10 -t 1000 teste_performance

# Exemplo: 20 clientes durante 60 segundos
pgbench -c 20 -T 60 teste_performance

3. Simulação de Carga Real com Scripts Customizados

3.1. Estrutura de um script SQL customizado

O pgbench aceita scripts SQL com variáveis especiais. Cada linha é uma transação, e o \set permite definir variáveis.

3.2. Exemplos de scripts realistas: e-commerce

Imagine um cenário de e-commerce: consultar pedidos, verificar estoque e atualizar status.

-- script_ecommerce.sql
\set client_id random(1, 100000)
\set product_id random(1, 50000)
\set quantity random(1, 5)

BEGIN;
SELECT o.order_id, o.status 
FROM orders o 
WHERE o.client_id = :client_id 
LIMIT 10;

SELECT p.stock_quantity 
FROM products p 
WHERE p.product_id = :product_id;

UPDATE products 
SET stock_quantity = stock_quantity - :quantity 
WHERE product_id = :product_id 
AND stock_quantity >= :quantity;
END;

3.3. Uso de variáveis aleatórias

O pgbench oferece funções como random() e variáveis de sistema (:scale, :client_id) para simular comportamentos reais.

-- script_vendas.sql
\set aid random(1, 100000 * :scale)
\set bid random(1, 1 * :scale)
\set tid random(1, 10 * :scale)
\set delta random(-5000, 5000)

BEGIN;
UPDATE pgbench_accounts SET abalance = abalance + :delta WHERE aid = :aid;
SELECT abalance FROM pgbench_accounts WHERE aid = :aid;
UPDATE pgbench_tellers SET tbalance = tbalance + :delta WHERE tid = :tid;
UPDATE pgbench_branches SET bbalance = bbalance + :delta WHERE bid = :bid;
INSERT INTO pgbench_history (tid, bid, aid, delta, mtime) VALUES (:tid, :bid, :aid, :delta, CURRENT_TIMESTAMP);
END;

Execute com:

pgbench -c 10 -T 30 -f script_vendas.sql teste_performance

4. Métricas e Relatórios de Performance

4.1. Saída padrão do pgbench

Após a execução, o pgbench exibe:

transaction type: script_vendas.sql
scaling factor: 100
query mode: simple
number of clients: 10
number of threads: 1
duration: 30 s
number of transactions actually processed: 4521
latency average = 66.384 ms
tps = 150.678 (including connections establishing)
tps = 150.789 (excluding connections establishing)
  • TPS: transações por segundo — quanto maior, melhor.
  • Latência média: tempo médio por transação.
  • Para percentis (p50, p90, p99), use --log e ferramentas externas.

4.2. Análise de logs detalhados

pgbench -c 10 -T 30 -f script_vendas.sql --log=pgbench_log.csv teste_performance

# O arquivo pgbench_log.csv conterá:
# client_id, transaction_no, time, interval_time

Use --aggregate-interval=5 para médias a cada 5 segundos.

4.3. Monitoramento adicional durante o teste

Conecte-se ao banco em paralelo:

-- No banco de teste
SELECT * FROM pg_stat_activity WHERE state = 'active';

-- Para consultas lentas
SELECT query, calls, total_time, mean_time 
FROM pg_stat_statements 
ORDER BY total_time DESC 
LIMIT 10;

5. Interpretação de Resultados e Identificação de Gargalos

5.1. Latência alta vs. TPS baixo

  • Latência alta + TPS baixo: provável gargalo de CPU ou I/O.
  • Latência baixa + TPS baixo: contenção de locks ou configuração errada de pool de conexões.
  • Latência crescente com mais clientes: escalabilidade limitada (ponto de saturação).

5.2. Correlação com recursos do sistema

# Monitorar CPU, memória e I/O durante o teste
top -p $(pgrep -f postgres)
iostat -x 1

5.3. Casos comuns

  • Lock contention: muitas transações tentando modificar a mesma linha.
  • Índices ausentes: consultas com EXPLAIN ANALYZE mostrando sequential scans.
  • Checkpoints frequentes: shared_buffers pequeno ou checkpoint_timeout baixo.

6. Estratégias para Otimização com Base nos Testes

6.1. Ajuste de configuração do PostgreSQL

-- Exemplo de parâmetros para workload de escrita intensa
shared_buffers = '2GB'           # 25% da RAM
work_mem = '64MB'                # para consultas complexas
checkpoint_completion_target = 0.9
checkpoint_timeout = '15min'

6.2. Criação e revisão de índices

EXPLAIN ANALYZE SELECT * FROM orders WHERE client_id = 123;

-- Se mostrar Sequential Scan, criar índice:
CREATE INDEX idx_orders_client_id ON orders(client_id);

Repita o teste e compare o TPS.

6.3. Refinamento do script de carga

  • Reduza transações desnecessárias (ex.: consultas que não impactam o fluxo principal).
  • Use batch inserts quando possível.
-- Em vez de 100 inserts individuais, use:
INSERT INTO orders (client_id, product_id, quantity) 
SELECT random(1,100000), random(1,50000), random(1,5) 
FROM generate_series(1,100);

7. Boas Práticas e Cuidados em Ambiente de Produção

7.1. Isolamento do ambiente de teste

Nunca execute testes de carga em produção. Use um banco dedicado ou um snapshot restaurado.

# Criar banco a partir de um dump
createdb teste_producao_snapshot
pg_restore -d teste_producao_snapshot /caminho/do/dump.sql

7.2. Repetibilidade

Para comparar resultados, mantenha a mesma escala, número de clientes e seed aleatória:

pgbench -i -s 100 teste_performance
pgbench -c 10 -T 60 -f script.sql --random-seed=42 teste_performance

7.3. Documentação dos cenários

Registre para cada teste:
- Versão do PostgreSQL
- Parâmetros de configuração
- Script usado
- Métricas obtidas (TPS, latência, percentis)
- Recursos do servidor (CPU, RAM, disco)

Referências