Otimização de Performance — De 100 a 1 Milhão de QPS
A otimização de performance é como construir uma rodovia — você começa corrigindo as seções mais congestionadas (os gargalos), medindo os resultados após cada seção concluída, em vez de alargar toda a estrada indiscriminadamente. Otimização às cegas é desperdício; otimização orientada por medição é a abordagem mais eficiente.
1. O Que Você Vai Aprender
- Resolução de Bloqueio Assíncrono: Armadilhas do
asyncioe Reescrita de Código Síncrono comrun_in_executor - Otimização de Banco de Dados: Estratégias de Indexação, Otimização de Consultas, Ajuste do Pool de Conexões
- Modelo de Concorrência: Número de Workers Uvicorn e Configuração Gunicorn
- Compressão de Resposta:
GZipMiddlewaree Otimização de Serialização JSON (orjson) - Cenário Alice: O Caminho de Otimização do PriceTracker de 500 QPS em Servidor Único para 1 Milhão de QPS em Cluster
2. A História Real da Alice
(1) Problema: 500 QPS em servidor único não é suficiente
Após o lançamento do PriceTracker, um único servidor só conseguia lidar com 500 QPS, mas durante grandes promoções, o tráfego subia para 5.000 QPS, causando timeouts nas respostas da API. Charlie adicionou mais servidores, mas a melhoria foi insignificante — o pool de conexões do banco de dados estava esgotado, o código síncrono bloqueava o event loop, e a serialização JSON consumia 40% da CPU.
(2) Uma Abordagem Sistemática para Otimização de Performance
A otimização de performance não é adivinhação; é um ciclo de medição → identificação de gargalos → otimização → validação. Resolvemos problemas camada por camada, do nível de código (async/serialização) ao nível de banco de dados (índices/pools de conexão) até o nível arquitetural (Workers/balanceamento de carga).
(3) Resultado
O QPS do PriceTracker foi otimizado de 500 para 10.000 (em servidor único), e com um cluster de balanceamento de carga, pode atingir 1 milhão de QPS, enquanto a latência P99 foi reduzida de 500 ms para 30 ms.
3. Níveis de Otimização de Performance
(1) Modelo de Otimização em Três Camadas
graph TD
L3[Camada de Infraestrutura] --> L2[Camada de Banco de Dados]
L2 --> L1[Camada de Aplicação]
L1 --- A1[async/await - Detecção de bloqueio]
L1 --- A2[Serialização - orjson]
L1 --- A3[Compressão - GZip]
L2 --- D1[Índices - Otimização de consultas]
L2 --- D2[Pool de Conexões - Ajuste de tamanho]
L2 --- D3[Padrões de Consulta - Prevenção N+1]
L3 --- I1[Workers - Config Gunicorn]
L3 --- I2[Load Balancer - Nginx]
L3 --- I3[Auto-scaling - K8s HPA]
(2) Caminhos para Melhorar o QPS
flowchart LR
A[500 QPS\nBaseline] -->|async + orjson| B[2000 QPS]
B -->|DB indexes + pool| C[10000 QPS]
C -->|4 Workers| D[40000 QPS]
D -->|Nginx LB x 5| E[200000 QPS]
E -->|K8s x 5 pods| F[1000000 QPS]
| Fase | Método de Otimização | QPS | Fator de Melhoria |
|---|---|---|---|
| Baseline | Configuração Padrão | 500 | 1x |
| Camada de Aplicação | async + orjson | 2.000 | 4x |
| Camada de Banco de Dados | Índices + Pool de Conexões | 10.000 | 20x |
| Workers | 4 Workers + Gunicorn | 40.000 | 80x |
| Balanceamento de Carga | 5 instâncias Nginx | 200.000 | 400x |
| Elasticidade K8s | 5 Pods Auto-scaling | 1.000.000 | 2000x |
4. Otimização da Camada de Aplicação
(1) Resolução de Bloqueio Assíncrono
(1) ▶Exemplo: Código Síncrono Que Bloqueia o Event Loop
import asyncio
import time
# RUIM: I/O síncrono bloqueia o event loop
async def get_price_bad(product_id: int):
time.sleep(0.1) # Bloqueia TODAS as outras requisições por 100ms!
return {"product_id": product_id, "price": 9.99}
# BOM: usar asyncio para I/O
async def get_price_good(product_id: int):
await asyncio.sleep(0.1) # Não-bloqueante, outras requisições continuam
return {"product_id": product_id, "price": 9.99}
Saída:
# Função definida com sucesso
(2) ▶Exemplo: run_in_executor encapsula código síncrono
import asyncio
from functools import partial
# Função síncrona que não pode ser tornada async
def sync_scrape_price(url: str) -> float:
# Usa a biblioteca requests (apenas síncrona)
import requests
response = requests.get(url, timeout=10)
return parse_price(response.text)
async def get_price_async(product_id: int):
loop = asyncio.get_event_loop()
# Executar função síncrona no pool de threads - não bloqueia o event loop
price = await loop.run_in_executor(
None, # Pool de threads padrão
partial(sync_scrape_price, f"https://api.example.com/prices/{product_id}"),
)
return {"product_id": product_id, "price": price}
Saída:
# Função definida com sucesso
(2) Otimização de Serialização JSON
(3) ▶Exemplo: Usando orjson em vez de json
# Instalar: uv add orjson
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
app = FastAPI(default_response_class=ORJSONResponse)
@app.get("/api/v1/products")
async def list_products():
# orjson é 3-10x mais rápido que json da stdlib
return {"products": [{"id": i, "name": f"Product {i}"} for i in range(100)]}
Saída:
# Função definida com sucesso
| Biblioteca de Serialização | Velocidade | Descrição |
|---|---|---|
| json (biblioteca padrão) | Referência | Implementação Python pura |
| orjson | 3-10x | Implementado em Rust, lida automaticamente com datetime |
| ujson | 2-3x | Implementação C, alguns problemas de compatibilidade |
(4) ▶Exemplo: Compressão com GZipMiddleware
from fastapi import FastAPI
from starlette.middleware.gzip import GZipMiddleware
app = FastAPI()
app.add_middleware(GZipMiddleware, minimum_size=1000) # Comprimir respostas > 1KB
@app.get("/api/v1/products")
async def list_products():
# Respostas JSON grandes são automaticamente comprimidas
return {"products": [...]} # 50KB → ~5KB com gzip
Saída:
# Função definida com sucesso
5. Otimização da Camada de Banco de Dados
(1) Estratégia de Indexação
(1) ▶Exemplo: Design de Índices do PriceTracker
from sqlalchemy import Index
class Product(Base):
__tablename__ = "products"
__table_args__ = (
# Índices de coluna única para filtros comuns
Index("ix_products_category", "category"),
Index("ix_products_created_at", "created_at"),
# Índice composto para consultas de categoria + faixa de preço
Index("ix_products_category_base_price", "category", "base_price"),
# Índice parcial apenas para produtos ativos (específico do PostgreSQL)
Index("ix_products_active", "created_at", postgresql_where=("deleted_at IS NULL")),
)
...
Saída:
# Execução Bem-sucedida
| Tipo de Índice | Consultas Adequadas | Exemplo |
|---|---|---|
| Índice de coluna única | Filtro de igualdade/faixa | WHERE category = ? |
| Índice Composto | Combinações de Múltiplas Condições | WHERE category = ? AND price > ? |
| Índice Parcial | Subconjunto Condicional | WHERE deleted_at IS NULL |
| Índice de Cobertura | Evitar Lookup na Tabela | INCLUDE (name, price) |
(2) Ajuste do Pool de Conexões
(2) ▶Exemplo: Configuração de Pool de Conexões em Nível de Produção
engine = create_async_engine(
DATABASE_URL,
pool_size=25, # Conexões persistentes por worker
max_overflow=10, # Conexões extras quando pool esgotado
pool_timeout=30, # Tempo de espera por conexão disponível
pool_recycle=1800, # Reciclar conexões após 30 min
pool_pre_ping=True, # Testar conexão antes de usar
echo=False, # Desativar log SQL em produção
)
Saída:
# Execução Bem-sucedida
| Parâmetro | Valor Recomendado | Descrição |
|---|---|---|
pool_size |
núcleos CPU x 2 + 1 | Contagem básica de conexões |
max_overflow |
pool_size x 0.5 | Número máximo de conexões de burst |
pool_timeout |
30s | Timeout |
pool_recycle |
1800s | Prevenir MySQL/PG de fechar conexões ociosas |
pool_pre_ping |
True | Prevenir uso de conexões desconectadas |
6. Otimização do Modelo de Concorrência
(1) Configuração de Workers
(1) ▶Exemplo: Gunicorn + Uvicorn Workers
# Produção: Gunicorn gerencia Uvicorn workers
gunicorn app.main:app \
--workers 9 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000 \
--timeout 120 \
--graceful-timeout 30 \
--access-logfile - \
--error-logfile -
Saída:
# Comando executado com sucesso
# No Dockerfile
CMD ["gunicorn", "app.main:app", \
"--workers", "9", \
"--worker-class", "uvicorn.workers.UvicornWorker", \
"--bind", "0.0.0.0:8000"]
| Configuração | Fórmula/Valor | Descrição |
|---|---|---|
| Número de Workers | (2 x CPU) + 1 |
4 núcleos = 9 workers |
| worker-class | UvicornWorker | Worker ASGI |
| timeout | 120s | Worker morto por timeout |
| graceful-timeout | 30s | Timeout de desligamento gracioso |
| max-requests | 10.000 | Reinício automático para prevenir vazamentos de memória |
❓Perguntas Frequentes
run_in_executor?min(32, os.cpu_count() + 4). Para tarefas CPU-intensivas, use ProcessPoolExecutor para evitar limitações do GIL.default_response_class=ORJSONResponse do FastAPI serializa automaticamente modelos Pydantic usando orjson para gerar a saída model_dump().max_connections do PostgreSQL é 100; você precisará aumentar ou diminuir o pool_size conforme necessário.📖Resumo
- Modelo de três camadas para otimização de performance: Camada de aplicação (async/serialização) → Camada de banco de dados (índices/pool de conexões) → Camada de infraestrutura (Workers/LB)
- Encapsule I/O síncrono com
run_in_executorpara prevenir bloqueio do event loop - orjson é 3 a 10 vezes mais rápido que json para serialização, e GZipMiddleware comprime respostas grandes
- Índices de banco de dados são projetados com base em padrões de consulta (índices de coluna única, compostos e parciais); o tamanho do pool de conexões é coordenado com o número de workers
- Gunicorn + Uvicorn Workers:
(2 x CPU) + 1workers, autoscaling K8s para um milhão de QPS
📝Exercícios
- Exercício Básico (Dificuldade ⭐): Instale orjson, altere a classe de resposta padrão do FastAPI para ORJSONResponse, e use curl para comparar os tempos de resposta para JSON e orjson. Dica:
default_response_class=ORJSONResponse - Exercício Avançado (Dificuldade ⭐⭐): Adicione índices à tabela
productsdo PriceTracker (category,created_at, e um índice composto decategoryebase_price), e compare os tempos de execução da mesma consulta antes e depois de adicionar os índices. Dica:Index("ix_name", "col1", "col2")+EXPLAIN ANALYZE - Desafio (Dificuldade ⭐⭐⭐): Otimização abrangente de performance — serialização orjson + GZipMiddleware + ajuste de pool de conexões + Gunicorn com 4 workers. Use Locust para executar testes de carga e compare QPS e latência P99 antes e depois da otimização. Dica:
locust -f locustfile.py --host=http://localhost:8000
---|



