Alertas, Cardinalidad y Ruido
Alertas, Cardinalidad y Ruido
El problema de la alerta fatiga
Alert fatigue (fatiga de alertas) es uno de los problemas más graves y más ignorados en operaciones de software. Ocurre cuando el volumen y calidad de las alertas son tan malos que el equipo comienza a ignorarlas.
Las consecuencias son severas:
- Una alerta crítica real se pierde en el ruido
- El equipo on-call vive con ansiedad constante
- La rotación de ingenieros aumenta (nadie quiere ser on-call)
- Los incidentes tardan más en detectarse porque las alertas son ignoradas
Un estudio de PagerDuty (2023) encontró que más del 30% de las páginas de on-call son falsos positivos, y que los equipos con alta tasa de alertas ruidosas tienen el doble de burnout que equipos con alertas bien calibradas.
Anatomía de una alerta efectiva
Una alerta efectiva tiene cinco características:
mindmap
root((Alerta Efectiva))
Accionable
Hay algo que hacer cuando se dispara
El runbook es claro
Urgente correctamente
Corresponde al impacto real
No todo es P1
Contextual
Dice qué pasó y dónde
Link al dashboard relevante
Libre de falsos positivos
No se dispara cuando no hay problema
No falla por ruido estadístico
Libre de falsos negativos
Se dispara cuando hay un problema real
No se silencia automáticamente
Alertas basadas en síntomas vs. causas
La regla más importante en el diseño de alertas: alerta en síntomas, no en causas.
Un síntoma es algo que el usuario experimenta. Una causa es un estado interno del sistema.
graph LR
subgraph "❌ Alertas en Causas (Ruido)"
C1[CPU > 90%]
C2[Memory > 85%]
C3[DB connections > 80%]
C4[Disk > 70%]
C5[Pod restarts > 5]
end
subgraph "✅ Alertas en Síntomas (Impacto)"
S1[Error rate > 1% en 5min]
S2[p99 latencia > 2s por 5min]
S3[Checkout success rate < 99%]
S4[SLO error budget > 10% burn en 1h]
end
C1 -->|puede causar| S1
C2 -->|puede causar| S2
C3 -->|puede causar| S1
El problema con las alertas en causas:
- CPU al 95% puede ser perfectamente normal bajo carga alta con baja latencia
- Disk al 70% puede ser un estado permanente normal
- Pod restarts pueden ser reintentos normales
La ventaja de alertar en síntomas:
- Son directamente proporcionales al impacto real en el usuario
- Son más estables y tienen menos falsos positivos
- El runbook es más claro: “el checkout está fallando” vs. “la CPU está alta”
Diseño de alertas por severidad
No todas las alertas son iguales. Define niveles de severidad claros con procedimientos distintos:
Severidad crítica (P1) — Pager de noche
Criterio: Hay impacto significativo en usuarios ahora mismo. Alguien debe despertar a las 3AM.
Ejemplos:
- Checkout completamente caído (error rate 100%)
- SLO error budget se agotará en menos de 2 horas
- Fuga de datos detectada
Respuesta: Responder en menos de 5 minutos, resolver o escalar.
Severidad alta (P2) — Responder pronto
Criterio: Impacto parcial en usuarios, degradación significativa.
Ejemplos:
- Error rate 5-20% en un endpoint crítico
- Latencia p99 degradada 3x pero no afecta la mayoría
- SLO burn rate alto pero con tiempo de reacción
Respuesta: Responder en menos de 30 minutos, generalmente durante horario laboral.
Severidad media (P3) — Ticket
Criterio: Anomalía que no afecta usuarios actualmente pero podría escalar.
Ejemplos:
- Tendencia de crecimiento en errores
- Capacidad al 70%, podría alcanzar 90% en 48h
- Componente secundario degradado
Respuesta: Ticket, resolver en el próximo sprint.
flowchart LR
P1[P1 Critical] --> |PagerDuty\nWake everyone| I1[Respuesta en 5 min\n24/7]
P2[P2 High] --> |Slack/SMS\nBusiness hours OK| I2[Respuesta en 30 min\nDurante horario laboral]
P3[P3 Medium] --> |Ticket creado\nauto| I3[Resolver en próximo\nsprint]
P4[P4 Low] --> |Log en sistema| I4[Backlog\ncuando sea posible]
Configurando alertas con Prometheus y Alertmanager
Anatomía de una alerta en Prometheus
groups:
- name: checkout.rules
rules:
- alert: CheckoutHighErrorRate
# La expresión que define cuándo se dispara
expr: |
(
rate(http_requests_total{service="checkout", status=~"5.."}[5m]) /
rate(http_requests_total{service="checkout"}[5m])
) > 0.05
# Cuánto tiempo debe cumplirse antes de notificar (evita flapping)
for: 5m
# Metadata para routing y enriquecimiento
labels:
severity: critical
team: checkout
slo: checkout_availability
# Información contextual para el on-call
annotations:
summary: "Checkout error rate above 5%"
description: |
Service {{ $labels.service }} has error rate {{ $value | humanizePercentage }}
over the last 5 minutes. SLO target is 99.9% availability.
dashboard: "https://grafana.example.com/d/checkout-service"
runbook: "https://wiki.example.com/runbooks/checkout-errors"
Alertmanager: routing y deduplication
# alertmanager.yml
global:
smtp_smarthost: 'smtp.example.com:587'
route:
group_by: ['alertname', 'service'] # Agrupa alertas similares
group_wait: 30s # Espera antes de enviar el primer grupo
group_interval: 5m # Mínimo entre notificaciones del mismo grupo
repeat_interval: 4h # Re-notificar si no se resuelve en 4h
receiver: 'slack-default'
routes:
- match:
severity: critical
receiver: 'pagerduty'
continue: true # Seguir evaluando otras rutas
- match:
team: checkout
receiver: 'slack-checkout-team'
receivers:
- name: 'pagerduty'
pagerduty_configs:
- service_key: 'tu-service-key'
- name: 'slack-checkout-team'
slack_configs:
- channel: '#checkout-alerts'
title: '{{ .GroupLabels.alertname }}'
text: '{{ range .Alerts }}{{ .Annotations.description }}{{ end }}'
Cardinalidad: el problema que escala silenciosamente
Ya introdujimos la cardinalidad en el capítulo de métricas. Aquí exploramos cómo destruye sistemas en producción y cómo prevenirlo.
¿Qué es una explosión de cardinalidad?
Una explosión de cardinalidad ocurre cuando agregas etiquetas de alto cardinality a tus métricas, creando tantas series de tiempo únicas que el sistema de métricas se satura.
graph TD
M1["http_requests_total\n{service, method, status}\n3 × 6 × 10 = 180 series"] --> OK1[✅ Manejable]
M2["http_requests_total\n{service, method, status, endpoint}\n3 × 6 × 10 × 500 = 90,000 series"] --> OK2[⚠️ Vigilar]
M3["http_requests_total\n{service, method, status, user_id}\n3 × 6 × 10 × 1,000,000 = 180M series"] --> BOOM[💥 Destroys Prometheus\nOOM, crash, dataloss]
Señales de que tienes un problema de cardinalidad
- Prometheus usa más memoria de la esperada
- Las queries de Prometheus son lentas (>10 segundos)
- Ves warnings de “too many samples” en Prometheus
- El almacenamiento crece desproporcionadamente
Detección con Prometheus
# Ver las métricas con más series de tiempo
topk(10, count by (__name__)({__name__=~".+"}))
# Ver las etiquetas con más valores únicos para una métrica
count(count by (user_id)(http_requests_total))
Solución para datos de alta cardinalidad
Si necesitas analizar por user_id u otros campos de alta cardinalidad, usa logs o trazas — no métricas.
flowchart LR
D[Dato a analizar] --> Q{¿Cardinalidad?}
Q --> |Baja\n<1000 valores| M[Métricas\nPrometheus/InfluxDB]
Q --> |Alta\n>10000 valores| L[Logs + Trazas\ncon indexado por ese campo]
Anti-patrones de alertas más comunes
1. Alertas que siempre se disparan
Si una alerta se dispara más de una vez al día en promedio, está mal calibrada. Revisa el umbral o el for (duración antes de notificar).
2. Alertas sin runbook
Una alerta sin runbook obliga al on-call a investigar from scratch cada vez. El runbook debe existir antes de que la alerta llegue a producción.
Un runbook mínimo incluye:
- Qué significa la alerta
- Qué preguntar/revisar primero
- Los 3-5 pasos más comunes de resolución
- A quién escalar si no se resuelve
3. Alertas por umbrales estáticos en sistemas dinámicos
Si tu tráfico varía entre 100 req/s en la madrugada y 10,000 req/s en horas pico, una alerta de “latencia > 500ms” puede dispararse falsamente durante el pico normal.
Solución: alertas basadas en tasas y comparaciones históricas:
# ❌ Umbral estático (se dispara en picos normales)
http_request_duration_seconds{quantile="0.99"} > 0.5
# ✅ Comparación con baseline histórico (mismo momento del día/semana)
(
http_request_duration_seconds{quantile="0.99"}
) > (
avg_over_time(http_request_duration_seconds{quantile="0.99"}[1w]) * 3
)
4. Alertas que no distinguen entre transientes y permanentes
# ❌ Se dispara con un spike de 1 minuto
expr: error_rate > 0.05
for: 0m # Inmediato
# ✅ Solo se dispara si persiste (más accionable)
expr: error_rate > 0.05
for: 5m # Debe persistir 5 minutos
5. Demasiadas alertas en la misma ruta de pager
Si 20 servicios distintos tienen alertas que llegan al mismo canal de Slack o al mismo PagerDuty schedule, el on-call está constantemente interrumpido.
La solución es routing claro: cada equipo tiene su canal, sus schedules, su escalation path.
Mantenimiento del sistema de alertas
Las alertas deben mantenerse activamente. Un conjunto de alertas creado hace 2 años y nunca revisado es casi seguro fuente de ruido.
Revisión periódica: las preguntas a hacer
Por cada alerta, trimestralmente:
flowchart TD
Q1{¿La alerta se disparó\nalguna vez en 90 días?} --> |No| DELETE[Eliminar o convertir\nen alerta informativa]
Q1 --> |Sí| Q2{¿Fue accionable\ncada vez que se disparó?}
Q2 --> |No, muchos falsos positivos| TUNE[Ajustar umbral\no añadir for duration]
Q2 --> |Sí| Q3{¿El runbook está\nactualizado?}
Q3 --> |No| UPDATE[Actualizar runbook]
Q3 --> |Sí| OK[Alerta saludable ✅]
Métricas de salud de tu sistema de alertas
Deberías medir la calidad de tus alertas:
Signal-to-noise ratio = alertas accionables / total de alertas
Objetivo: > 90%
MTTACK (Mean Time to Acknowledge) = tiempo promedio hasta que alguien acknowledges
Objetivo: < 5 minutos para P1
False positive rate = alertas que se resuelven sin ninguna acción
Objetivo: < 10%
Dead Man’s Switch: alertas inversas
Una técnica avanzada: el dead man’s switch es una alerta que se dispara cuando algo que debería ocurrir deja de ocurrir.
# Esta alerta se dispara si NO recibimos heartbeats en 5 minutos
# (es decir, si el job de backup dejó de correr)
alert: BackupJobMissing
expr: absent(backup_last_success_timestamp_seconds) or
(time() - backup_last_success_timestamp_seconds > 86400)
for: 5m
annotations:
summary: "Backup job hasn't run in 24 hours"
Casos de uso clásicos:
- El job de backup dejó de ejecutarse
- El pipeline de datos dejó de procesar
- El health check del servicio dejó de emitir métricas (el servicio murió silenciosamente)