El Módulo 6 terminó con una pregunta incómoda: la plataforma es segura, pero ¿sabemos si funciona? En un monolito, "funciona" se comprobaba mirando un proceso y un log. En Kilómetro Cero hay seis servicios, tres bases de datos, un clúster de Kafka, Redis, MinIO, un gateway y un IdP, repartidos por decenas de contenedores: nadie puede mirar todo eso a la vez, y cuando algo va mal casi nunca lo dice a gritos. Un inventario que responde en 900 ms en lugar de 40 sigue "funcionando"; un consumidor de pedidos.eventos que se ha quedado atrás tres horas no lanza ninguna excepción; un certificado de Vault que caduca a las 03:00 de un domingo solo se nota cuando el primer pedido de la mañana falla. Esta lección trata de convertir la plataforma en algo observable a través de su primer pilar, las métricas: qué medir, cómo recogerlo con Prometheus, cómo expresar en números lo que Kilómetro Cero le promete a Ana, y cómo alertar antes de que ella se entere. Los logs y las trazas, los otros dos pilares, son la lección 07-02.
Contenido
- Observabilidad y sus tres pilares
- Qué medir: infraestructura, plataforma, aplicación y negocio
- Tipos de métrica y el problema de la cardinalidad
- Prometheus: modelo pull, exporters y descubrimiento
- PromQL esencial: tasas, percentiles y agregaciones
- SLI, SLO, SLA y presupuesto de error
- Alertas: síntomas, burn rate y Alertmanager
- Dashboards en Grafana y monitoreo activo
- Errores comunes y consejos
- Ejercicios y soluciones
- Conclusión
- Observabilidad y sus tres pilares
Monitorizar es comprobar cosas que ya sabemos que pueden fallar: el disco se llena, el proceso muere, la latencia sube. Observabilidad es una propiedad del sistema: la capacidad de responder a preguntas que no habíamos previsto ("¿por qué los pedidos de Lleida con queso-fresco tardan el doble desde ayer?") a partir de lo que el sistema emite hacia fuera. En un sistema distribuido la diferencia importa porque las preguntas imprevistas son la norma: los fallos interesantes son combinaciones de piezas, no piezas aisladas.
Las tres señales que un sistema emite, y que juntas dan observabilidad, son:
| Pilar | Qué es | Pregunta que responde | Coste por evento | Lección |
|---|---|---|---|---|
| Métricas | Números agregados en el tiempo (contadores, medidas) | "¿Cuánto? ¿Con qué frecuencia? ¿Está peor que hace una hora?" | Muy bajo (una serie por combinación de etiquetas, no por evento) | 07-01 |
| Logs | Eventos discretos con contexto | "¿Qué pasó exactamente con el pedido P-2026-000125?" |
Alto (un registro por evento) | 07-02 |
| Trazas | El recorrido de una petición por varios servicios | "¿Dónde se fue el tiempo de esta petición? ¿Qué llamó a qué?" | Medio-alto (muestreado) | 07-02 |
Las métricas son el punto de partida porque son las únicas de las tres que escalan con el número de series, no con el tráfico: contar 10 pedidos por minuto o 10 000 cuesta lo mismo. Son la señal que dispara alertas y la que pinta el panel de la Semana de la Vendimia. Los logs y las trazas entran después, cuando la métrica dice "algo va mal" y hay que averiguar qué.
- Qué medir: infraestructura, plataforma, aplicación y negocio
Un error frecuente es empezar por la infraestructura (CPU, memoria) porque es lo que dan los agentes por defecto, y acabar con cien gráficas que no dicen si Ana puede comprar. Conviene pensar en cuatro capas, de abajo arriba, y elegir en cada una pocas métricas con sentido.
2.1 Infraestructura
Lo que mide node_exporter en cada máquina o cAdvisor en cada contenedor: CPU, memoria, disco (espacio y latencia de E/S), red (bytes, errores, paquetes descartados). Para estos recursos vale el método USE de Brendan Gregg: para cada recurso, Utilización (porcentaje de tiempo ocupado), Saturación (trabajo encolado que no cabe: run queue, swap, cola de disco) y Errores.
2.2 Plataforma
Las piezas intermedias tienen métricas propias que anticipan problemas de la aplicación:
- Kafka: el lag de cada grupo de consumidores (mensajes producidos menos consumidos, por partición). Un lag creciente en
analiticaes tolerable; enrepartosignifica que el panel defurgoneta-3muestra posiciones viejas. También particiones sin réplica sincronizada (se verá en 07-03). - PostgreSQL: conexiones activas frente al máximo (
max_connections), transacciones por segundo, retraso de la réplicapedidos-db-replicarespecto al primario, tamaño de tablas y bloqueos en espera. - Redis: hit ratio de la caché de catálogo (aciertos / (aciertos + fallos)), memoria usada frente al límite, claves expulsadas, latencia de comandos.
- Cassandra: latencia de lectura y escritura por tabla, compactaciones pendientes, nodos caídos vistos por gossip.
2.3 Aplicación: las cuatro señales doradas
Google popularizó en Site Reliability Engineering las cuatro señales doradas, las que hay que mirar en cualquier servicio que atiende peticiones:
- Latencia: cuánto tarda en responder, distinguiendo peticiones correctas de fallidas (un error rápido no es "buena latencia").
- Tráfico: cuánta demanda recibe (peticiones por segundo, mensajes consumidos por segundo).
- Errores: qué fracción de peticiones falla, explícita (
5xx,UNAVAILABLEde gRPC) o implícitamente (respondió200pero tardó más del acuerdo). - Saturación: cómo de "lleno" está el servicio: hilos ocupados, conexiones del pool en uso, cola de trabajo.
El método RED (Tom Wilkie) es la versión para servicios: Rate, Errors, Duration. Coincide con las tres primeras señales doradas y es lo que instrumentamos de forma uniforme en todos los servicios de km0/.
| Método | Se aplica a | Mide | Ejemplo en Kilómetro Cero |
|---|---|---|---|
| RED | Servicios (cosas que atienden peticiones) | Rate, Errors, Duration | pedidos: peticiones/s a POST /pedidos, % de 5xx, p99 de duración |
| USE | Recursos (cosas con capacidad finita) | Utilization, Saturation, Errors | Disco del broker de Kafka: 85 % usado, cola de E/S de 12, 0 errores |
| Señales doradas | Sistemas orientados a usuario | Latencia, tráfico, errores, saturación | Lo anterior más "pool de conexiones a km0_pedidos al 90 %" |
Tabla de señales doradas por servicio:
| Servicio | Latencia | Tráfico | Errores | Saturación |
|---|---|---|---|---|
catalogo |
p99 de GET /catalogo/* (objetivo < 200 ms) |
peticiones/s por mercado | % 5xx; fallos de Redis |
hit ratio de Redis; conexiones a PostgreSQL |
pedidos |
p99 de POST /pedidos (< 500 ms) |
pedidos/min | % 5xx; sagas compensadas / total |
pool de km0_pedidos; hilos HTTP ocupados |
inventario |
p99 de ReservarStock (< 100 ms) |
llamadas gRPC/s por método | % UNAVAILABLE + DEADLINE_EXCEEDED |
conexiones a km0_inventario; lag de replicación inv-bcn/inv-vlc |
pagos |
p99 de Cobrar (< 2 s, depende de la pasarela) |
cobros/min | % rechazados por error técnico (no por tarjeta rechazada) | llamadas en vuelo a la pasarela |
reparto |
retraso entre posición emitida y mostrada | posiciones/s (140 repartidores × 1/5 s ≈ 28/s) | mensajes rechazados | lag del consumidor de reparto.posiciones |
analitica |
duración del DAG km0_ventas_diarias |
eventos/s consumidos | tareas fallidas de Airflow | lag de pedidos.eventos; ejecutores de Spark ocupados |
2.4 Negocio
Las métricas de negocio son las que explican a Jordi Sala y Marta Puig si la plataforma cumple su propósito, y a menudo detectan fallos que las técnicas no ven: si pedidos responde 200 en 80 ms pero los pedidos por minuto caen a cero un sábado a las 11:00, algo se ha roto antes (el front, Kong, Keycloak). Para Kilómetro Cero: pedidos creados por minuto (por mercado y por productor), tasa de pago rechazado, valor medio del pedido, reservas de stock fallidas por producto, entregas a tiempo. Se instrumentan igual que las técnicas, en el mismo Prometheus.
- Tipos de métrica y el problema de la cardinalidad
Prometheus (y OpenTelemetry Metrics, que usa el mismo modelo conceptual) distingue cuatro tipos:
| Tipo | Qué es | Solo puede | Ejemplo | Consulta típica |
|---|---|---|---|---|
| Counter | Contador acumulado desde el arranque del proceso | Subir (se reinicia a 0 si el proceso reinicia) | km0_pedidos_total{estado="confirmado"} |
rate(...[5m]) |
| Gauge | Valor instantáneo | Subir y bajar | km0_stock_unidades{producto="tomate-rosa"}, conexiones activas |
valor directo, avg_over_time |
| Histogram | Distribución en buckets acumulativos + suma + cuenta | Subir | km0_peticion_duracion_segundos_bucket{le="0.5"} |
histogram_quantile(0.99, ...) |
| Summary | Cuantiles calculados en el cliente | Subir | ..._duracion{quantile="0.99"} |
valor directo (no agregable entre instancias) |
Dos aclaraciones que evitan muchos errores:
- Un counter nunca se lee en crudo.
km0_pedidos_totalvale 1 284 522 y no dice nada; lo que interesa es su derivada:rate(km0_pedidos_total[5m])= pedidos por segundo en los últimos 5 minutos. Prometheus gestiona los reinicios (cuando el contador baja, asume reinicio y no produce una tasa negativa). - Histogram frente a summary: el histogram guarda cuántas observaciones cayeron por debajo de cada límite (
le="0.1",le="0.25",le="0.5", ...). Los percentiles se calculan en el servidor y, sobre todo, se pueden agregar entre instancias: el p99 depedidoscon 3 réplicas se obtiene sumando buckets. Un summary calcula el p99 en cada proceso y no hay forma correcta de combinar tres p99 (la media de percentiles no es un percentil). En un sistema distribuido, salvo casos muy concretos, se usa histogram.
Cardinalidad
Cada combinación distinta de nombre + etiquetas es una serie temporal que Prometheus guarda en memoria y en disco. km0_peticion_duracion_segundos{servicio, metodo, codigo} con 6 servicios × 10 métodos × 5 códigos × 12 buckets = 3 600 series: bien. Si a alguien se le ocurre añadir pedido_id como etiqueta, cada pedido crea una serie nueva por bucket: un millón de pedidos son doce millones de series, y Prometheus muere. La regla: una etiqueta solo puede tomar un conjunto pequeño y acotado de valores (servicio, método, código de estado, mercado, estado del pedido). Nunca identificadores de entidad (pedido_id, usuario, request_id), ni direcciones IP de clientes, ni rutas con parámetros sin normalizar (/pedidos/P-2026-000123 debe registrarse como /pedidos/{id}). Lo que necesite el identificador va a los logs o a las trazas (07-02).
Un caso límite interesante es km0_stock_unidades{producto}: con unos cientos de productos es aceptable; con cien mil no. En Kilómetro Cero se limita a los productos de las campañas activas y, para el resto, se publica un gauge agregado por productor.
- Prometheus: modelo pull, exporters y descubrimiento
Prometheus funciona en modo pull: no espera a que los servicios le envíen datos, sino que cada 15 segundos (configurable) hace GET /metrics a cada objetivo y guarda lo que encuentra. El formato es texto plano:
# HELP km0_pedidos_total Pedidos procesados por estado final
# TYPE km0_pedidos_total counter
km0_pedidos_total{estado="confirmado"} 1284522
km0_pedidos_total{estado="rechazado_stock"} 3311
km0_pedidos_total{estado="rechazado_pago"} 8720El pull tiene consecuencias importantes en un sistema distribuido:
- Prometheus sabe cuándo un objetivo no responde: la métrica
up{job="pedidos", instance="pedidos-2:8000"}vale 0. Con un modelo push, un servicio muerto simplemente deja de enviar y hay que inferir la ausencia. - Los servicios no necesitan conocer la dirección de Prometheus ni gestionar colas de envío.
- Necesita descubrir los objetivos: en
docker-composebasta con listarlos; en Kubernetes (07-05) los descubre preguntando a la API; en la nube, a la API del proveedor. Para trabajos efímeros (unJobque dura 3 segundos) existe el Pushgateway, que es la excepción, no la norma.
Los componentes que no exponen /metrics por sí mismos se cubren con exporters: procesos pequeños que traducen el estado de PostgreSQL, Kafka, Redis, etc. al formato de Prometheus. En Kilómetro Cero:
flowchart LR
subgraph servicios["Servicios km0 (prometheus_client)"]
C[catalogo :8000/metrics]
P[pedidos :8000/metrics]
I[inventario :9464/metrics]
R[reparto]
end
subgraph exporters["Exporters"]
PGX[postgres_exporter<br/>km0_inventario, pedidos-db-*]
KX[kafka_exporter<br/>lag por grupo]
RX[redis_exporter<br/>hit ratio, memoria]
NX[node_exporter<br/>CPU, disco, red]
BX[blackbox_exporter<br/>sondas HTTP/TLS]
end
PROM[(Prometheus<br/>scrape cada 15 s)]
AM[Alertmanager]
G[Grafana]
OPS[Canal #km0-operaciones<br/>Jordi, Marta]
servicios --> PROM
exporters --> PROM
PROM -- reglas de alerta --> AM
AM --> OPS
G -- PromQL --> PROM
4.1 Instrumentar los servicios: servicios/comun/metricas.py
Todos los servicios comparten un módulo con las métricas comunes, para que el nombre y las etiquetas sean idénticos y los paneles valgan para cualquiera de ellos.
# km0/servicios/comun/metricas.py
"""Métricas Prometheus compartidas por los servicios de Kilómetro Cero."""
import time
from contextlib import contextmanager
from prometheus_client import Counter, Gauge, Histogram, start_http_server
# Buckets pensados para latencias de servicio: de 5 ms a 10 s.
# Cada bucket es "cuántas observaciones cayeron por debajo de este límite".
BUCKETS_LATENCIA = (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10)
PEDIDOS_TOTAL = Counter(
"km0_pedidos_total",
"Pedidos procesados por estado final",
["estado"], # confirmado | rechazado_stock | rechazado_pago | compensado
)
PETICION_DURACION = Histogram(
"km0_peticion_duracion_segundos",
"Duración de las peticiones atendidas (HTTP o gRPC)",
["servicio", "metodo", "codigo"],
buckets=BUCKETS_LATENCIA,
)
STOCK_UNIDADES = Gauge(
"km0_stock_unidades",
"Unidades disponibles por producto (solo productos en campaña)",
["producto"],
)
PETICIONES_EN_VUELO = Gauge(
"km0_peticiones_en_vuelo",
"Peticiones que se están atendiendo ahora mismo (saturación)",
["servicio"],
)
@contextmanager
def medir(servicio: str, metodo: str):
"""Mide la duración de un bloque y la registra con el código de salida.
Uso:
with medir("pedidos", "POST /pedidos") as ctx:
...
ctx["codigo"] = "201"
El código se fija dentro del bloque; si salta una excepción se anota 500.
"""
ctx = {"codigo": "200"}
PETICIONES_EN_VUELO.labels(servicio=servicio).inc()
inicio = time.perf_counter()
try:
yield ctx
except Exception:
ctx["codigo"] = "500"
raise
finally:
duracion = time.perf_counter() - inicio
PETICION_DURACION.labels(
servicio=servicio, metodo=metodo, codigo=ctx["codigo"]
).observe(duracion)
PETICIONES_EN_VUELO.labels(servicio=servicio).dec()
def exponer_metricas(puerto: int = 9464) -> None:
"""Arranca un servidor HTTP mínimo con /metrics en un hilo aparte.
Lo usan los servicios que no tienen servidor HTTP propio (inventario, reparto).
Los que sí lo tienen (pedidos, catalogo) montan /metrics en su app.
"""
start_http_server(puerto)Puntos que conviene entender:
Counter,GaugeeHistogramson registros globales del proceso: se declaran una vez, a nivel de módulo, y cualquier parte del servicio los usa.prometheus_clientlos expone todos juntos en/metrics..labels(...)selecciona la serie concreta; la primera vez que se usa una combinación, se crea. Por eso hay que controlar qué valores se pasan (cardinalidad).medirregistra el código también cuando hay excepción: sin eseexcept, las peticiones que fallan con error interno no aparecerían en el histograma, y la latencia parecería mejor de lo que es.PETICIONES_EN_VUELOes la señal de saturación más barata de obtener: cuántas peticiones hay a la vez. Si enpedidossupera el número de hilos del servidor, las siguientes esperan en cola.
4.2 Interceptor gRPC en inventario
inventario ya tiene AuthInterceptor y ServicioInterceptor (06-04). Se añade uno más, que envuelve cada llamada y traduce el código de estado gRPC a etiqueta:
# km0/servicios/inventario/metricas_interceptor.py
import grpc
from servicios.comun.metricas import medir
class MetricasInterceptor(grpc.ServerInterceptor):
"""Registra duración y código de cada RPC del servicio inventario."""
def intercept_service(self, continuation, handler_call_details):
# handler_call_details.method es "/km0.inventario.v1.Inventario/ReservarStock"
metodo = handler_call_details.method.rsplit("/", 1)[-1]
handler = continuation(handler_call_details)
if handler is None or not handler.unary_unary:
return handler # streaming: se mediría de otra forma
original = handler.unary_unary
def envuelto(peticion, contexto):
with medir("inventario", metodo) as ctx:
try:
respuesta = original(peticion, contexto)
# Si el handler llamó a contexto.abort(), no llegamos aquí.
ctx["codigo"] = "OK"
return respuesta
except grpc.RpcError as e:
ctx["codigo"] = e.code().name # UNAVAILABLE, NOT_FOUND, ...
raise
return grpc.unary_unary_rpc_method_handler(
envuelto,
request_deserializer=handler.request_deserializer,
response_serializer=handler.response_serializer,
)Y en el arranque del servidor, el orden de los interceptores importa: métricas primero, para medir también las llamadas rechazadas por autenticación.
# km0/servicios/inventario/servidor.py (fragmento)
from servicios.comun.metricas import exponer_metricas, STOCK_UNIDADES
servidor = grpc.server(
futures.ThreadPoolExecutor(max_workers=32),
interceptors=[MetricasInterceptor(), AuthInterceptor(), ServicioInterceptor()],
)
exponer_metricas(puerto=9464)
# Tras cada ajuste o reserva confirmada, el servicio refresca el gauge:
def publicar_stock(producto: str, unidades: int) -> None:
if producto in PRODUCTOS_EN_CAMPANA: # tomate-rosa, queso-curado, vino-crianza...
STOCK_UNIDADES.labels(producto=producto).set(unidades)4.3 Middleware HTTP en pedidos y endpoint /metrics
pedidos es una aplicación FastAPI; un middleware mide cada petición y /metrics se monta en la misma app:
# km0/servicios/pedidos/app.py (fragmento)
from fastapi import FastAPI, Request
from prometheus_client import make_asgi_app
from servicios.comun.metricas import medir, PEDIDOS_TOTAL
app = FastAPI()
app.mount("/metrics", make_asgi_app()) # expone todas las métricas del proceso
@app.middleware("http")
async def middleware_metricas(request: Request, call_next):
# Normalizar la ruta: /pedidos/P-2026-000123 -> /pedidos/{id}
ruta = request.scope.get("route")
plantilla = ruta.path if ruta else "desconocida"
with medir("pedidos", f"{request.method} {plantilla}") as ctx:
respuesta = await call_next(request)
ctx["codigo"] = str(respuesta.status_code)
return respuesta
@app.post("/pedidos", status_code=201)
async def crear_pedido(pedido: PedidoEntrada):
resultado = await saga_pedido.ejecutar(pedido) # 03-05
PEDIDOS_TOTAL.labels(estado=resultado.estado).inc()
return resultadoEl uso de la plantilla de la ruta (/pedidos/{id}) en lugar de la URL real es la aplicación práctica de la regla de cardinalidad.
4.4 observabilidad/prometheus.yml
# km0/observabilidad/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s # cada cuánto se evalúan las reglas de alerta
external_labels:
entorno: produccion
rule_files:
- /etc/prometheus/alertas.yml
alerting:
alertmanagers:
- static_configs:
- targets: ["alertmanager:9093"]
scrape_configs:
- job_name: pedidos
static_configs:
- targets: ["pedidos-1:8000", "pedidos-2:8000", "pedidos-3:8000"]
- job_name: catalogo
static_configs:
- targets: ["catalogo-1:8000", "catalogo-2:8000"]
- job_name: inventario
static_configs:
- targets: ["inventario-1:9464", "inventario-2:9464"]
- job_name: reparto
static_configs:
- targets: ["reparto:9464"]
- job_name: postgres
static_configs:
- targets: ["postgres-exporter-inventario:9187",
"postgres-exporter-pedidos-primario:9187",
"postgres-exporter-pedidos-replica:9187"]
- job_name: kafka
static_configs:
- targets: ["kafka-exporter:9308"]
- job_name: redis
static_configs:
- targets: ["redis-exporter:9121"]
- job_name: nodos
static_configs:
- targets: ["node-exporter:9100"]
# Sondas sintéticas: Prometheus pide al blackbox_exporter que compruebe cada URL.
- job_name: blackbox_http
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://api.km0.example/api/v1/catalogo/mercados
- https://api.km0.example/salud/vivo
relabel_configs:
- source_labels: [__address__]
target_label: __param_target # la URL pasa como parámetro ?target=
- source_labels: [__param_target]
target_label: instance # y se conserva como etiqueta
- target_label: __address__
replacement: blackbox-exporter:9115 # a quien se hace el scrape realmenteEn docker-compose.yml la pila de observabilidad se añade junto a los servicios:
# km0/docker-compose.yml (fragmento)
prometheus:
image: prom/prometheus:v2.53.0
volumes:
- ./observabilidad/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./observabilidad/alertas.yml:/etc/prometheus/alertas.yml:ro
- prometheus-datos:/prometheus
command: ["--config.file=/etc/prometheus/prometheus.yml",
"--storage.tsdb.retention.time=30d"]
ports: ["9090:9090"]
alertmanager:
image: prom/alertmanager:v0.27.0
volumes:
- ./observabilidad/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
ports: ["9093:9093"]
grafana:
image: grafana/grafana:11.1.0
volumes:
- ./observabilidad/grafana/provisioning:/etc/grafana/provisioning:ro
- ./observabilidad/grafana/dashboards:/var/lib/grafana/dashboards:ro
environment:
GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/grafana_admin
ports: ["3000:3000"]
postgres-exporter-inventario:
image: prometheuscommunity/postgres-exporter:v0.15.0
environment:
DATA_SOURCE_NAME: "postgresql://exporter@inventario-db:5432/km0_inventario?sslmode=require"
kafka-exporter:
image: danielqsj/kafka-exporter:v1.7.0
command: ["--kafka.server=kafka-1:9092", "--kafka.server=kafka-2:9092"]
redis-exporter:
image: oliver006/redis_exporter:v1.62.0
environment:
REDIS_ADDR: "redis:6379"
node-exporter:
image: prom/node-exporter:v1.8.2
pid: host
blackbox-exporter:
image: prom/blackbox-exporter:v0.25.0El usuario exporter de PostgreSQL es de solo lectura sobre las vistas pg_stat_*; en un despliegue real se obtendría de Vault (06-04).
- PromQL esencial: tasas, percentiles y agregaciones
PromQL es el lenguaje de consulta. Cuatro construcciones cubren el 90 % de los usos:
rate(counter[ventana]): tasa por segundo en la ventana. Peticiones por segundo a pedidos, sumando las tres réplicas y desglosando por método:
Se usa el sufijo _count del histograma (número de observaciones) como contador de peticiones: cada petición medida es una observación.
Tasa de error: fracción de peticiones con código 5xx:
sum(rate(km0_peticion_duracion_segundos_count{servicio="pedidos", codigo=~"5.."}[5m]))
/
sum(rate(km0_peticion_duracion_segundos_count{servicio="pedidos"}[5m]))histogram_quantile(q, buckets): percentil a partir de los buckets. El p99 de pedidos, agregando todas las instancias:
histogram_quantile(
0.99,
sum by (le) (rate(km0_peticion_duracion_segundos_bucket{servicio="pedidos", metodo="POST /pedidos"}[5m]))
)El sum by (le) es obligatorio: hay que sumar los buckets de todas las instancias conservando la etiqueta le (límite del bucket), que es la que histogram_quantile necesita para interpolar.
increase(counter[ventana]): cuánto ha subido en la ventana (útil para "pedidos en la última hora"):
Otras que aparecen a diario: avg_over_time(gauge[10m]) para suavizar un gauge, absent(up{job="pedidos"}) para detectar que no hay ningún objetivo, topk(5, ...) para los cinco productos con menos stock, y operadores entre vectores (/, and, unless) que emparejan series por etiquetas.
Percentiles frente a medias
Con 1 000 peticiones de 50 ms y 10 de 5 s, la media es 99 ms: "todo bien". El p99 es 5 s: uno de cada cien clientes espera cinco segundos. En un sistema distribuido, donde una página del catálogo hace varias llamadas internas, la latencia de cola se amplifica: si cada llamada tiene un 1 % de probabilidad de ser lenta y una petición hace 10, un 10 % de las peticiones ven al menos una llamada lenta. Por eso los objetivos se fijan sobre percentiles (p50 para "lo normal", p99 para "lo que sufre alguien cada minuto") y nunca sobre medias.
- SLI, SLO, SLA y presupuesto de error
En 01-03 se calcularon los "nueves": un servicio con 99,9 % de disponibilidad puede estar caído 43 minutos al mes. Aquella cuenta hablaba de "estar caído"; ahora hay instrumentos para definirlo con precisión.
- SLI (Service Level Indicator): una medida concreta del servicio, expresada como fracción de eventos buenos sobre el total. "Fracción de peticiones a
POST /pedidosque responden sin5xxen menos de 500 ms." - SLO (Service Level Objective): el objetivo interno para ese SLI en una ventana. "El 99,9 % de las peticiones, medido en ventanas de 30 días."
- SLA (Service Level Agreement): el compromiso contractual con consecuencias (penalizaciones). Siempre más laxo que el SLO: Kilómetro Cero promete a los productores un 99,5 % y se exige un 99,9 %.
Los SLO del servicio pedidos:
| SLI | SLO | Cómo se calcula en PromQL |
|---|---|---|
Disponibilidad: peticiones sin 5xx |
99,9 % en 30 días | 1 - (sum(rate(..._count{codigo=~"5.."}[30d])) / sum(rate(..._count[30d]))) |
| Latencia: peticiones con duración < 500 ms | 99 % en 30 días (p99 < 500 ms) | sum(rate(..._bucket{le="0.5"}[30d])) / sum(rate(..._count[30d])) |
Nótese cómo el histograma permite expresar el SLO de latencia como una fracción: "cuántas observaciones cayeron en el bucket le="0.5"" dividido por el total es exactamente "fracción de peticiones por debajo de 500 ms". Esto solo funciona si 0,5 es un límite de bucket, razón por la que BUCKETS_LATENCIA incluye 0.5 y no, por ejemplo, 0.4 y 0.6.
Presupuesto de error
Un SLO del 99,9 % significa que se permite un 0,1 % de peticiones malas: ese es el presupuesto de error. Con 2 millones de peticiones al mes, son 2 000 errores. El presupuesto convierte la fiabilidad en una moneda que se puede gastar:
- Si queda presupuesto, el equipo despliega rápido, prueba cosas, hace experimentos de caos (07-06).
- Si el presupuesto se agota (un incidente de 40 minutos se lo lleva casi entero), se congelan los despliegues no urgentes y se invierte en fiabilidad.
Es también lo que da sentido a las alertas: no importa un pico de errores en sí, importa a qué velocidad se consume el presupuesto.
- Alertas: síntomas, burn rate y Alertmanager
7.1 Alertar por síntomas, no por causas
Una alerta debe despertar a alguien solo cuando un usuario está sufriendo o va a sufrir, y ese alguien debe poder hacer algo. "CPU al 85 % en inventario-2" no cumple ni lo uno ni lo otro: puede ser normal en la Semana de la Vendimia, y no dice qué hacer. "El 2 % de los pedidos falla desde hace 5 minutos" sí. Las causas (CPU, memoria, lag) van a los dashboards, para diagnosticar cuando la alerta de síntoma salta, o se convierten en alertas de baja prioridad (ticket, no llamada).
Las excepciones razonables son las alertas predictivas sobre causas con plazo cierto: un certificado que caduca en 12 h o un disco que se llenará en 4 h al ritmo actual (predict_linear) son avisos que evitan el síntoma.
7.2 Burn rate multiventana
El burn rate es la velocidad a la que se consume el presupuesto de error, relativa a la velocidad "justa". Burn rate 1 significa que, al ritmo actual, el presupuesto de 30 días se agota exactamente en 30 días. Burn rate 14,4 significa que se agota en 2 días. La técnica recomendada por el libro SRE Workbook es alertar con dos ventanas a la vez, larga y corta:
| Severidad | Ventana larga | Ventana corta | Burn rate | Presupuesto consumido antes de alertar | Se agota en |
|---|---|---|---|---|---|
| Página (llamada) | 1 h | 5 min | 14,4 | 2 % | ~2 días |
| Página | 6 h | 30 min | 6 | 5 % | ~5 días |
| Ticket | 3 días | 6 h | 1 | 10 % | 30 días |
La ventana larga evita que un pico de 30 segundos despierte a nadie; la ventana corta hace que la alerta se resuelva en cuanto el problema para (sin ella, la alerta de 1 h seguiría activa una hora después de arreglarlo).
7.3 observabilidad/alertas.yml
# km0/observabilidad/alertas.yml
groups:
- name: slo_pedidos
rules:
# Reglas de grabación: precalculan la tasa de error en varias ventanas.
# Nombre convencional: nivel:metrica:operacion_ventana
- record: job:km0_pedidos_tasa_error:ratio_rate5m
expr: |
sum(rate(km0_peticion_duracion_segundos_count{servicio="pedidos",codigo=~"5.."}[5m]))
/ sum(rate(km0_peticion_duracion_segundos_count{servicio="pedidos"}[5m]))
- record: job:km0_pedidos_tasa_error:ratio_rate1h
expr: |
sum(rate(km0_peticion_duracion_segundos_count{servicio="pedidos",codigo=~"5.."}[1h]))
/ sum(rate(km0_peticion_duracion_segundos_count{servicio="pedidos"}[1h]))
- record: job:km0_pedidos_tasa_error:ratio_rate30m
expr: |
sum(rate(km0_peticion_duracion_segundos_count{servicio="pedidos",codigo=~"5.."}[30m]))
/ sum(rate(km0_peticion_duracion_segundos_count{servicio="pedidos"}[30m]))
- record: job:km0_pedidos_tasa_error:ratio_rate6h
expr: |
sum(rate(km0_peticion_duracion_segundos_count{servicio="pedidos",codigo=~"5.."}[6h]))
/ sum(rate(km0_peticion_duracion_segundos_count{servicio="pedidos"}[6h]))
# SLO 99,9 % => presupuesto 0,001. Burn rate 14,4 => tasa de error > 0,0144
- alert: PedidosBurnRateRapido
expr: |
job:km0_pedidos_tasa_error:ratio_rate1h > (14.4 * 0.001)
and
job:km0_pedidos_tasa_error:ratio_rate5m > (14.4 * 0.001)
for: 2m
labels:
severidad: pagina
servicio: pedidos
annotations:
resumen: "pedidos consume el presupuesto de error 14x más rápido de lo permitido"
descripcion: "Tasa de error 1h = {{ $value | humanizePercentage }}. Runbook: runbooks/pedidos-errores.md"
- alert: PedidosBurnRateLento
expr: |
job:km0_pedidos_tasa_error:ratio_rate6h > (6 * 0.001)
and
job:km0_pedidos_tasa_error:ratio_rate30m > (6 * 0.001)
for: 15m
labels:
severidad: pagina
servicio: pedidos
annotations:
resumen: "pedidos consume el presupuesto de error 6x más rápido de lo permitido (6h)"
- alert: PedidosLatenciaP99
expr: |
histogram_quantile(0.99, sum by (le) (
rate(km0_peticion_duracion_segundos_bucket{servicio="pedidos",metodo="POST /pedidos"}[10m])
)) > 0.5
for: 10m
labels:
severidad: pagina
servicio: pedidos
annotations:
resumen: "p99 de POST /pedidos por encima de 500 ms durante 10 minutos"
- name: plataforma
rules:
- alert: KafkaLagReparto
expr: sum by (consumergroup) (kafka_consumergroup_lag{topic="reparto.posiciones"}) > 5000
for: 5m
labels: {severidad: pagina, servicio: reparto}
annotations:
resumen: "El panel de reparto va con retraso: lag {{ $value }} en reparto.posiciones"
- alert: KafkaLagAnalitica
expr: sum by (consumergroup) (kafka_consumergroup_lag{topic="pedidos.eventos", consumergroup="analitica"}) > 200000
for: 30m
labels: {severidad: ticket, servicio: analitica}
annotations:
resumen: "analitica acumula lag en pedidos.eventos (no urgente, pero revisar)"
- alert: PostgresConexionesAltas
expr: pg_stat_activity_count / pg_settings_max_connections > 0.85
for: 5m
labels: {severidad: ticket}
annotations:
resumer: "{{ $labels.instance }} usa más del 85 % de sus conexiones"
- alert: ObjetivoCaido
expr: up == 0
for: 3m
labels: {severidad: pagina}
annotations:
resumen: "{{ $labels.job }}/{{ $labels.instance }} no responde al scrape"
- name: seguridad_operativa
rules:
# blackbox_exporter publica la fecha de caducidad del certificado que ve al sondear
- alert: CertificadoCaducaPronto
expr: (probe_ssl_earliest_cert_expiry - time()) < 12 * 3600
labels: {severidad: pagina}
annotations:
resumen: "El certificado de {{ $labels.instance }} caduca en menos de 12 h"
# Cada servicio expone km0_vault_lease_segundos_restantes (gauge) para sus credenciales dinámicas
- alert: VaultLeaseNoRenovado
expr: km0_vault_lease_segundos_restantes < 900
for: 5m
labels: {severidad: pagina}
annotations:
resumen: "{{ $labels.servicio }} tiene un lease de Vault a menos de 15 min y no se renueva"
- alert: AuditoriaCadenaRota
expr: increase(km0_auditoria_verificaciones_fallidas_total[15m]) > 0
labels: {severidad: pagina}
annotations:
resumen: "La verificación de la cadena de auditoría ha fallado (06-05)"Sobre estas reglas:
- Las reglas de grabación (
record:) calculan una vez la expresión cara y la guardan como métrica nueva; las alertas y los dashboards la reutilizan. Con ventanas de 6 h sobre histogramas, hacerlo así ahorra mucho a Prometheus. for:exige que la condición se mantenga ese tiempo antes de disparar. Es el primer filtro contra alertas por parpadeos.- Los certificados de 24 h de Vault (06-04) hacen que la alerta a 12 h sea el umbral natural: si a mitad de vida no se ha renovado, la renovación automática está rota.
km0_vault_lease_segundos_restantesykm0_auditoria_verificaciones_fallidas_totalson métricas que los propios servicios publican: la instrumentación de negocio y de seguridad usa el mismo mecanismo que la de latencia.
7.4 Alertmanager: rutas, agrupación, silencios
Prometheus evalúa y dispara; Alertmanager decide a quién avisar, cómo agrupar y cuándo callar:
# km0/observabilidad/alertmanager.yml
global:
resolve_timeout: 5m
route:
receiver: operadores-ticket # destino por defecto
group_by: [alertname, servicio] # una notificación por alerta+servicio, no por instancia
group_wait: 30s # espera a agrupar alertas que llegan juntas
group_interval: 5m
repeat_interval: 4h
routes:
- matchers: [severidad = pagina]
receiver: operadores-guardia
repeat_interval: 1h
- matchers: [servicio = analitica]
receiver: equipo-datos
receivers:
- name: operadores-guardia
slack_configs:
- api_url_file: /run/secrets/slack_webhook
channel: "#km0-operaciones"
title: "[GUARDIA] {{ .CommonAnnotations.resumen }}"
text: "{{ range .Alerts }}{{ .Annotations.descripcion }}\n{{ end }}"
# En producción, además: pagerduty_configs / opsgenie_configs para llamar al móvil
- name: operadores-ticket
email_configs:
- to: [email protected]
- name: equipo-datos
slack_configs:
- api_url_file: /run/secrets/slack_webhook
channel: "#km0-datos"
inhibit_rules:
# Si un objetivo está caído, no avisar además de su latencia o sus errores
- source_matchers: [alertname = ObjetivoCaido]
target_matchers: [severidad = pagina]
equal: [servicio]Silencios: cuando Jordi va a reiniciar inventario-2 para una migración, crea un silencio de 30 minutos con un matcher (instance="inventario-2:9464") desde la interfaz o con amtool silence add instance=inventario-2:9464 --duration=30m --comment="migración". Sin silencios, la alternativa es ignorar alertas, que es el principio de la fatiga de alertas: cuando la mitad de las notificaciones son ruido, nadie lee la que importa. Reglas prácticas: cada alerta de página debe tener un runbook (07-03) y una acción posible; una alerta que salta y "se arregla sola" tres veces seguidas se degrada a ticket o se elimina; el número de páginas por semana se revisa como una métrica más.
- Dashboards en Grafana y monitoreo activo
8.1 Qué poner en el panel de la Semana de la Vendimia
Un dashboard no es una colección de todo lo que se puede graficar; es una narración de arriba abajo: primero si el negocio va bien, luego si los servicios cumplen, luego por qué. Para la campaña de Bodega Roble Alto:
| Fila | Paneles | Fuente |
|---|---|---|
| Negocio | Pedidos/min (total y con vino-crianza); tasa de pago rechazado; presupuesto de error restante del mes (%) |
km0_pedidos_total, reglas de grabación |
SLO de pedidos |
Tasa de error 5m/1h con línea en 0,1 %; p50/p95/p99 con línea en 500 ms; burn rate actual | reglas de grabación, histogram_quantile |
| Dependencias | p99 de ReservarStock; stock de vino-crianza en inv-bcn/inv-vlc; hit ratio de Redis; lag de pedidos.eventos por grupo |
inventario, km0_stock_unidades, exporters |
| Saturación | km0_peticiones_en_vuelo por servicio; conexiones de PostgreSQL; CPU de los brokers |
gauges, exporters |
| Borde | 429 por minuto en Kong (06-05); sondas sintéticas verdes/rojas |
Kong, blackbox |
Grafana se provisiona desde ficheros para que el dashboard viva en el repositorio, no en clics:
# km0/observabilidad/grafana/provisioning/datasources/prometheus.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
url: http://prometheus:9090
isDefault: true
# km0/observabilidad/grafana/provisioning/dashboards/km0.yml
apiVersion: 1
providers:
- name: km0
folder: Kilómetro Cero
type: file
options:
path: /var/lib/grafana/dashboardsY un dashboard JSON reducido a dos paneles, para ver la estructura:
{
"title": "KM0 - Semana de la Vendimia",
"uid": "km0-vendimia",
"refresh": "30s",
"panels": [
{
"type": "stat", "title": "Pedidos / min", "gridPos": {"x": 0, "y": 0, "w": 6, "h": 4},
"targets": [{"expr": "sum(rate(km0_pedidos_total{estado=\"confirmado\"}[5m])) * 60"}]
},
{
"type": "timeseries", "title": "POST /pedidos p50 / p95 / p99", "gridPos": {"x": 6, "y": 0, "w": 18, "h": 8},
"fieldConfig": {"defaults": {"unit": "s", "thresholds": {"steps": [
{"color": "green", "value": null}, {"color": "red", "value": 0.5}]}}},
"targets": [
{"legendFormat": "p50", "expr": "histogram_quantile(0.50, sum by (le) (rate(km0_peticion_duracion_segundos_bucket{servicio=\"pedidos\",metodo=\"POST /pedidos\"}[5m])))"},
{"legendFormat": "p95", "expr": "histogram_quantile(0.95, sum by (le) (rate(km0_peticion_duracion_segundos_bucket{servicio=\"pedidos\",metodo=\"POST /pedidos\"}[5m])))"},
{"legendFormat": "p99", "expr": "histogram_quantile(0.99, sum by (le) (rate(km0_peticion_duracion_segundos_bucket{servicio=\"pedidos\",metodo=\"POST /pedidos\"}[5m])))"}
]
}
]
}8.2 Monitoreo pasivo y activo
Todo lo anterior es pasivo: mide el tráfico que hay. Si a las 04:00 no hay tráfico, la tasa de error es 0/0 y no dice nada; si Kong tiene mal una ruta que nadie ha usado todavía, tampoco. El monitoreo activo (sondas sintéticas) genera tráfico artificial para comprobar el camino completo: el blackbox_exporter pide GET /api/v1/catalogo/mercados a través de Kong cada 15 s y publica probe_success, probe_duration_seconds y probe_ssl_earliest_cert_expiry. Una sonda más ambiciosa (un pequeño script que crea y cancela un pedido de prueba con el usuario [email protected], marcado para no contar en analítica) verifica la saga entera. Estas sondas son las que detectan el certificado caducado del domingo antes que Ana.
8.3 OpenTelemetry Metrics
Todo lo visto usa prometheus_client directamente. OpenTelemetry es el estándar abierto que unifica métricas, logs y trazas en un SDK y un protocolo (OTLP): con él, km0_peticion_duracion_segundos sería un Histogram del MeterProvider de OTel, exportado a un Collector que lo entrega a Prometheus (o a cualquier otro backend). La semántica (tipos, etiquetas, cardinalidad, PromQL) es la misma. En Kilómetro Cero se mantiene prometheus_client para métricas porque es más simple, y se adopta OpenTelemetry para las trazas y la correlación con logs en 07-02, que es donde su valor es mayor.
Errores Comunes y Consejos
- Medir solo infraestructura. CPU y memoria no dicen si Ana puede comprar. Empieza por RED en cada servicio y por dos métricas de negocio; añade USE de recursos como apoyo al diagnóstico.
- Etiquetas de alta cardinalidad.
pedido_id,usuario, IPs o rutas sin normalizar multiplican las series hasta tumbar Prometheus. Si lo necesitas para investigar, va a logs o trazas. - Summaries en servicios replicados. Los cuantiles calculados en el cliente no se pueden agregar entre instancias. Usa histogramas con buckets que incluyan los umbrales de tus SLO.
- Alertar sobre medias o sobre causas. La media oculta la cola; la CPU al 80 % no es un problema en sí. Alerta sobre SLI de usuario y burn rate; el resto, a dashboards o tickets.
- Alertas sin
for:ni runbook. Cada parpadeo despierta a alguien y nadie sabe qué hacer. Toda alerta de página tiene unfor, un dueño y un runbook enlazado en la anotación. - Olvidar las excepciones en el middleware. Si la métrica solo se registra cuando la petición termina bien, la latencia y los errores de las que fallan desaparecen. Registra en
finally. - Confiar solo en el monitoreo pasivo. Sin tráfico no hay señal. Una sonda sintética por camino crítico (catálogo, crear pedido, login) cubre las horas valle y los cambios de configuración no probados.
- Dashboards hechos a mano en Grafana. Se pierden, se duplican, nadie sabe cuál es el bueno. Provisiónalos desde el repositorio, junto al código que instrumentan.
Ejercicios
Ejercicio 1. Durante la Semana del Queso Artesano, Quesería Montblanc pregunta cuántos pedidos con queso-curado se han confirmado en la última hora, y Marta quiere saber si inventario está respondiendo peor que ayer. (a) ¿Puede responderse la primera pregunta con km0_pedidos_total{estado="confirmado"} tal como está definida? Si no, ¿qué cambiarías y qué riesgo de cardinalidad tendría? (b) Escribe la consulta PromQL que da el p95 de ReservarStock en los últimos 5 minutos agregando las dos instancias de inventario, y explica por qué no vale avg(histogram_quantile(...)) por instancia. (c) ¿Qué gauge te diría si el problema está en las réplicas inv-bcn/inv-vlc en lugar de en el servicio?
Ejercicio 2. El SLO de pedidos es 99,9 % de éxito en 30 días y el servicio recibe de media 3 millones de peticiones al mes. (a) ¿Cuántos errores caben en el presupuesto? (b) A las 10:00 del sábado empieza un incidente en el que el 8 % de las peticiones falla. Con un tráfico de 100 peticiones/s, ¿en cuánto tiempo se agota el presupuesto entero? (c) ¿Cuál de las dos alertas de burn rate (14,4 en 1 h/5 min; 6 en 6 h/30 min) saltará primero, y aproximadamente cuándo, teniendo en cuenta los for:? (d) A las 10:20 el incidente se resuelve; ¿por qué se resuelve la alerta poco después aunque la ventana de 1 h siga contaminada?
Ejercicio 3. Un lunes Jordi encuentra 47 notificaciones del fin de semana en #km0-operaciones: 30 de PostgresConexionesAltas en la réplica de pedidos (que sube al 90 % cada noche durante el DAG km0_ventas_diarias y baja sola), 12 de ObjetivoCaido para reparto durante un despliegue programado de 10 minutos, y 5 de KafkaLagAnalitica. Ninguna correspondía a un problema real. Propón, para cada grupo, el cambio concreto (en alertas.yml, en alertmanager.yml o en el procedimiento) que evita el ruido sin perder la detección de un problema real, y explica qué señal de síntoma cubriría el caso en que sí lo fuera.
Soluciones
Ejercicio 1.
(a) No: km0_pedidos_total solo tiene la etiqueta estado, no sabe qué productos llevaba cada pedido. Dos opciones: un counter aparte km0_lineas_pedido_total{producto} incrementado por cada línea confirmada (cardinalidad = número de productos, aceptable si se limita a los productos en campaña o si el catálogo es de unos cientos; con decenas de miles de productos habría que agregar por productor o responder esa pregunta desde analitica, no desde Prometheus), o añadir producto a km0_pedidos_total, que es peor porque un pedido con tres productos se contaría tres veces y rompería la métrica de pedidos. Nunca pedido_id. (b) histogram_quantile(0.95, sum by (le) (rate(km0_peticion_duracion_segundos_bucket{servicio="inventario", metodo="ReservarStock"}[5m]))). Sumar primero los buckets de las dos instancias por le reconstruye la distribución conjunta y el percentil se calcula sobre ella; promediar dos p95 calculados por separado no da el p95 del conjunto (si una instancia atiende el 90 % del tráfico con p95 = 40 ms y la otra el 10 % con p95 = 800 ms, la media, 420 ms, no describe a nadie). (c) El retraso de replicación que publica postgres_exporter (pg_replication_lag_seconds o el equivalente sobre pg_stat_replication) para inv-bcn e inv-vlc; si ese gauge sube, las lecturas en réplica devuelven stock viejo y inventario puede estar reintentando o esperando; si está a cero, el problema está en el servicio o en el primario.
Ejercicio 2.
(a) 0,1 % de 3 000 000 = 3 000 errores al mes. (b) 100 peticiones/s × 8 % = 8 errores/s; 3 000 / 8 = 375 s ≈ 6 minutos y cuarto. Todo el presupuesto del mes se va en poco más de seis minutos. (c) Burn rate = 0,08 / 0,001 = 80, muy por encima de ambos umbrales. La ventana de 5 min supera el 1,44 % casi de inmediato; la de 1 h necesita acumular: con la hora previa limpia, la tasa de error en 1 h alcanza 1,44 % cuando llevan unos 11 minutos de incidente (0,08 × t / 60 min = 0,0144 → t ≈ 10,8 min); con for: 2m, PedidosBurnRateRapido avisa hacia las 10:13. La de 6 h necesitaría 0,006 × 360 / 0,08 = 27 minutos más for: 15m, así que no llega a saltar si el incidente dura 20 minutos. (d) Porque la regla exige las dos ventanas a la vez (and): al resolverse, la ventana de 5 min baja por debajo del umbral en unos minutos, y aunque la de 1 h siga por encima hasta las 11:20, la conjunción deja de cumplirse y Alertmanager marca la alerta como resuelta. Esa es precisamente la razón de la ventana corta.
Ejercicio 3.
PostgresConexionesAltas en la réplica durante el DAG: es una causa, no un síntoma, y es un comportamiento esperado; opciones: subir el umbral solo para la réplica con un unless/matcher por instance, añadir for: 30m (el DAG dura menos), o mejor, degradarla a severidad: ticket (ya lo es) y enrutar los tickets a un resumen diario en lugar de a Slack en tiempo real (group_interval/repeat_interval largos en la ruta por defecto); el síntoma real que sí debe avisar es la latencia p99 de pedidos o el retraso de replicación, que están cubiertos por PedidosLatenciaP99. ObjetivoCaido en despliegues: el procedimiento de despliegue debe crear un silencio (amtool silence add job=reparto --duration=15m --comment="despliegue") como paso del pipeline; además, con varias réplicas la alerta debería ser "todas las instancias del job caídas" (absent(up{job="reparto"} == 1) o count(up{job="reparto"} == 1) == 0), no una instancia; el síntoma que cubre un despliegue realmente roto es el lag de reparto.posiciones (KafkaLagReparto) y la sonda sintética del panel. KafkaLagAnalitica: 200 000 con for: 30m es demasiado sensible para un consumidor batch que se pone al día cada noche; subir el umbral, alargar for a varias horas, o cambiarla a una alerta sobre el síntoma de analítica: "el DAG km0_ventas_diarias no ha terminado a las 07:00" (métrica de Airflow) o "la última ejecución falló". En todos los casos, el principio es el mismo: la alerta de página protege una experiencia de usuario concreta; lo demás es información para el diagnóstico.
Conclusión
Un sistema distribuido no avisa cuando falla; hay que construir los instrumentos. La observabilidad se apoya en tres señales y esta lección ha desarrollado la primera: las métricas, la única que escala con el número de series y no con el tráfico. Hemos visto qué medir en cuatro capas (infraestructura con USE, plataforma, aplicación con RED y las señales doradas, negocio), los cuatro tipos de métrica y por qué el histograma es el tipo adecuado para latencias en servicios replicados, la regla de cardinalidad que prohíbe pedido_id como etiqueta, el modelo pull de Prometheus con sus exporters y sondas, y el PromQL imprescindible (rate, histogram_quantile con sum by (le), increase). Sobre esa base, los "nueves" de 01-03 se han convertido en SLI y SLO concretos para pedidos (99,9 % de éxito, p99 < 500 ms), con un presupuesto de error que se gasta y alertas de burn rate multiventana que avisan por síntomas, enrutadas por Alertmanager a quien puede actuar, y un dashboard provisionado que cuenta la Semana de la Vendimia de arriba abajo.
Ahora, cuando PedidosBurnRateRapido salte un sábado a las 10:13, Jordi sabrá que el 8 % de los pedidos falla. Lo que las métricas no le dirán es cuáles, ni por qué: si es inventario que rechaza reservas de vino-crianza, la pasarela de pagos, o un traceparent que se perdió en una cabecera Kafka. Para eso hacen falta los otros dos pilares: los logs estructurados y centralizados, que cuentan qué pasó con el pedido P-2026-000125, y las trazas distribuidas, que muestran en qué servicio se fueron los 480 ms. Esa es la siguiente lección.
Curso de Arquitecturas Distribuidas
Módulo 1: Introducción a los Sistemas Distribuidos
- Conceptos Básicos de Sistemas Distribuidos
- Modelos de Sistemas Distribuidos
- Ventajas y Desafíos de los Sistemas Distribuidos
- Las Falacias de la Computación Distribuida
- Tiempo, Relojes y Ordenación de Eventos
- Del Monolito a la Plataforma Distribuida: el Caso Kilómetro Cero
Módulo 2: Comunicación en Sistemas Distribuidos
- Protocolos de Comunicación
- RPC y RMI
- gRPC y Serialización de Datos
- Mensajería y Colas de Mensajes
- Patrones de Comunicación Asíncrona
Módulo 3: Consistencia y Replicación
- Modelos de Consistencia
- El Teorema CAP y PACELC
- Algoritmos de Consenso
- Replicación de Datos
- Transacciones Distribuidas y Sagas
Módulo 4: Almacenamiento Distribuido
- Particionado de Datos y Hashing Consistente
- Sistemas de Archivos Distribuidos
- Almacenamiento de Objetos
- Bases de Datos Distribuidas
- Cachés Distribuidos
Módulo 5: Computación Distribuida
- Modelos de Computación Distribuida
- MapReduce y Hadoop
- Spark y Computación en Memoria
- Procesamiento de Flujos de Datos
- Planificación de Trabajos y Pipelines de Datos
Módulo 6: Seguridad en Sistemas Distribuidos
- Autenticación y Autorización
- Cifrado y Protección de Datos
- Gestión de Identidades
- Seguridad entre Servicios: mTLS y Gestión de Secretos
- Puertas de Enlace, Limitación de Tasa y Auditoría
Módulo 7: Monitoreo y Mantenimiento
- Monitoreo de Sistemas Distribuidos
- Logs Centralizados y Trazabilidad Distribuida
- Gestión de Fallos y Recuperación
- Patrones de Resiliencia: Timeouts, Reintentos y Circuit Breaker
- Automatización y Orquestación
- Pruebas en Sistemas Distribuidos e Ingeniería del Caos
