A la lliçó anterior vam instal·lar metrics-server i per fi vam saber quant consumeix cada component de Rutas Norte ara mateix. També en vam descobrir el límit infranquejable: no recorda res. La pregunta "què va passar ahir a la nit a les tres de la matinada?" continua sense resposta, i amb ella totes les que de debò importen: quantes reserves per minut estem confirmant?, ha pujat la latència des de l'últim desplegament?, quant falta perquè s'ompli el disc de postgres-reserves?

Aquesta lliçó desplega Prometheus, l'estàndard de facto per monitorar Kubernetes i el segon projecte graduat de la CNCF després del mateix Kubernetes. Anem a entendre el seu model de dades, d'on surten realment les mètriques d'un clúster (una de les majors fonts de confusió per a qui comença), com instrumentar api-reserves amb mètriques de negoci, com desplegar l'estoc amb l'operador que vam anunciar a 06-07, i com escriure PromQL que respongui a preguntes reals de Rutas Norte. En acabar, la plataforma tindrà memòria.

Contingut

  1. Què aporta Prometheus davant de metrics-server
  2. El model d'extracció (pull) i els endpoints /metrics
  3. El model de dades: sèries, etiquetes i els quatre tipus de mètrica
  4. D'on surten les mètriques d'un clúster: la taula que aclareix la confusió
  5. Instrumentar api-reserves amb mètriques de negoci
  6. Desplegar el kube-prometheus-stack amb Helm
  7. El Prometheus Operator i els seus recursos personalitzats
  8. ServiceMonitor de Rutas Norte i diagnòstic d'objectius
  9. PromQL des de zero i amb sentit
  10. Els quatre indicadors daurats aplicats a Rutas Norte
  11. Emmagatzematge, retenció i els límits de Prometheus
  12. Errors comuns i consells
  13. Exercicis

  1. Què aporta Prometheus davant de metrics-server

Tres coses, i les tres són les que ens faltaven.

Històric. Prometheus desa cada mostra en una base de dades de sèries temporals al disc. Amb la retenció per defecte de l'estoc tindrem uns quants dies; configurant l'emmagatzematge, setmanes o mesos. La pregunta de les tres de la matinada té resposta.

Consultes. PromQL és un llenguatge complet per agregar, filtrar, derivar i comparar sèries. No és "veure un número": és "dóna'm la taxa d'errors 5xx d'api-reserves a l'entorn de producció, agrupada per ruta, durant l'última hora".

Alertes. Prometheus avalua expressions periòdicament i, quan es compleixen durant un temps sostingut, dispara alertes que Alertmanager encamina a qui correspongui. Això és el que fa que algú se n'assabenti a les tres de la matinada sense estar mirant una pantalla.

I una quarta, menys òbvia però decisiva: qualsevol cosa pot exposar mètriques. metrics-server només sap de CPU i memòria. Prometheus recull el que sigui que un endpoint HTTP li ofereixi: peticions per ruta, reserves confirmades, mida de la cua de correus, connexions actives de PostgreSQL, certificats a punt de caducar.

Capacitat metrics-server Prometheus
Històric ~1 minut en memòria Dies o mesos al disc
Mètriques Només CPU i memòria Qualsevol exposada a /metrics
Llenguatge de consulta No PromQL
Alertes No Sí (amb Alertmanager, 07-04)
Mètriques de negoci No
Cost d'operació Trivial Considerable (RAM i disc)
El fa servir l'HPA? Sí, de manera nativa Sí, amb un adaptador (09-04)

Tots dos conviuen. No substituïm metrics-server: el complementem.

  1. El model d'extracció (pull) i els endpoints /metrics

Aquí hi ha la decisió de disseny que defineix tota la resta.

La majoria de sistemes de monitoratge tradicionals funcionen per empenta (push): l'aplicació envia les seves mètriques a un servidor central. Prometheus fa el contrari: extracció (pull). Prometheus té una llista d'objectius (targets) i, cada cert interval (típicament 30 segons), fa un GET a cadascun.

flowchart LR
    P[Prometheus] -->|"GET /metrics cada 30s"| A["api-reserves :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 al disc<br/>blocs de 2 h)]

Per què és millor extreure que rebre?

  • Prometheus sap si un objectiu ha caigut. Si el GET falla, la mètrica sintètica up val 0. Amb push, l'absència de dades és ambigua: l'aplicació ha caigut o simplement no ha enviat res?
  • El control del ritme el té el sistema de monitoratge, no les aplicacions. Cent aplicacions mal programades no el poden inundar.
  • Descobriment automàtic. Prometheus consulta l'API de Kubernetes per saber quins pods existeixen. Un pod nou apareix com a objectiu automàticament, sense configurar res a l'aplicació.
  • Depuració trivial. Pots fer curl a l'endpoint /metrics des del teu portàtil i veure exactament el mateix que veu Prometheus.

L'excepció són els processos de vida molt curta, com el nostre CronJob informes-ocupacio: pot acabar abans que Prometheus l'escanegi. Per a aquests casos existeix el Pushgateway, un intermediari al qual el job empeny les seves mètriques i que Prometheus escaneja després. És l'única excepció legítima i convé fer-la servir amb moderació.

Com es veu un endpoint /metrics

kubectl -n rutas-norte-pro port-forward deploy/api-reserves 8080:8080 &
curl -s localhost:8080/metrics | head -30
# HELP rutasnorte_peticions_total Total de peticions HTTP ateses
# TYPE rutasnorte_peticions_total counter
rutasnorte_peticions_total{ruta="/api/rutes",metode="GET",codi="200"} 184203
rutasnorte_peticions_total{ruta="/api/rutes",metode="GET",codi="500"} 47
rutasnorte_peticions_total{ruta="/api/reserves",metode="POST",codi="201"} 9821
rutasnorte_peticions_total{ruta="/api/reserves",metode="POST",codi="422"} 318
# HELP rutasnorte_reserves_confirmades_total Reserves confirmades i pagades
# TYPE rutasnorte_reserves_confirmades_total counter
rutasnorte_reserves_confirmades_total{origen="web"} 8742
rutasnorte_reserves_confirmades_total{origen="mobil"} 1079
# HELP rutasnorte_connexions_pool_actives Connexions en ús del pool de PostgreSQL
# TYPE rutasnorte_connexions_pool_actives gauge
rutasnorte_connexions_pool_actives 7

És text pla. Cada línia és nom{etiquetes} valor. Les línies # HELP i # TYPE són metadades: descripció i tipus. Aquest format, deliberadament simple, és tot el contracte entre una aplicació i Prometheus.

  1. El model de dades: sèries, etiquetes i els quatre tipus de mètrica

Sèries temporals i etiquetes

Una sèrie temporal a Prometheus s'identifica de manera única pel seu nom de mètrica més el conjunt exacte de les seves etiquetes. Això és fonamental:

rutasnorte_peticions_total{ruta="/api/rutes", metode="GET", codi="200"}   → sèrie A
rutasnorte_peticions_total{ruta="/api/rutes", metode="GET", codi="500"}   → sèrie B (diferent)
rutasnorte_peticions_total{ruta="/api/reserves", metode="POST", codi="201"} → sèrie C (diferent)

Canviar el valor d'una sola etiqueta crea una sèrie completament nova. Cada sèrie ocupa memòria i disc, i d'aquí neix el perill més citat de Prometheus: la cardinalitat.

Regla d'or de la cardinalitat: no facis servir mai com a etiqueta un valor amb molts valors possibles. Res d'identificadors d'usuari, de reserva, de sessió, adreces IP de client, marques de temps o missatges d'error complets.

Un exemple real del perill. Si api-reserves etiquetés cada petició amb el dni del client:

rutasnorte_peticions_total{dni="12345678Z", ruta="/api/reserves"}   ← NO!

Amb 200 000 clients i 15 rutes, tindries 3 milions de sèries només d'aquella mètrica. Prometheus es quedaria sense memòria en minuts. I, a més, estaries ficant dades personals al sistema de monitoratge, amb les implicacions que veurem a 07-05.

Etiquetes que Prometheus afegeix per si mateix a tot el que recull a Kubernetes (i que farem servir constantment):

Etiqueta Exemple Origen
job api-reserves Nom del treball de recol·lecció
instance 10.244.2.17:8080 IP i port de l'objectiu concret
namespace rutas-norte-pro Metadades de Kubernetes
pod api-reserves-7d9f8c4b5-x2klm Metadades de Kubernetes
container api Metadades de Kubernetes
node rutas-norte-worker-2 Metadades de Kubernetes

Els quatre tipus de mètrica

Tipus Pot Exemple a Rutas Norte Funció típica per consultar-lo
counter Només pujar (o reiniciar-se a 0) rutasnorte_peticions_total rate(), increase()
gauge Pujar i baixar rutasnorte_connexions_pool_actives Valor directe, avg, max
histogram Comptar en cubetes configurables rutasnorte_duracio_peticio_segons histogram_quantile()
summary Percentils calculats al client Poc recomanable en general Valor directe

counter (comptador). Un valor que només creix: peticions ateses, errors, bytes enviats, reserves confirmades. No es consulta mai directament. El valor absolut 184203 no diu res: són moltes? des de quan? Es consulta sempre amb rate() o increase(), que calculen quant ha crescut per unitat de temps. Per convenció, el seu nom acaba en _total.

Prometheus detecta automàticament els reinicis del comptador (quan un pod es recrea, el comptador torna a 0) i rate() els compensa. No te n'has de preocupar.

gauge (mesurador). Un valor que puja i baixa: connexions actives, temperatura, memòria en ús, mida d'una cua. Es consulta directament. És el tipus de gairebé tot el que exposa cAdvisor sobre memòria.

histogram (histograma). El tipus més potent i el pitjor entès. En lloc de desar cada valor individual, l'aplicació classifica cada observació en cubetes acumulatives predefinides. Una mètrica d'histograma exposa en realitat tres coses:

# Cubetes acumulatives: "quantes peticions van trigar MENYS de X segons"
rutasnorte_duracio_peticio_segons_bucket{ruta="/api/reserves",le="0.05"} 8140
rutasnorte_duracio_peticio_segons_bucket{ruta="/api/reserves",le="0.1"}  9210
rutasnorte_duracio_peticio_segons_bucket{ruta="/api/reserves",le="0.5"}  9780
rutasnorte_duracio_peticio_segons_bucket{ruta="/api/reserves",le="1"}    9812
rutasnorte_duracio_peticio_segons_bucket{ruta="/api/reserves",le="+Inf"} 9821
# Suma de tots els valors observats (per calcular la mitjana)
rutasnorte_duracio_peticio_segons_sum{ruta="/api/reserves"} 743.28
# Nombre total d'observacions
rutasnorte_duracio_peticio_segons_count{ruta="/api/reserves"} 9821

Lectura: 8140 peticions van trigar menys de 50 ms; 9210 van trigar menys de 100 ms (incloses les 8140 anteriors: són acumulatives); 9821 en total, per tant 9 peticions van trigar més d'1 segon.

Amb aquestes dades, la funció histogram_quantile() pot estimar qualsevol percentil. L'avantatge enorme davant del summary: com que les cubetes són comptadors normals, es poden sumar entre pods. Pots calcular el percentil 95 de latència de tot api-reserves agregant les seves sis rèpliques. Amb un summary això és matemàticament impossible.

El cost: cada cubeta és una sèrie. Un histograma amb 10 cubetes i 15 rutes són 150 sèries per pod. Tria les cubetes amb criteri, ajustades a la latència real del teu servei.

summary (resum). L'aplicació calcula els percentils ella mateixa i els exposa. Barat de consultar, però no agregable entre instàncies: el percentil 95 del pod A i el del pod B no es poden combinar per obtenir el del servei. A Kubernetes, on tot té diverses rèpliques, això el descarta gairebé sempre. Fes-lo servir només si necessites un percentil exacte d'una única instància.

Convencions de noms

Prometheus té convencions que convé respectar perquè les eines i els quadres de comandament les assumeixen:

  • Prefix amb el nom del sistema: rutasnorte_.
  • Unitat base al nom i sempre en unitats base del SI: _segons no _ms, _bytes no _mb.
  • Els comptadors acaben en _total.
  • Noms en snake_case, sense majúscules.

  1. D'on surten les mètriques d'un clúster: la taula que aclareix la confusió

Aquesta és la secció que resol el dubte que té tothom la primera setmana: "per què necessito quatre coses diferents per monitorar Kubernetes?".

La resposta curta: perquè són quatre capes de realitat diferents i cap d'elles pot veure les altres.

Font Què observa Exemple de mètrica Cal instal·lar-lo?
kubelet / cAdvisor L'ús real dels contenidors (cgroups) container_memory_working_set_bytes No: ve al kubelet
node-exporter El sistema operatiu del node node_filesystem_avail_bytes Sí, com a DaemonSet
kube-state-metrics L'estat dels objectes de l'API kube_deployment_status_replicas_unavailable Sí, com a Deployment
Exportadors d'aplicació Les entranyes d'un programari concret pg_stat_database_numbackends Sí, un per programari
Instrumentació pròpia La lògica de negoci rutasnorte_reserves_confirmades_total Sí, al codi

Anem una a una, perquè la distinció importa moltíssim a l'hora d'escriure consultes.

kubelet / cAdvisor: el que els contenidors consumeixen

És la mateixa font que alimenta metrics-server (07-02), però Prometheus la llegeix directament i amb molt més detall. Mètriques clau:

container_cpu_usage_seconds_total{pod="api-reserves-...", container="api"}
container_memory_working_set_bytes{pod="api-reserves-...", container="api"}
container_cpu_cfs_throttled_periods_total{pod="api-reserves-...", container="api"}
container_network_receive_bytes_total{pod="api-reserves-..."}
container_fs_writes_bytes_total{pod="postgres-reserves-0"}

Fixa't en la tercera: el comptador de períodes estrangulats que a 07-02 vam dir que kubectl top no ens pot donar. Aquí està, i és la que confirmarà el diagnòstic de throttling d'api-reserves.

Avís pràctic: aquestes mètriques també apareixen amb container="" (el valor agregat del pod, inclòs el contenidor pause d'infraestructura). Gairebé sempre voldràs filtrar amb container!="" per no comptar el doble.

node-exporter: el que li passa a la màquina

És un DaemonSet (un pod per node, com vam estudiar a 06-02) que llegeix /proc i /sys del sistema amfitrió. Veu coses que cap contenidor no pot veure:

node_filesystem_avail_bytes{mountpoint="/var/lib/kubelet"}
node_memory_MemAvailable_bytes
node_load1
node_cpu_seconds_total{mode="idle"}
node_network_transmit_bytes_total

És el que t'avisa que el disc del node s'està omplint, que la càrrega mitjana està pels núvols o que la targeta de xarxa està saturada. Cap contenidor no et pot dir això de si mateix.

kube-state-metrics: el que l'API de Kubernetes creu

Aquesta és la que més costa entendre i la que més falta fa. No mesura el consum de res. Es connecta a l'API de Kubernetes i converteix l'estat dels objectes en mètriques:

kube_deployment_spec_replicas{deployment="api-reserves"}                  6
kube_deployment_status_replicas_available{deployment="api-reserves"}      4
kube_pod_container_status_restarts_total{pod="api-reserves-...", container="api"}  7
kube_pod_status_phase{pod="postgres-reserves-0", phase="Running"}         1
kube_job_status_failed{job_name="informes-ocupacio-28934520"}             1
kube_persistentvolumeclaim_status_phase{persistentvolumeclaim="dades-postgres-reserves-0", phase="Bound"} 1
kube_pod_container_resource_requests{pod="api-reserves-...", resource="cpu"}  0.2

Amb aquestes mètriques pots alertar de coses que cap de les altres fonts no coneix: un Deployment amb menys rèpliques disponibles de les desitjades, un pod reiniciant-se en bucle, un Job que ha fallat, un PVC que porta vint minuts en Pending.

I una cosa molt útil: kube_pod_container_resource_requests exposa les requests declarades. Creuant-la amb container_memory_working_set_bytes de cAdvisor obtens, en una sola consulta PromQL, exactament l'anàlisi de recalibració que a 07-02 vam haver de fer amb un script de bash.

Exportadors: traductors per a programari de tercers

PostgreSQL no parla el format de Prometheus. Un exportador és un procés que es connecta al programari, consulta les seves estadístiques internes i les tradueix a /metrics.

I aquí connectem amb 06-04: ja el tenim desplegat. Quan vam afegir el sidecar exportador de mètriques al pod de postgres-reserves, vam dir explícitament que Prometheus el consumiria en aquesta lliçó. Ha arribat el moment.

# Recordatori del sidecar que vam afegir a 06-04, dins del StatefulSet
# postgres-reserves. Cal notar que és un sidecar natiu (initContainer amb
# restartPolicy: Always), com vam veure en aquella lliçó.
initContainers:
  - name: exportador-pg
    image: quay.io/prometheuscommunity/postgres-exporter:v0.15.0
    restartPolicy: Always          # sidecar natiu 1.29+
    ports:
      - name: metriques
        containerPort: 9187
    env:
      - name: DATA_SOURCE_URI
        value: "127.0.0.1:5432/reserves?sslmode=disable"
      - name: DATA_SOURCE_USER
        valueFrom:
          secretKeyRef:
            name: credencials-postgres
            key: usuari-metriques
      - name: DATA_SOURCE_PASS
        valueFrom:
          secretKeyRef:
            name: credencials-postgres
            key: password-metriques
    resources:
      requests:
        cpu: "20m"
        memory: "32Mi"
      limits:
        cpu: "100m"
        memory: "64Mi"

El que exposa, i que ens farà falta:

pg_stat_database_numbackends{datname="reserves"}          # connexions obertes
pg_stat_database_xact_commit{datname="reserves"}          # transaccions confirmades
pg_stat_database_deadlocks{datname="reserves"}            # interbloqueigs
pg_database_size_bytes{datname="reserves"}                # mida de la base de dades
pg_stat_replication_replay_lag                            # retard de rèplica
pg_up                                                     # respon PostgreSQL?

Existeixen exportadors per a pràcticament tot: redis_exporter per a redis-cache, nginx-prometheus-exporter per a botiga-web, blackbox_exporter per comprovar des de fora que https://www.rutasnorte.example respon.

Instrumentació pròpia: l'única que sap de negoci

Cap de les quatre fonts anteriors no et pot dir quants bitllets has venut. Això només ho sap el teu codi. És la secció següent.

El resum visual

flowchart TB
    subgraph Node["Node rutas-norte-worker-2"]
        subgraph Pod1["Pod api-reserves"]
            APP["contenidor api<br/>/metrics propi"]
        end
        subgraph Pod2["Pod postgres-reserves-0"]
            PG["contenidor postgres"]
            EXP["sidecar exportador<br/>:9187/metrics"]
            EXP -.consulta.-> PG
        end
        KUBELET["kubelet + cAdvisor<br/>ús real de cgroups"]
        NE["node-exporter<br/>DaemonSet<br/>/proc i /sys"]
    end
    KSM["kube-state-metrics<br/>estat dels objectes"]
    API[(API Server)]
    KSM -.consulta.-> API
    PROM[Prometheus]
    APP --> PROM
    EXP --> PROM
    KUBELET --> PROM
    NE --> PROM
    KSM --> PROM

  1. Instrumentar api-reserves amb mètriques de negoci

La instrumentació pròpia és el que separa un quadre de comandament d'infraestructura d'un que li importa al negoci. "El pod fa servir 300 Mi de RAM" no interessa al director de Rutas Norte. "Estem confirmant 12 reserves per minut, un 40 % menys que ahir a aquesta hora" sí.

api-reserves està en Node.js, així que fem servir la llibreria client oficial prom-client. Existeixen llibreries equivalents per a Java, Go, Python, .NET i pràcticament qualsevol llenguatge.

// metriques.js — instrumentació d'api-reserves
const client = require('prom-client');

// El registre és el contenidor de totes les mètriques d'aquest procés.
const registre = new client.Registry();

// Etiquetes que s'afegeixen a TOTES les mètriques d'aquest procés.
// Vénen de la Downward API (03-03), així que cada pod s'identifica sol.
registre.setDefaultLabels({
  component: 'api-reserves',
  entorn: process.env.ENTORN || 'dev',
  versio: process.env.APP_VERSION || 'desconeguda',
});

// Mètriques per defecte del runtime: memòria del heap, GC, event loop, etc.
// Molt útils i gratis: una sola línia.
client.collectDefaultMetrics({ register: registre });

// ---------------------------------------------------------------------------
// 1. COUNTER: peticions HTTP ateses.
//    Etiquetes de BAIXA cardinalitat: ruta normalitzada (no la URL real),
//    mètode i codi. Mai l'id de reserva ni el DNI del client.
// ---------------------------------------------------------------------------
const peticionsTotal = new client.Counter({
  name: 'rutasnorte_peticions_total',
  help: 'Total de peticions HTTP ateses per api-reserves',
  labelNames: ['ruta', 'metode', 'codi'],
  registers: [registre],
});

// ---------------------------------------------------------------------------
// 2. HISTOGRAM: durada de les peticions.
//    Les cubetes estan triades per a la NOSTRA latència real: la majoria de
//    peticions triguen entre 20 i 200 ms, i l'SLO està en 500 ms.
//    Cubetes mal triades donen percentils inútils.
// ---------------------------------------------------------------------------
const duracioPeticio = new client.Histogram({
  name: 'rutasnorte_duracio_peticio_segons',
  help: 'Durada de les peticions HTTP en segons',
  labelNames: ['ruta', 'metode'],
  buckets: [0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
  registers: [registre],
});

// ---------------------------------------------------------------------------
// 3. COUNTER de negoci: reserves confirmades i pagades.
//    Aquesta és LA mètrica que mira el negoci. Si cau a zero, tant se val
//    que tots els pods estiguin Running: la plataforma no està venent.
// ---------------------------------------------------------------------------
const reservesConfirmades = new client.Counter({
  name: 'rutasnorte_reserves_confirmades_total',
  help: 'Reserves confirmades i pagades correctament',
  labelNames: ['origen', 'trajecte_tipus'],   // 'web'|'mobil', 'nacional'|'regional'
  registers: [registre],
});

// ---------------------------------------------------------------------------
// 4. GAUGE: estat del pool de connexions a PostgreSQL.
//    És exactament la dada que a 07-01 provocava l'incident invisible.
//    Ara, a més que la readiness ho detecti, quedarà registrat.
// ---------------------------------------------------------------------------
const connexionsPool = new client.Gauge({
  name: 'rutasnorte_connexions_pool_actives',
  help: 'Connexions actualment en ús del pool de PostgreSQL',
  registers: [registre],
  collect() {
    // collect() s'executa en el moment del scrape: sempre valor fresc.
    this.set(poolPg.totalCount - poolPg.idleCount);
  },
});

// ---------------------------------------------------------------------------
// 5. COUNTER de la passarel·la de pagaments externa.
//    Ens permetrà distingir "falla el nostre codi" de "falla el proveïdor".
// ---------------------------------------------------------------------------
const cridesPassarela = new client.Counter({
  name: 'rutasnorte_passarela_pagaments_crides_total',
  help: 'Crides a la passarel·la de pagaments externa',
  labelNames: ['resultat'],   // 'ok' | 'error' | 'timeout'
  registers: [registre],
});

module.exports = {
  registre, peticionsTotal, duracioPeticio,
  reservesConfirmades, connexionsPool, cridesPassarela,
};

I el middleware que ho connecta amb Express:

// servidor.js
const express = require('express');
const m = require('./metriques');
const app = express();

// Middleware que mesura TOTES les peticions.
app.use((req, res, next) => {
  // IMPORTANT: fem servir la ruta de l'encaminador (/api/reserves/:id), no la
  // URL real (/api/reserves/48213). Si féssim servir la URL real, cada reserva
  // crearia una sèrie nova: explosió de cardinalitat garantida.
  const fiTemporitzador = m.duracioPeticio.startTimer();

  res.on('finish', () => {
    const ruta = req.route ? req.baseUrl + req.route.path : 'desconeguda';
    fiTemporitzador({ ruta, metode: req.method });
    m.peticionsTotal.inc({ ruta, metode: req.method, codi: res.statusCode });
  });
  next();
});

// L'endpoint que Prometheus escanejarà.
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', m.registre.contentType);
  res.end(await m.registre.metrics());
});

// A la lògica de negoci, en confirmar una reserva:
async function confirmarReserva(reserva) {
  await desarABaseDeDades(reserva);
  m.reservesConfirmades.inc({
    origen: reserva.origen,
    trajecte_tipus: reserva.esNacional ? 'nacional' : 'regional',
  });
}

app.listen(8080);

Dues decisions a subratllar:

  1. Ruta normalitzada, no URL real. req.route.path dóna /api/reserves/:id, amb la qual cosa totes les reserves comparteixen sèrie. Fer servir req.originalUrl donaria /api/reserves/48213, /api/reserves/48214... una sèrie per reserva. És l'error de cardinalitat més freqüent del món.
  2. /metrics al mateix port que l'aplicació, o en un a part. Aquí el deixem al 8080 per simplicitat. En producció és preferible exposar-lo en un port diferent (per exemple 9090) que no estigui publicat a l'Ingress, perquè les mètriques no siguin accessibles des d'internet. Les NetworkPolicies de rutas-norte-pro (04-06) hauran de permetre el trànsit des del namespace de monitoratge a aquell port: recorda que tota conversa nova necessita la seva política.

  1. Desplegar el kube-prometheus-stack amb Helm

Instal·lar Prometheus a mà a Kubernetes significa gestionar una vintena d'objectes entre Deployments, ConfigMaps, Services, RBAC i emmagatzematge. La comunitat ha empaquetat tot això en un chart de Helm anomenat kube-prometheus-stack.

Nota: Helm és el gestor de paquets de Kubernetes i l'estudiarem a fons a 10-03. Aquí el fem servir com a eina d'instal·lació; per ara n'hi ha prou d'entendre que un chart és una plantilla parametritzable de manifests i que helm install els genera i els aplica.

# Afegir el repositori de charts de la comunitat
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# Namespace dedicat per a tot el monitoratge
kubectl create namespace monitoratge
kubectl label namespace monitoratge app.kubernetes.io/part-of=rutas-norte

Fitxer de valors adaptat a Rutas Norte:

# k8s/base/monitoratge/valors-prometheus.yaml
prometheus:
  prometheusSpec:
    # Retenció: 30 dies o 45 GiB, el que arribi abans. Un límit de mida
    # és imprescindible: sense ell, el disc s'omple i Prometheus mor.
    retention: 30d
    retentionSize: "45GiB"

    # CRÍTIC: per defecte, l'operador NOMÉS recull ServiceMonitors que
    # portin l'etiqueta release=<nom-del-release>. Posant-ho a false,
    # recollirà els de qualsevol namespace sense etiqueta especial.
    # És la causa número 1 de "el meu ServiceMonitor no apareix".
    serviceMonitorSelectorNilUsesHelmValues: false
    podMonitorSelectorNilUsesHelmValues: false
    ruleSelectorNilUsesHelmValues: false

    # Emmagatzematge persistent amb la StorageClass ràpida de 05-04.
    # Sense això, Prometheus desa en un emptyDir i ho perd TOT en reiniciar-se:
    # exactament el problema que veníem a resoldre.
    storageSpec:
      volumeClaimTemplate:
        spec:
          storageClassName: rutasnorte-rapida
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 50Gi

    resources:
      requests:
        cpu: "500m"
        memory: "3Gi"
      limits:
        memory: "6Gi"

    # Etiquetes externes: identifiquen AQUEST Prometheus. Imprescindibles
    # si algun dia federem diversos clústers (11-05).
    externalLabels:
      cluster: rutas-norte
      region: eu-west

grafana:
  enabled: true                    # el configurem a 07-04
  adminPassword: canviar-en-produccio

alertmanager:
  enabled: true                    # el configurem a 07-04

# node-exporter: un pod per node, com el recol·lector de logs de 06-02
nodeExporter:
  enabled: true

kubeStateMetrics:
  enabled: true

# A minikube, alguns components del pla de control no són accessibles
# com en un clúster real; els desactivem per evitar objectius en vermell.
kubeControllerManager:
  enabled: false
kubeScheduler:
  enabled: false
kubeEtcd:
  enabled: false
kubeProxy:
  enabled: false
helm install monitoratge prometheus-community/kube-prometheus-stack \
  --namespace monitoratge \
  --values k8s/base/monitoratge/valors-prometheus.yaml \
  --wait --timeout 10m

Què acabem d'instal·lar:

Component Tipus Funció
Prometheus Operator Deployment Tradueix els recursos personalitzats a configuració de Prometheus
Prometheus StatefulSet (creat per l'operador) El servidor: recol·lecta, emmagatzema i avalua regles
Alertmanager StatefulSet (creat per l'operador) Encamina i agrupa les alertes (07-04)
Grafana Deployment Visualització (07-04)
node-exporter DaemonSet Mètriques del sistema operatiu de cada node
kube-state-metrics Deployment Mètriques de l'estat dels objectes de l'API
Regles i quadres de comandament PrometheusRule i ConfigMaps Un conjunt molt complet d'alertes ja escrites
kubectl -n monitoratge get pods
NAME                                                     READY   STATUS    RESTARTS   AGE
alertmanager-monitoratge-kube-pr-alertmanager-0          2/2     Running   0          3m
monitoratge-grafana-6d84b8c7f9-w2xnk                     3/3     Running   0          3m
monitoratge-kube-pr-operator-7c9b4d5f68-hj4kp            1/1     Running   0          3m
monitoratge-kube-state-metrics-59d7b8c644-pl3mv          1/1     Running   0          3m
monitoratge-prometheus-node-exporter-4kx8n               1/1     Running   0          3m
monitoratge-prometheus-node-exporter-9wq2t               1/1     Running   0          3m
monitoratge-prometheus-node-exporter-mz7bd               1/1     Running   0          3m
prometheus-monitoratge-kube-pr-prometheus-0              2/2     Running   0          3m

Accés a la interfície de Prometheus:

kubectl -n monitoratge port-forward svc/monitoratge-kube-pr-prometheus 9090:9090
# Obrir http://localhost:9090

  1. El Prometheus Operator i els seus recursos personalitzats

Aquí es materialitza el que vam anunciar a 06-07. El Prometheus Operator és l'exemple canònic del patró operador: un controlador que observa recursos personalitzats i reconcilia l'estat real fins que coincideixi.

Sense operador, afegir un objectiu nou a Prometheus significa editar un fitxer prometheus.yml, ficar-lo en un ConfigMap, recarregar la configuració i esperar. Un procés manual, centralitzat i propens a errors.

Amb operador, l'equip que desplega api-reserves crea un objecte ServiceMonitor al costat de la seva pròpia aplicació, al seu propi namespace, i l'operador el detecta i regenera la configuració de Prometheus automàticament. El monitoratge passa a ser responsabilitat descentralitzada, versionada a Git juntament amb la resta de manifests.

flowchart LR
    DEV["Equip d'api-reserves<br/>k8s/base/api-reserves/servicemonitor.yaml"] -->|kubectl apply| API[(API Server)]
    API -->|watch| OP[Prometheus Operator]
    OP -->|genera i actualitza| SEC["Secret amb<br/>prometheus.yaml"]
    SEC -->|muntat a| PROM[Pod de Prometheus]
    OP -->|recarrega| PROM
    PROM -->|"GET /metrics"| SVC[Service api-reserves]

Els cinc recursos personalitzats

Recurs Per a què serveix
Prometheus Declara una instància de Prometheus: rèpliques, retenció, emmagatzematge, quins monitors recull
ServiceMonitor "Escaneja els pods que hi ha darrere d'aquest Service" — la forma habitual
PodMonitor "Escaneja aquests pods directament" — quan no hi ha Service
PrometheusRule Regles d'alerta i regles de gravació (les desenvolupem a 07-04)
Alertmanager Declara una instància d'Alertmanager (07-04)

I AlertmanagerConfig, per configurar l'encaminament d'alertes per namespace. També a 07-04.

kubectl get crd | grep monitoring.coreos.com
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:14Z

Com vam veure a 06-06, tots aquests són CRDs. I com Certificate de cert-manager o VolumeSnapshot, es gestionen amb kubectl exactament igual que un Deployment.

  1. ServiceMonitor de Rutas Norte i diagnòstic d'objectius

El Service amb port de mètriques anomenat

Requisit imprescindible: el ServiceMonitor referencia el port pel seu nom, així que el Service l'ha de tenir anomenat.

# k8s/base/api-reserves/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: api-reserves
  namespace: rutas-norte-pro
  labels:
    app: api-reserves
    app.kubernetes.io/part-of: rutas-norte
    entorn: pro
spec:
  selector:
    app: api-reserves          # només app i entorn al selector,
    entorn: pro                # com marca la nostra convenció
  ports:
    - name: http               # nom obligatori per al ServiceMonitor
      port: 80
      targetPort: 8080
    - name: metriques          # el port que escanejarà Prometheus
      port: 9090
      targetPort: 9090

El ServiceMonitor d'api-reserves

# k8s/base/api-reserves/servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: api-reserves
  namespace: rutas-norte-pro     # viu al costat de l'aplicació, no amb Prometheus
  labels:
    app: api-reserves
    app.kubernetes.io/part-of: rutas-norte
spec:
  # Quins Services selecciona. ATENCIÓ: aquestes etiquetes es comparen amb les
  # de l'objecte Service, NO amb les dels pods. És l'error més freqüent.
  selector:
    matchLabels:
      app: api-reserves
      entorn: pro

  # En quins namespaces buscar aquests Services.
  namespaceSelector:
    matchNames:
      - rutas-norte-pro

  endpoints:
    - port: metriques            # NOM del port del Service, no el número
      path: /metrics
      interval: 30s              # cada quant s'escaneja
      scrapeTimeout: 10s         # sempre menor que l'interval

      # Trasllada etiquetes del pod a les mètriques, per poder filtrar
      # després per entorn o per component en PromQL.
      relabelings:
        - sourceLabels: [__meta_kubernetes_pod_label_entorn]
          targetLabel: entorn
        - sourceLabels: [__meta_kubernetes_pod_node_name]
          targetLabel: node

      # Descarta mètriques que no ens interessen i ocupen espai.
      # nodejs_gc_duration_seconds té molta cardinalitat i poc valor.
      metricRelabelings:
        - sourceLabels: [__name__]
          regex: 'nodejs_gc_duration_seconds.*'
          action: drop

El ServiceMonitor de l'exportador de postgres-reserves

Com que postgres-reserves és un StatefulSet amb Service headless (06-01), necessitem un Service addicional específic per a mètriques. Un Service headless (clusterIP: None) funciona igual de bé com a origen d'un ServiceMonitor, perquè l'operador fa servir els Endpoints, no la ClusterIP.

# k8s/base/postgres-reserves/service-metriques.yaml
apiVersion: v1
kind: Service
metadata:
  name: postgres-reserves-metriques
  namespace: rutas-norte-pro
  labels:
    app: postgres-reserves
    entorn: pro
    tipus: metriques
spec:
  clusterIP: None                # headless: no necessitem balanceig
  selector:
    app: postgres-reserves
    entorn: pro
  ports:
    - name: metriques
      port: 9187
      targetPort: 9187
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: postgres-reserves
  namespace: rutas-norte-pro
  labels:
    app: postgres-reserves
    app.kubernetes.io/part-of: rutas-norte
spec:
  selector:
    matchLabels:
      app: postgres-reserves
      entorn: pro
      tipus: metriques           # discrimina del Service de dades (5432)
  namespaceSelector:
    matchNames:
      - rutas-norte-pro
  endpoints:
    - port: metriques
      interval: 30s
      scrapeTimeout: 10s

Amb això, el sidecar que vam afegir a 06-04 comença a alimentar Prometheus. La promesa es compleix.

La NetworkPolicy que cal

La nostra convenció és clara: a rutas-norte-pro hi ha una deny-all i cada conversa s'autoritza una a una. Prometheus viu a monitoratge i vol parlar amb els ports de mètriques de rutas-norte-pro. Sense política, tots els objectius apareixeran caiguts.

# k8s/entorns/pro/netpol-permetre-prometheus.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: permetre-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: monitoratge
          podSelector:
            matchLabels:
              app.kubernetes.io/name: prometheus
      ports:
        - protocol: TCP
          port: 9090      # mètriques d'api-reserves
        - protocol: TCP
          port: 9187      # exportador de postgres-reserves

Verificar que l'objectiu apareix

A la interfície de Prometheus, Status → Targets. O per línia d'ordres:

kubectl -n monitoratge port-forward svc/monitoratge-kube-pr-prometheus 9090:9090 &

curl -s localhost:9090/api/v1/targets | jq -r '
  .data.activeTargets[] |
  select(.labels.job | test("api-reserves|postgres")) |
  "\(.labels.job)\t\(.scrapeUrl)\t\(.health)\t\(.lastError)"'
api-reserves	http://10.244.2.17:9090/metrics	up
api-reserves	http://10.244.1.23:9090/metrics	up
postgres-reserves	http://10.244.2.31:9187/metrics	up

Diagnòstic: el meu objectiu no apareix

És la pregunta d'aquesta lliçó. Llista de comprovació per ordre de freqüència:

# Causa Com confirmar-ho
1 El selector del ServiceMonitor no casa amb les etiquetes del Service kubectl get svc api-reserves --show-labels
2 El namespaceSelector no inclou el namespace del Service Revisar matchNames
3 L'operador ignora el ServiceMonitor per falta de l'etiqueta release Veure serviceMonitorSelector del recurs Prometheus
4 El nom del port de l'endpoint no existeix al Service kubectl get svc api-reserves -o jsonpath='{.spec.ports}'
5 El Service no té Endpoints (readiness fallant, 07-01) kubectl get endpointslices -l kubernetes.io/service-name=api-reserves
6 Una NetworkPolicy bloqueja el trànsit des de monitoratge L'objectiu apareix però amb health: down i error de connexió
7 L'aplicació no exposa /metrics en aquell port kubectl exec -it <pod> -- curl -s localhost:9090/metrics

El punt 3 mereix detall perquè és traïdor. El recurs Prometheus té un serviceMonitorSelector. Si el chart el va deixar amb release: monitoratge, només recollirà ServiceMonitor amb aquella etiqueta, i el teu serà ignorat en silenci: no hi ha esdeveniment, no hi ha error, simplement no apareix.

# Veure què està seleccionant el Prometheus desplegat
kubectl -n monitoratge get prometheus -o yaml | grep -A 8 serviceMonitorSelector

Dues solucions: posar serviceMonitorSelectorNilUsesHelmValues: false als valors (el que hem fet), o afegir l'etiqueta release: monitoratge a tots els teus ServiceMonitor.

Comprovar directament si l'objectiu està sent escanejat:

# Existeix la mètrica sintètica up per al nostre job?
curl -s 'localhost:9090/api/v1/query?query=up{job="api-reserves"}' | jq '.data.result'

Un array buit significa que l'objectiu no està configurat (problemes 1-4). Un resultat amb value: ["...", "0"] significa que està configurat però no respon (problemes 5-7).

  1. PromQL des de zero i amb sentit

PromQL espanta al principi i és més senzill del que sembla si s'aprèn per capes.

Capa 1: selectors de sèries

La consulta més simple és el nom d'una mètrica:

rutasnorte_peticions_total

Retorna totes les sèries amb aquell nom: una per cada combinació d'etiquetes de cada pod. Per filtrar, es fan servir les claus:

rutasnorte_peticions_total{entorn="pro"}

Els quatre operadors de comparació d'etiquetes:

Operador Significat Exemple
= Igual exacte {entorn="pro"}
!= Diferent {container!=""}
=~ Coincideix amb l'expressió regular {codi=~"5.."}
!~ No coincideix amb l'expressió regular {ruta!~"/salut|/preparat"}

Combinables, amb AND implícit:

rutasnorte_peticions_total{entorn="pro", codi=~"5..", ruta="/api/reserves"}

Lectura: peticions a /api/reserves en producció que van retornar un codi 5xx.

Capa 2: selectors de rang

Afegint [5m] obtens, en lloc d'un valor per sèrie, tots els valors dels últims 5 minuts:

rutasnorte_peticions_total{entorn="pro"}[5m]

Això s'anomena vector de rang i no es pot dibuixar directament: és matèria primera per a les funcions del nivell següent.

Capa 3: les funcions que es fan servir de debò

rate() — la funció més important de PromQL.

Calcula l'increment per segon d'un comptador dins d'una finestra. És el que converteix un comptador inútil (184203) en una dada amb sentit (12,4 peticions per segon).

rate(rutasnorte_peticions_total{entorn="pro"}[5m])

Tres coses que cal saber sobre rate():

  • Només funciona amb comptadors. Aplicar-la a un gauge dóna resultats sense sentit.
  • Compensa automàticament els reinicis del comptador quan un pod es recrea.
  • La finestra ha de contenir almenys 4 mostres. Amb interval: 30s, la finestra mínima raonable és [2m]; l'habitual és [5m]. Amb [1m] i escanejos de 30 s tindries dues mostres i resultats erràtics.

increase() — igual que rate() però dóna l'increment total de la finestra en lloc de per segon. És literalment rate() * segons_de_la_finestra. Més llegible per a preguntes humanes:

increase(rutasnorte_reserves_confirmades_total[1h])
# → "quantes reserves s'han confirmat l'última hora"

sum by i sum without — agregació.

rate() retorna una sèrie per pod i per combinació d'etiquetes. Gairebé mai vols això: vols el total del servei.

# Peticions per segon del servei sencer, agregant tots els pods,
# però mantenint el desglossament per codi de resposta
sum by (codi) (rate(rutasnorte_peticions_total{entorn="pro"}[5m]))

sum by (a, b) conserva només les etiquetes a i b. sum without (pod, instance) conserva totes menys aquestes. La segona forma sol ser més robusta davant de canvis.

Altres agregacions: avg, min, max, count, stddev, topk, bottomk.

topk() — els N més grans. Perfecte per a "qui s'està menjant el clúster?":

topk(5, sum by (pod) (rate(container_cpu_usage_seconds_total{namespace="rutas-norte-pro"}[5m])))

histogram_quantile() — percentils a partir d'histogrames.

histogram_quantile(
  0.95,
  sum by (le, ruta) (rate(rutasnorte_duracio_peticio_segons_bucket{entorn="pro"}[5m]))
)

Descomponent, de dins cap enfora:

  1. rate(..._bucket[5m]) → taxa d'increment de cada cubeta.
  2. sum by (le, ruta) → suma les cubetes de tots els pods, conservant le (el límit de la cubeta) i ruta. L'etiqueta le és obligatòria: sense ella, histogram_quantile no pot funcionar. És l'error més comú amb aquesta funció.
  3. histogram_quantile(0.95, ...) → estima el percentil 95.

El resultat és en segons: 0.412 significa que el 95 % de les peticions es resolen en menys de 412 ms.

Precisió: el resultat és una estimació per interpolació lineal dins de la cubeta. Si les teves cubetes són [0.1, 0.5, 1] i el p95 real està en 0,42 s, l'estimació pot desviar-se bastant. Per això vam triar cubetes ajustades a la nostra latència real a l'apartat 5.

Operadors aritmètics i comparació entre mètriques.

# Ús de memòria com a fracció del límit configurat
container_memory_working_set_bytes{namespace="rutas-norte-pro", container!=""}
  /
kube_pod_container_resource_limits{namespace="rutas-norte-pro", resource="memory"}

Prometheus aparella sèries de tots dos costats per les seves etiquetes comunes. Si les etiquetes no coincideixen, el resultat és buit: és la causa de la meitat de les consultes que "no retornen res". on() i ignoring() permeten controlar l'aparellament.

Les consultes concretes de Rutas Norte

Peticions per segon per ruta:

sum by (ruta) (rate(rutasnorte_peticions_total{entorn="pro"}[5m]))

Taxa d'error (proporció de 5xx sobre el total):

sum(rate(rutasnorte_peticions_total{entorn="pro", codi=~"5.."}[5m]))
  /
sum(rate(rutasnorte_peticions_total{entorn="pro"}[5m]))

Retorna un número entre 0 i 1. Multiplicat per 100 és el percentatge. Advertiment: si el denominador és 0 (sense trànsit), el resultat és NaN i desapareix del gràfic. És un comportament correcte: sense trànsit no hi ha taxa d'error a mesurar.

Percentil 95 de latència per ruta:

histogram_quantile(
  0.95,
  sum by (le, ruta) (rate(rutasnorte_duracio_peticio_segons_bucket{entorn="pro"}[5m]))
)

Reserves confirmades per minut (la mètrica del negoci):

sum(rate(rutasnorte_reserves_confirmades_total{entorn="pro"}[5m])) * 60

Saturació de CPU respecte al límit configurat:

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à fent servir el 95 % del seu límit: candidat immediat a throttling.

Confirmar el throttling que a 07-02 no podíem veure:

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]))

És la fracció de períodes de planificació en què el contenidor va ser estrangulat. Per sobre de 0.25 (25 %) hi ha un problema real de rendiment. Aquesta és exactament la consulta que hauria diagnosticat el problema d'api-reserves a 07-02 en deu segons.

Comparar consum real amb les requests: la recalibració de 07-02, ara automatitzada:

# Quant es fa servir respecte al que es reserva. Valors molt per sota d'1
# indiquen sobredimensionat; per sobre d'1, infradimensionat.
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"}
)

Amb Prometheus, aquesta consulta sobre 30 dies d'històric substitueix per complet l'script de bash de 07-02, i a més permet demanar el percentil real fent servir quantile_over_time.

Rèpliques no disponibles (fa servir kube-state-metrics):

kube_deployment_status_replicas_unavailable{namespace="rutas-norte-pro"} > 0

Objectiu caigut:

up{namespace="rutas-norte-pro"} == 0

Pods reiniciant-se en bucle:

increase(kube_pod_container_status_restarts_total{namespace="rutas-norte-pro"}[1h]) > 3

Connexions de PostgreSQL respecte al màxim (del sidecar de 06-04):

pg_stat_database_numbackends{datname="reserves"} / pg_settings_max_connections

Errors de la passarel·la de pagaments externa:

sum(rate(rutasnorte_passarela_pagaments_crides_total{resultat=~"error|timeout"}[5m]))
  /
sum(rate(rutasnorte_passarela_pagaments_crides_total[5m]))

Aquesta consulta permet distingir "la nostra plataforma falla" de "el proveïdor extern falla", que és una distinció molt valuosa a les tres de la matinada.

  1. Els quatre indicadors daurats aplicats a Rutas Norte

Els quatre indicadors daurats (golden signals), popularitzats pel llibre d'SRE de Google, són el marc que evita l'error de monitorar-ho tot i no entendre res.

Indicador Què mesura Pregunta que respon
Latència Quant triga una petició Va lent?
Trànsit Quanta demanda hi ha Quanta feina estem fent?
Errors Quina fracció falla Està fallant?
Saturació Com d'ple està el recurs més escàs Quant marge queda?

Un matís essencial sobre la latència: cal mesurar per separat la de les peticions que fallen. Un error 500 retornat en 3 ms baixa artificialment la latència mitjana i pot fer que un servei que falla massivament sembli rapidíssim.

Aplicació component a component

Component Latència Trànsit Errors Saturació
botiga-web p95 d'nginx peticions/s ràtio 5xx CPU vs límit
api-reserves p95 i p99 per ruta peticions/s ràtio 5xx + errors de passarel·la CPU vs límit, connexions del pool
postgres-reserves durada de transacció transaccions/s interbloqueigs + errors connexions/max, mida al disc
redis-cache latència d'ordre operacions/s ràtio d'errors de cau memòria vs maxmemory
worker-notificacions temps de procés per correu correus/min correus fallits profunditat de la cua
informes-ocupacio durada de l'execució execucions/dia jobs fallits (no aplica)

Les consultes concretes per a api-reserves, que seran la base del quadre de comandament de 07-04:

# LATÈNCIA (excloent errors, per no falsejar la dada)
histogram_quantile(0.95,
  sum by (le) (rate(rutasnorte_duracio_peticio_segons_bucket{entorn="pro"}[5m])))

# TRÀNSIT
sum(rate(rutasnorte_peticions_total{entorn="pro"}[5m]))

# ERRORS
sum(rate(rutasnorte_peticions_total{entorn="pro", codi=~"5.."}[5m]))
  / sum(rate(rutasnorte_peticions_total{entorn="pro"}[5m]))

# SATURACIÓ: dos eixos, CPU i pool de connexions
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_connexions_pool_actives{entorn="pro"}) / 20

Aquesta última consulta tanca un cercle del mòdul: és exactament el fenomen que a 07-01 deixava api-reserves viva però inservible. Aleshores només teníem una readiness que apartava el pod; ara tenim a més la dada registrada i consultable a posteriori.

Regles de gravació

Consultes com el p95 són cares: recorren moltes sèries. Si un quadre de comandament l'executa cada 15 segons amb 8 panells oberts, Prometheus pateix.

Les regles de gravació (recording rules) precalculen una consulta periòdicament i desen el resultat com una mètrica nova:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: rutas-norte-gravacio
  namespace: monitoratge
  labels:
    app.kubernetes.io/part-of: rutas-norte
spec:
  groups:
    - name: rutasnorte.indicadors-daurats
      interval: 30s
      rules:
        # Convenció de noms: nivell:metrica:operacio
        - record: apireserves:latencia_p95:5m
          expr: |
            histogram_quantile(0.95,
              sum by (le, ruta, entorn) (
                rate(rutasnorte_duracio_peticio_segons_bucket[5m])))

        - record: apireserves:peticions:rate5m
          expr: sum by (entorn, ruta) (rate(rutasnorte_peticions_total[5m]))

        - record: apireserves:ratio_errors:5m
          expr: |
            sum by (entorn) (rate(rutasnorte_peticions_total{codi=~"5.."}[5m]))
              /
            sum by (entorn) (rate(rutasnorte_peticions_total[5m]))

Ara els quadres de comandament i les alertes consulten apireserves:latencia_p95:5m, que és una única sèrie ja calculada. A 07-04 les farem servir intensivament.

  1. Emmagatzematge, retenció i els límits de Prometheus

Com desa les dades

Prometheus escriu a la seva pròpia base de dades de sèries temporals (TSDB):

  1. Les mostres noves van a un bloc en memòria i a un registre d'escriptura anticipada (WAL) al disc, que protegeix davant de caigudes.
  2. Cada 2 hores, aquell bloc es persisteix com un directori immutable al disc.
  3. Un procés de compactació va fusionant blocs petits en blocs majors.
  4. Els blocs que superen la retenció s'esborren sencers.

Conseqüència important: la retenció no esborra mostres soltes, esborra blocs complets. Amb retention: 30d pots tenir puntualment una mica més de 30 dies.

Estimar l'espai

La regla pràctica acceptada: entre 1 i 2 bytes per mostra després de la compressió.

Bytes ≈ retencio_segons × series_actives × bytes_per_mostra / interval_scrape

Rutas Norte, estimació:
  30 dies = 2 592 000 s
  ~120 000 sèries actives (els tres entorns + el sistema)
  1,7 bytes per mostra
  interval 30 s

  2 592 000 × 120 000 × 1,7 / 30 ≈ 17,6 GB

Amb 50 GiB de PVC anem de sobres, fins i tot amb marge per a la compactació (que necessita espai temporal). Per això vam posar també retentionSize: 45GiB: és el fre d'emergència si la cardinalitat creix més del previst.

Veure la cardinalitat real:

# Nombre total de sèries actives
prometheus_tsdb_head_series

# Les 10 mètriques amb més sèries: la llista de sospitosos de cardinalitat
topk(10, count by (__name__)({__name__=~".+"}))

Aquesta segona consulta és la primera que cal executar quan Prometheus comença a consumir massa memòria.

Per què Prometheus no és una base de dades eterna

Tres limitacions de disseny, deliberades:

  1. Emmagatzematge local, no distribuït. Les dades viuen al disc d'un pod. Sense replicació entre instàncies.
  2. No escala horitzontalment per si mateix. Dos Prometheus no comparteixen dades: cadascun té les seves.
  3. La retenció llarga és cara. Desar dos anys en local significa centenars de GB en un disc ràpid i un consum de memòria considerable en consultar.

Per a retenció llarga i visió multiclúster existeixen dos projectes que estenen Prometheus:

Projecte Enfocament
Thanos Un sidecar puja els blocs a emmagatzematge d'objectes (S3); un component query consulta de manera transparent el local i el remot
Mimir (Grafana) Rep les dades per remote_write en un sistema distribuït i multiinquilí

Tots dos permeten anys de retenció barata i consultar diversos clústers alhora. Per a Rutas Norte amb un sol clúster, 30 dies són suficients avui; quan el mòdul 11-05 abordi la gestió multiclúster, Thanos serà l'evolució natural.

Configurar remote_write des de l'operador és senzill:

prometheus:
  prometheusSpec:
    remoteWrite:
      - url: https://mimir.rutasnorte.example/api/v1/push
        writeRelabelConfigs:
          # Enviar només el que de debò volem conservar anys:
          # les mètriques de negoci, no les d'infraestructura.
          - sourceLabels: [__name__]
            regex: 'rutasnorte_.*'
            action: keep

Errors Comuns i Consells

1. Explosió de cardinalitat. L'error més greu i el més car. Etiquetar amb identificadors de reserva, DNI, IP de client o URL sense normalitzar multiplica les sèries per milers i tomba Prometheus per falta de memòria. Abans d'afegir una etiqueta, pregunta't quants valors diferents pot tenir. Si la resposta és "no ho sé", no l'afegeixis.

2. Consultar un comptador sense rate(). rutasnorte_peticions_total a seques retorna un valor acumulat des que va arrencar el pod. És un gràfic que només puja i no significa res. Sempre rate() o increase().

3. Finestra de rate() massa curta. Amb interval: 30s, un rate(...[1m]) té dues mostres i produeix soroll o forats. Regla: la finestra ha de ser almenys 4 vegades l'interval d'escaneig.

4. Oblidar le a sum by abans d'histogram_quantile. Si agregues sense conservar l'etiqueta le, la funció no pot reconstruir l'histograma i retorna NaN o res. És l'error número u amb percentils.

5. El ServiceMonitor selecciona etiquetes de pod en lloc de Service. El selector d'un ServiceMonitor es compara amb les etiquetes de l'objecte Service. Com que a Rutas Norte els Service porten les mateixes etiquetes que els pods, és fàcil no adonar-se'n fins que un Service té etiquetes diferents.

6. L'etiqueta release de l'operador. Si el recurs PrometheusserviceMonitorSelector: {matchLabels: {release: monitoratge}}, els teus ServiceMonitor sense aquella etiqueta s'ignoren sense cap missatge d'error. Comprovar-ho és el primer quan un objectiu no apareix.

7. Prometheus sense emmagatzematge persistent. Sense storageSpec, el chart fa servir un emptyDir i cada reinici del pod esborra tot l'històric. És exactament el problema que veníem a resoldre.

8. Sense retentionSize. Només amb retention: 90d, si la cardinalitat creix, el disc s'omple, Prometheus entra en CrashLoopBackOff i et quedes sense monitoratge justament quan més falta fa. Posa sempre un límit de mida, i que sigui un 80-85 % del PVC.

9. Alertar sobre mètriques d'un summary. No són agregables entre pods. Si la teva alerta fa servir el p99 d'un summary amb sis rèpliques, el número no significa res útil.

10. Monitorar el node amb mètriques de contenidor. cAdvisor no veu el sistema de fitxers del node ni la seva càrrega mitjana. Per a això hi ha node-exporter. Confondre'ls porta a alertes que no disparen mai.

11. Oblidar les NetworkPolicies. A rutas-norte-pro hi ha una deny-all. Si Prometheus no pot arribar als ports de mètriques, tots els objectius apareixen caiguts i l'error (context deadline exceeded) no és gens evident.

12. Instrumentar massa tard. La instrumentació de negoci s'afegeix al codi, així que requereix un cicle de desenvolupament. Com abans s'incorpori com a part del "llest per a producció", millor. Improvisar-la durant un incident és impossible.

Exercicis

Exercici 1 — Diagnosticar un ServiceMonitor que no funciona

L'equip ha desplegat redis-cache amb un exportador i ha creat aquests manifests. A la pàgina de targets de Prometheus no apareix res.

apiVersion: v1
kind: Service
metadata:
  name: redis-cache-metriques
  namespace: rutas-norte-pro
  labels:
    app.kubernetes.io/name: redis-cache
    app.kubernetes.io/part-of: rutas-norte
spec:
  selector:
    app: redis-cache
    entorn: pro
  ports:
    - port: 9121
      targetPort: 9121
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: redis-cache
  namespace: monitoratge
spec:
  selector:
    matchLabels:
      app: redis-cache
      entorn: pro
  endpoints:
    - port: metrics
      interval: 30s
  1. Identifica tres errors diferents.
  2. Escriu els manifests corregits.
  3. Escriu l'ordre PromQL o curl que confirma que el problema està resolt.

Exercici 2 — Escriure les consultes d'un incident

El dissabte del pont, entre les 12:00 i les 13:30, els clients es van queixar que el web anava lentíssim i algunes compres fallaven. Ja ha passat, ningú va prendre notes, i el director demana explicacions el dilluns.

Escriu les consultes PromQL que responen a cada pregunta. Indica de quina font (cAdvisor, kube-state-metrics, node-exporter, exportador o instrumentació pròpia) surt cada mètrica.

  1. Quantes peticions per segon atenia api-reserves en aquell interval, comparat amb el dissabte anterior?
  2. Quin va ser el percentil 99 de latència de /api/reserves durant l'incident?
  3. Estava api-reserves patint throttling de CPU?
  4. Es va quedar postgres-reserves sense connexions disponibles?
  5. Va fallar la passarel·la de pagaments externa, o l'error era nostre?
  6. Quantes reserves es van deixar de confirmar respecte al que s'esperava?

Exercici 3 — Instrumentar worker-notificacions

worker-notificacions consumeix d'una cua de Redis i envia correus de confirmació. Actualment no exposa cap mètrica. Dissenya la seva instrumentació:

  1. Enumera les mètriques que ha d'exposar, amb el seu nom complet, el seu tipus (counter, gauge, histogram) i les seves etiquetes, justificant la cardinalitat de cada etiqueta.
  2. Escriu el ServiceMonitor corresponent (el worker no té Service perquè no rep trànsit: resol aquest problema).
  3. Escriu les consultes PromQL dels quatre indicadors daurats per a aquest component.

Solucions

Solució 1

1. Els tres errors.

Error A — El selector del ServiceMonitor no casa amb les etiquetes del Service. El ServiceMonitor busca Services amb app: redis-cache i entorn: pro, però el Service té app.kubernetes.io/name: redis-cache i app.kubernetes.io/part-of: rutas-norte. No en coincideix cap. El spec.selector del Service (que sí que fa servir app/entorn) selecciona pods, no és el que mira el ServiceMonitor.

Error B — El port del Service no té nom, i l'endpoint referencia metrics. El ServiceMonitor busca un port anomenat metrics al Service; el Service defineix el port 9121 sense camp name. L'operador no troba el port i descarta l'endpoint.

Error C — El ServiceMonitor està a monitoratge sense namespaceSelector. Per defecte, un ServiceMonitor busca Services al seu propi namespace. Estant a monitoratge i el Service a rutas-norte-pro, no es trobaran mai. Dues solucions vàlides: moure el ServiceMonitor al namespace de l'aplicació (preferible, manté el monitoratge al costat del codi) o afegir namespaceSelector.

2. Manifests corregits.

apiVersion: v1
kind: Service
metadata:
  name: redis-cache-metriques
  namespace: rutas-norte-pro
  labels:
    app: redis-cache          # etiqueta que buscarà el ServiceMonitor
    entorn: pro
    app.kubernetes.io/part-of: rutas-norte
    tipus: metriques          # distingeix del Service de dades (6379)
spec:
  clusterIP: None
  selector:                   # això selecciona PODS: només app i entorn
    app: redis-cache
    entorn: pro
  ports:
    - name: metriques         # NOM obligatori
      port: 9121
      targetPort: 9121
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: redis-cache
  namespace: rutas-norte-pro  # al costat de l'aplicació
  labels:
    app: redis-cache
    app.kubernetes.io/part-of: rutas-norte
spec:
  selector:
    matchLabels:              # es comparen amb les etiquetes del SERVICE
      app: redis-cache
      entorn: pro
      tipus: metriques
  namespaceSelector:
    matchNames:
      - rutas-norte-pro
  endpoints:
    - port: metriques         # coincideix amb el name del port
      path: /metrics
      interval: 30s
      scrapeTimeout: 10s

I la NetworkPolicy, perquè a rutas-norte-pro hi ha deny-all:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: permetre-scrape-redis-cache
  namespace: rutas-norte-pro
spec:
  podSelector:
    matchLabels:
      app: redis-cache
      entorn: pro
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: monitoratge
      ports:
        - protocol: TCP
          port: 9121

3. Verificació.

kubectl -n monitoratge port-forward svc/monitoratge-kube-pr-prometheus 9090:9090 &

# Existeix l'objectiu i respon?
curl -s 'localhost:9090/api/v1/query?query=up{job="redis-cache-metriques"}' | jq '.data.result'
[
  {
    "metric": {"__name__": "up", "job": "redis-cache-metriques",
               "namespace": "rutas-norte-pro", "pod": "redis-cache-0"},
    "value": [1754472000, "1"]
  }
]

"1" és la confirmació. Si donés "0", l'objectiu està configurat però no respon: revisa la NetworkPolicy i que l'exportador escolti al 9121.

Comprovació addicional que l'operador l'ha recollit:

kubectl -n monitoratge get prometheus -o jsonpath='{.items[0].spec.serviceMonitorNamespaceSelector}'

Solució 2

Nota general: totes les consultes fan servir el modificador offset o un rang temporal explícit, ja que l'incident va passar. Aquí hi ha l'avantatge decisiu sobre kubectl top de 07-02: aquestes dades existeixen.

1. Trànsit durant l'incident, comparat amb el dissabte anterior.

Font: instrumentació pròpia d'api-reserves.

# Trànsit actual (visualitzat al rang de l'incident a Grafana)
sum(rate(rutasnorte_peticions_total{entorn="pro"}[5m]))

# Comparació amb fa exactament una setmana, superposada
sum(rate(rutasnorte_peticions_total{entorn="pro"}[5m] offset 7d))

# Factor de multiplicació
sum(rate(rutasnorte_peticions_total{entorn="pro"}[5m]))
  /
sum(rate(rutasnorte_peticions_total{entorn="pro"}[5m] offset 7d))

Un resultat de 5.8 significa que el dissabte del pont hi va haver gairebé sis vegades més trànsit que el dissabte normal: coherent amb el que sabem dels pics de ponts.

2. Percentil 99 de latència de /api/reserves.

Font: instrumentació pròpia (histograma).

histogram_quantile(0.99,
  sum by (le) (
    rate(rutasnorte_duracio_peticio_segons_bucket{
      entorn="pro", ruta="/api/reserves"}[5m])))

Es compara amb el p95 i amb la mitjana (_sum / _count) per veure si el problema afectava totes les peticions o només la cua:

rate(rutasnorte_duracio_peticio_segons_sum{entorn="pro", ruta="/api/reserves"}[5m])
  /
rate(rutasnorte_duracio_peticio_segons_count{entorn="pro", ruta="/api/reserves"}[5m])

Si la mitjana està bé i el p99 disparat, el problema afecta un subconjunt (per exemple, les peticions que toquen la passarel·la de pagaments). Si tots dos pugen, és sistèmic.

3. Throttling de CPU.

Font: cAdvisor / kubelet.

sum by (pod) (
  rate(container_cpu_cfs_throttled_periods_total{
    namespace="rutas-norte-pro", pod=~"api-reserves-.*"}[5m]))
/
sum by (pod) (
  rate(container_cpu_cfs_periods_total{
    namespace="rutas-norte-pro", pod=~"api-reserves-.*"}[5m]))

Un valor sostingut per sobre de 0.30 durant l'incident confirma que el límit de CPU estava frenant l'aplicació. Complementària, per veure si estava enganxada al sostre:

sum by (pod) (rate(container_cpu_usage_seconds_total{
  namespace="rutas-norte-pro", pod=~"api-reserves-.*", container="api"}[5m]))
/
sum by (pod) (kube_pod_container_resource_limits{
  namespace="rutas-norte-pro", pod=~"api-reserves-.*", resource="cpu"})

(La segona part ve de kube-state-metrics: és l'exemple perfecte de per què calen les dues fonts.)

4. Connexions de PostgreSQL.

Font: exportador sidecar de 06-04, més les mètriques pròpies del pool.

# Connexions en ús davant del màxim configurat
pg_stat_database_numbackends{datname="reserves"} / pg_settings_max_connections

# El pool de l'aplicació, que és el que es va exhaurir en el cas de 07-01
max(rutasnorte_connexions_pool_actives{entorn="pro"})

# Interbloqueigs: símptoma de contenció greu
increase(pg_stat_database_deadlocks{datname="reserves"}[5m])

Un numbackends / max_connections proper a 1 confirma que la base de dades estava saturada de connexions.

5. Va fallar la passarel·la externa o vam fallar nosaltres?

Font: instrumentació pròpia.

# Taxa d'error de la passarel·la
sum(rate(rutasnorte_passarela_pagaments_crides_total{resultat=~"error|timeout"}[5m]))
  /
sum(rate(rutasnorte_passarela_pagaments_crides_total[5m]))

# Desglossament: error o timeout? El timeout apunta a lentitud del proveïdor
sum by (resultat) (rate(rutasnorte_passarela_pagaments_crides_total[5m]))

Interpretació: si la taxa d'error de la passarel·la puja al 40 % mentre el nostre ratio_errors general també puja, l'error és del proveïdor i cal reclamar-l'hi. Si la passarel·la està al 0,1 % d'errors i nosaltres retornem 5xx, el problema és nostre. Aquesta distinció, impossible sense instrumentació, és el que es porta a la reunió del dilluns.

6. Reserves deixades de confirmar.

Font: instrumentació pròpia (mètrica de negoci).

# Reserves confirmades durant l'hora i mitja de l'incident
increase(rutasnorte_reserves_confirmades_total{entorn="pro"}[90m])

# El mateix el dissabte anterior a la mateixa hora
increase(rutasnorte_reserves_confirmades_total{entorn="pro"}[90m] offset 7d)

# Pèrdua estimada: reserves per minut ara vs. una setmana abans
sum(rate(rutasnorte_reserves_confirmades_total{entorn="pro"}[5m])) * 60

I un encreuament especialment revelador: si el trànsit es va multiplicar per 5,8 però les reserves confirmades només per 1,3, la diferència entre tots dos factors és una estimació directa del negoci perdut. Aquest número, en euros, és el que justifica el pressupost d'infraestructura.

Solució 3

1. Mètriques del component.

Nom Tipus Etiquetes Cardinalitat Justificació
rutasnorte_correus_enviats_total counter tipus, resultat 3 × 3 = 9 Volum i errors. tipus: confirmacio, cancellacio, recordatori. resultat: ok, error, rebutjat
rutasnorte_correu_duracio_segons histogram tipus 3 × 9 cubetes = 27 Latència de l'enviament, inclosa la crida al servidor SMTP
rutasnorte_cua_pendents gauge cap 1 La mètrica de saturació clau: si la cua creix sense parar, el worker no dóna l'abast
rutasnorte_cua_espera_segons gauge cap 1 Antiguitat del missatge més vell de la cua: mesura el retard percebut pel client
rutasnorte_reintents_total counter motiu 4 Reintents per tipus d'error: smtp_timeout, smtp_rebuig, xarxa, desconegut
rutasnorte_worker_cicle_actiu gauge cap 1 1 si el bucle de consum és viu. Alimenta també la liveness de 07-01

Cardinalitat total: unes 43 sèries per pod. Perfectament assumible.

Etiquetes descartades deliberadament i per què:

  • destinatari (correu del client): cardinalitat il·limitada i, sobretot, dada personal al sistema de monitoratge. Prohibit. Hi tornarem a 07-05.
  • id_reserva: cardinalitat il·limitada. Aquella dada pertany als logs, no a les mètriques. Les mètriques agreguen; els logs detallen.
  • missatge_error complet: cardinalitat impredictible. Se substitueix per motiu, un conjunt tancat de categories.

2. ServiceMonitor per a un component sense Service.

worker-notificacions no rep trànsit, així que no té Service. Dues opcions vàlides:

Opció A (recomanada): PodMonitor. És exactament el recurs dissenyat per a aquest cas.

apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: worker-notificacions
  namespace: rutas-norte-pro
  labels:
    app: worker-notificacions
    app.kubernetes.io/part-of: rutas-norte
spec:
  selector:
    matchLabels:            # aquí SÍ que es comparen amb etiquetes de POD
      app: worker-notificacions
      entorn: pro
  namespaceSelector:
    matchNames:
      - rutas-norte-pro
  podMetricsEndpoints:
    - port: metriques       # nom del port a l'spec del CONTENIDOR
      path: /metrics
      interval: 30s
      scrapeTimeout: 10s

Amb el port declarat al Deployment:

containers:
  - name: worker
    image: registry.rutasnorte.example/worker-notificacions:3.4.2
    ports:
      - name: metriques
        containerPort: 9100

Opció B: crear un Service headless només per a mètriques i fer servir un ServiceMonitor normal. Funciona, però crea un objecte que no aporta res més. El PodMonitor és més net.

I la NetworkPolicy corresponent:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: permetre-scrape-worker-notificacions
  namespace: rutas-norte-pro
spec:
  podSelector:
    matchLabels:
      app: worker-notificacions
      entorn: pro
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: monitoratge
      ports:
        - protocol: TCP
          port: 9100

3. Els quatre indicadors daurats.

# ---- LATÈNCIA: p95 del temps d'enviament d'un correu ----
histogram_quantile(0.95,
  sum by (le, tipus) (
    rate(rutasnorte_correu_duracio_segons_bucket{entorn="pro"}[5m])))

# ---- TRÀNSIT: correus enviats per minut ----
sum by (tipus) (rate(rutasnorte_correus_enviats_total{entorn="pro"}[5m])) * 60

# ---- ERRORS: fracció d'enviaments fallits ----
sum(rate(rutasnorte_correus_enviats_total{entorn="pro", resultat!="ok"}[5m]))
  /
sum(rate(rutasnorte_correus_enviats_total{entorn="pro"}[5m]))

# ---- SATURACIÓ: la cua, en tres angles complementaris ----

# a) Profunditat actual de la cua
max(rutasnorte_cua_pendents{entorn="pro"})

# b) Està creixent? Derivada de la profunditat en l'última mitja hora.
#    Positiu sostingut = el worker no dóna l'abast i cal escalar.
deriv(rutasnorte_cua_pendents{entorn="pro"}[30m])

# c) L'indicador que més importa al client: quant espera el correu més
#    antic. Un client que va pagar fa 10 minuts i no ha rebut res
#    comença a dubtar de si la seva compra s'ha fet.
max(rutasnorte_cua_espera_segons{entorn="pro"})

Consulta addicional de cobertura, molt valuosa per al negoci: relaciona dos components diferents per detectar correus perduts.

# Reserves confirmades menys correus de confirmació enviats l'última
# hora. Hauria de ser pràcticament zero. Un valor positiu gran significa
# que hi ha clients que han pagat i no han rebut res.
increase(rutasnorte_reserves_confirmades_total{entorn="pro"}[1h])
  -
increase(rutasnorte_correus_enviats_total{entorn="pro", tipus="confirmacio", resultat="ok"}[1h])

Aquest tipus de consulta —que creua mètriques de dos components per verificar una invariant del negoci— és de les més útils que es poden escriure, i a 07-04 la convertirem en alerta.

Conclusió

Rutas Norte ha passat de no recordar res a tenir memòria, llenguatge i capacitat de pregunta. En aquesta lliçó hem:

  • Entès el model d'extracció i per què és superior al d'empenta en un entorn dinàmic com Kubernetes.
  • Après el model de dades: sèries identificades per nom i etiquetes, l'amenaça permanent de la cardinalitat, i els quatre tipus de mètrica, amb l'histogram com l'únic que permet calcular percentils agregats entre rèpliques.
  • Aclarit la confusió clàssica sobre d'on surten les mètriques: cAdvisor mesura el consum real dels contenidors, node-exporter el sistema operatiu del node, kube-state-metrics l'estat dels objectes de l'API, els exportadors tradueixen programari de tercers —inclòs el sidecar de postgres-reserves que vam afegir a 06-04, ara per fi connectat— i la instrumentació pròpia és l'única que sap de negoci.
  • Instrumentat api-reserves amb peticions per ruta i codi, latència en histograma, reserves confirmades i l'estat del pool de connexions, tenint cura de la cardinalitat en cada decisió.
  • Desplegat el kube-prometheus-stack i treballat amb el Prometheus Operator que vam anunciar a 06-07, creant ServiceMonitor que viuen al costat de cada aplicació i sabent diagnosticar per què un objectiu no apareix.
  • Escrit PromQL de debò: rate, increase, sum by, topk, histogram_quantile, i les consultes concretes dels quatre indicadors daurats de cada component. Entre elles, la que confirma el throttling que a 07-02 només podíem sospitar.
  • Dimensionat l'emmagatzematge i la retenció, i entès per què Prometheus no és un arxiu etern i quan tocarà mirar cap a Thanos o Mimir.

Però continuem tenint un problema pràctic: tot això viu en una interfície web austera on cal escriure consultes a mà, i ningú l'està mirant a les tres de la matinada. Tenir la dada no serveix de res si ningú la veu ni ningú rep l'avís.

A 07-04 farem aquest salt: construirem amb Grafana el quadre de comandament de Rutas Norte, panell a panell, amb variables que serveixin per als tres entorns; escriurem el catàleg d'alertes de la plataforma amb PrometheusRule —inclosa la que prediu quan s'omplirà el disc de postgres-reserves abans que passi—; i configurarem Alertmanager perquè l'alerta correcta arribi a la persona correcta, agrupada, sense soroll i amb un enllaç al procediment que cal seguir.

Curs de Kubernetes

Mòdul 1: Introducció a Kubernetes

Mòdul 2: Components Principals de Kubernetes

Mòdul 3: Gestió de Configuració i Secrets

Mòdul 4: Xarxes a Kubernetes

Mòdul 5: Emmagatzematge a Kubernetes

Mòdul 6: Conceptes Avançats de Kubernetes

Mòdul 7: Monitoratge i Registre

Mòdul 8: Seguretat a Kubernetes

Mòdul 9: Escalat i Rendiment

Mòdul 10: Ecosistema i Eines de Kubernetes

Mòdul 11: Estudis de Cas i Aplicacions del Món Real

Mòdul 12: Preparació per a la Certificació de Kubernetes

© Copyright 2026. Tots els drets reservats