Al final de la lliçó anterior, Rutas Norte tenia per fi memòria: Prometheus desa trenta dies de mètriques de tots els components i li podem preguntar qualsevol cosa en PromQL. Però va quedar un problema obert: tot això viu en una interfície austera on cal escriure consultes a mà, i ningú l'està mirant a les tres de la matinada. Una dada que ningú veu i de la qual ningú rep avís no ha resolt res.
Aquesta lliçó fa aquest salt: de la consulta solta al quadre de comandament que explica una història d'un cop d'ull, i d'aquí a l'alerta que desperta algú perquè de debò cal. Veurem Grafana per visualitzar, PrometheusRule per definir quan alguna cosa va malament, i Alertmanager per decidir a qui s'avisa, quan i amb quina agrupació. I sobretot veurem el criteri que separa un sistema d'alertes útil d'un generador de soroll que tothom acaba silenciant: alertar sobre símptomes que percep el client, no sobre causes.
Contingut
- Grafana: què és i com es connecta a Prometheus
- Anatomia d'un panell i els tipus que de debò es fan servir
- Variables de panell: un quadre de comandament per als tres entorns
- Importar quadres de comandament de la comunitat
- El quadre de comandament de Rutas Norte, panell a panell
- Proveir quadres de comandament com a codi
- Regles d'alerta amb
PrometheusRulei la importància defor - El catàleg d'alertes de Rutas Norte
- Alertmanager: rutes, receptors, inhibició i silencis
- El criteri: alertar sobre símptomes, no sobre causes
- SLO i pressupost d'error de Rutas Norte
- Errors comuns i consells
- Exercicis
- Grafana: què és i com es connecta a Prometheus
Grafana és una eina de visualització que es connecta a fonts de dades —Prometheus, Elasticsearch, PostgreSQL, desenes més— i dibuixa quadres de comandament. No emmagatzema mètriques: només consulta i pinta. Si Prometheus cau, Grafana es queda en blanc.
Aquesta separació de responsabilitats és deliberada i molt sana: Prometheus s'especialitza a recollir, desar i avaluar; Grafana a mostrar. Cadascun fa bé una cosa.
Quan a 07-03 vam instal·lar el kube-prometheus-stack, Grafana va venir inclosa i amb la font de dades ja configurada. El chart crea automàticament la connexió al Service de Prometheus, així que no cal introduir cap URL a mà.
# Comprovar que Grafana està corrent
kubectl -n monitoratge get pods -l app.kubernetes.io/name=grafanaFixa't en el 3/3: a més del contenidor de Grafana hi ha dos sidecars, un que vigila els ConfigMaps de quadres de comandament i un altre els de fonts de dades. El primer serà clau a l'apartat 6.
Accés
Les credencials inicials són en un Secret creat pel chart:
kubectl -n monitoratge get secret monitoratge-grafana \
-o jsonpath='{.data.admin-user}' | base64 -d; echo
kubectl -n monitoratge get secret monitoratge-grafana \
-o jsonpath='{.data.admin-password}' | base64 -d; echoAbans de continuar. Aquell valor el vam posar al fitxer de valors de Helm a 07-03, i a
rutas-norte-proés inacceptable per dos motius: està en text pla en un fitxer versionat a Git, i és una contrasenya feble. En producció cal (a) generar la contrasenya com a Secret extern i referenciar-la ambadmin.existingSecret, i (b) millor encara, delegar l'autenticació en el proveïdor d'identitat de l'empresa (OAuth o LDAP), desactivant l'usuari local. Grafana dóna accés de lectura a totes les mètriques de la plataforma, incloses les de negoci.
A rutas-norte-pro publicaríem Grafana amb un Ingress i TLS gestionat per cert-manager, exactament com vam fer a 04-04 i 04-05:
# k8s/entorns/pro/ingress-grafana.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grafana
namespace: monitoratge
annotations:
cert-manager.io/cluster-issuer: letsencrypt-produccio
nginx.ingress.kubernetes.io/auth-type: basic # capa extra
nginx.ingress.kubernetes.io/auth-secret: grafana-basic-auth
spec:
ingressClassName: nginx
tls:
- hosts: [metriques.rutasnorte.example]
secretName: grafana-tls
rules:
- host: metriques.rutasnorte.example
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: monitoratge-grafana
port:
number: 80Verificar la font de dades
A la interfície: Connections → Data sources → Prometheus. Ha d'aparèixer configurada i amb el botó "Save & test" retornant verd. La URL interna que fa servir és el DNS del Service (04-03):
Un detall de configuració que convé ajustar: el camp Scrape interval ha de coincidir amb l'interval dels teus ServiceMonitor (30 s en el nostre cas). Grafana el fa servir per calcular la variable $__rate_interval, que veurem de seguida.
- Anatomia d'un panell i els tipus que de debò es fan servir
Un panell de Grafana té quatre parts, i entendre-les evita el 90 % dels quadres de comandament illegibles.
- La consulta
Una o diverses expressions PromQL. Grafana ofereix dos modes: el constructor visual (útil per aprendre) i el mode codi (el que faràs servir tan bon punt sàpigues PromQL).
Dues variables especials que Grafana substitueix automàticament:
| Variable | Què és | Quan fer-la servir |
|---|---|---|
$__rate_interval |
Finestra adaptada al zoom i a l'interval d'escaneig | Sempre dins de rate() |
$__interval |
Resolució del gràfic segons l'amplada en píxels | En increase() sobre finestres variables |
Fes servir sempre rate(metrica[$__rate_interval]) en lloc de rate(metrica[5m]). Amb la finestra fixa, en fer zoom a 7 dies el gràfic es torna sorollós o produeix forats; $__rate_interval s'adapta i garanteix que sempre hi hagi almenys quatre mostres a la finestra.
- La llegenda amb plantilles d'etiquetes
Per defecte, Grafana mostra la llegenda amb totes les etiquetes de la sèrie:
{__name__="rutasnorte_peticions_total", codi="200", component="api-reserves", entorn="pro", instance="10.244.2.17:9090", job="api-reserves", metode="GET", namespace="rutas-norte-pro", pod="api-reserves-7d9f8c4b5-x2klm", ruta="/api/rutes", versio="2.8.1"}Illegible. Amb una plantilla al camp Legend:
Regla pràctica: la llegenda ha de cabre en una línia i contenir només el que distingeix aquella sèrie de les altres del panell.
- Unitats i llindars
A Standard options → Unit. Aquest ajust és més important del que sembla:
| Mètrica | Unitat correcta | Sense ella es veu |
|---|---|---|
| Latència en segons | seconds (s) |
0.412 en lloc de 412 ms |
| Taxa d'error (0-1) | Percent (0.0-1.0) |
0.023 en lloc de 2,3 % |
| Memòria en bytes | bytes (IEC) |
536870912 en lloc de 512 MiB |
| Peticions per segon | requests/sec (rps) |
Un número nu sense context |
Els llindars (Thresholds) coloregen el panell segons el valor. Per a la taxa d'error d'api-reserves: verd fins a 0,01, groc de 0,01 a 0,05, vermell per sobre. Un quadre de comandament ben pintat permet detectar el problema des de l'altre costat de l'oficina, sense llegir cap número.
- El tipus de visualització
De les desenes que ofereix Grafana, a la pràctica se'n fan servir quatre:
| Tipus | Quan fer-lo servir | Exemple a Rutas Norte |
|---|---|---|
| Time series | Evolució d'un valor en el temps. El 70 % dels panells | Peticions/s, latència p95, memòria |
| Stat | Un número gran, l'estat actual d'un indicador | Reserves confirmades l'última hora |
| Table | Comparar l'estat de diverses entitats alhora | Estat de tots els pods, amb reinicis i edat |
| Heatmap | Distribució completa d'un histograma en el temps | Distribució de latències, no només el p95 |
Sobre el mapa de calor: és el tipus més infrautilitzat i el que més informació dóna sobre latència. Un p95 de 400 ms pot amagar dues poblacions molt diferents (una massa a 20 ms i una cua a 3 s) o una distribució uniforme. El mapa de calor les distingeix d'un cop d'ull, i s'alimenta directament de les cubetes de l'histograma:
Amb el format de la consulta posat a Heatmap i l'opció Format: Heatmap activada.
Tipus que convé evitar: els indicadors d'agulla (gauge) ocupen molt i diuen poc, i els pastissos són gairebé sempre una mala elecció per a dades temporals.
- Variables de panell: un quadre de comandament per als tres entorns
Sense variables, hauries de duplicar el quadre de comandament tres vegades: un per a rutas-norte-dev, un altre per a pre i un altre per a pro. Tres còpies que es desincronitzen a la primera modificació.
Les variables converteixen el quadre de comandament en una plantilla amb desplegables a la part superior.
Definir-les
A Dashboard settings → Variables.
Variable entorn (tipus Query):
Nom: entorn
Tipus: Query
Data source: Prometheus
Query: label_values(rutasnorte_peticions_total, entorn)
Sort: Alphabetical (asc)
Multi-value: Nolabel_values(metrica, etiqueta) és una funció específica de Grafana que retorna tots els valors diferents que té aquella etiqueta. El desplegable s'omple sol amb dev, pre i pro, i si demà afegim un quart entorn apareixerà automàticament.
Variable namespace (dependent de l'anterior):
Variable component (multivalor):
Nom: component
Query: label_values(kube_pod_info{namespace="$namespace"}, created_by_name)
Multi-value: Sí
Include All: Sí
All value: .*Amb Multi-value pots seleccionar-ne diversos alhora; amb Include All, l'opció "All".
Variable interval (tipus Interval, per ajustar la finestra de les taxes):
Fer-les servir a les consultes
# Amb una variable simple
sum(rate(rutasnorte_peticions_total{entorn="$entorn"}[$__rate_interval]))
# Amb una variable multivalor: OBLIGATORI fer servir =~ i el format regex
sum by (pod) (rate(container_cpu_usage_seconds_total{
namespace="$namespace",
pod=~"$component.*"
}[$__rate_interval]))Detall que causa molts maldecaps: amb variables multivalor, Grafana substitueix $component per (api-reserves|botiga-web). Això només funciona amb l'operador =~, mai amb =. Si fas servir =, la consulta no retorna res i no hi ha cap missatge d'error que ho expliqui.
Per forçar el format explícitament: ${component:regex} o ${component:pipe}.
El resultat
Un únic quadre de comandament, versionat una sola vegada, que serveix per als tres entorns. Canvies el desplegable de pro a pre i veus els mateixos panells amb les dades de l'altre entorn. I si afegim un component nou amb les etiquetes de la convenció de Rutas Norte, apareix sol al desplegable.
- Importar quadres de comandament de la comunitat
Grafana té un catàleg públic amb milers de quadres de comandament. S'importen pel seu identificador numèric: Dashboards → New → Import → enganxar l'ID.
Els que de debò interessen per a un clúster de Kubernetes:
| ID | Quadre de comandament | Què mostra |
|---|---|---|
| 315 | Kubernetes cluster monitoring | Visió general del clúster: nodes, pods, xarxa |
| 1860 | Node Exporter Full | Tot el que exposa node-exporter, molt complet |
| 6417 | Kubernetes Cluster (Prometheus) | Recursos per namespace i per workload |
| 9628 | PostgreSQL Database | Per a l'exportador de postgres-reserves |
| 11835 | Redis Dashboard | Per a redis-cache |
| 7645 | NGINX Ingress Controller | Trànsit de l'Ingress de 04-04 |
A més, el kube-prometheus-stack ja porta instal·lats una vintena de quadres de comandament molt bons (ús per namespace, per pod, per node, estat del pla de control). Abans d'importar res, mira el que ja tens.
Per què convé revisar-los abans de fiar-se'n
Un quadre de comandament de la comunitat és codi escrit per un desconegut per al seu clúster, no per al teu. Comprovacions obligatòries:
- Existeixen les mètriques al teu Prometheus? Molts quadres fan servir noms de versions antigues de kube-state-metrics o de cAdvisor. Un panell buit no sempre significa "tot bé": pot significar "aquesta mètrica no existeix aquí".
- Coincideix el nom de la font de dades? En importar, Grafana demana mapar la font de dades. Si el quadre espera una anomenada
Prometheusi la teva es diu d'una altra manera, tots els panells fallen. - Són raonables les consultes per a la teva escala? Un panell amb
rate(...[1m])sobre 50 000 sèries pot trigar 20 segons i castigar Prometheus cada vegada que algú obre la pàgina. - Reflecteix la teva realitat? Un quadre genèric de Kubernetes no sap què és
api-reservesni què és una reserva confirmada. Serveix per a infraestructura; no substitueix el quadre de comandament propi. - S'ha actualitzat recentment? Un quadre de 2019 farà servir mètriques que ja no existeixen.
Estratègia recomanada per a Rutas Norte:
- Quadres de la comunitat per a infraestructura genèrica (nodes, PostgreSQL, Redis, Ingress). No hi aportem res reinventant-los.
- Quadre propi per a la plataforma: els quatre indicadors daurats dels nostres components i les mètriques de negoci. Ningú de la comunitat pot escriure això per nosaltres.
- El quadre de comandament de Rutas Norte, panell a panell
Dissenyem el quadre de comandament principal. Estructura: una fila de resum a dalt i una fila per component a sota, totes plegables.
flowchart TB
subgraph DASH["Quadre de comandament: Plataforma Rutas Norte — [entorn] [namespace]"]
subgraph F0["Fila 0: Resum executiu"]
A["Reserves/min<br/>Stat"]
B["Taxa d'error<br/>Stat"]
C["Latència p95<br/>Stat"]
D["Pods no llestos<br/>Stat"]
end
subgraph F1["Fila 1: api-reserves"]
E["Trànsit"] --- F["Errors"] --- G["Latència p50/p95/p99"] --- H["Saturació"]
end
subgraph F2["Fila 2: postgres-reserves"]
I["Connexions"] --- J["Transaccions/s"] --- K["Mida al disc"] --- L["Interbloqueigs"]
end
subgraph F3["Fila 3: resta de components"]
M["botiga-web"] --- N["redis-cache"] --- O["worker-notificacions"] --- P["informes-ocupacio"]
end
end
Fila 0 — Resum executiu
Quatre panells Stat que responen d'un cop d'ull a "va bé la plataforma?".
Panell 1: Reserves confirmades per minut. La mètrica del negoci.
- Tipus: Stat, amb gràfic de tendència de fons (Graph mode: Area).
- Unitat:
short, sufixres/min. - Llindars: vermell per sota d'1, groc fins a 5, verd per sobre.
- Per què és el primer panell del quadre: si això cau a zero, tant se val que els pods estiguin
Running. La plataforma existeix per vendre bitllets.
Panell 2: Taxa d'error.
sum(rate(rutasnorte_peticions_total{entorn="$entorn", codi=~"5.."}[$__rate_interval]))
/
sum(rate(rutasnorte_peticions_total{entorn="$entorn"}[$__rate_interval]))- Unitat:
Percent (0.0-1.0). - Llindars: verd < 0,5 %, groc < 2 %, vermell ≥ 2 %.
Panell 3: Latència p95 global. Fem servir la regla de gravació que vam crear a 07-03:
- Unitat:
seconds (s). Llindars: verd < 0,3 s, groc < 1 s, vermell ≥ 1 s.
Panell 4: Pods no llestos. Directament de kube-state-metrics:
sum(kube_deployment_status_replicas_unavailable{namespace="$namespace"})
+
sum(kube_statefulset_status_replicas_current{namespace="$namespace"}
- kube_statefulset_status_replicas_ready{namespace="$namespace"})- Llindars: verd 0, vermell ≥ 1. Qualsevol valor diferent de zero és una anomalia.
Fila 1 — api-reserves: els quatre indicadors daurats
Trànsit (Time series):
Llegenda: {{ruta}}. Unitat: reqps.
Errors (Time series, amb dues consultes superposades):
# A: taxa d'error de la nostra plataforma
sum(rate(rutasnorte_peticions_total{entorn="$entorn", codi=~"5.."}[$__rate_interval]))
/ sum(rate(rutasnorte_peticions_total{entorn="$entorn"}[$__rate_interval]))
# B: taxa d'error de la passarel·la externa
sum(rate(rutasnorte_passarela_pagaments_crides_total{entorn="$entorn", resultat=~"error|timeout"}[$__rate_interval]))
/ sum(rate(rutasnorte_passarela_pagaments_crides_total{entorn="$entorn"}[$__rate_interval]))Superposar totes dues és una decisió de disseny deliberada: permet veure d'un cop si els nostres errors coincideixen amb els del proveïdor extern. Aquesta correlació visual estalvia vint minuts d'investigació en un incident.
Latència (Time series, tres percentils):
histogram_quantile(0.50, sum by (le) (rate(rutasnorte_duracio_peticio_segons_bucket{entorn="$entorn"}[$__rate_interval])))
histogram_quantile(0.95, sum by (le) (rate(rutasnorte_duracio_peticio_segons_bucket{entorn="$entorn"}[$__rate_interval])))
histogram_quantile(0.99, sum by (le) (rate(rutasnorte_duracio_peticio_segons_bucket{entorn="$entorn"}[$__rate_interval])))Llegendes: p50, p95, p99. Veure els tres junts és el que distingeix "tot va lent" (els tres pugen) de "hi ha una cua de peticions patològiques" (només puja el p99).
Saturació (Time series, dos eixos):
# CPU consumida davant del límit
sum by (pod) (rate(container_cpu_usage_seconds_total{namespace="$namespace", pod=~"api-reserves-.*", container="api"}[$__rate_interval]))
/ sum by (pod) (kube_pod_container_resource_limits{namespace="$namespace", pod=~"api-reserves-.*", resource="cpu"})
# Fracció de períodes estrangulats: la confirmació del throttling
sum by (pod) (rate(container_cpu_cfs_throttled_periods_total{namespace="$namespace", pod=~"api-reserves-.*"}[$__rate_interval]))
/ sum by (pod) (rate(container_cpu_cfs_periods_total{namespace="$namespace", pod=~"api-reserves-.*"}[$__rate_interval]))
# El pool de connexions: el fenomen de 07-01, ara visible
max(rutasnorte_connexions_pool_actives{entorn="$entorn"}) / 20Fila 2 — postgres-reserves
Tot del sidecar exportador de 06-04, ja connectat a 07-03:
# Connexions respecte al màxim
pg_stat_database_numbackends{datname="reserves"} / pg_settings_max_connections
# Transaccions per segon (confirmades i revertides)
rate(pg_stat_database_xact_commit{datname="reserves"}[$__rate_interval])
rate(pg_stat_database_xact_rollback{datname="reserves"}[$__rate_interval])
# Mida de la base de dades, amb projecció
pg_database_size_bytes{datname="reserves"}
# Espai lliure al PVC (ve del kubelet, no de l'exportador)
kubelet_volume_stats_available_bytes{persistentvolumeclaim="dades-postgres-reserves-0"}
/ kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="dades-postgres-reserves-0"}
# Interbloqueigs: zero és el normal, qualsevol cosa diferent mereix mirar-se
increase(pg_stat_database_deadlocks{datname="reserves"}[$__interval])Fila 3 — Resta de components
worker-notificacions — el panell més important és la profunditat de la cua:
max(rutasnorte_cua_pendents{entorn="$entorn"})
max(rutasnorte_cua_espera_segons{entorn="$entorn"})
sum(rate(rutasnorte_correus_enviats_total{entorn="$entorn", resultat="ok"}[$__rate_interval])) * 60informes-ocupacio — el CronJob de 06-03. No té indicadors daurats clàssics:
# Segons des de l'última execució amb èxit
time() - kube_job_status_completion_time{job_name=~"informes-ocupacio.*"}
# Durada de l'última execució
kube_job_status_completion_time{job_name=~"informes-ocupacio.*"}
- kube_job_status_start_time{job_name=~"informes-ocupacio.*"}Panell d'estat general (Table), molt útil com a resum:
Amb transformacions per mostrar pod, contenidor, reinicis i edat en columnes ordenables.
Un consell de disseny
Un quadre de comandament ha de respondre a una pregunta, no mostrar-ho tot. El nostre respon a "està la plataforma servint bé els clients i, si no, on és el problema?". Quadres amb quaranta panells no els mira ningú; vuit panells ben triats es miren cada dia.
- Proveir quadres de comandament com a codi
Si construeixes el quadre de comandament clicant a la interfície, viu a la base de dades SQLite interna de Grafana. I aquella base de dades, en un pod sense volum persistent, desapareix quan el pod es recrea. És un desastre que li passa a tothom una vegada.
A més, els quadres de comandament són configuració: han d'estar a Git, revisar-se en una petició de canvi i desplegar-se igual que la resta de manifests.
El mecanisme del sidecar
El chart desplega al costat de Grafana un contenidor sidecar que vigila tots els ConfigMaps del clúster buscant una etiqueta concreta. Quan en troba un, escriu el seu contingut al directori de quadres de comandament de Grafana, que el carrega automàticament.
# Fragment dels valors del chart (07-03) que activa el mecanisme
grafana:
sidecar:
dashboards:
enabled: true
label: grafana_dashboard # <-- l'etiqueta que busca
labelValue: "1"
searchNamespace: ALL # busca a tots els namespaces
folderAnnotation: grafana_folder
provider:
foldersFromFilesStructure: trueEl ConfigMap del quadre de comandament
# k8s/base/monitoratge/dashboard-rutas-norte.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: dashboard-rutas-norte
namespace: monitoratge
labels:
grafana_dashboard: "1" # el sidecar el detecta per aquesta etiqueta
app.kubernetes.io/part-of: rutas-norte
annotations:
grafana_folder: "Rutas Norte" # carpeta on apareixerà
data:
rutas-norte-general.json: |
{
"title": "Plataforma Rutas Norte — General",
"uid": "rutasnorte-general",
"tags": ["rutas-norte", "produccio"],
"timezone": "Europe/Madrid",
"refresh": "30s",
"time": { "from": "now-6h", "to": "now" },
"templating": {
"list": [
{
"name": "entorn",
"type": "query",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"query": "label_values(rutasnorte_peticions_total, entorn)",
"current": { "text": "pro", "value": "pro" },
"sort": 1
},
{
"name": "namespace",
"type": "query",
"datasource": { "type": "prometheus", "uid": "prometheus" },
"query": "label_values(kube_pod_info{namespace=~\"rutas-norte-$entorn\"}, namespace)",
"sort": 1
}
]
},
"panels": [
{
"id": 1,
"title": "Reserves confirmades per minut",
"type": "stat",
"gridPos": { "h": 5, "w": 6, "x": 0, "y": 0 },
"datasource": { "type": "prometheus", "uid": "prometheus" },
"targets": [
{
"expr": "sum(rate(rutasnorte_reserves_confirmades_total{entorn=\"$entorn\"}[$__rate_interval])) * 60",
"legendFormat": "reserves/min"
}
],
"fieldConfig": {
"defaults": {
"unit": "short",
"decimals": 1,
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "red", "value": null },
{ "color": "yellow","value": 1 },
{ "color": "green", "value": 5 }
]
}
}
},
"options": { "graphMode": "area", "colorMode": "background" }
},
{
"id": 2,
"title": "Taxa d'error 5xx",
"type": "stat",
"gridPos": { "h": 5, "w": 6, "x": 6, "y": 0 },
"datasource": { "type": "prometheus", "uid": "prometheus" },
"targets": [
{
"expr": "sum(rate(rutasnorte_peticions_total{entorn=\"$entorn\", codi=~\"5..\"}[$__rate_interval])) / sum(rate(rutasnorte_peticions_total{entorn=\"$entorn\"}[$__rate_interval]))",
"legendFormat": "taxa error"
}
],
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"decimals": 2,
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 0.005 },
{ "color": "red", "value": 0.02 }
]
}
}
}
},
{
"id": 3,
"title": "Latència d'api-reserves",
"type": "timeseries",
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 5 },
"datasource": { "type": "prometheus", "uid": "prometheus" },
"targets": [
{
"expr": "histogram_quantile(0.50, sum by (le) (rate(rutasnorte_duracio_peticio_segons_bucket{entorn=\"$entorn\"}[$__rate_interval])))",
"legendFormat": "p50"
},
{
"expr": "histogram_quantile(0.95, sum by (le) (rate(rutasnorte_duracio_peticio_segons_bucket{entorn=\"$entorn\"}[$__rate_interval])))",
"legendFormat": "p95"
},
{
"expr": "histogram_quantile(0.99, sum by (le) (rate(rutasnorte_duracio_peticio_segons_bucket{entorn=\"$entorn\"}[$__rate_interval])))",
"legendFormat": "p99"
}
],
"fieldConfig": {
"defaults": { "unit": "s", "custom": { "fillOpacity": 10 } }
}
}
]
}El flux de treball recomanat
No escriguis aquell JSON a mà. El procediment pràctic:
- Construeix el quadre de comandament a la interfície de Grafana, que és còmoda.
- Dashboard settings → JSON Model → Copiar.
- Enganxa'l al ConfigMap, dins de
data, amb la indentació correcta. kubectl applyi revisió en una petició de canvi.- A Grafana, marca aquell quadre de comandament com de només lectura per evitar que algú l'editi a la interfície i perdi els canvis al desplegament següent.
kubectl apply -f k8s/base/monitoratge/dashboard-rutas-norte.yaml
# El sidecar el detecta en segons; verificar-ho al seu log
kubectl -n monitoratge logs deploy/monitoratge-grafana -c grafana-sc-dashboard --tail=10{"time": "2026-08-06T11:42:03", "msg": "Working on configmap monitoratge/dashboard-rutas-norte"}
{"time": "2026-08-06T11:42:03", "msg": "Writing /tmp/dashboards/rutas-norte-general.json"}Quan arribem a 10-04 (Kustomize) i 10-05 (GitOps), aquest ConfigMap serà un artefacte més del repositori, desplegat automàticament en fer merge.
- Regles d'alerta amb
PrometheusRule i la importància de for
PrometheusRule i la importància de forGrafana té el seu propi motor d'alertes, però en un entorn amb el Prometheus Operator el natural és definir les alertes a Prometheus mitjançant el recurs PrometheusRule. Avantatges: viuen a Git al costat de l'aplicació, les avalua Prometheus (que és on són les dades) i les encamina Alertmanager.
Anatomia d'una regla
- alert: ApiReservesTaxaErrorAlta
expr: |
sum(rate(rutasnorte_peticions_total{entorn="pro", codi=~"5.."}[5m]))
/
sum(rate(rutasnorte_peticions_total{entorn="pro"}[5m]))
> 0.05
for: 5m
labels:
severity: critica
component: api-reserves
equip: plataforma
annotations:
summary: "api-reserves retorna més d'un 5% d'errors"
description: >
La taxa d'errors 5xx d'api-reserves en producció és del
{{ $value | humanizePercentage }} durant els últims 5 minuts.
Els clients no poden completar reserves.
runbook_url: "https://runbooks.rutasnorte.example/api-reserves-errors-5xx"
dashboard_url: "https://metriques.rutasnorte.example/d/rutasnorte-general"| Camp | Funció |
|---|---|
alert |
Nom de l'alerta. En PascalCase, descriptiu, sense espais |
expr |
Expressió PromQL. L'alerta està "activa" per a cada sèrie que retorni |
for |
Quant de temps s'ha de complir de manera continuada abans de disparar |
labels |
Metadades per a l'encaminament a Alertmanager. Aquí va la severitat |
annotations |
Text per a l'humà que la rep. No afecten l'encaminament |
La importància de for
Aquest és el camp que separa un sistema d'alertes usable d'un d'insuportable.
Sense for, l'alerta dispara tan bon punt l'expressió es compleix una sola vegada. Un pic de 30 segons per un desplegament, un reinici puntual o un escaneig perdut genera un avís. Multiplicat per vint alertes i tres entorns, és un canal de Slack que ningú llegeix.
Amb for: 5m, Prometheus observa l'expressió a cada cicle d'avaluació (per defecte cada 30 s) i només dispara si s'ha complert en totes les avaluacions d'aquests 5 minuts. Un sol cicle en què la condició no es compleixi reinicia el comptador.
Estats d'una alerta:
stateDiagram-v2
[*] --> Inactive: l'expressió no es compleix
Inactive --> Pending: l'expressió es compleix
Pending --> Inactive: deixa de complir-se abans d'esgotar for
Pending --> Firing: es compleix durant tot el període for
Firing --> Inactive: deixa de complir-se (s'envia la resolució)
Només en Firing s'envia res a Alertmanager. Pending és visible a la interfície de Prometheus (Alerts), cosa que és útil per depurar.
Criteri per triar for:
| Tipus d'alerta | for recomanat |
Raó |
|---|---|---|
Caiguda total (up == 0) |
2m |
Ràpida, però tolera un reinici o un desplegament |
| Taxa d'error | 5m |
Un pic curt no ha de despertar ningú |
| Latència alta | 10m |
Molt sorollosa si és més curta |
| Disc omplint-se | 15m |
És una tendència, no un esdeveniment |
| Certificat caducant | 1h |
No hi ha cap pressa |
Pod en CrashLoopBackOff |
10m |
Tolera una arrencada lenta legítima |
Anotacions i plantilles
Les anotacions admeten plantilles Go amb accés al valor i a les etiquetes:
| Expressió | Resultat |
|---|---|
{{ $value }} |
0.0734829 |
{{ $value | humanizePercentage }} |
7.35% |
{{ $value | humanize }} |
73.5m |
{{ $value | humanizeDuration }} |
1h 12m 30s |
{{ $labels.pod }} |
api-reserves-7d9f8c4b5-x2klm |
{{ $labels.namespace }} |
rutas-norte-pro |
El runbook_url no és opcional. Una alerta que arriba a les tres de la matinada a algú que no va escriure el codi i conté només "ApiReservesTaxaErrorAlta" és inútil. Amb un enllaç a un procediment escrit —què comprovar, en quin ordre, què fer, a qui escalar— l'alerta és accionable. Els runbooks complets són matèria d'11-06, però l'enllaç es posa des del primer dia.
- El catàleg d'alertes de Rutas Norte
# k8s/base/monitoratge/alertes-rutas-norte.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: alertes-rutas-norte
namespace: monitoratge
labels:
app.kubernetes.io/part-of: rutas-norte
prometheus: monitoratge
spec:
groups:
# =====================================================================
# GRUP 1: SÍMPTOMES. El que el client percep. Màxima prioritat.
# =====================================================================
- name: rutasnorte.simptomes
interval: 30s
rules:
- alert: PlataformaSenseVendes
expr: |
sum(rate(rutasnorte_reserves_confirmades_total{entorn="pro"}[10m])) == 0
and
sum(rate(rutasnorte_peticions_total{entorn="pro"}[10m])) > 0.5
for: 10m
labels:
severity: critica
equip: plataforma
annotations:
summary: "Rutas Norte no ha confirmat CAP reserva en 10 minuts"
description: >
Hi ha trànsit entrant ({{ $value | humanize }} peticions/s) però
zero reserves confirmades. La plataforma no està venent.
Aquesta és l'alerta més important del sistema.
runbook_url: "https://runbooks.rutasnorte.example/sense-vendes"
# Justificació: la condició "hi ha trànsit" evita que dispari de
# matinada, quan és normal no vendre res durant deu minuts.
- alert: ApiReservesTaxaErrorAlta
expr: |
sum(rate(rutasnorte_peticions_total{entorn="pro", codi=~"5.."}[5m]))
/ sum(rate(rutasnorte_peticions_total{entorn="pro"}[5m])) > 0.05
for: 5m
labels:
severity: critica
component: api-reserves
equip: plataforma
annotations:
summary: "api-reserves retorna més d'un 5% d'errors"
description: >
Taxa d'error del {{ $value | humanizePercentage }} durant
5 minuts. Els clients no poden completar les seves compres.
runbook_url: "https://runbooks.rutasnorte.example/api-errors-5xx"
- alert: ApiReservesLatenciaAlta
expr: apireserves:latencia_p95:5m{entorn="pro"} > 1
for: 10m
labels:
severity: avis
component: api-reserves
equip: plataforma
annotations:
summary: "El p95 de latència d'api-reserves supera 1 segon"
description: >
El percentil 95 és de {{ $value | humanizeDuration }} a la
ruta {{ $labels.ruta }}. L'SLO de Rutas Norte són 500 ms.
runbook_url: "https://runbooks.rutasnorte.example/api-latencia"
- alert: BotigaWebCaiguda
expr: |
sum(kube_deployment_status_replicas_available{
namespace="rutas-norte-pro", deployment="botiga-web"}) == 0
for: 2m
labels:
severity: critica
component: botiga-web
equip: plataforma
annotations:
summary: "No queda cap rèplica de botiga-web disponible"
description: "www.rutasnorte.example està caiguda per a tots els clients."
runbook_url: "https://runbooks.rutasnorte.example/botiga-web-caiguda"
# =====================================================================
# GRUP 2: CAUSES I CAPACITAT. Avisen abans que hi hagi símptoma.
# =====================================================================
- name: rutasnorte.capacitat
interval: 60s
rules:
- alert: DiscPostgresSOmplira
expr: |
predict_linear(
kubelet_volume_stats_available_bytes{
persistentvolumeclaim="dades-postgres-reserves-0"}[6h],
4 * 3600
) < 0
for: 30m
labels:
severity: critica
component: postgres-reserves
equip: plataforma
annotations:
summary: "El disc de postgres-reserves s'omplirà en menys de 4 hores"
description: >
Segons la tendència de les últimes 6 hores, el volum
dades-postgres-reserves-0 es quedarà sense espai en menys de
4 hores. Queden {{ $value | humanize1024 }}B lliures.
Ampliar el PVC (procediment de 05-05) ABANS que passi:
una base de dades amb el disc ple deixa d'acceptar escriptures.
runbook_url: "https://runbooks.rutasnorte.example/ampliar-pvc-postgres"
# predict_linear ajusta una recta de regressió sobre la finestra [6h]
# i extrapola 4*3600 segons cap endavant. Si el resultat és
# negatiu, significa que la recta creua el zero abans de 4 hores.
# És la diferència entre avisar d'un problema i avisar del futur.
- alert: PostgresConnexionsExhaurintse
expr: |
pg_stat_database_numbackends{datname="reserves"}
/ pg_settings_max_connections > 0.85
for: 10m
labels:
severity: avis
component: postgres-reserves
equip: plataforma
annotations:
summary: "postgres-reserves al {{ $value | humanizePercentage }} de connexions"
description: >
Quan s'esgotin, api-reserves fallarà la readiness i sortirà
dels Endpoints. Revisar consultes lentes i el pool de l'API.
runbook_url: "https://runbooks.rutasnorte.example/postgres-connexions"
- alert: CertificatCaducaAviat
expr: |
(certmanager_certificate_expiration_timestamp_seconds - time()) / 86400 < 15
for: 1h
labels:
severity: avis
equip: plataforma
annotations:
summary: "El certificat {{ $labels.name }} caduca en {{ $value | humanize }} dies"
description: >
cert-manager hauria de renovar-lo automàticament (04-05). Que no
ho hagi fet indica un problema amb l'emissor o amb el repte ACME.
runbook_url: "https://runbooks.rutasnorte.example/certificats"
- alert: ContenidorAmbThrottlingSever
expr: |
sum by (namespace, pod, container) (
rate(container_cpu_cfs_throttled_periods_total{namespace=~"rutas-norte-.*"}[5m]))
/ sum by (namespace, pod, container) (
rate(container_cpu_cfs_periods_total{namespace=~"rutas-norte-.*"}[5m]))
> 0.30
for: 15m
labels:
severity: avis
equip: plataforma
annotations:
summary: "{{ $labels.container }} estrangulat el {{ $value | humanizePercentage }} del temps"
description: >
El contenidor està arribant al seu limits.cpu de manera sostinguda.
Revisar la recalibració de recursos de 07-02.
runbook_url: "https://runbooks.rutasnorte.example/throttling-cpu"
# =====================================================================
# GRUP 3: SALUT DELS OBJECTES. Font: kube-state-metrics.
# =====================================================================
- name: rutasnorte.workloads
interval: 60s
rules:
- alert: PodEnCrashLoop
expr: |
increase(kube_pod_container_status_restarts_total{
namespace=~"rutas-norte-.*"}[15m]) > 3
for: 10m
labels:
severity: avis
equip: plataforma
annotations:
summary: "{{ $labels.pod }} s'ha reiniciat més de 3 vegades en 15 min"
description: >
Contenidor {{ $labels.container }} a {{ $labels.namespace }}.
Causes freqüents: OOMKilled, error d'arrencada, o una
livenessProbe massa agressiva (07-01).
Recollir evidències amb 'kubectl logs --previous' ABANS de tocar res.
runbook_url: "https://runbooks.rutasnorte.example/crashloop"
- alert: CronJobInformesNoExecutat
expr: |
time() - max(kube_job_status_completion_time{
job_name=~"informes-ocupacio.*"}) > 100000
for: 30m
labels:
severity: avis
component: informes-ocupacio
equip: dades
annotations:
summary: "El CronJob informes-ocupacio no s'ha executat amb èxit"
description: >
Han passat {{ $value | humanizeDuration }} des de l'última
execució correcta. El CronJob nocturn s'hauria d'executar cada
24 hores. Sense informes, el departament comercial es queda cec.
runbook_url: "https://runbooks.rutasnorte.example/cronjob-informes"
# 100000 segons ≈ 27,8 h: un marge sobre les 24 h del cron que
# tolera un retard puntual sense generar falsos positius.
- alert: JobFallit
expr: kube_job_status_failed{namespace=~"rutas-norte-.*"} > 0
for: 5m
labels:
severity: avis
equip: plataforma
annotations:
summary: "El Job {{ $labels.job_name }} ha fallat"
runbook_url: "https://runbooks.rutasnorte.example/job-fallit"
- alert: CuaNotificacionsCreixent
expr: |
max(rutasnorte_cua_espera_segons{entorn="pro"}) > 900
for: 10m
labels:
severity: avis
component: worker-notificacions
equip: plataforma
annotations:
summary: "Correus de confirmació amb més de 15 minuts de retard"
description: >
El missatge més antic porta {{ $value | humanizeDuration }}
en cua. Hi ha clients que han pagat i no han rebut res.
runbook_url: "https://runbooks.rutasnorte.example/cua-notificacions"
# =====================================================================
# GRUP 4: META. Alertes sobre el propi sistema de monitoratge.
# =====================================================================
- name: rutasnorte.meta
interval: 60s
rules:
- alert: ObjectiuPrometheusCaigut
expr: up{namespace=~"rutas-norte-.*|monitoratge"} == 0
for: 5m
labels:
severity: avis
equip: plataforma
annotations:
summary: "Prometheus no pot escanejar {{ $labels.job }}"
description: >
Objectiu {{ $labels.instance }} inabastable. Revisar
NetworkPolicies, l'endpoint /metrics i la readiness del pod.
runbook_url: "https://runbooks.rutasnorte.example/objectiu-caigut"
- alert: HPASenseMetriques
expr: |
kube_horizontalpodautoscaler_status_condition{
condition="ScalingActive", status="false"} == 1
for: 5m
labels:
severity: critica
equip: plataforma
annotations:
summary: "L'HPA {{ $labels.horizontalpodautoscaler }} no pot escalar"
description: >
L'autoescalat està inactiu, probablement perquè
metrics-server no respon (07-02). Durant un pic de trànsit
la plataforma NO escalarà i ningú se n'adonarà.
runbook_url: "https://runbooks.rutasnorte.example/hpa-inactiu"Aquesta última alerta és la mesura preventiva que vam prometre a 07-02: l'error silenciós de l'HPA ja no pot passar desapercebut.
Aplicar i verificar:
kubectl apply -f k8s/base/monitoratge/alertes-rutas-norte.yaml
# L'operador ha carregat les regles?
kubectl -n monitoratge get prometheusrule alertes-rutas-norte
# Prometheus les està avaluant?
curl -s localhost:9090/api/v1/rules | jq -r '
.data.groups[] | select(.name | startswith("rutasnorte")) |
.rules[] | "\(.name)\t\(.state // "recording")"'PlataformaSenseVendes inactive
ApiReservesTaxaErrorAlta inactive
ApiReservesLatenciaAlta pending
BotigaWebCaiguda inactive
DiscPostgresSOmplira inactiveProvar una alerta abans de confiar-hi és imprescindible. Una manera segura a rutas-norte-dev:
# Provocar deliberadament que un objectiu caigui
kubectl -n rutas-norte-dev scale deployment api-reserves --replicas=0
# Esperar el for i comprovar que l'alerta passa a firing
- Alertmanager: rutes, receptors, inhibició i silencis
Prometheus decideix què va malament. Alertmanager decideix a qui li ho explica, quan i com.
El problema que resol
Imagina que rutas-norte-worker-2 cau a les 03:14. Prometheus dispara, en qüestió de segons:
- 6 alertes
PodEnCrashLoop(els pods que hi vivien). - 4 alertes
ObjectiuPrometheusCaigut. - 1
BotigaWebCaiguda. - 1
ApiReservesTaxaErrorAlta. - 1
NodeNotReady(de les regles que porta l'estoc).
Sense Alertmanager, la persona de guàrdia rep tretze notificacions en dos minuts i ha de reconstruir mentalment que totes són el mateix problema. Amb Alertmanager ben configurat en rep una, agrupada, amb el node caigut destacat i les derivades silenciades.
L'arbre de rutes
# k8s/base/monitoratge/alertmanager-config.yaml
apiVersion: v1
kind: Secret
metadata:
name: alertmanager-monitoratge-kube-pr-alertmanager
namespace: monitoratge
stringData:
alertmanager.yaml: |
global:
resolve_timeout: 5m
smtp_smarthost: 'smtp.rutasnorte.example:587'
smtp_from: '[email protected]'
# -----------------------------------------------------------------
# ARBRE DE RUTES: s'avalua de dalt a baix; la primera coincidència
# guanya, tret que es marqui continue: true.
# -----------------------------------------------------------------
route:
receiver: 'equip-plataforma-slack' # receptor per defecte
# Quines alertes s'agrupen en una mateixa notificació.
# Agrupar per alertname + namespace significa: "totes les
# PodEnCrashLoop de rutas-norte-pro arriben en un únic missatge".
group_by: ['alertname', 'namespace', 'component']
# Després de la PRIMERA alerta d'un grup nou, esperar 30 s per si
# n'arriben més i enviar-les juntes. És el que converteix 13 missatges
# en 1 quan cau un node.
group_wait: 30s
# Si arriben alertes NOVES a un grup ja notificat, esperar 5 min
# abans d'enviar l'actualització.
group_interval: 5m
# Si l'alerta continua activa, repetir l'avís cada 4 hores.
# Ni tan curt que saturi ni tan llarg que s'oblidi.
repeat_interval: 4h
routes:
# 1. Alertes de prova i de desenvolupament: a un canal a part, sense
# despertar ningú. Evita que el soroll de dev arribi a la guàrdia.
- matchers:
- namespace =~ "rutas-norte-(dev|pre)"
receiver: 'canal-desenvolupament'
group_wait: 5m
repeat_interval: 24h
# 2. Alertes crítiques de producció: PagerDuty (desperta algú)
# I A MÉS Slack, gràcies a continue: true.
- matchers:
- severity = "critica"
- namespace =~ "rutas-norte-pro|monitoratge"
receiver: 'guardia-pagerduty'
group_wait: 10s # les crítiques, amb menys espera
repeat_interval: 1h # i es recorden més sovint
continue: true
- matchers:
- severity = "critica"
receiver: 'equip-plataforma-slack'
# 3. Alertes de l'equip de dades: al seu propi canal.
- matchers:
- equip = "dades"
receiver: 'equip-dades-slack'
repeat_interval: 12h
# -----------------------------------------------------------------
# INHIBICIÓ: una alerta greu silencia les derivades.
# -----------------------------------------------------------------
inhibit_rules:
# Si la botiga web està caiguda del tot, no cal avisar
# també que la seva latència és alta.
- source_matchers:
- alertname = "BotigaWebCaiguda"
target_matchers:
- severity = "avis"
- component = "botiga-web"
equal: ['namespace']
# Si el node està caigut, no avisar de cada pod que hi ha.
- source_matchers:
- alertname = "NodeNotReady"
target_matchers:
- alertname =~ "PodEnCrashLoop|ObjectiuPrometheusCaigut"
equal: ['node']
# Regla general: si hi ha una crítica del mateix component,
# els avisos d'aquell component es callen.
- source_matchers:
- severity = "critica"
target_matchers:
- severity = "avis"
equal: ['component', 'namespace']
# -----------------------------------------------------------------
# RECEPTORS
# -----------------------------------------------------------------
receivers:
- name: 'equip-plataforma-slack'
slack_configs:
- api_url_file: /etc/alertmanager/secrets/slack/url
channel: '#alertes-plataforma'
send_resolved: true
title: '{{ if eq .Status "firing" }}🔴{{ else }}✅{{ end }} {{ .CommonLabels.alertname }}'
text: |
{{ range .Alerts }}
*{{ .Annotations.summary }}*
{{ .Annotations.description }}
Entorn: `{{ .Labels.namespace }}` · Severitat: `{{ .Labels.severity }}`
<{{ .Annotations.runbook_url }}|📖 Runbook> · <{{ .Annotations.dashboard_url }}|📊 Quadre de comandament>
{{ end }}
- name: 'guardia-pagerduty'
pagerduty_configs:
- routing_key_file: /etc/alertmanager/secrets/pagerduty/key
description: '{{ .CommonAnnotations.summary }}'
severity: 'critical'
details:
runbook: '{{ .CommonAnnotations.runbook_url }}'
namespace: '{{ .CommonLabels.namespace }}'
- name: 'canal-desenvolupament'
slack_configs:
- api_url_file: /etc/alertmanager/secrets/slack/url
channel: '#alertes-desenvolupament'
send_resolved: false
- name: 'equip-dades-slack'
slack_configs:
- api_url_file: /etc/alertmanager/secrets/slack/url
channel: '#alertes-dades'
send_resolved: true
- name: 'correu-responsables'
email_configs:
- to: '[email protected]'
send_resolved: trueEls quatre temps
| Paràmetre | Què controla | Valor típic | Si és massa curt | Si és massa llarg |
|---|---|---|---|---|
group_wait |
Espera abans del primer avís d'un grup | 30 s (10 s crítica) | Arriben missatges solts, no agrupats | Es retarda la detecció |
group_interval |
Espera abans d'avisar d'alertes noves del grup | 5 m | Spam en incidents que evolucionen | T'assabentes tard que empitjora |
repeat_interval |
Cada quant es recorda una alerta activa | 4 h (1 h crítica) | Fatiga i silenciament massiu | S'oblida un problema obert |
resolve_timeout |
Quant esperar sense dades abans de donar-la per resolta | 5 m | Falses resolucions | Alertes fantasma |
Inhibició
La inhibició és el que converteix tretze missatges en un. La sintaxi té tres parts:
source_matchers: l'alerta que silencia.target_matchers: les alertes que se silencien.equal: les etiquetes que han de coincidir entre totes dues. Sense això, unaBotigaWebCaigudaadevsilenciaria els avisos depro.
El camp equal és el que més s'oblida i el que fa que la inhibició sigui segura.
Silencis durant un manteniment
Un silenci és una supressió temporal creada per una persona, normalment abans d'una intervenció planificada: ampliar el PVC de PostgreSQL, migrar un node, fer un desplegament gran.
Des de la interfície d'Alertmanager (Silences → New Silence), o per API:
kubectl -n monitoratge port-forward svc/monitoratge-kube-pr-alertmanager 9093:9093 &
# Silenciar totes les alertes de postgres-reserves durant 2 hores
curl -s -X POST http://localhost:9093/api/v2/silences \
-H 'Content-Type: application/json' \
-d '{
"matchers": [
{"name": "component", "value": "postgres-reserves", "isRegex": false},
{"name": "namespace", "value": "rutas-norte-pro", "isRegex": false}
],
"startsAt": "2026-08-07T02:00:00Z",
"endsAt": "2026-08-07T04:00:00Z",
"createdBy": "joan.costa",
"comment": "Ampliació programada del PVC de postgres-reserves (tiquet OPS-1842)"
}'Bones pràctiques amb els silencis:
- Sempre amb data de fi. Un silenci indefinit és una alerta esborrada de facto, i tothom se n'oblida.
- Sempre amb comentari i tiquet. D'aquí a sis setmanes ningú recordarà per què existeix.
- El més específic possible. Silenciar
namespace=rutas-norte-prosencer durant una intervenció a la base de dades et deixa cec davant d'un problema no relacionat. - Revisar els silencis actius periòdicament. Un silenci oblidat ha amagat més d'un incident greu.
# Llistar silencis actius
curl -s http://localhost:9093/api/v2/silences | \
jq -r '.[] | select(.status.state=="active") |
"\(.id)\t\(.comment)\tfins \(.endsAt)"'
- El criteri: alertar sobre símptomes, no sobre causes
Tot l'anterior és mecànica. Això és criteri, i és el que determina si el sistema serveix.
El problema de la fatiga d'alertes
Un equip que rep quaranta notificacions al dia deixa de llegir-les en dues setmanes. Quan arriba la que de debò importa, es perd entre el soroll. Un sistema amb massa alertes és pitjor que un sense cap, perquè genera una falsa sensació de cobertura.
Regla 1: alertar sobre símptomes, no sobre causes
Un símptoma és una cosa que el client percep. Una causa és una cosa del sistema que pot traduir-se o no en símptoma.
| Causa (mala alerta) | Símptoma (bona alerta) | Per què |
|---|---|---|
"El pod api-reserves-x2klm s'ha reiniciat" |
"La taxa d'error d'api-reserves supera el 5 %" |
Amb 6 rèpliques i readiness, un reinici no afecta ningú |
| "La CPU del node està al 85 %" | "El p95 de latència supera 1 s" | Un node al 85 % pot ser perfectament sa |
| "Hi ha 4 rèpliques en lloc de 6" | "La botiga web està caiguda" | 4 rèpliques poden bastar de matinada |
"La memòria de redis-cache està al 70 %" |
"La taxa d'encerts de cau ha caigut" | El 70 % pot ser l'estat normal |
La prova definitiva: si aquesta alerta dispara a les tres de la matinada i ningú fa res, passa alguna cosa dolenta? Si la resposta és no, no hauria de despertar ningú.
Això no significa que les causes no es monitorin: es visualitzen al quadre de comandament i es consulten durant el diagnòstic. Simplement no desperten ningú.
Excepció legítima: les alertes de capacitat predictives. DiscPostgresSOmplira és una causa, no un símptoma. Es justifica perquè avisa amb hores d'antelació d'un símptoma catastròfic i irreversible que encara es pot evitar. Aquest és el criteri per fer excepcions: antelació suficient per actuar i conseqüència greu si no s'actua.
Regla 2: cada alerta ha de ser accionable
Abans de crear una alerta, respon per escrit a tres preguntes:
- Què ha de fer qui la rebi? Si la resposta és "mirar si s'arregla sol", no és una alerta: és un panell.
- Està escrit aquell procediment? Si no, escriu-lo abans d'activar l'alerta i posa'l a
runbook_url. - Pot actuar la persona de guàrdia, o cal escalar sempre? Si sempre cal escalar, encamina directament a l'equip que pot actuar.
Regla 3: revisar les alertes després de cada incident
Després de cada incident, dues preguntes:
- Ens va avisar alguna alerta? Si no, en falta una. Crea-la.
- Ens van avisar alertes que no van aportar res? Si sí, en sobra. Esborra-la o puja el seu llindar.
Sense aquesta revisió periòdica, el catàleg només creix i acaba sent soroll.
Nivells de severitat de Rutas Norte
| Severitat | Significat | Canal | Exemple |
|---|---|---|---|
critica |
Afecta clients ara. Requereix acció immediata, de dia o de nit | PagerDuty + Slack | PlataformaSenseVendes |
avis |
Afectarà si no s'actua. S'atén en horari laboral | Slack | PostgresConnexionsExhaurintse |
info |
Context. No requereix acció | Només al quadre de comandament | Un desplegament ha acabat |
Regla dura: si una alerta critica no justifica una trucada de telèfon a les quatre de la matinada, no és crítica.
- SLO i pressupost d'error de Rutas Norte
Els SLO (Service Level Objectives) formalitzen la pregunta "què significa que la plataforma funciona bé?" amb un número acordat amb el negoci.
Definir els SLO
Un SLO té tres parts: un indicador (SLI, què es mesura), un objectiu (quin valor ha d'assolir) i una finestra (en quant de temps s'avalua).
SLO de Rutas Norte, acordats amb la direcció:
| Servei | Indicador (SLI) | Objectiu (SLO) | Finestra |
|---|---|---|---|
api-reserves disponibilitat |
% de peticions sense error 5xx | 99,5 % | 30 dies |
api-reserves latència |
% de peticions sota 500 ms | 99,0 % | 30 dies |
botiga-web disponibilitat |
% de peticions sense error 5xx | 99,9 % | 30 dies |
| Correus de confirmació | % lliurats en menys de 5 min | 99,0 % | 30 dies |
Un matís important: 99,5 % no és 100 %, i això és deliberat. Perseguir el 100 % és infinitament car i frena qualsevol canvi. L'SLO reconeix que un percentatge d'error és acceptable.
El pressupost d'error
El pressupost d'error és el complement de l'SLO: l'error que et pots permetre.
SLO de disponibilitat d'api-reserves: 99,5 % en 30 dies
Pressupost d'error = 100 % − 99,5 % = 0,5 %
En temps: 30 dies × 24 h × 0,5 % = 3 h 36 min d'indisponibilitat al mes
En peticions: amb ~4 milions de peticions/mes → 20 000 peticions poden fallarAquest pressupost és una eina de decisió, no una dada curiosa:
- Si queda pressupost, l'equip pot desplegar, experimentar i assumir riscos. La velocitat de canvi no està limitada.
- Si el pressupost està esgotat, es congelen els canvis que no siguin correccions de fiabilitat fins al període següent. Es deixa d'afegir funcionalitat i s'arregla el que falla.
És un mecanisme objectiu que substitueix la discussió eterna entre "cal treure la funcionalitat nova" i "cal estabilitzar la plataforma".
Mesurar el pressupost en PromQL
- name: rutasnorte.slo
interval: 60s
rules:
# Ràtio d'èxit a la finestra de 30 dies
- record: apireserves:slo_disponibilitat:30d
expr: |
1 - (
sum(increase(rutasnorte_peticions_total{entorn="pro", codi=~"5.."}[30d]))
/
sum(increase(rutasnorte_peticions_total{entorn="pro"}[30d]))
)
# Pressupost d'error CONSUMIT, en tant per u.
# 0 = intacte, 1 = esgotat, >1 = SLO incomplert.
- record: apireserves:pressupost_error_consumit:30d
expr: |
(
sum(increase(rutasnorte_peticions_total{entorn="pro", codi=~"5.."}[30d]))
/
sum(increase(rutasnorte_peticions_total{entorn="pro"}[30d]))
) / 0.005
# SLO de latència: fracció de peticions sota 500 ms
- record: apireserves:slo_latencia:30d
expr: |
sum(increase(rutasnorte_duracio_peticio_segons_bucket{entorn="pro", le="0.5"}[30d]))
/
sum(increase(rutasnorte_duracio_peticio_segons_count{entorn="pro"}[30d]))Alertes per consum del pressupost
En lloc d'alertar sobre un llindar fix d'errors, s'alerta sobre la velocitat a la qual es consumeix el pressupost (burn rate). És més intel·ligent: tolera un pic curt però detecta ràpid una hemorràgia.
- alert: PressupostErrorConsumitRapid
expr: |
(
sum(rate(rutasnorte_peticions_total{entorn="pro", codi=~"5.."}[1h]))
/ sum(rate(rutasnorte_peticions_total{entorn="pro"}[1h]))
) > (14.4 * 0.005)
for: 5m
labels:
severity: critica
equip: plataforma
annotations:
summary: "S'està consumint el pressupost d'error 14 vegades més ràpid del sostenible"
description: >
A aquest ritme, el pressupost de 30 dies s'esgotarà en uns 2 dies.
Taxa d'error actual: {{ $value | humanizePercentage }}.
runbook_url: "https://runbooks.rutasnorte.example/pressupost-error"
- alert: PressupostErrorGairebeExhaurit
expr: apireserves:pressupost_error_consumit:30d > 0.90
for: 30m
labels:
severity: avis
equip: plataforma
annotations:
summary: "Queda menys del 10% del pressupost d'error del mes"
description: >
Consumit el {{ $value | humanizePercentage }} del pressupost.
Considerar congelar els desplegaments no relacionats amb fiabilitat.El factor 14,4 no és arbitrari: és el ritme al qual es consumiria el pressupost complet de 30 dies en uns 2 dies. Els factors habituals són 14,4 (1 h, crítica), 6 (6 h, crítica) i 1 (3 d, avís), combinant-se per detectar tant hemorràgies ràpides com dessagnats lents.
Un panell Stat amb apireserves:pressupost_error_consumit:30d, en tant per cent i amb llindars a 50/80/100, és probablement el panell més útil del quadre de comandament per parlar amb el negoci.
Errors Comuns i Consells
1. Quadres de comandament que no persisteixen. Construïts a la interfície, sense ConfigMap ni volum persistent, desapareixen en recrear-se el pod. Proveeix-los sempre com a codi.
2. Fer servir rate(metrica[5m]) en lloc de rate(metrica[$__rate_interval]). Amb finestra fixa, en fer zoom a 30 dies el gràfic s'omple de forats o de soroll.
3. Variable multivalor amb = en lloc de =~. Grafana la substitueix per (a|b|c), que només funciona amb l'operador d'expressió regular. El panell queda buit sense cap error visible.
4. Alertes sense for. L'error que més ràpid converteix un sistema d'alertes en soroll. Tot pic de 30 segons genera una notificació.
5. Alertes sense runbook_url. A les tres de la matinada, un nom d'alerta sense procediment associat obliga a improvisar. Escriu el runbook abans d'activar l'alerta.
6. Alertar sobre causes en lloc de símptomes. "El pod s'ha reiniciat" no és un problema si hi ha sis rèpliques i readiness. "Els clients no poden comprar" sí que ho és.
7. Oblidar equal a les regles d'inhibició. Sense ell, una alerta de dev pot silenciar els avisos de pro. Un error silenciós i perillós.
8. Silencis sense data de fi. És esborrar una alerta i oblidar-ho. Revisa periòdicament els silencis actius.
9. Llindars copiats d'un altre sistema. Un p95 d'1 s pot ser excel·lent per a un informe i catastròfic per a un autocompletat. Els llindars surten de les teves dades i del teu SLO.
10. Massa alertes crítiques. Si tot és crític, res no ho és. Reserva critica per al que justifica una trucada de matinada.
11. No provar les alertes. Una alerta amb una consulta mal escrita no dispara mai i dóna falsa tranquil·litat. Provoca la condició deliberadament a rutas-norte-dev i verifica que arriba la notificació al canal correcte.
12. Contrasenya de Grafana a Git. El valor adminPassword del fitxer de Helm és text pla versionat. Fes servir un Secret extern o autenticació delegada.
Exercicis
Exercici 1 — Corregir un catàleg d'alertes defectuós
Un company ha escrit aquestes alertes. Identifica els problemes de cadascuna i reescriu el PrometheusRule corregit.
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: alertes-equip
namespace: monitoratge
spec:
groups:
- name: alertes
rules:
- alert: CPUAlta
expr: |
sum by (pod) (rate(container_cpu_usage_seconds_total{namespace="rutas-norte-pro"}[5m])) > 0.5
labels:
severity: critica
annotations:
summary: "CPU alta"
- alert: PodReiniciat
expr: kube_pod_container_status_restarts_total{namespace="rutas-norte-pro"} > 0
for: 1m
labels:
severity: critica
annotations:
summary: "Un pod s'ha reiniciat"
- alert: MemoriaAlta
expr: container_memory_working_set_bytes{namespace="rutas-norte-pro"} > 400000000
for: 30s
labels:
severity: critica
annotations:
summary: "Memòria alta a {{ $labels.pod }}"Exercici 2 — Dissenyar l'alerta predictiva de redis-cache
redis-cache té maxmemory configurat en 1 GiB amb política allkeys-lru. Quan s'omple, comença a expulsar claus i la taxa d'encerts s'esfondra, cosa que fa que api-reserves consulti PostgreSQL molt més i tot vagi lent.
L'exportador de Redis exposa:
redis_memory_used_bytes
redis_memory_max_bytes
redis_keyspace_hits_total
redis_keyspace_misses_total
redis_evicted_keys_total- Escriu la consulta PromQL de la taxa d'encerts de cau (proporció d'encerts sobre el total).
- Escriu una alerta que avisi abans que el problema afecti els clients, fent servir
predict_linear. - Escriu una segona alerta sobre el símptoma, per a quan ja estigui passant, i explica per què calen totes dues.
- Quines regles d'inhibició hi afegiries?
Exercici 3 — Configurar l'encaminament per a un incident nocturn
Rutas Norte defineix aquesta política de guàrdia:
- De 08:00 a 20:00, feiners: totes les alertes van al canal
#alertes-plataformade Slack. - Fora d'aquest horari: només les
criticaderutas-norte-provan a PagerDuty; la resta espera l'endemà al matí. - Les alertes de l'equip de dades (
equip: dades) no van mai a PagerDuty. - El dissabte del pont de maig hi ha una migració programada de
postgres-reservesde 02:00 a 05:00.
- Pot Alertmanager encaminar per franja horària? Si sí, escriu la configuració.
- Escriu l'arbre de rutes complet que implementa la política.
- Escriu l'ordre que crea el silenci per a la migració programada, amb les bones pràctiques de l'apartat 9.
Solucions
Solució 1
Problemes de CPUAlta:
- Sense
for: dispara tan bon punt un pod supera 0,5 nuclis un instant. Una arrencada de JVM o una compactació puntual generen notificació. - Alerta sobre una causa, no un símptoma. 0,5 nuclis pot ser perfectament normal per a
postgres-reserves. Ningú sap què fer en rebre-la. - Llindar absolut sense context. 0,5 nuclis significa coses diferents segons el
limitsde cada component. - Severitat
criticainjustificada: no desperta ningú que pugui fer res útil. - Sense
runbook_urlni descripció.
Problemes de PodReiniciat:
> 0sobre un comptador acumulat:kube_pod_container_status_restarts_totalno torna a baixar mai. Un pod que es va reiniciar fa tres setmanes manté l'alerta disparada per sempre. Cal fer servirincrease(...[finestra]).- Un reinici aïllat no és un problema amb sis rèpliques i readiness (07-01).
for: 1mmassa curt i severitat exagerada.
Problemes de MemoriaAlta:
for: 30s: sorollosíssim.- Llindar en bytes crus i absolut: 400 MB és molt per a
botiga-webi poquíssim per apostgres-reserves. S'ha de comparar amb ellimitsdel contenidor. - Sense filtrar
container!="": inclou la sèrie agregada del pod i el contenidorpause, duplicant les alertes. - Alerta sobre causa. El que importa és si el contenidor serà
OOMKilled, no un número de bytes.
Versió corregida:
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: alertes-equip
namespace: monitoratge
labels:
app.kubernetes.io/part-of: rutas-norte
spec:
groups:
- name: rutasnorte.recursos
interval: 60s
rules:
# Substitueix CPUAlta: alerta sobre l'efecte real (throttling),
# relatiu al límit del propi contenidor, no absolut.
- alert: ContenidorEstrangulat
expr: |
sum by (namespace, pod, container) (
rate(container_cpu_cfs_throttled_periods_total{namespace="rutas-norte-pro"}[5m]))
/ sum by (namespace, pod, container) (
rate(container_cpu_cfs_periods_total{namespace="rutas-norte-pro"}[5m]))
> 0.30
for: 15m
labels:
severity: avis
equip: plataforma
annotations:
summary: "{{ $labels.container }} estrangulat el {{ $value | humanizePercentage }} del temps"
description: >
El contenidor {{ $labels.container }} del pod {{ $labels.pod }}
arriba al seu limits.cpu de manera sostinguda i això degrada la latència.
Revisar la recalibració de recursos de 07-02.
runbook_url: "https://runbooks.rutasnorte.example/throttling-cpu"
# Substitueix PodReiniciat: fa servir increase() sobre una finestra,
# exigeix diversos reinicis i baixa la severitat.
- alert: PodEnCrashLoop
expr: |
increase(kube_pod_container_status_restarts_total{
namespace="rutas-norte-pro"}[15m]) > 3
for: 10m
labels:
severity: avis
equip: plataforma
annotations:
summary: "{{ $labels.pod }} reiniciat més de 3 vegades en 15 minuts"
description: >
Contenidor {{ $labels.container }}. Causes freqüents: OOMKilled,
error de configuració o livenessProbe massa agressiva (07-01).
Recollir 'kubectl logs --previous' ABANS de tocar res.
runbook_url: "https://runbooks.rutasnorte.example/crashloop"
# Substitueix MemoriaAlta: relativa al límit, amb for raonable
# i filtrant la sèrie agregada del pod.
- alert: MemoriaAPropDelLimit
expr: |
container_memory_working_set_bytes{namespace="rutas-norte-pro", container!=""}
/ on (namespace, pod, container)
kube_pod_container_resource_limits{namespace="rutas-norte-pro", resource="memory"}
> 0.90
for: 15m
labels:
severity: avis
equip: plataforma
annotations:
summary: "{{ $labels.container }} al {{ $value | humanizePercentage }} del seu límit de memòria"
description: >
El contenidor és a prop del seu limits.memory i serà OOMKilled
si el supera. Revisar si hi ha una fuita o si el límit és curt.
runbook_url: "https://runbooks.rutasnorte.example/memoria-limit"
# Alerta sobre el SÍMPTOMA real: el contenidor JA va ser mort.
- alert: ContenidorOOMKilled
expr: |
increase(kube_pod_container_status_last_terminated_reason{
namespace="rutas-norte-pro", reason="OOMKilled"}[15m]) > 0
for: 1m
labels:
severity: critica
equip: plataforma
annotations:
summary: "{{ $labels.container }} ha estat mort per falta de memòria"
description: >
El kernel ha mort el contenidor per superar el seu limits.memory
(codi de sortida 137). Ampliar el límit o corregir la fuita.
runbook_url: "https://runbooks.rutasnorte.example/oomkilled"Canvis de fons, més enllà de la sintaxi: s'ha passat de tres alertes sobre causes absolutes a alertes relatives al límit del propi contenidor, amb finestres raonables, severitats proporcionades i runbooks. L'única critica és la que reflecteix un fet consumat i accionable.
Solució 2
1. Taxa d'encerts de cau.
sum(rate(redis_keyspace_hits_total{entorn="pro"}[5m]))
/
(
sum(rate(redis_keyspace_hits_total{entorn="pro"}[5m]))
+
sum(rate(redis_keyspace_misses_total{entorn="pro"}[5m]))
)Es fan servir taxes i no valors absoluts perquè els comptadors acumulats des de l'arrencada del pod dilueixen qualsevol degradació recent.
2. Alerta predictiva (la causa, amb antelació).
- alert: RedisCacheSOmplira
expr: |
predict_linear(
(redis_memory_max_bytes{entorn="pro"} - redis_memory_used_bytes{entorn="pro"})[2h:],
2 * 3600
) < 0
for: 20m
labels:
severity: avis
component: redis-cache
equip: plataforma
annotations:
summary: "redis-cache s'omplirà en menys de 2 hores"
description: >
Segons la tendència de les últimes 2 hores, redis-cache arribarà al seu
maxmemory d'1 GiB en menys de 2 hores i començarà a expulsar claus.
Memòria lliure actual: {{ $value | humanize1024 }}B.
Acció: ampliar maxmemory o revisar el TTL de les claus de disponibilitat.
runbook_url: "https://runbooks.rutasnorte.example/redis-memoria"Nota sobre la sintaxi: predict_linear necessita un vector de rang. En aplicar-lo sobre una resta de dos gauges cal fer servir un subquery ([2h:]), perquè l'expressió resultant no és una sèrie simple. És un detall que confon molt.
Alternativa més simple, sobre una sola mètrica:
predict_linear(redis_memory_used_bytes{entorn="pro"}[2h], 2*3600)
> avg(redis_memory_max_bytes{entorn="pro"})3. Alerta sobre el símptoma.
- alert: RedisTaxaEncertsDegradada
expr: |
(
sum(rate(redis_keyspace_hits_total{entorn="pro"}[10m]))
/ (sum(rate(redis_keyspace_hits_total{entorn="pro"}[10m]))
+ sum(rate(redis_keyspace_misses_total{entorn="pro"}[10m])))
) < 0.80
for: 15m
labels:
severity: avis
component: redis-cache
equip: plataforma
annotations:
summary: "La taxa d'encerts de redis-cache ha caigut al {{ $value | humanizePercentage }}"
description: >
El normal és un 95%. Amb una taxa baixa, api-reserves consulta
postgres-reserves molt més del previst i la latència puja.
Comprovar si redis-cache està expulsant claus per falta de memòria.
runbook_url: "https://runbooks.rutasnorte.example/redis-encerts"
- alert: RedisExpulsantClaus
expr: sum(rate(redis_evicted_keys_total{entorn="pro"}[5m])) > 10
for: 10m
labels:
severity: avis
component: redis-cache
equip: plataforma
annotations:
summary: "redis-cache expulsa {{ $value | humanize }} claus/s per falta de memòria"
runbook_url: "https://runbooks.rutasnorte.example/redis-memoria"Per què calen totes dues. Compleixen funcions diferents en el temps:
- La predictiva avisa amb dues hores de marge, quan encara es pot actuar sense presses i sense impacte per al client. És una alerta de capacitat.
- La de símptoma cobreix el cas en què la predicció falli: un canvi brusc de patró (per exemple, un desplegament que emmagatzema en cau objectes molt més grans) pot omplir Redis en minuts sense que cap tendència ho anticipi. La regressió lineal només prediu bé el que es comporta linealment.
Confiar només en la predictiva és assumir que el futur s'assembla al passat. Confiar només en el símptoma és renunciar a prevenir.
4. Regles d'inhibició.
inhibit_rules:
# Si Redis ja està expulsant claus, la predicció que s'omplirà
# ja no aporta res: el fet ha passat.
- source_matchers:
- alertname = "RedisExpulsantClaus"
target_matchers:
- alertname = "RedisCacheSOmplira"
equal: ['component', 'namespace']
# Si la latència d'api-reserves està disparada (símptoma que pateix el
# client), els avisos de redis-cache són la causa: no calen dues
# notificacions separades del mateix incident.
- source_matchers:
- alertname = "ApiReservesLatenciaAlta"
- severity = "critica"
target_matchers:
- component = "redis-cache"
- severity = "avis"
equal: ['namespace']La primera regla és un exemple perfecte del criteri d'inhibició: quan la predicció es compleix, la predicció sobra.
Solució 3
1. Pot Alertmanager encaminar per franja horària?
Sí. Des d'Alertmanager 0.22 existeixen els intervals de temps (time_intervals), que es referencien a les rutes amb active_time_intervals (la ruta només aplica dins de l'interval) o mute_time_intervals (la ruta se silencia dins de l'interval).
time_intervals:
- name: horari-laboral
time_intervals:
- weekdays: ['monday:friday']
times:
- start_time: '08:00'
end_time: '20:00'
location: 'Europe/Madrid'
- name: fora-dhorari
time_intervals:
- weekdays: ['monday:friday']
times:
- start_time: '20:00'
end_time: '24:00'
- start_time: '00:00'
end_time: '08:00'
location: 'Europe/Madrid'
- weekdays: ['saturday', 'sunday']
location: 'Europe/Madrid'El camp location és imprescindible: sense ell, Alertmanager fa servir UTC i a l'estiu la guàrdia començaria dues hores abans del previst.
2. Arbre de rutes complet.
route:
receiver: 'equip-plataforma-slack'
group_by: ['alertname', 'namespace', 'component']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
# ------------------------------------------------------------------
# 1. Equip de dades: SEMPRE al seu canal, MAI a PagerDuty.
# Va primer perquè la primera coincidència guanya.
# ------------------------------------------------------------------
- matchers:
- equip = "dades"
receiver: 'equip-dades-slack'
repeat_interval: 12h
# ------------------------------------------------------------------
# 2. dev i pre: no desperten mai ningú.
# ------------------------------------------------------------------
- matchers:
- namespace =~ "rutas-norte-(dev|pre)"
receiver: 'canal-desenvolupament'
group_wait: 5m
repeat_interval: 24h
# ------------------------------------------------------------------
# 3. FORA D'HORARI: només les crítiques de producció desperten.
# ------------------------------------------------------------------
- matchers:
- severity = "critica"
- namespace =~ "rutas-norte-pro|monitoratge"
active_time_intervals: ['fora-dhorari']
receiver: 'guardia-pagerduty'
group_wait: 10s
repeat_interval: 1h
continue: true # que també quedi registre a Slack
# 3b. La resta, fora d'horari, només a Slack: es llegeix al matí.
- matchers:
- namespace =~ "rutas-norte-pro|monitoratge"
active_time_intervals: ['fora-dhorari']
receiver: 'equip-plataforma-slack'
group_wait: 5m
repeat_interval: 12h # sense insistir de matinada
# ------------------------------------------------------------------
# 4. HORARI LABORAL: tot a Slack, amb les crítiques més àgils.
# ------------------------------------------------------------------
- matchers:
- severity = "critica"
active_time_intervals: ['horari-laboral']
receiver: 'equip-plataforma-slack'
group_wait: 10s
repeat_interval: 1h
- active_time_intervals: ['horari-laboral']
receiver: 'equip-plataforma-slack'Punts a destacar del disseny:
- L'ordre importa: la ruta de l'equip de dades va primer perquè les seves alertes crítiques no acabin a PagerDuty per la ruta 3.
continue: truea la ruta 3 permet que l'alerta arribi als dos llocs: PagerDuty desperta algú i Slack deixa constància per a la revisió de l'endemà.- Les alertes no crítiques fora d'horari tenen
repeat_interval: 12hper no omplir el canal de matinada.
Consideració de robustesa: aquest encaminament per horari assumeix que la guàrdia és sempre la mateixa persona. En equips amb rotació, l'habitual és delegar la lògica de torns a la mateixa eina de guàrdia (PagerDuty, Opsgenie), que gestiona calendaris, escalats i substitucions molt millor que Alertmanager. Aquí les regles horàries serveixen sobretot per decidir què mereix despertar algú, no a qui.
3. Silenci per a la migració programada.
kubectl -n monitoratge port-forward svc/monitoratge-kube-pr-alertmanager 9093:9093 &
curl -s -X POST http://localhost:9093/api/v2/silences \
-H 'Content-Type: application/json' \
-d '{
"matchers": [
{"name": "component", "value": "postgres-reserves", "isRegex": false},
{"name": "namespace", "value": "rutas-norte-pro", "isRegex": false}
],
"startsAt": "2026-05-02T00:00:00Z",
"endsAt": "2026-05-02T03:15:00Z",
"createdBy": "[email protected]",
"comment": "Migració programada de postgres-reserves 02:00-05:00 CEST. Tiquet OPS-1842. Aprovada al comitè de canvis del 28/04. Responsable de guàrdia: Marta Ruiz."
}' | jq -r '.silenceID'Bones pràctiques aplicades i el seu perquè:
- Hores en UTC.
02:00-05:00 CESTés00:00-03:00 UTC. És l'error més freqüent en crear silencis i deixa alertes sense silenciar justament durant la finestra. - 15 minuts de marge sobre la finestra prevista (fins a les 03:15 UTC), perquè les migracions s'allarguen.
- Matchers específics: només
postgres-reservesapro. Un silenci de tot el namespace amagaria un problema no relacionat abotiga-webdurant tres hores. - Comentari amb tiquet, aprovació i responsable: d'aquí a sis setmanes, qualsevol pot reconstruir per què existia.
endsAtobligatori, mai indefinit.
I el que no s'ha de silenciar durant la migració:
# Comprovar QUINES alertes queden cobertes pel silenci abans d'aplicar-lo:
# les de la plataforma completa (PlataformaSenseVendes, BotigaWebCaiguda) NO
# porten l'etiqueta component=postgres-reserves, així que continuen actives.
# Això és deliberat: si la migració deixa la plataforma sense vendre, cal
# assabentar-se'n immediatament encara que la causa sigui la migració mateixa.
curl -s http://localhost:9093/api/v2/alerts | \
jq -r '.[] | select(.status.silencedBy | length > 0) | .labels.alertname'Verificació després de la finestra:
# Confirmar que el silenci ha expirat i no en queda cap d'oblidat
curl -s http://localhost:9093/api/v2/silences | \
jq -r '.[] | select(.status.state=="active") |
"\(.id)\t\(.createdBy)\t\(.comment)\tfins \(.endsAt)"'Aquesta última ordre s'hauria d'executar com a part de la revisió setmanal de l'equip: un silenci oblidat és una alerta que ja no existeix.
Conclusió
Rutas Norte ja no només recorda: ara mostra i avisa. En aquesta lliçó hem:
- Connectat Grafana a Prometheus i après l'anatomia d'un panell: la consulta amb
$__rate_interval, la llegenda amb plantilles d'etiquetes, les unitats i els llindars que fan llegible un número, i els quatre tipus que de debò es fan servir, inclòs el mapa de calor per veure la distribució completa de latències. - Fet servir variables de panell per tenir un únic quadre de comandament que serveix per a
dev,preipro, evitant tres còpies que es desincronitzen. - Construït el quadre de comandament de Rutas Norte panell a panell: una fila de resum executiu encapçalada per les reserves confirmades per minut —perquè si això cau a zero tant se val que tots els pods estiguin
Running— i una fila per component amb els quatre indicadors daurats. - Proveït els quadres de comandament com a codi en ConfigMaps amb l'etiqueta
grafana_dashboard, perquè sobrevisquin a la recreació del pod i es revisin en una petició de canvi. - Escrit el catàleg d'alertes amb
PrometheusRule, entenent queforés el camp que separa un sistema usable del soroll, quepredict_linearpermet avisar que el disc de PostgreSQL s'omplirà abans que s'ompli, i querunbook_urlno és opcional. - Configurat Alertmanager: l'arbre de rutes amb els seus quatre temps, els receptors, la inhibició que converteix tretze notificacions en una quan cau un node, i els silencis amb data de fi i tiquet per als manteniments programats.
- I per damunt de la mecànica, el criteri: alertar sobre símptomes que percep el client, no sobre causes; que cada alerta sigui accionable; i fer servir els SLO i el pressupost d'error com a eina objectiva per decidir quan cal deixar d'afegir funcionalitat i posar-se a estabilitzar.
Però hi ha una pregunta que ni les mètriques ni els quadres de comandament poden respondre. Quan ApiReservesTaxaErrorAlta dispara a les 03:14, sabem que el 7 % de les peticions fallen, sabem en quina ruta i amb quina latència. El que no sabem és per què. L'excepció concreta, el missatge de la base de dades, la traça que assenyala la línia de codi: això no viu en una mètrica. Viu en els logs, que avui estan repartits pels nodes, es perden quan un pod es recrea i no hi ha manera de buscar-hi.
A 07-05 muntarem la pila completa de registre centralitzat que vam anunciar a 06-02 quan vam desplegar el recol·lector com a DaemonSet: Elasticsearch, Fluentd i Kibana. Veurem com passar de kubectl logs a una cerca que correlaciona tots els components, per què els logs estructurats en JSON són la decisió que més millora tot el sistema, i —molt important per a una plataforma que desa el DNI i el telèfon dels seus clients— què mai ha d'acabar escrit en un log.
Curs de Kubernetes
Mòdul 1: Introducció a Kubernetes
- Què és Kubernetes?
- Arquitectura de Kubernetes
- Conceptes i Terminologia Clau
- Configuració d'un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objectes, Manifests YAML i el Model Declaratiu
- El Projecte del Curs: la Plataforma Rutas Norte
Mòdul 2: Components Principals de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualitzacions, Rollbacks i Estratègies de Desplegament
- Serveis
- Namespaces
- Etiquetes, Selectors i Anotacions
Mòdul 3: Gestió de Configuració i Secrets
- ConfigMaps
- Secrets
- Variables d'Entorn
- Quotes i Límits de Recursos
- LimitRanges i Classes de Qualitat de Servei (QoS)
- ServiceAccounts i Accés a l'API des dels Pods
Mòdul 4: Xarxes a Kubernetes
- Xarxes de Clúster
- Tipus de Serveis
- DNS Intern i Descobriment de Serveis
- Controladors d'Ingress
- TLS i Gestió de Certificats amb cert-manager
- Polítiques de Xarxa
Mòdul 5: Emmagatzematge a Kubernetes
- Volums
- Volums Persistents
- Reclamacions de Volums Persistents
- Classes d'Emmagatzematge
- Aprovisionament Dinàmic, Expansió i Snapshots
- Còpies de Seguretat i Restauració de Dades
Mòdul 6: Conceptes Avançats de Kubernetes
- StatefulSets
- DaemonSets
- Treballs i CronJobs
- Init Containers, Sidecars i Patrons Multicontenidor
- Planificació: Afinitat, Taints i Toleracions
- Definicions de Recursos Personalitzats (CRDs)
- Operadors i el Patró Controlador
Mòdul 7: Monitoratge i Registre
- Verificacions de Salut i Sondes
- Servidor de Mètriques i kubectl top
- Monitoratge amb Prometheus
- Visualització i Alertes amb Grafana i Alertmanager
- Registre Centralitzat amb Elasticsearch, Fluentd i Kibana (EFK)
- Depuració d'Aplicacions i Esdeveniments del Clúster
Mòdul 8: Seguretat a Kubernetes
- Control d'Accés Basat en Rols (RBAC)
- Contextos de Seguretat i Enduriment del Contenidor
- Polítiques de Seguretat de Pods i Pod Security Standards
- Seguretat de Xarxa
- Seguretat d'Imatges
- Auditoria, Escaneig i Gestió de Vulnerabilitats
Mòdul 9: Escalat i Rendiment
- Autoescalat Horitzontal de Pods
- Autoescalat Vertical de Pods
- Autoescalat de Clúster
- Escalat per Esdeveniments i Mètriques Personalitzades amb KEDA
- Alta Disponibilitat: PodDisruptionBudgets i Topologia
- Ajust de Rendiment
Mòdul 10: Ecosistema i Eines de Kubernetes
- Minikube i Entorns Locals amb kind
- Kubeadm
- Helm
- Kustomize
- GitOps amb Argo CD i Flux
- Kubernetes Gestionat: EKS, AKS i GKE
Mòdul 11: Estudis de Cas i Aplicacions del Món Real
- Desplegament d'una Aplicació Web
- Execució d'Aplicacions amb Estat
- CI/CD amb Kubernetes
- Estratègies de Desplegament: Blue-Green i Canary
- Gestió Multi-Clúster
- Operació en Producció: Incidències, Runbooks i Costos
