En la lección anterior instalamos metrics-server y por fin supimos cuánto consume cada componente de Rutas Norte ahora mismo. También descubrimos su límite infranqueable: no recuerda nada. La pregunta "¿qué pasó anoche a las tres de la madrugada?" sigue sin respuesta, y con ella todas las que de verdad importan: ¿cuántas reservas por minuto estamos confirmando?, ¿ha subido la latencia desde el último despliegue?, ¿cuánto falta para que se llene el disco de postgres-reservas?
Esta lección despliega Prometheus, el estándar de facto para monitorizar Kubernetes y el segundo proyecto graduado de la CNCF después del propio Kubernetes. Vamos a entender su modelo de datos, de dónde salen realmente las métricas de un clúster (una de las mayores fuentes de confusión para quien empieza), cómo instrumentar api-reservas con métricas de negocio, cómo desplegar el stack con el operador que anunciamos en 06-07, y cómo escribir PromQL que responda a preguntas reales de Rutas Norte. Al terminar, la plataforma tendrá memoria.
Contenido
- Qué aporta Prometheus frente a metrics-server
- El modelo de extracción (pull) y los endpoints
/metrics - El modelo de datos: series, etiquetas y los cuatro tipos de métrica
- De dónde salen las métricas de un clúster: la tabla que aclara la confusión
- Instrumentar
api-reservascon métricas de negocio - Desplegar el kube-prometheus-stack con Helm
- El Prometheus Operator y sus recursos personalizados
ServiceMonitorde Rutas Norte y diagnóstico de objetivos- PromQL desde cero y con sentido
- Los cuatro indicadores dorados aplicados a Rutas Norte
- Almacenamiento, retención y los límites de Prometheus
- Errores comunes y consejos
- Ejercicios
- Qué aporta Prometheus frente a metrics-server
Tres cosas, y las tres son las que nos faltaban.
Histórico. Prometheus guarda cada muestra en una base de datos de series temporales en disco. Con la retención por defecto del stack tendremos varios días; configurando el almacenamiento, semanas o meses. La pregunta de las tres de la madrugada tiene respuesta.
Consultas. PromQL es un lenguaje completo para agregar, filtrar, derivar y comparar series. No es "ver un número": es "dame la tasa de errores 5xx de api-reservas en el entorno de producción, agrupada por ruta, durante la última hora".
Alertas. Prometheus evalúa expresiones periódicamente y, cuando se cumplen durante un tiempo sostenido, dispara alertas que Alertmanager enruta a quien corresponda. Eso es lo que hace que alguien se entere a las tres de la madrugada sin estar mirando una pantalla.
Y una cuarta, menos obvia pero decisiva: cualquier cosa puede exponer métricas. metrics-server solo sabe de CPU y memoria. Prometheus recoge lo que sea que un endpoint HTTP le ofrezca: peticiones por ruta, reservas confirmadas, tamaño de la cola de correos, conexiones activas de PostgreSQL, certificados a punto de caducar.
| Capacidad | metrics-server | Prometheus |
|---|---|---|
| Histórico | ~1 minuto en memoria | Días o meses en disco |
| Métricas | Solo CPU y memoria | Cualquiera expuesta en /metrics |
| Lenguaje de consulta | No | PromQL |
| Alertas | No | Sí (con Alertmanager, 07-04) |
| Métricas de negocio | No | Sí |
| Coste de operación | Trivial | Considerable (RAM y disco) |
| ¿Lo usa el HPA? | Sí, de forma nativa | Sí, con un adaptador (09-04) |
Los dos conviven. No sustituimos metrics-server: lo complementamos.
- El modelo de extracción (pull) y los endpoints
/metrics
/metricsAquí está la decisión de diseño que define todo lo demás.
La mayoría de sistemas de monitorización tradicionales funcionan por empuje (push): la aplicación envía sus métricas a un servidor central. Prometheus hace lo contrario: extracción (pull). Prometheus tiene una lista de objetivos (targets) y, cada cierto intervalo (típicamente 30 segundos), hace un GET a cada uno.
flowchart LR
P[Prometheus] -->|"GET /metrics cada 30s"| A["api-reservas :8080/metrics"]
P -->|"GET /metrics cada 30s"| B["exportador-pg :9187/metrics"]
P -->|"GET /metrics cada 30s"| C["kube-state-metrics :8080/metrics"]
P -->|"GET /metrics cada 30s"| D["node-exporter :9100/metrics"]
P --> TSDB[(TSDB en disco<br/>bloques de 2 h)]
¿Por qué es mejor extraer que recibir?
- Prometheus sabe si un objetivo está caído. Si el
GETfalla, la métrica sintéticaupvale 0. Con push, la ausencia de datos es ambigua: ¿la aplicación está caída o simplemente no ha enviado nada? - El control del ritmo lo tiene el sistema de monitorización, no las aplicaciones. Cien aplicaciones mal programadas no pueden inundarlo.
- Descubrimiento automático. Prometheus consulta la API de Kubernetes para saber qué pods existen. Un pod nuevo aparece como objetivo automáticamente, sin configurar nada en la aplicación.
- Depuración trivial. Puedes hacer
curlal endpoint/metricsdesde tu portátil y ver exactamente lo mismo que ve Prometheus.
La excepción son los procesos de vida muy corta, como nuestro CronJob informes-ocupacion: puede terminar antes de que Prometheus lo escanee. Para esos casos existe el Pushgateway, un intermediario al que el job empuja sus métricas y que Prometheus escanea después. Es la única excepción legítima y conviene usarla con moderación.
Cómo se ve un endpoint /metrics
kubectl -n rutas-norte-pro port-forward deploy/api-reservas 8080:8080 &
curl -s localhost:8080/metrics | head -30# HELP rutasnorte_peticiones_total Total de peticiones HTTP atendidas
# TYPE rutasnorte_peticiones_total counter
rutasnorte_peticiones_total{ruta="/api/rutas",metodo="GET",codigo="200"} 184203
rutasnorte_peticiones_total{ruta="/api/rutas",metodo="GET",codigo="500"} 47
rutasnorte_peticiones_total{ruta="/api/reservas",metodo="POST",codigo="201"} 9821
rutasnorte_peticiones_total{ruta="/api/reservas",metodo="POST",codigo="422"} 318
# HELP rutasnorte_reservas_confirmadas_total Reservas confirmadas y pagadas
# TYPE rutasnorte_reservas_confirmadas_total counter
rutasnorte_reservas_confirmadas_total{origen="web"} 8742
rutasnorte_reservas_confirmadas_total{origen="movil"} 1079
# HELP rutasnorte_conexiones_pool_activas Conexiones en uso del pool de PostgreSQL
# TYPE rutasnorte_conexiones_pool_activas gauge
rutasnorte_conexiones_pool_activas 7Es texto plano. Cada línea es nombre{etiquetas} valor. Las líneas # HELP y # TYPE son metadatos: descripción y tipo. Ese formato, deliberadamente simple, es todo el contrato entre una aplicación y Prometheus.
- El modelo de datos: series, etiquetas y los cuatro tipos de métrica
Series temporales y etiquetas
Una serie temporal en Prometheus se identifica de forma única por su nombre de métrica más el conjunto exacto de sus etiquetas. Esto es fundamental:
rutasnorte_peticiones_total{ruta="/api/rutas", metodo="GET", codigo="200"} → serie A
rutasnorte_peticiones_total{ruta="/api/rutas", metodo="GET", codigo="500"} → serie B (distinta)
rutasnorte_peticiones_total{ruta="/api/reservas", metodo="POST", codigo="201"} → serie C (distinta)Cambiar el valor de una sola etiqueta crea una serie completamente nueva. Cada serie ocupa memoria y disco, y de ahí nace el peligro más citado de Prometheus: la cardinalidad.
Regla de oro de la cardinalidad: nunca uses como etiqueta un valor con muchos valores posibles. Nada de identificadores de usuario, de reserva, de sesión, direcciones IP de cliente, marcas de tiempo o mensajes de error completos.
Un ejemplo real del peligro. Si api-reservas etiquetara cada petición con el dni del cliente:
Con 200 000 clientes y 15 rutas, tendrías 3 millones de series solo de esa métrica. Prometheus se quedaría sin memoria en minutos. Y, además, estarías metiendo datos personales en el sistema de monitorización, con las implicaciones que veremos en 07-05.
Etiquetas que Prometheus añade por sí mismo a todo lo que recoge en Kubernetes (y que usaremos constantemente):
| Etiqueta | Ejemplo | Origen |
|---|---|---|
job |
api-reservas |
Nombre del trabajo de recolección |
instance |
10.244.2.17:8080 |
IP y puerto del objetivo concreto |
namespace |
rutas-norte-pro |
Metadatos de Kubernetes |
pod |
api-reservas-7d9f8c4b5-x2klm |
Metadatos de Kubernetes |
container |
api |
Metadatos de Kubernetes |
node |
rutas-norte-worker-2 |
Metadatos de Kubernetes |
Los cuatro tipos de métrica
| Tipo | Puede | Ejemplo en Rutas Norte | Función típica para consultarlo |
|---|---|---|---|
counter |
Solo subir (o reiniciarse a 0) | rutasnorte_peticiones_total |
rate(), increase() |
gauge |
Subir y bajar | rutasnorte_conexiones_pool_activas |
Valor directo, avg, max |
histogram |
Contar en cubetas configurables | rutasnorte_duracion_peticion_segundos |
histogram_quantile() |
summary |
Percentiles calculados en el cliente | Poco recomendable en general | Valor directo |
counter (contador). Un valor que solo crece: peticiones atendidas, errores, bytes enviados, reservas confirmadas. Nunca se consulta directamente. El valor absoluto 184203 no dice nada: ¿son muchas? ¿desde cuándo? Se consulta siempre con rate() o increase(), que calculan cuánto ha crecido por unidad de tiempo. Por convención, su nombre acaba en _total.
Prometheus detecta automáticamente los reinicios del contador (cuando un pod se recrea, el contador vuelve a 0) y rate() los compensa. No tienes que preocuparte por ello.
gauge (medidor). Un valor que sube y baja: conexiones activas, temperatura, memoria en uso, tamaño de una cola. Se consulta directamente. Es el tipo de casi todo lo que expone cAdvisor sobre memoria.
histogram (histograma). El tipo más potente y el peor entendido. En lugar de guardar cada valor individual, la aplicación clasifica cada observación en cubetas acumulativas predefinidas. Una métrica de histograma expone en realidad tres cosas:
# Cubetas acumulativas: "cuántas peticiones tardaron MENOS de X segundos"
rutasnorte_duracion_peticion_segundos_bucket{ruta="/api/reservas",le="0.05"} 8140
rutasnorte_duracion_peticion_segundos_bucket{ruta="/api/reservas",le="0.1"} 9210
rutasnorte_duracion_peticion_segundos_bucket{ruta="/api/reservas",le="0.5"} 9780
rutasnorte_duracion_peticion_segundos_bucket{ruta="/api/reservas",le="1"} 9812
rutasnorte_duracion_peticion_segundos_bucket{ruta="/api/reservas",le="+Inf"} 9821
# Suma de todos los valores observados (para calcular la media)
rutasnorte_duracion_peticion_segundos_sum{ruta="/api/reservas"} 743.28
# Número total de observaciones
rutasnorte_duracion_peticion_segundos_count{ruta="/api/reservas"} 9821Lectura: 8140 peticiones tardaron menos de 50 ms; 9210 tardaron menos de 100 ms (incluidas las 8140 anteriores: son acumulativas); 9821 en total, luego 9 peticiones tardaron más de 1 segundo.
Con esos datos, la función histogram_quantile() puede estimar cualquier percentil. La ventaja enorme frente al summary: como las cubetas son contadores normales, se pueden sumar entre pods. Puedes calcular el percentil 95 de latencia de todo api-reservas agregando sus seis réplicas. Con un summary eso es matemáticamente imposible.
El coste: cada cubeta es una serie. Un histograma con 10 cubetas y 15 rutas son 150 series por pod. Elige las cubetas con criterio, ajustadas a la latencia real de tu servicio.
summary (resumen). La aplicación calcula los percentiles ella misma y los expone. Barato de consultar, pero no agregable entre instancias: el percentil 95 del pod A y el del pod B no se pueden combinar para obtener el del servicio. En Kubernetes, donde todo tiene varias réplicas, esto lo descarta casi siempre. Úsalo solo si necesitas un percentil exacto de una única instancia.
Convenciones de nombres
Prometheus tiene convenciones que conviene respetar porque las herramientas y los cuadros de mando las asumen:
- Prefijo con el nombre del sistema:
rutasnorte_. - Unidad base en el nombre y siempre en unidades base del SI:
_segundosno_ms,_bytesno_mb. - Los contadores acaban en
_total. - Nombres en
snake_case, sin mayúsculas.
- De dónde salen las métricas de un clúster: la tabla que aclara la confusión
Esta es la sección que resuelve la duda que tiene todo el mundo la primera semana: "¿por qué necesito cuatro cosas distintas para monitorizar Kubernetes?".
La respuesta corta: porque son cuatro capas de realidad diferentes y ninguna de ellas puede ver las otras.
| Fuente | Qué observa | Ejemplo de métrica | ¿Hay que instalarlo? |
|---|---|---|---|
| kubelet / cAdvisor | El uso real de los contenedores (cgroups) | container_memory_working_set_bytes |
No: viene en el kubelet |
| node-exporter | El sistema operativo del nodo | node_filesystem_avail_bytes |
Sí, como DaemonSet |
| kube-state-metrics | El estado de los objetos de la API | kube_deployment_status_replicas_unavailable |
Sí, como Deployment |
| Exportadores de aplicación | Las tripas de un software concreto | pg_stat_database_numbackends |
Sí, uno por software |
| Instrumentación propia | La lógica de negocio | rutasnorte_reservas_confirmadas_total |
Sí, en el código |
Vamos una a una, porque la distinción importa muchísimo a la hora de escribir consultas.
kubelet / cAdvisor: lo que los contenedores consumen
Es la misma fuente que alimenta a metrics-server (07-02), pero Prometheus la lee directamente y con mucho más detalle. Métricas clave:
container_cpu_usage_seconds_total{pod="api-reservas-...", container="api"}
container_memory_working_set_bytes{pod="api-reservas-...", container="api"}
container_cpu_cfs_throttled_periods_total{pod="api-reservas-...", container="api"}
container_network_receive_bytes_total{pod="api-reservas-..."}
container_fs_writes_bytes_total{pod="postgres-reservas-0"}Fíjate en la tercera: el contador de periodos estrangulados que en 07-02 dijimos que kubectl top no puede darnos. Aquí está, y es la que confirmará el diagnóstico de throttling de api-reservas.
Aviso práctico: estas métricas también aparecen con container="" (el valor agregado del pod, incluido el contenedor pause de infraestructura). Casi siempre querrás filtrar con container!="" para no contar el doble.
node-exporter: lo que le pasa a la máquina
Es un DaemonSet (un pod por nodo, como estudiamos en 06-02) que lee /proc y /sys del sistema anfitrión. Ve cosas que ningún contenedor puede ver:
node_filesystem_avail_bytes{mountpoint="/var/lib/kubelet"}
node_memory_MemAvailable_bytes
node_load1
node_cpu_seconds_total{mode="idle"}
node_network_transmit_bytes_totalEs el que te avisa de que el disco del nodo se está llenando, de que la carga media está por las nubes o de que la tarjeta de red está saturada. Ningún contenedor puede decirte eso de sí mismo.
kube-state-metrics: lo que la API de Kubernetes cree
Esta es la que más cuesta entender y la que más falta hace. No mide consumo de nada. Se conecta a la API de Kubernetes y convierte el estado de los objetos en métricas:
kube_deployment_spec_replicas{deployment="api-reservas"} 6
kube_deployment_status_replicas_available{deployment="api-reservas"} 4
kube_pod_container_status_restarts_total{pod="api-reservas-...", container="api"} 7
kube_pod_status_phase{pod="postgres-reservas-0", phase="Running"} 1
kube_job_status_failed{job_name="informes-ocupacion-28934520"} 1
kube_persistentvolumeclaim_status_phase{persistentvolumeclaim="datos-postgres-reservas-0", phase="Bound"} 1
kube_pod_container_resource_requests{pod="api-reservas-...", resource="cpu"} 0.2Con estas métricas puedes alertar de cosas que ninguna de las otras fuentes conoce: un Deployment con menos réplicas disponibles de las deseadas, un pod reiniciándose en bucle, un Job que ha fallado, un PVC que lleva veinte minutos en Pending.
Y algo muy útil: kube_pod_container_resource_requests expone las requests declaradas. Cruzando esa con container_memory_working_set_bytes de cAdvisor obtienes, en una sola consulta PromQL, exactamente el análisis de recalibración que en 07-02 tuvimos que hacer con un script de bash.
Exportadores: traductores para software de terceros
PostgreSQL no habla el formato de Prometheus. Un exportador es un proceso que se conecta al software, consulta sus estadísticas internas y las traduce a /metrics.
Y aquí conectamos con 06-04: ya lo tenemos desplegado. Cuando añadimos el sidecar exportador de métricas al pod de postgres-reservas, dijimos explícitamente que Prometheus lo consumiría en esta lección. Ha llegado el momento.
# Recordatorio del sidecar que añadimos en 06-04, dentro del StatefulSet
# postgres-reservas. Nótese que es un sidecar nativo (initContainer con
# restartPolicy: Always), como vimos en aquella lección.
initContainers:
- name: exportador-pg
image: quay.io/prometheuscommunity/postgres-exporter:v0.15.0
restartPolicy: Always # sidecar nativo 1.29+
ports:
- name: metricas
containerPort: 9187
env:
- name: DATA_SOURCE_URI
value: "127.0.0.1:5432/reservas?sslmode=disable"
- name: DATA_SOURCE_USER
valueFrom:
secretKeyRef:
name: credenciales-postgres
key: usuario-metricas
- name: DATA_SOURCE_PASS
valueFrom:
secretKeyRef:
name: credenciales-postgres
key: password-metricas
resources:
requests:
cpu: "20m"
memory: "32Mi"
limits:
cpu: "100m"
memory: "64Mi"Lo que expone, y que nos hará falta:
pg_stat_database_numbackends{datname="reservas"} # conexiones abiertas
pg_stat_database_xact_commit{datname="reservas"} # transacciones confirmadas
pg_stat_database_deadlocks{datname="reservas"} # interbloqueos
pg_database_size_bytes{datname="reservas"} # tamaño de la base de datos
pg_stat_replication_replay_lag # retraso de réplica
pg_up # ¿responde PostgreSQL?Existen exportadores para prácticamente todo: redis_exporter para redis-cache, nginx-prometheus-exporter para tienda-web, blackbox_exporter para comprobar desde fuera que https://www.rutasnorte.example responde.
Instrumentación propia: lo único que sabe de negocio
Ninguna de las cuatro fuentes anteriores puede decirte cuántos billetes has vendido. Eso solo lo sabe tu código. Es la siguiente sección.
El resumen visual
flowchart TB
subgraph Nodo["Nodo rutas-norte-worker-2"]
subgraph Pod1["Pod api-reservas"]
APP["contenedor api<br/>/metrics propio"]
end
subgraph Pod2["Pod postgres-reservas-0"]
PG["contenedor postgres"]
EXP["sidecar exportador<br/>:9187/metrics"]
EXP -.consulta.-> PG
end
KUBELET["kubelet + cAdvisor<br/>uso real de cgroups"]
NE["node-exporter<br/>DaemonSet<br/>/proc y /sys"]
end
KSM["kube-state-metrics<br/>estado de los objetos"]
API[(API Server)]
KSM -.consulta.-> API
PROM[Prometheus]
APP --> PROM
EXP --> PROM
KUBELET --> PROM
NE --> PROM
KSM --> PROM
- Instrumentar
api-reservas con métricas de negocio
api-reservas con métricas de negocioLa instrumentación propia es lo que separa un cuadro de mando de infraestructura de uno que le importa al negocio. "El pod usa 300 Mi de RAM" no interesa al director de Rutas Norte. "Estamos confirmando 12 reservas por minuto, un 40 % menos que ayer a esta hora" sí.
api-reservas está en Node.js, así que usamos la librería cliente oficial prom-client. Existen librerías equivalentes para Java, Go, Python, .NET y prácticamente cualquier lenguaje.
// metricas.js — instrumentación de api-reservas
const client = require('prom-client');
// El registro es el contenedor de todas las métricas de este proceso.
const registro = new client.Registry();
// Etiquetas que se añaden a TODAS las métricas de este proceso.
// Vienen de la Downward API (03-03), así que cada pod se identifica solo.
registro.setDefaultLabels({
componente: 'api-reservas',
entorno: process.env.ENTORNO || 'dev',
version: process.env.APP_VERSION || 'desconocida',
});
// Métricas por defecto del runtime: memoria del heap, GC, event loop, etc.
// Muy útiles y gratis: una sola línea.
client.collectDefaultMetrics({ register: registro });
// ---------------------------------------------------------------------------
// 1. COUNTER: peticiones HTTP atendidas.
// Etiquetas de BAJA cardinalidad: ruta normalizada (no la URL real),
// método y código. Jamás el id de reserva ni el DNI del cliente.
// ---------------------------------------------------------------------------
const peticionesTotal = new client.Counter({
name: 'rutasnorte_peticiones_total',
help: 'Total de peticiones HTTP atendidas por api-reservas',
labelNames: ['ruta', 'metodo', 'codigo'],
registers: [registro],
});
// ---------------------------------------------------------------------------
// 2. HISTOGRAM: duración de las peticiones.
// Las cubetas están elegidas para NUESTRA latencia real: la mayoría de
// peticiones tardan entre 20 y 200 ms, y el SLO está en 500 ms.
// Cubetas mal elegidas dan percentiles inútiles.
// ---------------------------------------------------------------------------
const duracionPeticion = new client.Histogram({
name: 'rutasnorte_duracion_peticion_segundos',
help: 'Duración de las peticiones HTTP en segundos',
labelNames: ['ruta', 'metodo'],
buckets: [0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
registers: [registro],
});
// ---------------------------------------------------------------------------
// 3. COUNTER de negocio: reservas confirmadas y pagadas.
// Esta es LA métrica que mira el negocio. Si cae a cero, da igual que
// todos los pods estén Running: la plataforma no está vendiendo.
// ---------------------------------------------------------------------------
const reservasConfirmadas = new client.Counter({
name: 'rutasnorte_reservas_confirmadas_total',
help: 'Reservas confirmadas y pagadas correctamente',
labelNames: ['origen', 'trayecto_tipo'], // 'web'|'movil', 'nacional'|'regional'
registers: [registro],
});
// ---------------------------------------------------------------------------
// 4. GAUGE: estado del pool de conexiones a PostgreSQL.
// Es exactamente el dato que en 07-01 provocaba el incidente invisible.
// Ahora, además de que la readiness lo detecte, quedará registrado.
// ---------------------------------------------------------------------------
const conexionesPool = new client.Gauge({
name: 'rutasnorte_conexiones_pool_activas',
help: 'Conexiones actualmente en uso del pool de PostgreSQL',
registers: [registro],
collect() {
// collect() se ejecuta en el momento del scrape: siempre valor fresco.
this.set(poolPg.totalCount - poolPg.idleCount);
},
});
// ---------------------------------------------------------------------------
// 5. COUNTER de la pasarela de pagos externa.
// Nos permitirá distinguir "falla nuestro código" de "falla el proveedor".
// ---------------------------------------------------------------------------
const llamadasPasarela = new client.Counter({
name: 'rutasnorte_pasarela_pagos_llamadas_total',
help: 'Llamadas a la pasarela de pagos externa',
labelNames: ['resultado'], // 'ok' | 'error' | 'timeout'
registers: [registro],
});
module.exports = {
registro, peticionesTotal, duracionPeticion,
reservasConfirmadas, conexionesPool, llamadasPasarela,
};Y el middleware que lo conecta con Express:
// servidor.js
const express = require('express');
const m = require('./metricas');
const app = express();
// Middleware que mide TODAS las peticiones.
app.use((req, res, next) => {
// IMPORTANTE: usamos la ruta del enrutador (/api/reservas/:id), no la URL
// real (/api/reservas/48213). Si usáramos la URL real, cada reserva
// crearía una serie nueva: explosión de cardinalidad garantizada.
const finTemporizador = m.duracionPeticion.startTimer();
res.on('finish', () => {
const ruta = req.route ? req.baseUrl + req.route.path : 'desconocida';
finTemporizador({ ruta, metodo: req.method });
m.peticionesTotal.inc({ ruta, metodo: req.method, codigo: res.statusCode });
});
next();
});
// El endpoint que Prometheus escaneará.
app.get('/metrics', async (req, res) => {
res.set('Content-Type', m.registro.contentType);
res.end(await m.registro.metrics());
});
// En la lógica de negocio, al confirmar una reserva:
async function confirmarReserva(reserva) {
await guardarEnBaseDeDatos(reserva);
m.reservasConfirmadas.inc({
origen: reserva.origen,
trayecto_tipo: reserva.esNacional ? 'nacional' : 'regional',
});
}
app.listen(8080);Dos decisiones a subrayar:
- Ruta normalizada, no URL real.
req.route.pathda/api/reservas/:id, con lo que todas las reservas comparten serie. Usarreq.originalUrldaría/api/reservas/48213,/api/reservas/48214... una serie por reserva. Es el error de cardinalidad más frecuente del mundo. /metricsen el mismo puerto que la aplicación, o en uno aparte. Aquí lo dejamos en el 8080 por simplicidad. En producción es preferible exponerlo en un puerto distinto (por ejemplo 9090) que no esté publicado en el Ingress, para que las métricas no sean accesibles desde internet. Las NetworkPolicies derutas-norte-pro(04-06) deberán permitir el tráfico desde el namespace de monitorización a ese puerto: recuerda que toda conversación nueva necesita su política.
- Desplegar el kube-prometheus-stack con Helm
Instalar Prometheus a mano en Kubernetes significa gestionar unos veinte objetos entre Deployments, ConfigMaps, Services, RBAC y almacenamiento. La comunidad ha empaquetado todo eso en un chart de Helm llamado kube-prometheus-stack.
Nota: Helm es el gestor de paquetes de Kubernetes y lo estudiaremos a fondo en 10-03. Aquí lo usamos como herramienta de instalación; por ahora basta con entender que un chart es una plantilla parametrizable de manifiestos y que
helm installlos genera y los aplica.
# Añadir el repositorio de charts de la comunidad
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
# Namespace dedicado para toda la monitorización
kubectl create namespace monitorizacion
kubectl label namespace monitorizacion app.kubernetes.io/part-of=rutas-norteFichero de valores adaptado a Rutas Norte:
# k8s/base/monitorizacion/valores-prometheus.yaml
prometheus:
prometheusSpec:
# Retención: 30 días o 45 GiB, lo que llegue antes. Un límite de tamaño
# es imprescindible: sin él, el disco se llena y Prometheus muere.
retention: 30d
retentionSize: "45GiB"
# CRÍTICO: por defecto, el operador SOLO recoge ServiceMonitors que
# lleven la etiqueta release=<nombre-del-release>. Poniéndolo a false,
# recogerá los de cualquier namespace sin etiqueta especial.
# Es la causa número 1 de "mi ServiceMonitor no aparece".
serviceMonitorSelectorNilUsesHelmValues: false
podMonitorSelectorNilUsesHelmValues: false
ruleSelectorNilUsesHelmValues: false
# Almacenamiento persistente con la StorageClass rápida de 05-04.
# Sin esto, Prometheus guarda en un emptyDir y pierde TODO al reiniciarse:
# exactamente el problema que veníamos a resolver.
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: rutasnorte-rapida
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 50Gi
resources:
requests:
cpu: "500m"
memory: "3Gi"
limits:
memory: "6Gi"
# Etiquetas externas: identifican a ESTE Prometheus. Imprescindibles
# si algún día federamos varios clústeres (11-05).
externalLabels:
cluster: rutas-norte
region: eu-west
grafana:
enabled: true # lo configuramos en 07-04
adminPassword: cambiar-en-produccion
alertmanager:
enabled: true # lo configuramos en 07-04
# node-exporter: un pod por nodo, como el recolector de logs de 06-02
nodeExporter:
enabled: true
kubeStateMetrics:
enabled: true
# En minikube, algunos componentes del plano de control no son accesibles
# como en un clúster real; los desactivamos para evitar objetivos en rojo.
kubeControllerManager:
enabled: false
kubeScheduler:
enabled: false
kubeEtcd:
enabled: false
kubeProxy:
enabled: falsehelm install monitorizacion prometheus-community/kube-prometheus-stack \
--namespace monitorizacion \
--values k8s/base/monitorizacion/valores-prometheus.yaml \
--wait --timeout 10mQué acabamos de instalar:
| Componente | Tipo | Función |
|---|---|---|
| Prometheus Operator | Deployment | Traduce los recursos personalizados a configuración de Prometheus |
| Prometheus | StatefulSet (creado por el operador) | El servidor: recolecta, almacena y evalúa reglas |
| Alertmanager | StatefulSet (creado por el operador) | Enruta y agrupa las alertas (07-04) |
| Grafana | Deployment | Visualización (07-04) |
| node-exporter | DaemonSet | Métricas del sistema operativo de cada nodo |
| kube-state-metrics | Deployment | Métricas del estado de los objetos de la API |
| Reglas y cuadros de mando | PrometheusRule y ConfigMaps |
Un conjunto muy completo de alertas ya escritas |
NAME READY STATUS RESTARTS AGE
alertmanager-monitorizacion-kube-pr-alertmanager-0 2/2 Running 0 3m
monitorizacion-grafana-6d84b8c7f9-w2xnk 3/3 Running 0 3m
monitorizacion-kube-pr-operator-7c9b4d5f68-hj4kp 1/1 Running 0 3m
monitorizacion-kube-state-metrics-59d7b8c644-pl3mv 1/1 Running 0 3m
monitorizacion-prometheus-node-exporter-4kx8n 1/1 Running 0 3m
monitorizacion-prometheus-node-exporter-9wq2t 1/1 Running 0 3m
monitorizacion-prometheus-node-exporter-mz7bd 1/1 Running 0 3m
prometheus-monitorizacion-kube-pr-prometheus-0 2/2 Running 0 3mAcceso a la interfaz de Prometheus:
kubectl -n monitorizacion port-forward svc/monitorizacion-kube-pr-prometheus 9090:9090
# Abrir http://localhost:9090
- El Prometheus Operator y sus recursos personalizados
Aquí se materializa lo que anunciamos en 06-07. El Prometheus Operator es el ejemplo canónico del patrón operador: un controlador que observa recursos personalizados y reconcilia el estado real hasta que coincida.
Sin operador, añadir un objetivo nuevo a Prometheus significa editar un fichero prometheus.yml, meterlo en un ConfigMap, recargar la configuración y esperar. Un proceso manual, centralizado y propenso a errores.
Con operador, el equipo que despliega api-reservas crea un objeto ServiceMonitor junto a su propia aplicación, en su propio namespace, y el operador lo detecta y regenera la configuración de Prometheus automáticamente. La monitorización pasa a ser responsabilidad descentralizada, versionada en Git junto al resto de manifiestos.
flowchart LR
DEV["Equipo de api-reservas<br/>k8s/base/api-reservas/servicemonitor.yaml"] -->|kubectl apply| API[(API Server)]
API -->|watch| OP[Prometheus Operator]
OP -->|genera y actualiza| SEC["Secret con<br/>prometheus.yaml"]
SEC -->|montado en| PROM[Pod de Prometheus]
OP -->|recarga| PROM
PROM -->|"GET /metrics"| SVC[Service api-reservas]
Los cinco recursos personalizados
| Recurso | Para qué sirve |
|---|---|
Prometheus |
Declara una instancia de Prometheus: réplicas, retención, almacenamiento, qué monitores recoge |
ServiceMonitor |
"Escanea los pods que hay detrás de este Service" — la forma habitual |
PodMonitor |
"Escanea estos pods directamente" — cuando no hay Service |
PrometheusRule |
Reglas de alerta y reglas de grabación (las desarrollamos en 07-04) |
Alertmanager |
Declara una instancia de Alertmanager (07-04) |
Y AlertmanagerConfig, para configurar el enrutado de alertas por namespace. También en 07-04.
alertmanagerconfigs.monitoring.coreos.com 2026-08-06T09:31:12Z
alertmanagers.monitoring.coreos.com 2026-08-06T09:31:12Z
podmonitors.monitoring.coreos.com 2026-08-06T09:31:13Z
prometheuses.monitoring.coreos.com 2026-08-06T09:31:13Z
prometheusrules.monitoring.coreos.com 2026-08-06T09:31:13Z
servicemonitors.monitoring.coreos.com 2026-08-06T09:31:14ZComo vimos en 06-06, todos estos son CRDs. Y como Certificate de cert-manager o VolumeSnapshot, se manejan con kubectl exactamente igual que un Deployment.
ServiceMonitor de Rutas Norte y diagnóstico de objetivos
ServiceMonitor de Rutas Norte y diagnóstico de objetivosEl Service con puerto de métricas nombrado
Requisito imprescindible: el ServiceMonitor referencia el puerto por su nombre, así que el Service debe tenerlo nombrado.
# k8s/base/api-reservas/service.yaml
apiVersion: v1
kind: Service
metadata:
name: api-reservas
namespace: rutas-norte-pro
labels:
app: api-reservas
app.kubernetes.io/part-of: rutas-norte
entorno: pro
spec:
selector:
app: api-reservas # solo app y entorno en el selector,
entorno: pro # como marca nuestra convención
ports:
- name: http # nombre obligatorio para el ServiceMonitor
port: 80
targetPort: 8080
- name: metricas # el puerto que escaneará Prometheus
port: 9090
targetPort: 9090El ServiceMonitor de api-reservas
# k8s/base/api-reservas/servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: api-reservas
namespace: rutas-norte-pro # vive junto a la aplicación, no con Prometheus
labels:
app: api-reservas
app.kubernetes.io/part-of: rutas-norte
spec:
# Qué Services selecciona. OJO: estas etiquetas se comparan con las del
# objeto Service, NO con las de los pods. Es el error más frecuente.
selector:
matchLabels:
app: api-reservas
entorno: pro
# En qué namespaces buscar esos Services.
namespaceSelector:
matchNames:
- rutas-norte-pro
endpoints:
- port: metricas # NOMBRE del puerto del Service, no el número
path: /metrics
interval: 30s # cada cuánto se escanea
scrapeTimeout: 10s # siempre menor que el interval
# Traslada etiquetas del pod a las métricas, para poder filtrar
# después por entorno o por componente en PromQL.
relabelings:
- sourceLabels: [__meta_kubernetes_pod_label_entorno]
targetLabel: entorno
- sourceLabels: [__meta_kubernetes_pod_node_name]
targetLabel: node
# Descarta métricas que no nos interesan y ocupan espacio.
# nodejs_gc_duration_seconds tiene mucha cardinalidad y poco valor.
metricRelabelings:
- sourceLabels: [__name__]
regex: 'nodejs_gc_duration_seconds.*'
action: dropEl ServiceMonitor del exportador de postgres-reservas
Como postgres-reservas es un StatefulSet con Service headless (06-01), necesitamos un Service adicional específico para métricas. Un Service headless (clusterIP: None) funciona igual de bien como origen de un ServiceMonitor, porque el operador usa los Endpoints, no la ClusterIP.
# k8s/base/postgres-reservas/service-metricas.yaml
apiVersion: v1
kind: Service
metadata:
name: postgres-reservas-metricas
namespace: rutas-norte-pro
labels:
app: postgres-reservas
entorno: pro
tipo: metricas
spec:
clusterIP: None # headless: no necesitamos balanceo
selector:
app: postgres-reservas
entorno: pro
ports:
- name: metricas
port: 9187
targetPort: 9187
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: postgres-reservas
namespace: rutas-norte-pro
labels:
app: postgres-reservas
app.kubernetes.io/part-of: rutas-norte
spec:
selector:
matchLabels:
app: postgres-reservas
entorno: pro
tipo: metricas # discrimina del Service de datos (5432)
namespaceSelector:
matchNames:
- rutas-norte-pro
endpoints:
- port: metricas
interval: 30s
scrapeTimeout: 10sCon esto, el sidecar que añadimos en 06-04 empieza a alimentar Prometheus. La promesa se cumple.
La NetworkPolicy que hace falta
Nuestra convención es clara: en rutas-norte-pro hay una deny-all y cada conversación se autoriza una a una. Prometheus vive en monitorizacion y quiere hablar con los puertos de métricas de rutas-norte-pro. Sin política, todos los objetivos aparecerán caídos.
# k8s/entornos/pro/netpol-permitir-prometheus.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: permitir-scrape-prometheus
namespace: rutas-norte-pro
spec:
podSelector:
matchLabels:
app.kubernetes.io/part-of: rutas-norte
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitorizacion
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 9090 # métricas de api-reservas
- protocol: TCP
port: 9187 # exportador de postgres-reservasVerificar que el objetivo aparece
En la interfaz de Prometheus, Status → Targets. O por línea de comandos:
kubectl -n monitorizacion port-forward svc/monitorizacion-kube-pr-prometheus 9090:9090 &
curl -s localhost:9090/api/v1/targets | jq -r '
.data.activeTargets[] |
select(.labels.job | test("api-reservas|postgres")) |
"\(.labels.job)\t\(.scrapeUrl)\t\(.health)\t\(.lastError)"'api-reservas http://10.244.2.17:9090/metrics up
api-reservas http://10.244.1.23:9090/metrics up
postgres-reservas http://10.244.2.31:9187/metrics upDiagnóstico: mi objetivo no aparece
Es la pregunta de esta lección. Checklist en orden de frecuencia:
| # | Causa | Cómo confirmarlo |
|---|---|---|
| 1 | El selector del ServiceMonitor no casa con las etiquetas del Service |
kubectl get svc api-reservas --show-labels |
| 2 | El namespaceSelector no incluye el namespace del Service |
Revisar matchNames |
| 3 | El operador ignora el ServiceMonitor por falta de la etiqueta release |
Ver serviceMonitorSelector del recurso Prometheus |
| 4 | El nombre del puerto del endpoint no existe en el Service |
kubectl get svc api-reservas -o jsonpath='{.spec.ports}' |
| 5 | El Service no tiene Endpoints (readiness fallando, 07-01) | kubectl get endpointslices -l kubernetes.io/service-name=api-reservas |
| 6 | Una NetworkPolicy bloquea el tráfico desde monitorizacion |
El objetivo aparece pero con health: down y error de conexión |
| 7 | La aplicación no expone /metrics en ese puerto |
kubectl exec -it <pod> -- curl -s localhost:9090/metrics |
El punto 3 merece detalle porque es traicionero. El recurso Prometheus tiene un serviceMonitorSelector. Si el chart lo dejó con release: monitorizacion, solo recogerá ServiceMonitor con esa etiqueta, y el tuyo será ignorado en silencio: no hay evento, no hay error, simplemente no aparece.
# Ver qué está seleccionando el Prometheus desplegado
kubectl -n monitorizacion get prometheus -o yaml | grep -A 8 serviceMonitorSelectorDos soluciones: poner serviceMonitorSelectorNilUsesHelmValues: false en los valores (lo que hicimos), o añadir la etiqueta release: monitorizacion a todos tus ServiceMonitor.
Comprobar directamente si el objetivo está siendo escaneado:
# ¿Existe la métrica sintética up para nuestro job?
curl -s 'localhost:9090/api/v1/query?query=up{job="api-reservas"}' | jq '.data.result'Un array vacío significa que el objetivo no está configurado (problemas 1-4). Un resultado con value: ["...", "0"] significa que está configurado pero no responde (problemas 5-7).
- PromQL desde cero y con sentido
PromQL asusta al principio y es más sencillo de lo que parece si se aprende por capas.
Capa 1: selectores de series
La consulta más simple es el nombre de una métrica:
Devuelve todas las series con ese nombre: una por cada combinación de etiquetas de cada pod. Para filtrar, se usan las llaves:
Los cuatro operadores de comparación de etiquetas:
| Operador | Significado | Ejemplo |
|---|---|---|
= |
Igual exacto | {entorno="pro"} |
!= |
Distinto | {container!=""} |
=~ |
Coincide con la expresión regular | {codigo=~"5.."} |
!~ |
No coincide con la expresión regular | {ruta!~"/salud|/preparado"} |
Combinables, con AND implícito:
Lectura: peticiones a /api/reservas en producción que devolvieron un código 5xx.
Capa 2: selectores de rango
Añadiendo [5m] obtienes, en lugar de un valor por serie, todos los valores de los últimos 5 minutos:
Esto se llama vector de rango y no se puede dibujar directamente: es materia prima para las funciones del siguiente nivel.
Capa 3: las funciones que se usan de verdad
rate() — la función más importante de PromQL.
Calcula el incremento por segundo de un contador dentro de una ventana. Es lo que convierte un contador inútil (184203) en un dato con sentido (12,4 peticiones por segundo).
Tres cosas que hay que saber sobre rate():
- Solo funciona con contadores. Aplicarla a un gauge da resultados sin sentido.
- Compensa automáticamente los reinicios del contador cuando un pod se recrea.
- La ventana debe contener al menos 4 muestras. Con
interval: 30s, la ventana mínima razonable es[2m]; lo habitual es[5m]. Con[1m]y escaneos de 30 s tendrías dos muestras y resultados erráticos.
increase() — igual que rate() pero da el incremento total de la ventana en vez de por segundo. Es literalmente rate() * segundos_de_la_ventana. Más legible para preguntas humanas:
increase(rutasnorte_reservas_confirmadas_total[1h])
# → "cuántas reservas se han confirmado en la última hora"sum by y sum without — agregación.
rate() devuelve una serie por pod y por combinación de etiquetas. Casi nunca quieres eso: quieres el total del servicio.
# Peticiones por segundo del servicio entero, agregando todos los pods,
# pero manteniendo el desglose por código de respuesta
sum by (codigo) (rate(rutasnorte_peticiones_total{entorno="pro"}[5m]))sum by (a, b) conserva solo las etiquetas a y b. sum without (pod, instance) conserva todas menos esas. La segunda forma suele ser más robusta ante cambios.
Otras agregaciones: avg, min, max, count, stddev, topk, bottomk.
topk() — los N mayores. Perfecto para "¿quién se está comiendo el clúster?":
histogram_quantile() — percentiles a partir de histogramas.
histogram_quantile(
0.95,
sum by (le, ruta) (rate(rutasnorte_duracion_peticion_segundos_bucket{entorno="pro"}[5m]))
)Descomponiendo, de dentro afuera:
rate(..._bucket[5m])→ tasa de incremento de cada cubeta.sum by (le, ruta)→ suma las cubetas de todos los pods, conservandole(el límite de la cubeta) yruta. La etiquetalees obligatoria: sin ella,histogram_quantileno puede funcionar. Es el error más común con esta función.histogram_quantile(0.95, ...)→ estima el percentil 95.
El resultado es en segundos: 0.412 significa que el 95 % de las peticiones se resuelven en menos de 412 ms.
Precisión: el resultado es una estimación por interpolación lineal dentro de la cubeta. Si tus cubetas son [0.1, 0.5, 1] y el p95 real está en 0,42 s, la estimación puede desviarse bastante. Por eso elegimos cubetas ajustadas a nuestra latencia real en el apartado 5.
Operadores aritméticos y comparación entre métricas.
# Uso de memoria como fracción del límite configurado
container_memory_working_set_bytes{namespace="rutas-norte-pro", container!=""}
/
kube_pod_container_resource_limits{namespace="rutas-norte-pro", resource="memory"}Prometheus empareja series de ambos lados por sus etiquetas comunes. Si las etiquetas no coinciden, el resultado es vacío: es la causa de la mitad de las consultas que "no devuelven nada". on() e ignoring() permiten controlar el emparejamiento.
Las consultas concretas de Rutas Norte
Peticiones por segundo por ruta:
Tasa de error (proporción de 5xx sobre el total):
sum(rate(rutasnorte_peticiones_total{entorno="pro", codigo=~"5.."}[5m]))
/
sum(rate(rutasnorte_peticiones_total{entorno="pro"}[5m]))Devuelve un número entre 0 y 1. Multiplicado por 100 es el porcentaje. Advertencia: si el denominador es 0 (sin tráfico), el resultado es NaN y desaparece del gráfico. Es un comportamiento correcto: sin tráfico no hay tasa de error que medir.
Percentil 95 de latencia por ruta:
histogram_quantile(
0.95,
sum by (le, ruta) (rate(rutasnorte_duracion_peticion_segundos_bucket{entorno="pro"}[5m]))
)Reservas confirmadas por minuto (la métrica del negocio):
Saturación de CPU respecto al límite configurado:
sum by (pod) (rate(container_cpu_usage_seconds_total{namespace="rutas-norte-pro", container!=""}[5m]))
/
sum by (pod) (kube_pod_container_resource_limits{namespace="rutas-norte-pro", resource="cpu"})Un valor de 0.95 significa que el pod está usando el 95 % de su límite: candidato inmediato a throttling.
Confirmar el throttling que en 07-02 no podíamos ver:
sum by (pod) (rate(container_cpu_cfs_throttled_periods_total{namespace="rutas-norte-pro"}[5m]))
/
sum by (pod) (rate(container_cpu_cfs_periods_total{namespace="rutas-norte-pro"}[5m]))Es la fracción de periodos de planificación en los que el contenedor fue estrangulado. Por encima de 0.25 (25 %) hay un problema real de rendimiento. Esta es exactamente la consulta que habría diagnosticado el problema de api-reservas en 07-02 en diez segundos.
Comparar consumo real con las requests: la recalibración de 07-02, ahora automatizada:
# Cuánto se usa respecto a lo que se reserva. Valores muy por debajo de 1
# indican sobredimensionado; por encima de 1, infradimensionado.
sum by (pod, container) (
container_memory_working_set_bytes{namespace="rutas-norte-pro", container!=""}
)
/
sum by (pod, container) (
kube_pod_container_resource_requests{namespace="rutas-norte-pro", resource="memory"}
)Con Prometheus, esta consulta sobre 30 días de histórico sustituye por completo al script de bash de 07-02, y además permite pedir el percentil real usando quantile_over_time.
Réplicas no disponibles (usa kube-state-metrics):
Objetivo caído:
Pods reiniciándose en bucle:
Conexiones de PostgreSQL respecto al máximo (del sidecar de 06-04):
Fallos de la pasarela de pagos externa:
sum(rate(rutasnorte_pasarela_pagos_llamadas_total{resultado=~"error|timeout"}[5m]))
/
sum(rate(rutasnorte_pasarela_pagos_llamadas_total[5m]))Esta consulta permite distinguir "nuestra plataforma falla" de "el proveedor externo falla", que es una distinción muy valiosa a las tres de la madrugada.
- Los cuatro indicadores dorados aplicados a Rutas Norte
Los cuatro indicadores dorados (golden signals), popularizados por el libro de SRE de Google, son el marco que evita el error de monitorizar todo y no entender nada.
| Indicador | Qué mide | Pregunta que responde |
|---|---|---|
| Latencia | Cuánto tarda una petición | ¿Va lento? |
| Tráfico | Cuánta demanda hay | ¿Cuánto trabajo estamos haciendo? |
| Errores | Qué fracción falla | ¿Está fallando? |
| Saturación | Cuán lleno está el recurso más escaso | ¿Cuánto margen queda? |
Un matiz esencial sobre la latencia: hay que medir por separado la de las peticiones que fallan. Un error 500 devuelto en 3 ms baja artificialmente la latencia media y puede hacer que un servicio que falla masivamente parezca rapidísimo.
Aplicación componente a componente
| Componente | Latencia | Tráfico | Errores | Saturación |
|---|---|---|---|---|
tienda-web |
p95 de nginx | peticiones/s | ratio 5xx | CPU vs límite |
api-reservas |
p95 y p99 por ruta | peticiones/s | ratio 5xx + fallos de pasarela | CPU vs límite, conexiones del pool |
postgres-reservas |
duración de transacción | transacciones/s | interbloqueos + errores | conexiones/max, tamaño en disco |
redis-cache |
latencia de comando | operaciones/s | ratio de fallos de caché | memoria vs maxmemory |
worker-notificaciones |
tiempo de proceso por correo | correos/min | correos fallidos | profundidad de la cola |
informes-ocupacion |
duración de la ejecución | ejecuciones/día | jobs fallidos | (no aplica) |
Las consultas concretas para api-reservas, que serán la base del cuadro de mando de 07-04:
# LATENCIA (excluyendo errores, para no falsear el dato)
histogram_quantile(0.95,
sum by (le) (rate(rutasnorte_duracion_peticion_segundos_bucket{entorno="pro"}[5m])))
# TRÁFICO
sum(rate(rutasnorte_peticiones_total{entorno="pro"}[5m]))
# ERRORES
sum(rate(rutasnorte_peticiones_total{entorno="pro", codigo=~"5.."}[5m]))
/ sum(rate(rutasnorte_peticiones_total{entorno="pro"}[5m]))
# SATURACIÓN: dos ejes, CPU y pool de conexiones
sum by (pod) (rate(container_cpu_cfs_throttled_periods_total{namespace="rutas-norte-pro"}[5m]))
/ sum by (pod) (rate(container_cpu_cfs_periods_total{namespace="rutas-norte-pro"}[5m]))
max(rutasnorte_conexiones_pool_activas{entorno="pro"}) / 20Esa última consulta cierra un círculo del módulo: es exactamente el fenómeno que en 07-01 dejaba a api-reservas viva pero inservible. Entonces solo teníamos una readiness que apartaba el pod; ahora tenemos además el dato registrado y consultable a posteriori.
Reglas de grabación
Consultas como el p95 son caras: recorren muchas series. Si un cuadro de mando la ejecuta cada 15 segundos con 8 paneles abiertos, Prometheus sufre.
Las reglas de grabación (recording rules) precalculan una consulta periódicamente y guardan el resultado como una métrica nueva:
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: rutas-norte-grabacion
namespace: monitorizacion
labels:
app.kubernetes.io/part-of: rutas-norte
spec:
groups:
- name: rutasnorte.indicadores-dorados
interval: 30s
rules:
# Convención de nombres: nivel:metrica:operacion
- record: apireservas:latencia_p95:5m
expr: |
histogram_quantile(0.95,
sum by (le, ruta, entorno) (
rate(rutasnorte_duracion_peticion_segundos_bucket[5m])))
- record: apireservas:peticiones:rate5m
expr: sum by (entorno, ruta) (rate(rutasnorte_peticiones_total[5m]))
- record: apireservas:ratio_errores:5m
expr: |
sum by (entorno) (rate(rutasnorte_peticiones_total{codigo=~"5.."}[5m]))
/
sum by (entorno) (rate(rutasnorte_peticiones_total[5m]))Ahora los cuadros de mando y las alertas consultan apireservas:latencia_p95:5m, que es una única serie ya calculada. En 07-04 las usaremos intensivamente.
- Almacenamiento, retención y los límites de Prometheus
Cómo guarda los datos
Prometheus escribe en su propia base de datos de series temporales (TSDB):
- Las muestras nuevas van a un bloque en memoria y a un registro de escritura anticipada (WAL) en disco, que protege ante caídas.
- Cada 2 horas, ese bloque se persiste como un directorio inmutable en disco.
- Un proceso de compactación va fusionando bloques pequeños en bloques mayores.
- Los bloques que superan la retención se borran enteros.
Consecuencia importante: la retención no borra muestras sueltas, borra bloques completos. Con retention: 30d puedes tener puntualmente algo más de 30 días.
Estimar el espacio
La regla práctica aceptada: entre 1 y 2 bytes por muestra tras la compresión.
Bytes ≈ retención_segundos × series_activas × bytes_por_muestra / intervalo_scrape
Rutas Norte, estimación:
30 días = 2 592 000 s
~120 000 series activas (los tres entornos + el sistema)
1,7 bytes por muestra
intervalo 30 s
2 592 000 × 120 000 × 1,7 / 30 ≈ 17,6 GBCon 50 GiB de PVC vamos sobrados, incluso con margen para la compactación (que necesita espacio temporal). Por eso pusimos también retentionSize: 45GiB: es el freno de emergencia si la cardinalidad crece más de lo previsto.
Ver la cardinalidad real:
# Número total de series activas
prometheus_tsdb_head_series
# Las 10 métricas con más series: la lista de sospechosos de cardinalidad
topk(10, count by (__name__)({__name__=~".+"}))Esta segunda consulta es la primera que hay que ejecutar cuando Prometheus empieza a consumir demasiada memoria.
Por qué Prometheus no es una base de datos eterna
Tres limitaciones de diseño, deliberadas:
- Almacenamiento local, no distribuido. Los datos viven en el disco de un pod. Sin replicación entre instancias.
- No escala horizontalmente por sí mismo. Dos Prometheus no comparten datos: cada uno tiene los suyos.
- La retención larga es cara. Guardar dos años en local significa cientos de GB en un disco rápido y un consumo de memoria considerable al consultar.
Para retención larga y visión multiclúster existen dos proyectos que extienden Prometheus:
| Proyecto | Enfoque |
|---|---|
| Thanos | Un sidecar sube los bloques a almacenamiento de objetos (S3); un componente query consulta de forma transparente lo local y lo remoto |
| Mimir (Grafana) | Recibe los datos por remote_write en un sistema distribuido y multiinquilino |
Ambos permiten años de retención barata y consultar varios clústeres a la vez. Para Rutas Norte con un solo clúster, 30 días son suficientes hoy; cuando el módulo 11-05 aborde la gestión multiclúster, Thanos será la evolución natural.
Configurar remote_write desde el operador es sencillo:
prometheus:
prometheusSpec:
remoteWrite:
- url: https://mimir.rutasnorte.example/api/v1/push
writeRelabelConfigs:
# Enviar solo lo que de verdad queremos conservar años:
# las métricas de negocio, no las de infraestructura.
- sourceLabels: [__name__]
regex: 'rutasnorte_.*'
action: keepErrores Comunes y Consejos
1. Explosión de cardinalidad. El error más grave y el más caro. Etiquetar con identificadores de reserva, DNI, IP de cliente o URLs sin normalizar multiplica las series por miles y tumba Prometheus por falta de memoria. Antes de añadir una etiqueta, pregúntate cuántos valores distintos puede tener. Si la respuesta es "no lo sé", no la añadas.
2. Consultar un contador sin rate(). rutasnorte_peticiones_total a secas devuelve un valor acumulado desde que arrancó el pod. Es un gráfico que solo sube y no significa nada. Siempre rate() o increase().
3. Ventana de rate() demasiado corta. Con interval: 30s, un rate(...[1m]) tiene dos muestras y produce ruido o huecos. Regla: la ventana debe ser al menos 4 veces el intervalo de escaneo.
4. Olvidar le en sum by antes de histogram_quantile. Si agregas sin conservar la etiqueta le, la función no puede reconstruir el histograma y devuelve NaN o nada. Es el fallo número uno con percentiles.
5. El ServiceMonitor selecciona etiquetas de pod en vez de Service. El selector de un ServiceMonitor se compara con las etiquetas del objeto Service. Como en Rutas Norte los Service llevan las mismas etiquetas que los pods, es fácil no darse cuenta hasta que un Service tiene etiquetas distintas.
6. La etiqueta release del operador. Si el recurso Prometheus tiene serviceMonitorSelector: {matchLabels: {release: monitorizacion}}, tus ServiceMonitor sin esa etiqueta se ignoran sin ningún mensaje de error. Comprobarlo es lo primero cuando un objetivo no aparece.
7. Prometheus sin almacenamiento persistente. Sin storageSpec, el chart usa un emptyDir y cada reinicio del pod borra todo el histórico. Es exactamente el problema que veníamos a resolver.
8. Sin retentionSize. Solo con retention: 90d, si la cardinalidad crece, el disco se llena, Prometheus entra en CrashLoopBackOff y te quedas sin monitorización justo cuando más falta hace. Pon siempre un límite de tamaño, y que sea un 80-85 % del PVC.
9. Alertar sobre métricas de un summary. No son agregables entre pods. Si tu alerta usa el p99 de un summary con seis réplicas, el número no significa nada útil.
10. Monitorizar el nodo con métricas de contenedor. cAdvisor no ve el sistema de ficheros del nodo ni su carga media. Para eso está node-exporter. Confundirlos lleva a alertas que nunca disparan.
11. Olvidar las NetworkPolicies. En rutas-norte-pro hay una deny-all. Si Prometheus no puede alcanzar los puertos de métricas, todos los objetivos aparecen caídos y el error (context deadline exceeded) no es nada evidente.
12. Instrumentar demasiado tarde. La instrumentación de negocio se añade en el código, así que requiere un ciclo de desarrollo. Cuanto antes se incorpore como parte del "listo para producción", mejor. Improvisarla durante un incidente es imposible.
Ejercicios
Ejercicio 1 — Diagnosticar un ServiceMonitor que no funciona
El equipo ha desplegado redis-cache con un exportador y ha creado estos manifiestos. En la página de targets de Prometheus no aparece nada.
apiVersion: v1
kind: Service
metadata:
name: redis-cache-metricas
namespace: rutas-norte-pro
labels:
app.kubernetes.io/name: redis-cache
app.kubernetes.io/part-of: rutas-norte
spec:
selector:
app: redis-cache
entorno: pro
ports:
- port: 9121
targetPort: 9121
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: redis-cache
namespace: monitorizacion
spec:
selector:
matchLabels:
app: redis-cache
entorno: pro
endpoints:
- port: metrics
interval: 30s- Identifica tres errores distintos.
- Escribe los manifiestos corregidos.
- Escribe el comando PromQL o
curlque confirma que el problema está resuelto.
Ejercicio 2 — Escribir las consultas de un incidente
El sábado del puente, entre las 12:00 y las 13:30, los clientes se quejaron de que la web iba lentísima y algunas compras fallaban. Ya pasó, nadie tomó notas, y el director pide explicaciones el lunes.
Escribe las consultas PromQL que responden a cada pregunta. Indica de qué fuente (cAdvisor, kube-state-metrics, node-exporter, exportador o instrumentación propia) sale cada métrica.
- ¿Cuántas peticiones por segundo atendía
api-reservasen ese intervalo, comparado con el sábado anterior? - ¿Cuál fue el percentil 99 de latencia de
/api/reservasdurante el incidente? - ¿Estuvo
api-reservassufriendo throttling de CPU? - ¿Se quedó
postgres-reservassin conexiones disponibles? - ¿Falló la pasarela de pagos externa, o el fallo era nuestro?
- ¿Cuántas reservas se dejaron de confirmar respecto a lo esperado?
Ejercicio 3 — Instrumentar worker-notificaciones
worker-notificaciones consume de una cola de Redis y envía correos de confirmación. Actualmente no expone ninguna métrica. Diseña su instrumentación:
- Enumera las métricas que debe exponer, con su nombre completo, su tipo (
counter,gauge,histogram) y sus etiquetas, justificando la cardinalidad de cada etiqueta. - Escribe el
ServiceMonitorcorrespondiente (el worker no tiene Service porque no recibe tráfico: resuelve ese problema). - Escribe las consultas PromQL de los cuatro indicadores dorados para este componente.
Soluciones
Solución 1
1. Los tres errores.
Error A — El selector del ServiceMonitor no casa con las etiquetas del Service. El ServiceMonitor busca Services con app: redis-cache y entorno: pro, pero el Service tiene app.kubernetes.io/name: redis-cache y app.kubernetes.io/part-of: rutas-norte. No coincide ninguna. El spec.selector del Service (que sí usa app/entorno) selecciona pods, no es lo que mira el ServiceMonitor.
Error B — El puerto del Service no tiene nombre, y el endpoint referencia metrics. El ServiceMonitor busca un puerto llamado metrics en el Service; el Service define el puerto 9121 sin campo name. El operador no encuentra el puerto y descarta el endpoint.
Error C — El ServiceMonitor está en monitorizacion sin namespaceSelector. Por defecto, un ServiceMonitor busca Services en su propio namespace. Estando en monitorizacion y el Service en rutas-norte-pro, no se encontrarán nunca. Dos soluciones válidas: mover el ServiceMonitor al namespace de la aplicación (preferible, mantiene la monitorización junto al código) o añadir namespaceSelector.
2. Manifiestos corregidos.
apiVersion: v1
kind: Service
metadata:
name: redis-cache-metricas
namespace: rutas-norte-pro
labels:
app: redis-cache # etiqueta que buscará el ServiceMonitor
entorno: pro
app.kubernetes.io/part-of: rutas-norte
tipo: metricas # distingue del Service de datos (6379)
spec:
clusterIP: None
selector: # esto selecciona PODS: solo app y entorno
app: redis-cache
entorno: pro
ports:
- name: metricas # NOMBRE obligatorio
port: 9121
targetPort: 9121
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: redis-cache
namespace: rutas-norte-pro # junto a la aplicación
labels:
app: redis-cache
app.kubernetes.io/part-of: rutas-norte
spec:
selector:
matchLabels: # se comparan con las etiquetas del SERVICE
app: redis-cache
entorno: pro
tipo: metricas
namespaceSelector:
matchNames:
- rutas-norte-pro
endpoints:
- port: metricas # coincide con el name del puerto
path: /metrics
interval: 30s
scrapeTimeout: 10sY la NetworkPolicy, porque en rutas-norte-pro hay deny-all:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: permitir-scrape-redis-cache
namespace: rutas-norte-pro
spec:
podSelector:
matchLabels:
app: redis-cache
entorno: pro
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitorizacion
ports:
- protocol: TCP
port: 91213. Verificación.
kubectl -n monitorizacion port-forward svc/monitorizacion-kube-pr-prometheus 9090:9090 &
# ¿Existe el objetivo y responde?
curl -s 'localhost:9090/api/v1/query?query=up{job="redis-cache-metricas"}' | jq '.data.result'[
{
"metric": {"__name__": "up", "job": "redis-cache-metricas",
"namespace": "rutas-norte-pro", "pod": "redis-cache-0"},
"value": [1754472000, "1"]
}
]"1" es la confirmación. Si diera "0", el objetivo está configurado pero no responde: revisa la NetworkPolicy y que el exportador escuche en el 9121.
Comprobación adicional de que el operador lo ha recogido:
kubectl -n monitorizacion get prometheus -o jsonpath='{.items[0].spec.serviceMonitorNamespaceSelector}'Solución 2
Nota general: todas las consultas usan el modificador offset o un rango temporal explícito, ya que el incidente pasó. Aquí está la ventaja decisiva sobre kubectl top de 07-02: estos datos existen.
1. Tráfico durante el incidente, comparado con el sábado anterior.
Fuente: instrumentación propia de api-reservas.
# Tráfico actual (visualizado en el rango del incidente en Grafana)
sum(rate(rutasnorte_peticiones_total{entorno="pro"}[5m]))
# Comparación con hace exactamente una semana, superpuesta
sum(rate(rutasnorte_peticiones_total{entorno="pro"}[5m] offset 7d))
# Factor de multiplicación
sum(rate(rutasnorte_peticiones_total{entorno="pro"}[5m]))
/
sum(rate(rutasnorte_peticiones_total{entorno="pro"}[5m] offset 7d))Un resultado de 5.8 significa que el sábado del puente hubo casi seis veces más tráfico que el sábado normal: coherente con lo que sabemos de los picos de puentes.
2. Percentil 99 de latencia de /api/reservas.
Fuente: instrumentación propia (histograma).
histogram_quantile(0.99,
sum by (le) (
rate(rutasnorte_duracion_peticion_segundos_bucket{
entorno="pro", ruta="/api/reservas"}[5m])))Se compara con el p95 y con la media (_sum / _count) para ver si el problema afectaba a todas las peticiones o solo a la cola:
rate(rutasnorte_duracion_peticion_segundos_sum{entorno="pro", ruta="/api/reservas"}[5m])
/
rate(rutasnorte_duracion_peticion_segundos_count{entorno="pro", ruta="/api/reservas"}[5m])Si la media está bien y el p99 disparado, el problema afecta a un subconjunto (por ejemplo, las peticiones que tocan la pasarela de pagos). Si ambos suben, es sistémico.
3. Throttling de CPU.
Fuente: cAdvisor / kubelet.
sum by (pod) (
rate(container_cpu_cfs_throttled_periods_total{
namespace="rutas-norte-pro", pod=~"api-reservas-.*"}[5m]))
/
sum by (pod) (
rate(container_cpu_cfs_periods_total{
namespace="rutas-norte-pro", pod=~"api-reservas-.*"}[5m]))Un valor sostenido por encima de 0.30 durante el incidente confirma que el límite de CPU estaba frenando la aplicación. Complementaria, para ver si estaba pegada al techo:
sum by (pod) (rate(container_cpu_usage_seconds_total{
namespace="rutas-norte-pro", pod=~"api-reservas-.*", container="api"}[5m]))
/
sum by (pod) (kube_pod_container_resource_limits{
namespace="rutas-norte-pro", pod=~"api-reservas-.*", resource="cpu"})(La segunda parte viene de kube-state-metrics: es el ejemplo perfecto de por qué hacen falta las dos fuentes.)
4. Conexiones de PostgreSQL.
Fuente: exportador sidecar de 06-04, más las métricas propias del pool.
# Conexiones en uso frente al máximo configurado
pg_stat_database_numbackends{datname="reservas"} / pg_settings_max_connections
# El pool de la aplicación, que es el que se agotó en el caso de 07-01
max(rutasnorte_conexiones_pool_activas{entorno="pro"})
# Interbloqueos: síntoma de contención grave
increase(pg_stat_database_deadlocks{datname="reservas"}[5m])Un numbackends / max_connections cercano a 1 confirma que la base de datos estaba saturada de conexiones.
5. ¿Falló la pasarela externa o fallamos nosotros?
Fuente: instrumentación propia.
# Tasa de fallo de la pasarela
sum(rate(rutasnorte_pasarela_pagos_llamadas_total{resultado=~"error|timeout"}[5m]))
/
sum(rate(rutasnorte_pasarela_pagos_llamadas_total[5m]))
# Desglose: ¿error o timeout? El timeout apunta a lentitud del proveedor
sum by (resultado) (rate(rutasnorte_pasarela_pagos_llamadas_total[5m]))Interpretación: si la tasa de fallo de la pasarela sube al 40 % mientras nuestro ratio_errores general también sube, el fallo es del proveedor y hay que reclamarle. Si la pasarela está al 0,1 % de fallos y nosotros devolvemos 5xx, el problema es nuestro. Esta distinción, imposible sin instrumentación, es lo que se lleva a la reunión del lunes.
6. Reservas dejadas de confirmar.
Fuente: instrumentación propia (métrica de negocio).
# Reservas confirmadas durante la hora y media del incidente
increase(rutasnorte_reservas_confirmadas_total{entorno="pro"}[90m])
# Lo mismo el sábado anterior a la misma hora
increase(rutasnorte_reservas_confirmadas_total{entorno="pro"}[90m] offset 7d)
# Pérdida estimada: reservas por minuto ahora vs. una semana antes
sum(rate(rutasnorte_reservas_confirmadas_total{entorno="pro"}[5m])) * 60Y un cruce especialmente revelador: si el tráfico se multiplicó por 5,8 pero las reservas confirmadas solo por 1,3, la diferencia entre ambos factores es una estimación directa del negocio perdido. Ese número, en euros, es el que justifica el presupuesto de infraestructura.
Solución 3
1. Métricas del componente.
| Nombre | Tipo | Etiquetas | Cardinalidad | Justificación |
|---|---|---|---|---|
rutasnorte_correos_enviados_total |
counter |
tipo, resultado |
3 × 3 = 9 | Volumen y fallos. tipo: confirmacion, cancelacion, recordatorio. resultado: ok, error, rechazado |
rutasnorte_correo_duracion_segundos |
histogram |
tipo |
3 × 9 cubetas = 27 | Latencia del envío, incluida la llamada al servidor SMTP |
rutasnorte_cola_pendientes |
gauge |
ninguna | 1 | La métrica de saturación clave: si la cola crece sin parar, el worker no da abasto |
rutasnorte_cola_espera_segundos |
gauge |
ninguna | 1 | Antigüedad del mensaje más viejo de la cola: mide el retraso percibido por el cliente |
rutasnorte_reintentos_total |
counter |
motivo |
4 | Reintentos por tipo de fallo: smtp_timeout, smtp_rechazo, red, desconocido |
rutasnorte_worker_ciclo_activo |
gauge |
ninguna | 1 | 1 si el bucle de consumo está vivo. Alimenta también la liveness de 07-01 |
Cardinalidad total: unas 43 series por pod. Perfectamente asumible.
Etiquetas descartadas deliberadamente y por qué:
destinatario(correo del cliente): cardinalidad ilimitada y, sobre todo, dato personal en el sistema de monitorización. Prohibido. Volveremos sobre esto en 07-05.id_reserva: cardinalidad ilimitada. Ese dato pertenece a los logs, no a las métricas. Las métricas agregan; los logs detallan.mensaje_errorcompleto: cardinalidad impredecible. Se sustituye pormotivo, un conjunto cerrado de categorías.
2. ServiceMonitor para un componente sin Service.
worker-notificaciones no recibe tráfico, así que no tiene Service. Dos opciones válidas:
Opción A (recomendada): PodMonitor. Es exactamente el recurso diseñado para este caso.
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
name: worker-notificaciones
namespace: rutas-norte-pro
labels:
app: worker-notificaciones
app.kubernetes.io/part-of: rutas-norte
spec:
selector:
matchLabels: # aquí SÍ se comparan con etiquetas de POD
app: worker-notificaciones
entorno: pro
namespaceSelector:
matchNames:
- rutas-norte-pro
podMetricsEndpoints:
- port: metricas # nombre del puerto en el spec del CONTENEDOR
path: /metrics
interval: 30s
scrapeTimeout: 10sCon el puerto declarado en el Deployment:
containers:
- name: worker
image: registry.rutasnorte.example/worker-notificaciones:3.4.2
ports:
- name: metricas
containerPort: 9100Opción B: crear un Service headless solo para métricas y usar un ServiceMonitor normal. Funciona, pero crea un objeto que no aporta nada más. El PodMonitor es más limpio.
Y la NetworkPolicy correspondiente:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: permitir-scrape-worker-notificaciones
namespace: rutas-norte-pro
spec:
podSelector:
matchLabels:
app: worker-notificaciones
entorno: pro
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitorizacion
ports:
- protocol: TCP
port: 91003. Los cuatro indicadores dorados.
# ---- LATENCIA: p95 del tiempo de envío de un correo ----
histogram_quantile(0.95,
sum by (le, tipo) (
rate(rutasnorte_correo_duracion_segundos_bucket{entorno="pro"}[5m])))
# ---- TRÁFICO: correos enviados por minuto ----
sum by (tipo) (rate(rutasnorte_correos_enviados_total{entorno="pro"}[5m])) * 60
# ---- ERRORES: fracción de envíos fallidos ----
sum(rate(rutasnorte_correos_enviados_total{entorno="pro", resultado!="ok"}[5m]))
/
sum(rate(rutasnorte_correos_enviados_total{entorno="pro"}[5m]))
# ---- SATURACIÓN: la cola, en tres ángulos complementarios ----
# a) Profundidad actual de la cola
max(rutasnorte_cola_pendientes{entorno="pro"})
# b) ¿Está creciendo? Derivada de la profundidad en la última media hora.
# Positivo sostenido = el worker no da abasto y hay que escalar.
deriv(rutasnorte_cola_pendientes{entorno="pro"}[30m])
# c) El indicador que más importa al cliente: cuánto espera el correo más
# antiguo. Un cliente que pagó hace 10 minutos y no ha recibido nada
# empieza a dudar de si su compra se ha hecho.
max(rutasnorte_cola_espera_segundos{entorno="pro"})Consulta adicional de cobertura, muy valiosa para el negocio: relaciona dos componentes distintos para detectar correos perdidos.
# Reservas confirmadas menos correos de confirmación enviados en la última
# hora. Debería ser prácticamente cero. Un valor positivo grande significa
# que hay clientes que han pagado y no han recibido nada.
increase(rutasnorte_reservas_confirmadas_total{entorno="pro"}[1h])
-
increase(rutasnorte_correos_enviados_total{entorno="pro", tipo="confirmacion", resultado="ok"}[1h])Este tipo de consulta —que cruza métricas de dos componentes para verificar una invariante del negocio— es de las más útiles que se pueden escribir, y en 07-04 la convertiremos en alerta.
Conclusión
Rutas Norte ha pasado de no recordar nada a tener memoria, lenguaje y capacidad de pregunta. En esta lección hemos:
- Entendido el modelo de extracción y por qué es superior al de empuje en un entorno dinámico como Kubernetes.
- Aprendido el modelo de datos: series identificadas por nombre y etiquetas, la amenaza permanente de la cardinalidad, y los cuatro tipos de métrica, con el
histogramcomo el único que permite calcular percentiles agregados entre réplicas. - Aclarado la confusión clásica sobre de dónde salen las métricas: cAdvisor mide el consumo real de los contenedores, node-exporter el sistema operativo del nodo, kube-state-metrics el estado de los objetos de la API, los exportadores traducen software de terceros —incluido el sidecar de
postgres-reservasque añadimos en 06-04, ahora por fin conectado— y la instrumentación propia es lo único que sabe de negocio. - Instrumentado
api-reservascon peticiones por ruta y código, latencia en histograma, reservas confirmadas y el estado del pool de conexiones, cuidando la cardinalidad en cada decisión. - Desplegado el kube-prometheus-stack y trabajado con el Prometheus Operator que anunciamos en 06-07, creando
ServiceMonitorque viven junto a cada aplicación y sabiendo diagnosticar por qué un objetivo no aparece. - Escrito PromQL de verdad:
rate,increase,sum by,topk,histogram_quantile, y las consultas concretas de los cuatro indicadores dorados de cada componente. Entre ellas, la que confirma el throttling que en 07-02 solo podíamos sospechar. - Dimensionado el almacenamiento y la retención, y entendido por qué Prometheus no es un archivo eterno y cuándo tocará mirar hacia Thanos o Mimir.
Pero seguimos teniendo un problema práctico: todo esto vive en una interfaz web austera donde hay que escribir consultas a mano, y nadie está mirándola a las tres de la madrugada. Tener el dato no sirve de nada si nadie lo ve ni nadie recibe el aviso.
En 07-04 daremos ese salto: construiremos con Grafana el cuadro de mando de Rutas Norte, panel a panel, con variables que sirvan para los tres entornos; escribiremos el catálogo de alertas de la plataforma con PrometheusRule —incluida la que predice cuándo se llenará el disco de postgres-reservas antes de que ocurra—; y configuraremos Alertmanager para que la alerta correcta llegue a la persona correcta, agrupada, sin ruido y con un enlace al procedimiento que hay que seguir.
Curso de Kubernetes
Módulo 1: Introducción a Kubernetes
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
