RFC-016: Performance Monitoring e Optimization
| Campo | Valor |
|---|---|
| Status | Draft |
| Author | Time de Tecnologia |
| Created | 2026-02-05 |
| Updated | 2026-02-05 |
| Reunião | A ser agendada |
| Deciders | Todo o time técnico |
Status
🟡 Draft - RFC em elaboração, aguardando apresentação
Contexto e Problema
Performance mencionada em documentação mas sem estratégia formal:
- Performance budgets não definidos
- Load testing não é feito sistematicamente
- Database query optimization é reativa
- Caching strategy não está documentada
- APM (Application Performance Monitoring) não está configurado
Por que resolver isso agora?
- Usuários reportam lentidão ocasional
- Falta visibilidade de performance bottlenecks
- N+1 queries não são detectados
- Load testing é manual e raro
- Não sabemos capacity limits
Impacto de não resolver
- Performance degrada gradualmente
- Bottlenecks não detectados até ficarem críticos
- Capacity planning impossível
- Bad user experience
- Custos aumentam (over-provisioning por falta de dados)
Documentação relacionada:
- docs/observability/monitoring.md - Métricas atuais
- docs/architecture/overview.md - Arquitetura
- docs/infrastructure/database/overview.md - Performance DB
- docs/architecture/aws-services.md - CloudFront, ElastiCache
- docs/observability/cloudwatch.md - X-Ray tracing
Proposta de Solução
Performance budgets, load testing semanal, query optimization obrigatória, e caching strategy clara.
Performance Budgets
API Latency: - Target: P50 < 200ms, P95 < 500ms, P99 < 1s - Hard limit: P99 < 2s (blocker)
Page Load (Frontend): - Target: First Contentful Paint < 1.5s - Target: Time to Interactive < 3s - Hard limit: TTI < 5s
Database Queries: - Target: < 100ms per query - Hard limit: < 500ms per query - N+1 queries: Zero tolerance
Lambda Cold Start: - Target: < 1s - Mitigation: Provisioned concurrency para críticos
API Throughput: - Target: Suportar 100 RPS (requests per second) - Peak: 500 RPS (com auto-scaling)
Load Testing
Cadência: - Semanal: Smoke load tests (baseline) - Pre-release: Full load tests - Quarterly: Stress tests (encontrar limites)
Ferramentas: Locust
# locustfile.py
from locust import HttpUser, task, between
class APIUser(HttpUser):
wait_time = between(1, 3)
host = "https://staging.seuapp.com"
def on_start(self):
"""Login once per user."""
response = self.client.post("/api/v1/auth/login", json={
"email": "load-test@example.com",
"password": "test123"
})
self.token = response.json()["access_token"]
self.headers = {"Authorization": f"Bearer {self.token}"}
@task(3)
def list_products(self):
"""List products (most common operation)."""
self.client.get("/api/v1/products", headers=self.headers)
@task(1)
def create_order(self):
"""Create order (less frequent)."""
self.client.post("/api/v1/orders", headers=self.headers, json={
"product_id": 123,
"quantity": 1
})
@task(2)
def get_user_profile(self):
"""Get user profile."""
self.client.get("/api/v1/users/me", headers=self.headers)
Executar load tests:
# Baseline (semanal)
locust -f locustfile.py \
--headless \
--users 50 \
--spawn-rate 5 \
--run-time 5m \
--html report.html
# Stress test (quarterly)
locust -f locustfile.py \
--headless \
--users 500 \
--spawn-rate 50 \
--run-time 30m \
--html stress-report.html
CI/CD integration:
# .github/workflows/load-test.yml
name: Weekly Load Test
on:
schedule:
- cron: '0 2 * * 1' # Monday 2am
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run load test
run: |
pip install locust
locust -f locustfile.py \
--headless \
--users 50 \
--spawn-rate 5 \
--run-time 5m \
--csv=results
- name: Check performance budgets
run: |
# Parse results.csv
# Fail if P95 > 500ms or error rate > 1%
python scripts/check_performance_budgets.py results_stats.csv
Query Optimization
N+1 Detection:
# Django: django-silk or django-debug-toolbar
# settings.py (staging only)
if DEBUG:
MIDDLEWARE += ['silk.middleware.SilkyMiddleware']
INSTALLED_APPS += ['silk']
# Detecta N+1 queries automaticamente
Obrigatório: select_related / prefetch_related
# ❌ Bad: N+1 queries
users = User.objects.all()
for user in users:
print(user.profile.bio) # Query per user!
# ✅ Good: 1 query
users = User.objects.select_related('profile').all()
for user in users:
print(user.profile.bio) # No extra query
# ✅ Good: prefetch_related para many-to-many
users = User.objects.prefetch_related('orders').all()
FastAPI (SQLAlchemy):
# ✅ Joinedload para relationships
from sqlalchemy.orm import joinedload
users = await db.execute(
select(User).options(joinedload(User.profile))
)
# ✅ Selectinload para collections
users = await db.execute(
select(User).options(selectinload(User.orders))
)
Database Indexes:
# Django
class Product(models.Model):
name = models.CharField(max_length=200, db_index=True) # Single index
class Meta:
indexes = [
models.Index(fields=['name', 'category']), # Composite index
models.Index(fields=['-created_at']), # DESC index
]
Slow Query Monitoring:
-- PostgreSQL: habilitar pg_stat_statements
CREATE EXTENSION pg_stat_statements;
-- Queries mais lentas
SELECT
query,
calls,
total_time,
mean_time,
max_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 20;
Caching Strategy
Levels:
- Browser (CloudFront): Static assets (1 ano)
- CDN (CloudFront): API responses públicas (5 min)
- Application (Redis): Hot data (5-60 min)
- Database (PostgreSQL): Query cache, shared buffers
Redis caching patterns:
# Cache-aside pattern
async def get_product(product_id: int):
# 1. Try cache
cached = await redis.get(f'product:{product_id}')
if cached:
return json.loads(cached)
# 2. Fetch from DB
product = await db.get(Product, product_id)
# 3. Cache for 5 min
await redis.set(
f'product:{product_id}',
json.dumps(product.dict()),
ex=300
)
return product
# Cache invalidation
async def update_product(product_id: int, data: dict):
product = await db.get(Product, product_id)
# Update...
await db.commit()
# Invalidate cache
await redis.delete(f'product:{product_id}')
CloudFront caching:
# SAM template
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
CacheBehaviors:
- PathPattern: /api/v1/products*
TargetOriginId: api-gateway
CachePolicyId: !Ref APIGatewayCachePolicy
AllowedMethods: [GET, HEAD, OPTIONS]
CachedMethods: [GET, HEAD]
APIGatewayCachePolicy:
Type: AWS::CloudFront::CachePolicy
Properties:
CachePolicyConfig:
MinTTL: 0
MaxTTL: 300 # 5 min
DefaultTTL: 60 # 1 min
ParametersInCacheKeyAndForwardedToOrigin:
QueryStringsConfig:
QueryStringBehavior: all
HeadersConfig:
HeaderBehavior: whitelist
Headers: [Authorization]
APM (Application Performance Monitoring)
AWS X-Ray:
from aws_xray_sdk.core import xray_recorder
from aws_xray_sdk.core import patch_all
# Patch libraries
patch_all() # boto3, requests, httpx, psycopg2
# Manual subsegments
@xray_recorder.capture('get_user_with_orders')
async def get_user_with_orders(user_id: int):
# Automatically traced
user = await db.get(User, user_id)
orders = await db.query(Order).filter_by(user_id=user_id).all()
return user, orders
# Metadata
xray_recorder.put_metadata('user_id', user_id)
xray_recorder.put_annotation('is_premium', user.is_premium)
CloudWatch Insights queries:
-- P95 latency por endpoint
fields @timestamp, request_path, duration_ms
| filter request_path like /^\/api\/v1\//
| stats pct(duration_ms, 95) as p95_latency by request_path
| sort p95_latency desc
-- Slowest database queries
fields @timestamp, query, duration_ms
| filter event = "database_query"
| sort duration_ms desc
| limit 20
Performance Testing em CI/CD
# .github/workflows/performance-test.yml
name: Performance Test
on:
pull_request:
branches: [main]
jobs:
performance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to perf test environment
run: sam deploy --config-env perf-test
- name: Run load test
run: |
locust -f locustfile.py \
--headless \
--users 50 \
--spawn-rate 10 \
--run-time 2m \
--csv=results
- name: Check budgets
run: |
# Fail if P95 > 500ms
python scripts/check_performance_budgets.py results_stats.csv
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const results = fs.readFileSync('results_stats.csv', 'utf8');
// Parse and comment metrics on PR
Alternativas Consideradas
Opção 1: k6 ao invés de Locust
Prós: - Mais performático (Go) - Scripting em JavaScript
Contras: - Time conhece Python melhor
Por que não escolhemos: Locust em Python é mais acessível para o time.
Opção 2: Memcached ao invés de Redis
Prós: - Mais simples - Apenas caching
Contras: - Redis tem mais features (pub/sub, sorted sets) - Redis é padrão atual
Por que não escolhemos: Redis oferece mais value.
Opção 3: Datadog APM ao invés de X-Ray
Prós: - Interface melhor - Mais features
Contras: - Custo alto - X-Ray já está disponível
Por que não escolhemos: X-Ray é suficiente e sem custo adicional.
Análise de Impacto
Impacto Técnico
- Performance budgets enforcement
- Load testing semanal automatizado
- Query optimization obrigatória
- Caching em múltiplos níveis
Impacto em Negócio
- ✅ User experience consistentemente boa
- ✅ Capacity planning baseado em dados
- ✅ Custos otimizados
- ✅ SLAs cumpridos
- ⚠️ Overhead de load testing
Riscos
Risco: Load tests causam custos AWS
Mitigação: Ambiente separado para perf testing, limites configurados.
Plano de Implementação
Fase 1: Budgets (Semana 1)
- Definir performance budgets
- Medir baseline atual
- Documentar
Fase 2: Load Testing (Semana 2)
- Configurar Locust
- Criar scenarios de teste
- CI/CD integration
Fase 3: Query Optimization (Semana 3-4)
- Audit queries atuais
- Adicionar índices faltantes
- Refatorar N+1 queries
Fase 4: Caching (Semana 5-6)
- Implementar Redis caching
- CloudFront para static
- Cache invalidation strategy
Fase 5: APM (Semana 7)
- Configurar X-Ray
- Instrumentar código crítico
- Dashboards
Métricas de Sucesso
Após 1 mês: - ✅ Performance budgets definidos e medidos - ✅ Load testing semanal rodando - ✅ P95 latency < 500ms
Após 3 meses: - ✅ Zero N+1 queries - ✅ Cache hit rate > 80% - ✅ Custos AWS otimizados (-20%)
Após 6 meses: - ✅ P99 latency < 1s consistentemente - ✅ Capacity planning baseado em dados - ✅ User satisfaction com performance (+30%)