A lo largo del módulo hemos ido posponiendo una decisión: qué es "ir bien". Tenemos métricas (06-01), trazas (06-02), patrones de resiliencia (06-03) y autoescalado (06-04), pero nadie ha dicho todavía cuántos errores en POST /v1/pedidos son aceptables, cuánto puede tardar la saga antes de que alguien deba mirar, ni a quién despierta el teléfono cuando la DLQ se llena a las tres de la mañana. Sin esas respuestas, la observabilidad es una colección de paneles bonitos que se consultan después del desastre. Esta lección la convierte en un sistema de operación: SLOs que traducen la experiencia del cliente a números, alertas que suenan solo cuando esos números peligran, guardias y runbooks que saben qué hacer, y una forma de gestionar incidentes y aprender de ellos sin buscar culpables.
Contenido
- SLI, SLO y SLA: definiciones y presupuesto de error
- Elegir los SLIs de TechCorp
- SLOs concretos y su presupuesto mensual
- Expresar los SLIs en PromQL con reglas de grabación
- Alertas basadas en SLO: burn rate multiventana
- Alertas por síntoma y por qué no alertar por causas
- Alertmanager: rutas por equipo, agrupación, silencios e inhibición
- Guardias y runbooks
- Gestión de incidentes: roles, severidades y comunicación
- Postmortem sin culpa: INC-2031
- Revisión de SLOs y política de presupuesto de error
- Tabla resumen de las alertas de TechCorp
- SLI, SLO y SLA: definiciones y presupuesto de error
| Término | Qué es | Quién lo fija | Ejemplo TechCorp |
|---|---|---|---|
| SLI (Service Level Indicator) | Una medida de la calidad del servicio, normalmente una proporción de eventos buenos sobre el total | Ingeniería | Proporción de POST /v1/pedidos que no devuelven 5xx |
| SLO (Service Level Objective) | Un objetivo interno sobre un SLI en una ventana | Ingeniería + producto | 99,9 % de los POST /v1/pedidos sin 5xx en 30 días |
| SLA (Service Level Agreement) | Un contrato con consecuencias (penalizaciones) si se incumple | Negocio/legal | "Disponibilidad de la API del 99,5 % mensual" con los marketplaces que integran TechCorp |
Reglas: el SLA es siempre más laxo que el SLO (si el SLO es 99,9 %, el SLA 99,5 %: el margen es lo que evita pagar penalizaciones); un SLI se expresa como proporción (0-1) para poder sumar, comparar y calcular presupuestos; y un SLO del 100 % no existe: cada nueve adicional multiplica el coste y ninguna dependencia (la nube, el PSP) lo ofrece.
El presupuesto de error es la parte del SLO que se permite fallar: 1 − SLO. Con un SLO del 99,9 % en 30 días, el 0,1 % de las peticiones pueden fallar; expresado en tiempo, si el servicio estuviera totalmente caído, 43 minutos al mes. Es la idea más útil de la lección: convierte "fiabilidad" en una moneda que se gasta en despliegues, experimentos y accidentes, y que cuando se agota obliga a parar (apartado 11).
- Elegir los SLIs de TechCorp
Un buen SLI mide lo que el cliente percibe, no lo que a ingeniería le resulta cómodo. Ana Ruiz no sabe qué es la CPU; sabe si su pedido se ha creado, si la página de productos carga rápido y si le llega la confirmación. Los cuatro SLIs de TechCorp, uno por pregunta:
| SLI | Pregunta del cliente | Definición (bueno / total) | Métrica de origen (06-01) | Equipo dueño |
|---|---|---|---|---|
| Disponibilidad de pedidos | "¿Puedo comprar?" | POST /v1/pedidos con código ≠ 5xx / todos los POST /v1/pedidos |
http_requests_total medida en el gateway |
Pedidos |
| Latencia de catálogo | "¿Carga rápido la tienda?" | GET /v1/productos en < 300 ms / todos |
http_request_duration_seconds_bucket{le="0.3"} |
Experiencia de compra |
| Saga a tiempo | "¿Me confirman el pedido enseguida?" | Pedidos confirmados en < 60 s desde su creación / pedidos que llegan a resolverse | saga_duracion_segundos_bucket{le="60"} |
Pedidos (y Pagos, Inventario) |
| Frescura del outbox | (interno: predice el anterior) | Instantes en que outbox_pendientes < 100 / instantes medidos |
outbox_pendientes |
Pedidos |
Notas de diseño:
- El SLI de disponibilidad se mide en el gateway (lo que el cliente ve), no en Pedidos: si Pedidos está sano pero el gateway no enruta, el cliente sufre igual. Los 4xx no cuentan como fallo: un 422 por datos inválidos es culpa del cliente; un 503 por Catálogo caído (06-03) sí cuenta, aunque sea "de otro".
- El de latencia se define como proporción de peticiones bajo un umbral, no como "el p95 < 300 ms". La proporción es un SLI limpio (bueno/total) y aprovecha directamente el bucket
le="0.3"del histograma; el p95 se sigue mirando en Grafana. - El de la saga necesita el histograma de 06-01 y excluye del denominador los pedidos cancelados por
SIN_STOCKoPAGO_RECHAZADO(resolverse rápido con un rechazo también es "a tiempo": se cuentasaga_duracion_segundosal cerrar el pedido en cualquier estado final). - El del outbox es un SLI interno: no lo ve el cliente, pero cuando falla el de la saga fallará minutos después. Un buen sistema de SLOs tiene pocos SLIs de cara al cliente y algunos internos que avisan antes.
- SLOs concretos y su presupuesto mensual
| SLO | Objetivo | Ventana | Presupuesto de error | En minutos de "caída total" al mes |
|---|---|---|---|---|
Disponibilidad POST /v1/pedidos |
99,9 % | 30 días | 0,1 % | 43 min |
Latencia GET /v1/productos < 300 ms |
99,5 % | 30 días | 0,5 % | 216 min |
| Saga confirmada < 60 s | 99,5 % | 30 días | 0,5 % | 216 min |
| Outbox fresco (< 100 pendientes) | 99,9 % | 30 días | 0,1 % | 43 min |
Por qué esos números y no otros: 99,9 % en pedidos porque cada minuto sin vender cuesta dinero medible (Black Friday, 01-05) y porque la infraestructura de 05-02/05-04 (2 réplicas, rolling sin cortes) lo hace alcanzable; 99,5 % en latencia y saga porque un 0,5 % de páginas lentas o confirmaciones tardías es molesto pero no impide comprar, y porque dependen del PSP y de MongoDB en pico. La cuenta de minutos: 30 días × 24 h × 60 min × (1 − SLO); para 99,9 %: 43.200 × 0,001 = 43,2 min. Y una advertencia: los minutos son una intuición; el presupuesto real se mide en peticiones. Una caída del 100 % durante 43 minutos a las 4 de la mañana consume mucho menos presupuesto (pocas peticiones) que un 5 % de errores durante 8 horas en horario comercial.
- Expresar los SLIs en PromQL con reglas de grabación
Calcular histogram_quantile o divisiones de rate sobre 30 días en cada consulta es caro. Las reglas de grabación (recording rules) precalculan cada SLI en una serie nueva, con un nombre convencional sli:<nombre>:ratio_rate<ventana>, y las alertas y los paneles consultan esa serie:
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: slos-techcorp
namespace: techcorp
labels: { release: kube-prometheus-stack }
spec:
groups:
- name: sli-pedidos-disponibilidad
interval: 30s
rules:
# proporción de errores 5xx en POST /v1/pedidos, en varias ventanas
- record: sli:pedidos_errores:ratio_rate5m
expr: |
sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST", codigo=~"5.."}[5m]))
/ sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST"}[5m]))
- record: sli:pedidos_errores:ratio_rate30m
expr: |
sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST", codigo=~"5.."}[30m]))
/ sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST"}[30m]))
- record: sli:pedidos_errores:ratio_rate1h
expr: |
sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST", codigo=~"5.."}[1h]))
/ sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST"}[1h]))
- record: sli:pedidos_errores:ratio_rate6h
expr: |
sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST", codigo=~"5.."}[6h]))
/ sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST"}[6h]))
- record: sli:pedidos_errores:ratio_rate30d
expr: |
sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST", codigo=~"5.."}[30d]))
/ sum(rate(http_requests_total{servicio="gateway", ruta="/v1/pedidos", metodo="POST"}[30d]))
- name: sli-catalogo-latencia
rules:
- record: sli:catalogo_lentas:ratio_rate5m
expr: |
1 - (
sum(rate(http_request_duration_seconds_bucket{servicio="servicio-catalogo", ruta="/v1/productos", le="0.3"}[5m]))
/ sum(rate(http_request_duration_seconds_count{servicio="servicio-catalogo", ruta="/v1/productos"}[5m]))
)
- name: sli-saga
rules:
- record: sli:saga_tardias:ratio_rate1h
expr: |
1 - (sum(rate(saga_duracion_segundos_bucket{le="60"}[1h])) / sum(rate(saga_duracion_segundos_count[1h])))
- name: presupuesto-error
rules:
- record: slo:pedidos_presupuesto_restante:ratio
expr: 1 - (sli:pedidos_errores:ratio_rate30d / 0.001) # 1 = intacto, 0 = agotado, negativo = excedido- Cada regla
recordguarda el resultado deexprcomo una serie nueva cadainterval. Las alertas del apartado siguiente combinan las ventanas de 5 m, 30 m, 1 h y 6 h; el panel de "presupuesto restante" usa la de 30 d. - Se registran las proporciones de fallo (errores, lentas, tardías) en lugar de las de éxito: los burn rates se calculan sobre el fallo.
- El de latencia usa
1 − (buenas/total):bucket{le="0.3"}cuenta las que tardaron ≤ 300 ms. slo:pedidos_presupuesto_restante:ratioes la métrica que se enseña a negocio: "nos queda el 62 % del presupuesto de este mes".
Las 30 días de ventana requieren retención suficiente en Prometheus (o Thanos/Mimir para el largo plazo, mención). Los cuatro SLIs se documentan también como texto en techcorp/plataforma/observabilidad/slos.md: definición, dueño, objetivo y fecha de revisión.
- Alertas basadas en SLO: burn rate multiventana
La alerta ingenua "tasa de errores > 1 % durante 5 minutos" tiene dos problemas: dispara con un pico de dos minutos que no gasta apenas presupuesto, y no dispara con un 0,5 % sostenido tres días que sí lo agota. La alternativa del SRE book es alertar por velocidad de consumo del presupuesto (burn rate): 1× significa que al ritmo actual el presupuesto se agota exactamente al final de la ventana de 30 días; 14,4× significa que se agotaría en 2 días (30/14,4). Se combinan dos ventanas (una larga para asegurar que es real, una corta para que la alerta se apague pronto al resolverse):
| Alerta | Ventana larga | Ventana corta | Burn rate | Presupuesto consumido para disparar | Severidad |
|---|---|---|---|---|---|
| Rápida | 1 h | 5 m | 14,4× | 2 % en 1 h | critical (página) |
| Lenta | 6 h | 30 m | 6× | 5 % en 6 h | warning (ticket) |
Para el SLO de 99,9 % (presupuesto 0,001), 14,4× equivale a una tasa de errores del 1,44 % y 6× al 0,6 %:
- name: alertas-slo-pedidos
rules:
- alert: PedidosErrorBudgetBurnRapido
expr: |
sli:pedidos_errores:ratio_rate1h > (14.4 * 0.001)
and
sli:pedidos_errores:ratio_rate5m > (14.4 * 0.001)
for: 2m
labels:
severity: critical
equipo: pedidos
slo: pedidos-disponibilidad
annotations:
resumen: "POST /v1/pedidos consume el presupuesto de error 14,4x más rápido de lo previsto"
descripcion: "Tasa de 5xx {{ $value | humanizePercentage }} en la última hora (SLO 99,9 %). Ver runbook."
runbook: "https://runbooks.techcorp.internal/pedidos/error-budget-burn"
dashboard: "https://grafana.techcorp.internal/d/red-servicio?var-servicio=servicio-pedidos"
- alert: PedidosErrorBudgetBurnLento
expr: |
sli:pedidos_errores:ratio_rate6h > (6 * 0.001)
and
sli:pedidos_errores:ratio_rate30m > (6 * 0.001)
for: 15m
labels: { severity: warning, equipo: pedidos, slo: pedidos-disponibilidad }
annotations:
resumen: "POST /v1/pedidos consume el presupuesto de error 6x más rápido de lo previsto"
runbook: "https://runbooks.techcorp.internal/pedidos/error-budget-burn"exprconand: las dos ventanas deben superar el umbral a la vez. La de 1 h evita que un pico de segundos despierte a nadie; la de 5 m hace que la alerta se resuelva minutos después de arreglar la causa (sin ella seguiría activa hasta que la hora entera se limpiara).for: 2m: la condición debe mantenerse dos minutos antes de disparar; contra el ruido de un solo scrape.labels:severitydecide el canal (apartado 7);equipodecide el destinatario;sloagrupa.annotations: lo que lee la persona de guardia a las tres de la mañana. Siemprerunbookydashboard; una alerta sin runbook no se despliega (regla de Plataforma, comprobada por un linter en CI).
Las mismas dos alertas se replican para la latencia de catálogo (CatalogoLatenciaBurnRapido, presupuesto 0,005: umbrales 7,2 % y 3 % de lentas) y la saga (SagaTardiaBurnRapido, sobre sli:saga_tardias). Con cuatro SLOs y dos alertas cada uno son ocho alertas de SLO en total: pocas, significativas y todas ligadas a algo que el cliente nota.
- Alertas por síntoma y por qué no alertar por causas
Las alertas de SLO cubren "el cliente sufre". Hay un segundo grupo de alertas por síntoma que anticipan que sufrirá o que algo está roto aunque el cliente aún no lo note; deben ser pocas y con umbral claro:
- name: alertas-sintomas
rules:
- alert: OutboxAtascado
expr: outbox_pendientes{servicio="servicio-pedidos"} > 100
for: 5m
labels: { severity: critical, equipo: pedidos }
annotations: { resumen: "El outbox de Pedidos acumula {{ $value }} eventos sin publicar", runbook: "https://runbooks.techcorp.internal/pedidos/outbox-atascado" }
- alert: DlqConMensajes
expr: rabbitmq_queue_messages_ready{queue=~".+\\.dlq"} > 0
for: 10m
labels: { severity: warning, equipo: "{{ if eq $labels.queue \"pagos.stock.dlq\" }}pagos{{ else }}pedidos{{ end }}" }
annotations: { resumen: "La cola {{ $labels.queue }} tiene {{ $value }} mensajes", runbook: "https://runbooks.techcorp.internal/comun/dlq" }
- alert: PodEnCrashLoop
expr: max_over_time(kube_pod_container_status_waiting_reason{namespace="techcorp", reason="CrashLoopBackOff"}[5m]) > 0
for: 5m
labels: { severity: warning, equipo: plataforma }
annotations: { resumen: "{{ $labels.pod }} en CrashLoopBackOff", runbook: "https://runbooks.techcorp.internal/plataforma/crashloop" }
- alert: CircuitoAbierto
expr: circuit_breaker_estado == 2
for: 5m
labels: { severity: warning, equipo: pedidos }
annotations: { resumen: "Circuito abierto hacia {{ $labels.dependencia }} desde {{ $labels.servicio }}", runbook: "https://runbooks.techcorp.internal/comun/circuito-abierto" }(La equipo con plantilla en DlqConMensajes es una simplificación; en la práctica se hace una regla por cola con su equipo dueño de 02-01.) Otras del mismo tipo: CertificadoCaduca (probe_ssl_earliest_cert_expiry de Blackbox Exporter, < 14 días, mención), SondaReadyFallando más de 10 minutos, RabbitMQNodoCaido.
Lo que no se alerta como página: las causas. "CPU > 80 %", "memoria > 90 %", "disco al 70 %" pueden significar un problema o un HPA haciendo su trabajo (06-04); si el cliente no sufre y ningún síntoma aparece, no hay nada que hacer a las tres de la mañana. Se dejan como paneles y, como mucho, como severity: info a un canal de Slack que se lee por la mañana. La disciplina es esta: página → el cliente sufre o va a sufrir en minutos; ticket → algo está mal y debe arreglarse esta semana; info → conviene saberlo. Cada alerta que despierta a alguien y no requiere acción es una alerta que hay que borrar o degradar.
- Alertmanager: rutas por equipo, agrupación, silencios e inhibición
Prometheus evalúa las reglas; Alertmanager decide a quién y cómo avisar. Configuración del equipo de Plataforma (secreto alertmanager-config, versionado con los valores del chart):
route:
receiver: slack-plataforma # destino por defecto
group_by: [alertname, equipo] # una notificación por alerta y equipo, no por pod
group_wait: 30s # esperar 30 s a que lleguen alertas relacionadas antes de la primera notificación
group_interval: 5m
repeat_interval: 4h # recordatorio si sigue activa
routes:
- matchers: [severity="critical"]
receiver: pagerduty-guardia # página a quien esté de guardia
continue: true # y además sigue evaluando: también llega a Slack
- matchers: [equipo="pedidos"]
receiver: slack-pedidos
- matchers: [equipo="pagos"]
receiver: slack-pagos-comunicaciones
- matchers: [equipo="experiencia"]
receiver: slack-experiencia-compra
- matchers: [equipo="plataforma"]
receiver: slack-plataforma
receivers:
- name: pagerduty-guardia
pagerduty_configs:
- routing_key_file: /etc/alertmanager/secrets/pagerduty
severity: critical
- name: slack-pedidos
slack_configs:
- api_url_file: /etc/alertmanager/secrets/slack
channel: "#alertas-pedidos"
title: '{{ .CommonLabels.alertname }} ({{ .Status }})'
text: '{{ range .Alerts }}{{ .Annotations.resumen }} — <{{ .Annotations.runbook }}|runbook>{{ "\n" }}{{ end }}'
# slack-pagos-comunicaciones, slack-experiencia-compra, slack-plataforma: análogos
inhibit_rules:
- source_matchers: [alertname="RabbitMQNodoCaido"]
target_matchers: [alertname=~"DlqConMensajes|OutboxAtascado"]
equal: [namespace] # si RabbitMQ está caído, no avisar además de sus consecuencias- Routing por
severityyequipo:criticalva a PagerDuty (la guardia) y al Slack del equipo (continue: true);warningsolo al Slack del equipo. Los cuatro canales corresponden a los cuatro equipos de 02-01. - Agrupación (
group_by): si 12 pods de Catálogo entran enCrashLoopBackOff, llega una notificación con 12 alertas, no 12 mensajes. - Silencios: durante un mantenimiento planificado (migración de RabbitMQ un domingo) se crea un silencio en la interfaz de Alertmanager con
matchers(equipo="pedidos", 2 h) y un comentario; las alertas se evalúan pero no se envían. - Inhibición: una alerta "raíz" (
RabbitMQNodoCaido) suprime las derivadas (OutboxAtascado,DlqConMensajes) para que la guardia reciba un aviso, no diez. - Slack y PagerDuty son ficticios en el curso; el patrón sirve igual con Opsgenie, Teams o correo.
- Guardias y runbooks
TechCorp, con ~25 técnicos, monta una guardia semanal por rotación entre los desarrolladores de los cuatro equipos (no solo Plataforma: quien construye, opera), con una persona primaria y otra secundaria, horario de compensación acordado y una regla: la guardia solo recibe alertas critical con runbook. Todo lo demás espera al horario laboral.
Un runbook es la página que se abre desde la alerta y dice qué mirar y qué hacer, escrita para alguien que no conoce el servicio y está medio dormido. Plantilla y ejemplo para PedidosErrorBudgetBurnRapido:
# Runbook: PedidosErrorBudgetBurnRapido
Severidad: critical · Equipo: Pedidos · Última revisión: 2026-08-01 · Dueño: Luis
## Qué significa
POST /v1/pedidos devuelve 5xx a más del 1,44 % de las peticiones durante la última hora
y los últimos 5 min. Los clientes no pueden comprar. Cada minuto cuenta contra el SLO 99,9 %.
## Comprobar en 5 minutos
1. Grafana "RED por servicio" (servicio=gateway, ruta=/v1/pedidos): ¿qué códigos? 503 → dependencia; 502/504 → Pedidos no responde; 500 → bug.
2. Grafana "RED por servicio" (servicio=servicio-pedidos): ¿latencia alta? ¿pods vivos? kubectl -n techcorp get pods -l app=servicio-pedidos
3. Loki: {app="servicio-pedidos", nivel="error"} | json | line_format "{{.err.codigo}} {{.mensaje}}"
- DEPENDENCIA_NO_DISPONIBLE + dependencia=catalogo → ir a "Catálogo caído" (abajo)
- errores de pg → ir a "Base de datos"
4. ¿Ha habido despliegue en la última hora? argocd app history servicio-pedidos
Sí → revertir primero, investigar después: argocd app rollback servicio-pedidos <id-anterior>
Canary (05-04): kubectl -n techcorp annotate ingress pedidos-canary nginx.ingress.kubernetes.io/canary-weight=0
5. Jaeger: buscar servicio=servicio-pedidos, error=true, últimos 15 min: ¿en qué span falla?
## Remedios por causa
- Catálogo caído: comprobar pods de catalogo; el breaker (06-03) ya protege; si es MongoDB → escalar a Plataforma.
- Base de datos: kubectl -n techcorp get pods -l app=postgres; conexiones: SELECT count(*) FROM pg_stat_activity; si están al máximo, reiniciar el pod de pedidos con más conexiones ociosas.
- Pods en CrashLoop: kubectl -n techcorp logs -l app=servicio-pedidos --previous | head; suele ser config (04-03) → revisar el último cambio del ConfigMap.
- Mensajes en DLQ tras la recuperación: node scripts/reprocesarDlq.js --cola pedidos.saga --max 200 (06-03) desde un pod efímero.
## Escalar a
Luis (líder de Pedidos) tras 15 min sin causa; Plataforma si es infraestructura; Marta si SEV1 (> 30 min o Black Friday).
## Después
Registrar en el canal #incidentes; si duró > 15 min o afectó a clientes, abrir postmortem (plantilla).Cada alerta de la tabla del apartado 12 tiene el suyo en techcorp/plataforma/runbooks/, versionados en Git y con fecha de última revisión: un runbook con comandos que ya no funcionan es peor que ninguno. La regla de oro es que quien resuelve un incidente sin runbook lo escribe al terminar.
- Gestión de incidentes: roles, severidades y comunicación
Un incidente es cualquier situación que degrada el servicio y requiere coordinación. Cuando salta una página, la persona de guardia hace tres cosas: reconoce la alerta (para que no siga escalando), abre un canal (#inc-2031-pagos-stock en Slack) y evalúa la severidad:
| Severidad | Criterio | Ejemplo TechCorp | Respuesta |
|---|---|---|---|
| SEV1 | Los clientes no pueden comprar o hay pérdida/daño de datos | Gateway caído; POST /v1/pedidos con > 50 % de 5xx; cobros duplicados |
Página inmediata, comandante asignado, comunicación a negocio cada 30 min, todos los equipos disponibles |
| SEV2 | Degradación importante o parte del flujo roto, con workaround o impacto parcial | Saga atascada (pedidos en STOCK_RESERVADO sin confirmar); catálogo lento con p95 > 1 s; notificaciones no salen |
Página, se resuelve en horario ampliado, comunicación al inicio y al cierre |
| SEV3 | Impacto menor o interno; sin efecto visible para el cliente | DLQ con 3 mensajes; un pod en CrashLoopBackOff con réplicas sanas; presupuesto de error al 20 % |
Ticket, se resuelve en horario laboral |
Los roles en un SEV1/SEV2, para que no todo el mundo teclee a la vez:
- Comandante del incidente (incident commander): coordina, decide y asigna; no depura personalmente. Suele ser la persona de guardia hasta que alguien con más contexto la releva de forma explícita ("asumo el mando").
- Comunicador: mantiene informados a negocio y soporte (un mensaje cada 30 min en
#estado-plataformacon: qué pasa, impacto, qué se está haciendo, próxima actualización) para que nadie interrumpa a quien resuelve. - Operadores/investigadores: uno o varios; siguen el runbook, ejecutan cambios y anotan cada acción con hora en el canal.
Principios: estabilizar antes que entender (revertir el despliegue, escalar réplicas, desactivar el canary, reprocesar después: la causa raíz se busca cuando el cliente ya no sufre); una cronología en tiempo real en el canal (la reconstruirá el postmortem); y comunicación con negocio en su lenguaje ("no se pueden crear pedidos desde las 10:14; estimamos recuperación en 20 min; los pedidos ya creados no se ven afectados"), nunca en el nuestro ("pagos.stock está atascada por un mensaje venenoso").
- Postmortem sin culpa: INC-2031
Todo SEV1 y SEV2 termina con un postmortem en la semana siguiente. "Sin culpa" (blameless) significa que se asume que las personas actuaron razonablemente con la información que tenían, y que la pregunta es qué del sistema (código, herramientas, procesos, alertas) permitió que ocurriera y que durara lo que duró. Si el postmortem busca a quién regañar, la próxima vez la información se esconde. Plantilla y ejemplo resumido:
# Postmortem INC-2031 — pagos.stock atascada 40 minutos por un mensaje venenoso
Fecha: 2026-07-22 · Severidad: SEV2 · Duración: 40 min (10:14–10:54) · Comandante: guardia (Elena, Pagos)
Autores: equipo de Pagos y comunicaciones · Estado: acciones en curso
## Resumen
Un evento stock.reservado con `lineas: null` (bug en Inventario tras el despliegue 2.3.0) hizo que el
consumidor de pagos.stock lanzara TypeError. Con prefetch(1) en Pagos y reintento inmediato con requeue,
el mensaje volvía a la cabeza de la cola en bucle y bloqueó los demás cobros. 61 pedidos quedaron en
STOCK_RESERVADO; el vigilante canceló 38 por TIMEOUT_PAGO antes de la resolución.
## Impacto
61 clientes sin confirmación durante hasta 40 min; 38 pedidos cancelados y notificados como "problema en el pago"
(23 recompraron). Presupuesto de error de la saga consumido: 31 % del mes.
## Cronología (UTC+2)
10:03 Despliegue servicio-inventario 2.3.0 (canary 10 % → 100 % a las 10:10).
10:14 Primer TypeError en pagos.stock (Loki). Sin alerta: no existía DlqConMensajes ni SagaTardiaBurn.
10:31 Soporte avisa en Slack: clientes sin confirmación. La guardia abre #inc-2031.
10:38 Panel de la saga: 40 pedidos en STOCK_RESERVADO; pagos.stock con 58 mensajes listos y ninguno consumido.
10:44 Se identifica el mensaje venenoso en Loki por eventoId; se purga manualmente de la cola.
10:46 Cola desatascada; los cobros pendientes se procesan en 2 min.
10:50 Se revierte inventario a 2.2.4 (argocd rollback).
10:54 Saga normal. Se cierra el incidente.
## Causas (5 porqués, resumido)
1. Inventario publicó un evento inválido → falta de validación de salida contra el contrato (03-06); el test de Pact
no cubría lineas nulas.
2. Pagos reintentaba en bucle → nack con requeue inmediato y prefetch(1) (aún no aplicaba 06-03).
3. Nadie lo vio en 17 min → sin alerta de síntoma sobre colas ni de SLO sobre la saga; el panel existía, nadie lo mira.
4. Tardó 13 min más en localizarse → sin runbook para "cola atascada".
## Qué funcionó
El vigilante de TIMEOUT_PAGO limitó el daño (reservas liberadas, clientes avisados). Los logs por eventoId permitieron
localizar el mensaje en minutos una vez se supo dónde mirar.
## Acciones correctivas (tareas con dueño y fecha)
- [Pagos] Cola de reintento con TTL + DLQ a la primera para errores permanentes; prefetch 3. → hecho en 06-03
- [Pedidos] Alertas DlqConMensajes y SagaTardiaBurnRapido/Lento. → esta lección
- [Inventario] Validar eventos de salida con el esquema del contrato antes de escribir en outbox; test de Pact.
- [Plataforma] Runbook "cola atascada / DLQ" y script reprocesarDlq.js. → hecho en 06-03
- [Todos] Añadir "¿qué alerta lo habría detectado?" como pregunta fija del postmortem.Las acciones se convierten en tareas en el backlog con dueño y fecha, y se revisan en la siguiente reunión de operaciones; un postmortem cuyas acciones no se ejecutan es un documento inútil. Y se comparte con toda la empresa: los incidentes son la fuente de aprendizaje más cara que existe; desaprovecharla es doble pérdida.
- Revisión de SLOs y política de presupuesto de error
Los SLOs no son eternos. Cada trimestre, cada equipo revisa con producto: ¿el SLI mide lo que el cliente nota? ¿el objetivo es demasiado ambicioso (siempre en rojo, nadie lo cree) o demasiado laxo (siempre al 100 %, no dice nada)? ¿han cambiado las expectativas (un marketplace nuevo exige más)? Se ajustan objetivos, se retiran SLIs inútiles y se añaden los que faltan.
Y el presupuesto de error se usa con una política escrita, acordada entre Marta y los equipos:
| Presupuesto restante en el mes | Política |
|---|---|
| > 50 % | Normal: despliegues, experimentos, game days (06-03) |
| 20-50 % | Precaución: se revisan los despliegues de riesgo con otra persona; canary más largo (05-04) |
| < 20 % | Solo cambios de fiabilidad y correcciones; se congelan las funcionalidades hasta que el presupuesto se recupere (la ventana de 30 días avanza) |
| Agotado / negativo | Congelación total salvo correcciones; el equipo dedica el sprint a fiabilidad; postmortem obligatorio de cada contribuidor |
Es el equilibrio que faltaba en el debate de velocidad de 05-03: las métricas DORA empujan a desplegar más a menudo; el presupuesto de error dice hasta cuándo. Un equipo con presupuesto sobrante debería atreverse a más (más despliegues, más experimentos), no menos: no gastar el presupuesto también es un desperdicio.
- Tabla resumen de las alertas de TechCorp
| Alerta | Condición | Severidad | Equipo | Runbook |
|---|---|---|---|---|
PedidosErrorBudgetBurnRapido |
5xx en POST /v1/pedidos > 1,44 % en 1 h y 5 m |
critical | Pedidos | pedidos/error-budget-burn |
PedidosErrorBudgetBurnLento |
> 0,6 % en 6 h y 30 m | warning | Pedidos | pedidos/error-budget-burn |
CatalogoLatenciaBurnRapido / Lento |
GET /v1/productos > 300 ms en > 7,2 % (1 h/5 m) / > 3 % (6 h/30 m) |
critical / warning | Experiencia de compra | catalogo/latencia |
SagaTardiaBurnRapido / Lento |
Sagas > 60 s en > 7,2 % / > 3 % | critical / warning | Pedidos | pedidos/saga-atascada |
OutboxAtascado |
outbox_pendientes > 100 durante 5 m |
critical | Pedidos | pedidos/outbox-atascado |
DlqConMensajes |
Cualquier *.dlq con mensajes 10 m |
warning | Dueño de la cola | comun/dlq |
CircuitoAbierto |
circuit_breaker_estado == 2 durante 5 m |
warning | Dueño del servicio | comun/circuito-abierto |
PodEnCrashLoop |
CrashLoopBackOff en techcorp 5 m |
warning | Plataforma | plataforma/crashloop |
RabbitMQNodoCaido |
Nodo del clúster de RabbitMQ no disponible | critical | Plataforma | plataforma/rabbitmq |
CertificadoCaduca |
Certificado TLS < 14 días | warning | Plataforma | plataforma/certificados |
PresupuestoErrorBajo |
slo:*_presupuesto_restante:ratio < 0.2 |
info | Cada equipo | comun/politica-presupuesto |
Once alertas (más las variantes por cola). Si en tres meses alguna no ha requerido acción nunca, se degrada o se elimina; si un incidente no fue detectado por ninguna, se añade la que lo habría hecho.
Errores Comunes y Consejos
- Cien alertas. Cada una parece razonable; juntas producen fatiga y la guardia las ignora todas. Empezar por las de SLO y cuatro síntomas; añadir solo desde postmortems.
- Alertar por umbral fijo de errores ("> 1 % durante 5 m"). Ruido con picos, silencio con degradaciones lentas. Burn rate multiventana.
- SLO del 99,99 % "porque suena bien". Son 4 minutos al mes: incompatible con rolling de dependencias, con el PSP y con el presupuesto de la empresa. Elegir lo que el negocio necesita y la arquitectura permite.
- Medir el SLI donde conviene y no donde el cliente está. Pedidos al 100 % con el gateway caído es un SLO cumplido y una tienda cerrada.
- Alertas sin runbook. Despertar a alguien para que empiece de cero. Runbook o no se despliega.
- Postmortem con nombres y adjetivos ("X desplegó sin probar"). La próxima vez nadie contará lo que pasó. Sistema, no personas.
- Acciones del postmortem sin dueño ni fecha. Se olvidan en una semana. Tareas en el backlog, revisadas.
- La guardia solo en Plataforma. Los desarrolladores no ven las consecuencias de su código. Quien construye, opera (con formación y compensación).
- Consejo: montar un panel único "estado de SLOs" con las cuatro proporciones y el presupuesto restante, y ponerlo en la pantalla del equipo; es la conversación con producto que faltaba.
- Consejo: probar las alertas:
amtool alert add PedidosErrorBudgetBurnRapido severity=critical equipo=pedidosenvía una alerta de prueba y comprueba el enrutado sin esperar a un incidente real.
Ejercicios
Ejercicio 1: SLO y presupuesto para Notificaciones
Define un SLI y un SLO para servicio-notificaciones ("el cliente recibe el correo de confirmación"), indica de qué métrica saldría (puedes proponer una nueva coherente con 06-01), calcula el presupuesto mensual y justifica una severidad para su alerta.
Ejercicio 2: la alerta de la saga
Escribe la regla SagaTardiaBurnRapido completa (expresión con las reglas de grabación necesarias, for, labels, annotations) para el SLO del 99,5 %, y explica por qué su umbral es 7,2 % y no 1,44 %.
Ejercicio 3: clasificar y actuar
Son las 02:10. Salta OutboxAtascado (outbox_pendientes = 340) y, a la vez, DlqConMensajes en inventario.pedidos.dlq con 2 mensajes. POST /v1/pedidos responde 202 con normalidad. Clasifica la severidad, decide qué alerta atiendes primero y describe los cinco primeros minutos siguiendo el estilo del runbook.
Soluciones
Ejercicio 1
SLI: proporción de correos de confirmación entregados al proveedor en menos de 5 minutos desde pedido.confirmado, sobre el total de pedido.confirmado recibidos. Métrica: histograma notificacion_latencia_segundos{tipo="confirmacion"} observado en Notificaciones desde ocurrido_en del evento hasta la respuesta 2xx del proveedor, con buckets [1, 5, 30, 60, 300, 900]; SLI = rate(..._bucket{le="300"}) / rate(..._count). SLO: 99 % en 30 días (un correo tardío es molesto, no impide comprar; el proveedor externo no garantiza más). Presupuesto: 1 % ≈ 432 minutos de "caída total" al mes, o unas 900 confirmaciones tardías de 90.000. Alerta: NotificacionesTardiasBurnRapido con severity: warning (no critical): no despierta a nadie; los correos se reintentan desde la cola (06-03) y llegan cuando el proveedor vuelve. Si el retraso supera horas pasaría a SEV3/SEV2 por impacto en soporte, pero no a página.
Ejercicio 2
Reglas de grabación necesarias: sli:saga_tardias:ratio_rate1h (ya definida) y sli:saga_tardias:ratio_rate5m (misma expresión con [5m]).
- alert: SagaTardiaBurnRapido
expr: |
sli:saga_tardias:ratio_rate1h > (14.4 * 0.005)
and
sli:saga_tardias:ratio_rate5m > (14.4 * 0.005)
for: 2m
labels: { severity: critical, equipo: pedidos, slo: saga-a-tiempo }
annotations:
resumen: "Las sagas tardan más de 60 s en {{ $value | humanizePercentage }} de los pedidos"
descripcion: "El presupuesto de error del SLO 'saga < 60 s' (99,5 %) se consume 14,4x más rápido de lo previsto. Suele ser Pagos, Inventario o RabbitMQ."
runbook: "https://runbooks.techcorp.internal/pedidos/saga-atascada"
dashboard: "https://grafana.techcorp.internal/d/saga-pedidos"El umbral es burn rate × presupuesto: 14,4 × 0,005 = 0,072 (7,2 %). Con un SLO del 99,5 % el presupuesto es cinco veces mayor que con 99,9 %, y por tanto la tasa de fallo que lo agota "en dos días" también es cinco veces mayor. El burn rate es el mismo (14,4×), el porcentaje absoluto depende del SLO.
Ejercicio 3
Severidad: POST /v1/pedidos sigue aceptando pedidos, pero ninguno avanza (los eventos no salen del outbox): la saga está parada para todo el que compre ahora, y en 10 minutos el vigilante no actuará (los pedidos están en PENDIENTE, no en STOCK_RESERVADO), así que se acumularán pedidos sin confirmar. Es un SEV2 (flujo roto con impacto creciente; no SEV1 porque no hay pérdida de datos: el outbox conserva todo). Se atiende primero OutboxAtascado: es critical, es la causa más "aguas arriba" y la DLQ de Inventario con 2 mensajes es warning y puede esperar (además podría ser consecuencia: si RabbitMQ va mal, ambas alertas encajan). Cinco minutos: (1) reconocer en PagerDuty, abrir #inc-XXXX-outbox; (2) Grafana "Saga de pedidos": outbox_pendientes sube en línea recta desde las 01:55, pedidos_creados_total normal, rabbitmq_queue_messages_ready{queue="inventario.pedidos"} = 0 → el relay no publica; (3) kubectl -n techcorp get pods -l app=servicio-pedidos → 2 pods Running; Loki {app="servicio-pedidos"} | json | mensaje=~"relay.*|rabbit.*" → "canal cerrado por el broker" a las 01:55 y ningún "lote publicado" desde entonces: el relay perdió el canal y no reconectó (un bug de reconexión); (4) remedio inmediato: kubectl -n techcorp rollout restart deployment/servicio-pedidos (los pods nuevos abren canal, el relay vacía el outbox en un minuto; el rolling de 05-04 no corta el POST); comprobar que outbox_pendientes baja; (5) anotar la cronología, ver los 2 mensajes de inventario.pedidos.dlq (probablemente relacionados: eventos publicados a medias antes del corte; se reprocesan con reprocesarDlq.js una vez el relay va), mensaje al comunicador/#estado-plataforma y, por la mañana, postmortem con acción "el relay debe reconectar con backoff y /health/ready debe fallar si el canal está cerrado" (06-03).
Conclusión
Con esta lección TechCorp deja de "mirar paneles" para operar con objetivos. Cuatro SLIs dicen lo que el cliente nota (disponibilidad de POST /v1/pedidos, latencia de GET /v1/productos bajo 300 ms, sagas confirmadas en menos de 60 s, frescura del outbox), con SLOs del 99,9 %/99,5 % y su presupuesto de error en minutos y en peticiones; las reglas de grabación sli:*:ratio_rate<ventana> los precalculan; las alertas de burn rate multiventana (PedidosErrorBudgetBurnRapido a 14,4× en 1 h/5 m, …Lento a 6× en 6 h/30 m) y un puñado de alertas por síntoma (OutboxAtascado, DlqConMensajes, CircuitoAbierto, PodEnCrashLoop) llegan a través de Alertmanager al equipo dueño y, solo si son critical, a la guardia; cada una con runbook; los incidentes se gestionan con severidades SEV1-SEV3, comandante y comunicador, y terminan en un postmortem sin culpa cuyas acciones son tareas; y la política de presupuesto de error decide cuándo se despliega y cuándo se para. Con esto termina el módulo de monitoreo y mantenimiento: los servicios de TechCorp emiten logs estructurados y métricas RED y de negocio (06-01), trazas que unen la petición HTTP con la saga (06-02), sobreviven a los fallos con timeouts, reintentos, circuit breakers, colas de reintento y el vigilante de la saga (06-03), escalan solos y con pruebas de carga (06-04) y se operan contra SLOs con alertas, guardias y postmortems (06-05). El sistema ya es observable, resiliente, escalable y operable; lo que todavía no es, salvo por las menciones a JWT y a la redacción de datos, es seguro. El módulo 7 empieza por ahí: autenticación y autorización con JWT/OAuth2 y Keycloak (07-01), seguridad en la comunicación entre servicios (07-02), prácticas de seguridad en el código y los datos (07-03) y seguridad en contenedores y Kubernetes (07-04).
Curso de Microservicios
Módulo 1: Introducción a los Microservicios
- Conceptos Básicos de Microservicios
- Ventajas y Desventajas de los Microservicios
- Comparación con la Arquitectura Monolítica
- Cuándo Adoptar Microservicios: Criterios de Decisión
- El Caso Práctico del Curso: la Tienda Online de TechCorp
Módulo 2: Diseño de Microservicios
- Principios de Diseño de Microservicios
- Descomposición de Aplicaciones Monolíticas
- Definición de Bounded Contexts
- Gestión de Datos: una Base de Datos por Servicio
- Consistencia Distribuida: Sagas, CQRS y Event Sourcing
Módulo 3: Comunicación entre Microservicios
- APIs RESTful
- Mensajería Asíncrona
- Protocolos de Comunicación: gRPC, GraphQL
- API Gateway y Backend for Frontend
- Descubrimiento de Servicios y Balanceo de Carga
- Contratos y Versionado de APIs
Módulo 4: Implementación de Microservicios
- Elección de Tecnologías y Herramientas
- Desarrollo de un Microservicio Simple
- Gestión de Configuración
- Integración Práctica: Consumir APIs y Publicar Eventos
- Pruebas en Microservicios: Unitarias, de Integración y de Contrato
Módulo 5: Despliegue y Orquestación
- Contenedores y Docker
- Orquestación con Kubernetes
- CI/CD para Microservicios
- Estrategias de Despliegue: Rolling, Blue-Green y Canary
- Service Mesh: Istio y Linkerd
Módulo 6: Monitoreo y Mantenimiento
- Monitoreo y Logging
- Trazabilidad Distribuida con OpenTelemetry
- Gestión de Errores y Recuperación
- Escalabilidad y Rendimiento
- SLOs, Alertas y Gestión de Incidentes
Módulo 7: Seguridad en Microservicios
- Autenticación y Autorización
- Seguridad en la Comunicación
- Prácticas de Seguridad
- Seguridad en Contenedores y Kubernetes
