El Mòdul 6 va acabar amb una pregunta incòmoda: la plataforma és segura, però sabem si funciona? En un monòlit, "funciona" es comprovava mirant un procés i un log. A Quilòmetre Zero hi ha sis serveis, tres bases de dades, un clúster de Kafka, Redis, MinIO, un gateway i un IdP, repartits per desenes de contenidors: ningú no pot mirar tot això alhora, i quan alguna cosa va malament gairebé mai no ho diu a crits. Un inventari que respon en 900 ms en lloc de 40 continua "funcionant"; un consumidor de comandes.esdeveniments que s'ha quedat tres hores enrere no llança cap excepció; un certificat de Vault que caduca a les 03:00 d'un diumenge només es nota quan la primera comanda del matí falla. Aquesta lliçó tracta de convertir la plataforma en una cosa observable a través del seu primer pilar, les mètriques: què mesurar, com recollir-ho amb Prometheus, com expressar en números allò que Quilòmetre Zero promet a l'Anna, i com alertar abans que ella se n'assabenti. Els logs i les traces, els altres dos pilars, són la lliçó 07-02.
Contingut
- Observabilitat i els seus tres pilars
- Què mesurar: infraestructura, plataforma, aplicació i negoci
- Tipus de mètrica i el problema de la cardinalitat
- Prometheus: model pull, exporters i descobriment
- PromQL essencial: taxes, percentils i agregacions
- SLI, SLO, SLA i pressupost d'error
- Alertes: símptomes, burn rate i Alertmanager
- Dashboards a Grafana i monitoratge actiu
- Errors comuns i consells
- Exercicis i solucions
- Conclusió
- Observabilitat i els seus tres pilars
Monitorar és comprovar coses que ja sabem que poden fallar: el disc s'omple, el procés mor, la latència puja. L'observabilitat és una propietat del sistema: la capacitat de respondre preguntes que no havíem previst ("per què les comandes de Lleida amb formatge-fresc triguen el doble des d'ahir?") a partir del que el sistema emet cap enfora. En un sistema distribuït la diferència importa perquè les preguntes imprevistes són la norma: les fallades interessants són combinacions de peces, no peces aïllades.
Els tres senyals que un sistema emet, i que junts donen observabilitat, són:
| Pilar | Què és | Pregunta que respon | Cost per esdeveniment | Lliçó |
|---|---|---|---|---|
| Mètriques | Números agregats en el temps (comptadors, mesures) | "Quant? Amb quina freqüència? Està pitjor que fa una hora?" | Molt baix (una sèrie per combinació d'etiquetes, no per esdeveniment) | 07-01 |
| Logs | Esdeveniments discrets amb context | "Què va passar exactament amb la comanda P-2026-000125?" |
Alt (un registre per esdeveniment) | 07-02 |
| Traces | El recorregut d'una petició per diversos serveis | "On se n'ha anat el temps d'aquesta petició? Què ha cridat què?" | Mitjà-alt (mostrejat) | 07-02 |
Les mètriques són el punt de partida perquè són les úniques de les tres que escalen amb el nombre de sèries, no amb el trànsit: comptar 10 comandes per minut o 10 000 costa el mateix. Són el senyal que dispara alertes i el que pinta el panell de la Setmana de la Verema. Els logs i les traces entren després, quan la mètrica diu "alguna cosa va malament" i cal esbrinar què.
- Què mesurar: infraestructura, plataforma, aplicació i negoci
Un error freqüent és començar per la infraestructura (CPU, memòria) perquè és el que donen els agents per defecte, i acabar amb cent gràfiques que no diuen si l'Anna pot comprar. Convé pensar en quatre capes, de baix a dalt, i triar a cadascuna poques mètriques amb sentit.
2.1 Infraestructura
Allò que mesura node_exporter a cada màquina o cAdvisor a cada contenidor: CPU, memòria, disc (espai i latència d'E/S), xarxa (bytes, errors, paquets descartats). Per a aquests recursos serveix el mètode USE de Brendan Gregg: per a cada recurs, Utilització (percentatge de temps ocupat), Saturació (feina encuada que no hi cap: run queue, swap, cua de disc) i Errors.
2.2 Plataforma
Les peces intermèdies tenen mètriques pròpies que anticipen problemes de l'aplicació:
- Kafka: el lag de cada grup de consumidors (missatges produïts menys consumits, per partició). Un lag creixent a
analiticaés tolerable; arepartimentsignifica que el panell defurgoneta-3mostra posicions velles. També particions sense rèplica sincronitzada (es veurà a 07-03). - PostgreSQL: connexions actives respecte del màxim (
max_connections), transaccions per segon, retard de la rèplicacomandes-db-replicarespecte del primari, mida de taules i bloquejos en espera. - Redis: hit ratio de la memòria cau de catàleg (encerts / (encerts + fallades)), memòria usada respecte del límit, claus expulsades, latència d'ordres.
- Cassandra: latència de lectura i escriptura per taula, compactacions pendents, nodes caiguts vistos per gossip.
2.3 Aplicació: els quatre senyals daurats
Google va popularitzar a Site Reliability Engineering els quatre senyals daurats, els que cal mirar a qualsevol servei que atén peticions:
- Latència: quant triga a respondre, distingint peticions correctes de fallides (un error ràpid no és "bona latència").
- Trànsit: quanta demanda rep (peticions per segon, missatges consumits per segon).
- Errors: quina fracció de peticions falla, explícitament (
5xx,UNAVAILABLEde gRPC) o implícitament (ha respost200però ha trigat més del que s'havia acordat). - Saturació: com de "ple" està el servei: fils ocupats, connexions del pool en ús, cua de feina.
El mètode RED (Tom Wilkie) és la versió per a serveis: Rate, Errors, Duration. Coincideix amb els tres primers senyals daurats i és el que instrumentem de manera uniforme a tots els serveis de km0/.
| Mètode | S'aplica a | Mesura | Exemple a Quilòmetre Zero |
|---|---|---|---|
| RED | Serveis (coses que atenen peticions) | Rate, Errors, Duration | comandes: peticions/s a POST /comandes, % de 5xx, p99 de durada |
| USE | Recursos (coses amb capacitat finita) | Utilization, Saturation, Errors | Disc del broker de Kafka: 85 % usat, cua d'E/S de 12, 0 errors |
| Senyals daurats | Sistemes orientats a l'usuari | Latència, trànsit, errors, saturació | L'anterior més "pool de connexions a km0_comandes al 90 %" |
Taula de senyals daurats per servei:
| Servei | Latència | Trànsit | Errors | Saturació |
|---|---|---|---|---|
cataleg |
p99 de GET /cataleg/* (objectiu < 200 ms) |
peticions/s per mercat | % 5xx; fallades de Redis |
hit ratio de Redis; connexions a PostgreSQL |
comandes |
p99 de POST /comandes (< 500 ms) |
comandes/min | % 5xx; sagues compensades / total |
pool de km0_comandes; fils HTTP ocupats |
inventari |
p99 de ReservarEstoc (< 100 ms) |
crides gRPC/s per mètode | % UNAVAILABLE + DEADLINE_EXCEEDED |
connexions a km0_inventari; lag de replicació inv-bcn/inv-vlc |
pagaments |
p99 de Cobrar (< 2 s, depèn de la passarel·la) |
cobraments/min | % rebutjats per error tècnic (no per targeta rebutjada) | crides en vol a la passarel·la |
repartiment |
retard entre posició emesa i mostrada | posicions/s (140 repartidors × 1/5 s ≈ 28/s) | missatges rebutjats | lag del consumidor de repartiment.posicions |
analitica |
durada del DAG km0_vendes_diaries |
esdeveniments/s consumits | tasques fallides d'Airflow | lag de comandes.esdeveniments; executors de Spark ocupats |
2.4 Negoci
Les mètriques de negoci són les que expliquen a en Jordi Sala i la Marta Puig si la plataforma compleix el seu propòsit, i sovint detecten fallades que les tècniques no veuen: si comandes respon 200 en 80 ms però les comandes per minut cauen a zero un dissabte a les 11:00, alguna cosa s'ha trencat abans (el front, Kong, Keycloak). Per a Quilòmetre Zero: comandes creades per minut (per mercat i per productor), taxa de pagament rebutjat, valor mitjà de la comanda, reserves d'estoc fallides per producte, lliuraments a temps. S'instrumenten igual que les tècniques, al mateix Prometheus.
- Tipus de mètrica i el problema de la cardinalitat
Prometheus (i OpenTelemetry Metrics, que fa servir el mateix model conceptual) distingeix quatre tipus:
| Tipus | Què és | Només pot | Exemple | Consulta típica |
|---|---|---|---|---|
| Counter | Comptador acumulat des de l'arrencada del procés | Pujar (es reinicia a 0 si el procés es reinicia) | km0_comandes_total{estat="confirmada"} |
rate(...[5m]) |
| Gauge | Valor instantani | Pujar i baixar | km0_estoc_unitats{producte="tomaquet-rosa"}, connexions actives |
valor directe, avg_over_time |
| Histogram | Distribució en buckets acumulatius + suma + compte | Pujar | km0_peticio_durada_segons_bucket{le="0.5"} |
histogram_quantile(0.99, ...) |
| Summary | Quantils calculats al client | Pujar | ..._durada{quantile="0.99"} |
valor directe (no agregable entre instàncies) |
Dos aclariments que estalvien molts errors:
- Un counter mai no es llegeix en cru.
km0_comandes_totalval 1 284 522 i no diu res; el que interessa és la seva derivada:rate(km0_comandes_total[5m])= comandes per segon en els darrers 5 minuts. Prometheus gestiona els reinicis (quan el comptador baixa, assumeix reinici i no produeix una taxa negativa). - Histogram contra summary: l'histogram desa quantes observacions han caigut per sota de cada límit (
le="0.1",le="0.25",le="0.5", ...). Els percentils es calculen al servidor i, sobretot, es poden agregar entre instàncies: el p99 decomandesamb 3 rèpliques s'obté sumant buckets. Un summary calcula el p99 a cada procés i no hi ha cap manera correcta de combinar tres p99 (la mitjana de percentils no és un percentil). En un sistema distribuït, llevat de casos molt concrets, es fa servir histogram.
Cardinalitat
Cada combinació diferent de nom + etiquetes és una sèrie temporal que Prometheus desa en memòria i en disc. km0_peticio_durada_segons{servei, metode, codi} amb 6 serveis × 10 mètodes × 5 codis × 12 buckets = 3 600 sèries: bé. Si a algú se li acut afegir comanda_id com a etiqueta, cada comanda crea una sèrie nova per bucket: un milió de comandes són dotze milions de sèries, i Prometheus mor. La regla: una etiqueta només pot prendre un conjunt petit i acotat de valors (servei, mètode, codi d'estat, mercat, estat de la comanda). Mai identificadors d'entitat (comanda_id, usuari, request_id), ni adreces IP de clients, ni rutes amb paràmetres sense normalitzar (/comandes/P-2026-000123 s'ha de registrar com a /comandes/{id}). Allò que necessiti l'identificador va als logs o a les traces (07-02).
Un cas límit interessant és km0_estoc_unitats{producte}: amb uns centenars de productes és acceptable; amb cent mil no. A Quilòmetre Zero es limita als productes de les campanyes actives i, per a la resta, es publica un gauge agregat per productor.
- Prometheus: model pull, exporters i descobriment
Prometheus funciona en mode pull: no espera que els serveis li enviïn dades, sinó que cada 15 segons (configurable) fa GET /metrics a cada objectiu i desa el que hi troba. El format és text pla:
# HELP km0_comandes_total Comandes processades per estat final
# TYPE km0_comandes_total counter
km0_comandes_total{estat="confirmada"} 1284522
km0_comandes_total{estat="rebutjada_estoc"} 3311
km0_comandes_total{estat="rebutjada_pagament"} 8720El pull té conseqüències importants en un sistema distribuït:
- Prometheus sap quan un objectiu no respon: la mètrica
up{job="comandes", instance="comandes-2:8000"}val 0. Amb un model push, un servei mort simplement deixa d'enviar i cal inferir-ne l'absència. - Els serveis no necessiten conèixer l'adreça de Prometheus ni gestionar cues d'enviament.
- Necessita descobrir els objectius: a
docker-composen'hi ha prou amb llistar-los; a Kubernetes (07-05) els descobreix preguntant a l'API; al núvol, a l'API del proveïdor. Per a feines efímeres (unJobque dura 3 segons) existeix el Pushgateway, que és l'excepció, no la norma.
Els components que no exposen /metrics per si mateixos es cobreixen amb exporters: processos petits que tradueixen l'estat de PostgreSQL, Kafka, Redis, etc. al format de Prometheus. A Quilòmetre Zero:
flowchart LR
subgraph serveis["Serveis km0 (prometheus_client)"]
C[cataleg :8000/metrics]
P[comandes :8000/metrics]
I[inventari :9464/metrics]
R[repartiment]
end
subgraph exporters["Exporters"]
PGX[postgres_exporter<br/>km0_inventari, comandes-db-*]
KX[kafka_exporter<br/>lag per grup]
RX[redis_exporter<br/>hit ratio, memoria]
NX[node_exporter<br/>CPU, disc, xarxa]
BX[blackbox_exporter<br/>sondes HTTP/TLS]
end
PROM[(Prometheus<br/>scrape cada 15 s)]
AM[Alertmanager]
G[Grafana]
OPS[Canal #km0-operacions<br/>Jordi, Marta]
serveis --> PROM
exporters --> PROM
PROM -- regles d'alerta --> AM
AM --> OPS
G -- PromQL --> PROM
4.1 Instrumentar els serveis: serveis/comu/metriques.py
Tots els serveis comparteixen un mòdul amb les mètriques comunes, perquè el nom i les etiquetes siguin idèntics i els panells serveixin per a qualsevol d'ells.
# km0/serveis/comu/metriques.py
"""Mètriques Prometheus compartides pels serveis de Quilòmetre Zero."""
import time
from contextlib import contextmanager
from prometheus_client import Counter, Gauge, Histogram, start_http_server
# Buckets pensats per a latències de servei: de 5 ms a 10 s.
# Cada bucket és "quantes observacions han caigut per sota d'aquest límit".
BUCKETS_LATENCIA = (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10)
COMANDES_TOTAL = Counter(
"km0_comandes_total",
"Comandes processades per estat final",
["estat"], # confirmada | rebutjada_estoc | rebutjada_pagament | compensada
)
PETICIO_DURADA = Histogram(
"km0_peticio_durada_segons",
"Durada de les peticions ateses (HTTP o gRPC)",
["servei", "metode", "codi"],
buckets=BUCKETS_LATENCIA,
)
ESTOC_UNITATS = Gauge(
"km0_estoc_unitats",
"Unitats disponibles per producte (només productes en campanya)",
["producte"],
)
PETICIONS_EN_VOL = Gauge(
"km0_peticions_en_vol",
"Peticions que s'estan atenent ara mateix (saturació)",
["servei"],
)
@contextmanager
def mesurar(servei: str, metode: str):
"""Mesura la durada d'un bloc i la registra amb el codi de sortida.
Ús:
with mesurar("comandes", "POST /comandes") as ctx:
...
ctx["codi"] = "201"
El codi es fixa dins del bloc; si salta una excepció s'anota 500.
"""
ctx = {"codi": "200"}
PETICIONS_EN_VOL.labels(servei=servei).inc()
inici = time.perf_counter()
try:
yield ctx
except Exception:
ctx["codi"] = "500"
raise
finally:
durada = time.perf_counter() - inici
PETICIO_DURADA.labels(
servei=servei, metode=metode, codi=ctx["codi"]
).observe(durada)
PETICIONS_EN_VOL.labels(servei=servei).dec()
def exposar_metriques(port: int = 9464) -> None:
"""Arrenca un servidor HTTP mínim amb /metrics en un fil a part.
El fan servir els serveis que no tenen servidor HTTP propi (inventari, repartiment).
Els que sí que en tenen (comandes, cataleg) munten /metrics a la seva app.
"""
start_http_server(port)Punts que convé entendre:
Counter,GaugeiHistogramsón registres globals del procés: es declaren un cop, a nivell de mòdul, i qualsevol part del servei els fa servir.prometheus_clientels exposa tots junts a/metrics..labels(...)selecciona la sèrie concreta; la primera vegada que es fa servir una combinació, es crea. Per això cal controlar quins valors s'hi passen (cardinalitat).mesurarregistra el codi també quan hi ha excepció: sense aquestexcept, les peticions que fallen amb error intern no apareixerien a l'histograma, i la latència semblaria millor del que és.PETICIONS_EN_VOLés el senyal de saturació més barat d'obtenir: quantes peticions hi ha alhora. Si acomandessupera el nombre de fils del servidor, les següents esperen a la cua.
4.2 Interceptor gRPC a inventari
inventari ja té AuthInterceptor i ServeiInterceptor (06-04). Se n'hi afegeix un més, que embolcalla cada crida i tradueix el codi d'estat gRPC a etiqueta:
# km0/serveis/inventari/metriques_interceptor.py
import grpc
from serveis.comu.metriques import mesurar
class MetriquesInterceptor(grpc.ServerInterceptor):
"""Registra durada i codi de cada RPC del servei inventari."""
def intercept_service(self, continuation, handler_call_details):
# handler_call_details.method és "/km0.inventari.v1.Inventari/ReservarEstoc"
metode = handler_call_details.method.rsplit("/", 1)[-1]
handler = continuation(handler_call_details)
if handler is None or not handler.unary_unary:
return handler # streaming: es mesuraria d'una altra manera
original = handler.unary_unary
def embolcallat(peticio, context):
with mesurar("inventari", metode) as ctx:
try:
resposta = original(peticio, context)
# Si el handler ha cridat context.abort(), no arribem aquí.
ctx["codi"] = "OK"
return resposta
except grpc.RpcError as e:
ctx["codi"] = e.code().name # UNAVAILABLE, NOT_FOUND, ...
raise
return grpc.unary_unary_rpc_method_handler(
embolcallat,
request_deserializer=handler.request_deserializer,
response_serializer=handler.response_serializer,
)I a l'arrencada del servidor, l'ordre dels interceptors importa: mètriques primer, per mesurar també les crides rebutjades per autenticació.
# km0/serveis/inventari/servidor.py (fragment)
from serveis.comu.metriques import exposar_metriques, ESTOC_UNITATS
servidor = grpc.server(
futures.ThreadPoolExecutor(max_workers=32),
interceptors=[MetriquesInterceptor(), AuthInterceptor(), ServeiInterceptor()],
)
exposar_metriques(port=9464)
# Després de cada ajust o reserva confirmada, el servei refresca el gauge:
def publicar_estoc(producte: str, unitats: int) -> None:
if producte in PRODUCTES_EN_CAMPANYA: # tomaquet-rosa, formatge-curat, vi-crianca...
ESTOC_UNITATS.labels(producte=producte).set(unitats)4.3 Middleware HTTP a comandes i endpoint /metrics
comandes és una aplicació FastAPI; un middleware mesura cada petició i /metrics es munta a la mateixa app:
# km0/serveis/comandes/app.py (fragment)
from fastapi import FastAPI, Request
from prometheus_client import make_asgi_app
from serveis.comu.metriques import mesurar, COMANDES_TOTAL
app = FastAPI()
app.mount("/metrics", make_asgi_app()) # exposa totes les mètriques del procés
@app.middleware("http")
async def middleware_metriques(request: Request, call_next):
# Normalitzar la ruta: /comandes/P-2026-000123 -> /comandes/{id}
ruta = request.scope.get("route")
plantilla = ruta.path if ruta else "desconeguda"
with mesurar("comandes", f"{request.method} {plantilla}") as ctx:
resposta = await call_next(request)
ctx["codi"] = str(resposta.status_code)
return resposta
@app.post("/comandes", status_code=201)
async def crear_comanda(comanda: ComandaEntrada):
resultat = await saga_comanda.executar(comanda) # 03-05
COMANDES_TOTAL.labels(estat=resultat.estat).inc()
return resultatL'ús de la plantilla de la ruta (/comandes/{id}) en lloc de la URL real és l'aplicació pràctica de la regla de cardinalitat.
4.4 observabilitat/prometheus.yml
# km0/observabilitat/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s # cada quant s'avaluen les regles d'alerta
external_labels:
entorn: produccio
rule_files:
- /etc/prometheus/alertes.yml
alerting:
alertmanagers:
- static_configs:
- targets: ["alertmanager:9093"]
scrape_configs:
- job_name: comandes
static_configs:
- targets: ["comandes-1:8000", "comandes-2:8000", "comandes-3:8000"]
- job_name: cataleg
static_configs:
- targets: ["cataleg-1:8000", "cataleg-2:8000"]
- job_name: inventari
static_configs:
- targets: ["inventari-1:9464", "inventari-2:9464"]
- job_name: repartiment
static_configs:
- targets: ["repartiment:9464"]
- job_name: postgres
static_configs:
- targets: ["postgres-exporter-inventari:9187",
"postgres-exporter-comandes-primari:9187",
"postgres-exporter-comandes-replica:9187"]
- job_name: kafka
static_configs:
- targets: ["kafka-exporter:9308"]
- job_name: redis
static_configs:
- targets: ["redis-exporter:9121"]
- job_name: nodes
static_configs:
- targets: ["node-exporter:9100"]
# Sondes sintètiques: Prometheus demana al blackbox_exporter que comprovi cada URL.
- job_name: blackbox_http
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://api.km0.example/api/v1/cataleg/mercats
- https://api.km0.example/salut/viu
relabel_configs:
- source_labels: [__address__]
target_label: __param_target # la URL passa com a paràmetre ?target=
- source_labels: [__param_target]
target_label: instance # i es conserva com a etiqueta
- target_label: __address__
replacement: blackbox-exporter:9115 # a qui es fa el scrape realmentA docker-compose.yml la pila d'observabilitat s'afegeix al costat dels serveis:
# km0/docker-compose.yml (fragment)
prometheus:
image: prom/prometheus:v2.53.0
volumes:
- ./observabilitat/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./observabilitat/alertes.yml:/etc/prometheus/alertes.yml:ro
- prometheus-dades:/prometheus
command: ["--config.file=/etc/prometheus/prometheus.yml",
"--storage.tsdb.retention.time=30d"]
ports: ["9090:9090"]
alertmanager:
image: prom/alertmanager:v0.27.0
volumes:
- ./observabilitat/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
ports: ["9093:9093"]
grafana:
image: grafana/grafana:11.1.0
volumes:
- ./observabilitat/grafana/provisioning:/etc/grafana/provisioning:ro
- ./observabilitat/grafana/dashboards:/var/lib/grafana/dashboards:ro
environment:
GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/grafana_admin
ports: ["3000:3000"]
postgres-exporter-inventari:
image: prometheuscommunity/postgres-exporter:v0.15.0
environment:
DATA_SOURCE_NAME: "postgresql://exporter@inventari-db:5432/km0_inventari?sslmode=require"
kafka-exporter:
image: danielqsj/kafka-exporter:v1.7.0
command: ["--kafka.server=kafka-1:9092", "--kafka.server=kafka-2:9092"]
redis-exporter:
image: oliver006/redis_exporter:v1.62.0
environment:
REDIS_ADDR: "redis:6379"
node-exporter:
image: prom/node-exporter:v1.8.2
pid: host
blackbox-exporter:
image: prom/blackbox-exporter:v0.25.0L'usuari exporter de PostgreSQL és de només lectura sobre les vistes pg_stat_*; en un desplegament real s'obtindria de Vault (06-04).
- PromQL essencial: taxes, percentils i agregacions
PromQL és el llenguatge de consulta. Quatre construccions cobreixen el 90 % dels usos:
rate(counter[finestra]): taxa per segon dins la finestra. Peticions per segon a comandes, sumant les tres rèpliques i desglossant per mètode:
Es fa servir el sufix _count de l'histograma (nombre d'observacions) com a comptador de peticions: cada petició mesurada és una observació.
Taxa d'error: fracció de peticions amb codi 5xx:
sum(rate(km0_peticio_durada_segons_count{servei="comandes", codi=~"5.."}[5m]))
/
sum(rate(km0_peticio_durada_segons_count{servei="comandes"}[5m]))histogram_quantile(q, buckets): percentil a partir dels buckets. El p99 de comandes, agregant totes les instàncies:
histogram_quantile(
0.99,
sum by (le) (rate(km0_peticio_durada_segons_bucket{servei="comandes", metode="POST /comandes"}[5m]))
)El sum by (le) és obligatori: cal sumar els buckets de totes les instàncies conservant l'etiqueta le (límit del bucket), que és la que histogram_quantile necessita per interpolar.
increase(counter[finestra]): quant ha pujat dins la finestra (útil per a "comandes en la darrera hora"):
Altres que apareixen a diari: avg_over_time(gauge[10m]) per suavitzar un gauge, absent(up{job="comandes"}) per detectar que no hi ha cap objectiu, topk(5, ...) per als cinc productes amb menys estoc, i operadors entre vectors (/, and, unless) que aparellen sèries per etiquetes.
Percentils contra mitjanes
Amb 1 000 peticions de 50 ms i 10 de 5 s, la mitjana és 99 ms: "tot bé". El p99 és 5 s: un de cada cent clients espera cinc segons. En un sistema distribuït, on una pàgina del catàleg fa diverses crides internes, la latència de cua s'amplifica: si cada crida té un 1 % de probabilitat de ser lenta i una petició en fa 10, un 10 % de les peticions veuen almenys una crida lenta. Per això els objectius es fixen sobre percentils (p50 per a "el que és normal", p99 per a "el que pateix algú cada minut") i mai sobre mitjanes.
- SLI, SLO, SLA i pressupost d'error
A 01-03 es van calcular els "nous": un servei amb un 99,9 % de disponibilitat pot estar caigut 43 minuts al mes. Aquell càlcul parlava d'"estar caigut"; ara hi ha instruments per definir-ho amb precisió.
- SLI (Service Level Indicator): una mesura concreta del servei, expressada com a fracció d'esdeveniments bons sobre el total. "Fracció de peticions a
POST /comandesque responen sense5xxen menys de 500 ms." - SLO (Service Level Objective): l'objectiu intern per a aquest SLI en una finestra. "El 99,9 % de les peticions, mesurat en finestres de 30 dies."
- SLA (Service Level Agreement): el compromís contractual amb conseqüències (penalitzacions). Sempre més lax que l'SLO: Quilòmetre Zero promet als productors un 99,5 % i s'exigeix un 99,9 %.
Els SLO del servei comandes:
| SLI | SLO | Com es calcula en PromQL |
|---|---|---|
Disponibilitat: peticions sense 5xx |
99,9 % en 30 dies | 1 - (sum(rate(..._count{codi=~"5.."}[30d])) / sum(rate(..._count[30d]))) |
| Latència: peticions amb durada < 500 ms | 99 % en 30 dies (p99 < 500 ms) | sum(rate(..._bucket{le="0.5"}[30d])) / sum(rate(..._count[30d])) |
Fixeu-vos com l'histograma permet expressar l'SLO de latència com una fracció: "quantes observacions han caigut al bucket le="0.5"" dividit pel total és exactament "fracció de peticions per sota de 500 ms". Això només funciona si 0,5 és un límit de bucket, i per això BUCKETS_LATENCIA inclou 0.5 i no, per exemple, 0.4 i 0.6.
Pressupost d'error
Un SLO del 99,9 % significa que es permet un 0,1 % de peticions dolentes: aquest és el pressupost d'error. Amb 2 milions de peticions al mes, són 2 000 errors. El pressupost converteix la fiabilitat en una moneda que es pot gastar:
- Si queda pressupost, l'equip desplega ràpid, prova coses, fa experiments de caos (07-06).
- Si el pressupost s'esgota (un incident de 40 minuts se l'endú gairebé sencer), es congelen els desplegaments no urgents i s'inverteix en fiabilitat.
És també el que dona sentit a les alertes: no importa un pic d'errors en si mateix, importa a quina velocitat es consumeix el pressupost.
- Alertes: símptomes, burn rate i Alertmanager
7.1 Alertar per símptomes, no per causes
Una alerta ha de despertar algú només quan un usuari està patint o patirà, i aquest algú ha de poder fer-hi alguna cosa. "CPU al 85 % a inventari-2" no compleix ni una cosa ni l'altra: pot ser normal durant la Setmana de la Verema, i no diu què fer. "El 2 % de les comandes falla des de fa 5 minuts" sí. Les causes (CPU, memòria, lag) van als dashboards, per diagnosticar quan l'alerta de símptoma salta, o es converteixen en alertes de baixa prioritat (ticket, no trucada).
Les excepcions raonables són les alertes predictives sobre causes amb termini cert: un certificat que caduca d'aquí a 12 h o un disc que s'omplirà d'aquí a 4 h al ritme actual (predict_linear) són avisos que eviten el símptoma.
7.2 Burn rate multifinestra
El burn rate és la velocitat a la qual es consumeix el pressupost d'error, relativa a la velocitat "justa". Burn rate 1 significa que, al ritme actual, el pressupost de 30 dies s'esgota exactament en 30 dies. Burn rate 14,4 significa que s'esgota en 2 dies. La tècnica que recomana el llibre SRE Workbook és alertar amb dues finestres alhora, llarga i curta:
| Severitat | Finestra llarga | Finestra curta | Burn rate | Pressupost consumit abans d'alertar | S'esgota en |
|---|---|---|---|---|---|
| Pàgina (trucada) | 1 h | 5 min | 14,4 | 2 % | ~2 dies |
| Pàgina | 6 h | 30 min | 6 | 5 % | ~5 dies |
| Ticket | 3 dies | 6 h | 1 | 10 % | 30 dies |
La finestra llarga evita que un pic de 30 segons desperti ningú; la finestra curta fa que l'alerta es resolgui tan bon punt el problema s'atura (sense ella, l'alerta d'1 h continuaria activa una hora després d'haver-ho arreglat).
7.3 observabilitat/alertes.yml
# km0/observabilitat/alertes.yml
groups:
- name: slo_comandes
rules:
# Regles d'enregistrament: precalculen la taxa d'error en diverses finestres.
# Nom convencional: nivell:metrica:operacio_finestra
- record: job:km0_comandes_taxa_error:ratio_rate5m
expr: |
sum(rate(km0_peticio_durada_segons_count{servei="comandes",codi=~"5.."}[5m]))
/ sum(rate(km0_peticio_durada_segons_count{servei="comandes"}[5m]))
- record: job:km0_comandes_taxa_error:ratio_rate1h
expr: |
sum(rate(km0_peticio_durada_segons_count{servei="comandes",codi=~"5.."}[1h]))
/ sum(rate(km0_peticio_durada_segons_count{servei="comandes"}[1h]))
- record: job:km0_comandes_taxa_error:ratio_rate30m
expr: |
sum(rate(km0_peticio_durada_segons_count{servei="comandes",codi=~"5.."}[30m]))
/ sum(rate(km0_peticio_durada_segons_count{servei="comandes"}[30m]))
- record: job:km0_comandes_taxa_error:ratio_rate6h
expr: |
sum(rate(km0_peticio_durada_segons_count{servei="comandes",codi=~"5.."}[6h]))
/ sum(rate(km0_peticio_durada_segons_count{servei="comandes"}[6h]))
# SLO 99,9 % => pressupost 0,001. Burn rate 14,4 => taxa d'error > 0,0144
- alert: ComandesBurnRateRapid
expr: |
job:km0_comandes_taxa_error:ratio_rate1h > (14.4 * 0.001)
and
job:km0_comandes_taxa_error:ratio_rate5m > (14.4 * 0.001)
for: 2m
labels:
severitat: pagina
servei: comandes
annotations:
resum: "comandes consumeix el pressupost d'error 14x més ràpid del permès"
descripcio: "Taxa d'error 1h = {{ $value | humanizePercentage }}. Runbook: runbooks/comandes-errors.md"
- alert: ComandesBurnRateLent
expr: |
job:km0_comandes_taxa_error:ratio_rate6h > (6 * 0.001)
and
job:km0_comandes_taxa_error:ratio_rate30m > (6 * 0.001)
for: 15m
labels:
severitat: pagina
servei: comandes
annotations:
resum: "comandes consumeix el pressupost d'error 6x més ràpid del permès (6h)"
- alert: ComandesLatenciaP99
expr: |
histogram_quantile(0.99, sum by (le) (
rate(km0_peticio_durada_segons_bucket{servei="comandes",metode="POST /comandes"}[10m])
)) > 0.5
for: 10m
labels:
severitat: pagina
servei: comandes
annotations:
resum: "p99 de POST /comandes per sobre de 500 ms durant 10 minuts"
- name: plataforma
rules:
- alert: KafkaLagRepartiment
expr: sum by (consumergroup) (kafka_consumergroup_lag{topic="repartiment.posicions"}) > 5000
for: 5m
labels: {severitat: pagina, servei: repartiment}
annotations:
resum: "El panell de repartiment va amb retard: lag {{ $value }} a repartiment.posicions"
- alert: KafkaLagAnalitica
expr: sum by (consumergroup) (kafka_consumergroup_lag{topic="comandes.esdeveniments", consumergroup="analitica"}) > 200000
for: 30m
labels: {severitat: ticket, servei: analitica}
annotations:
resum: "analitica acumula lag a comandes.esdeveniments (no urgent, però cal revisar-ho)"
- alert: PostgresConnexionsAltes
expr: pg_stat_activity_count / pg_settings_max_connections > 0.85
for: 5m
labels: {severitat: ticket}
annotations:
resum: "{{ $labels.instance }} fa servir més del 85 % de les seves connexions"
- alert: ObjectiuCaigut
expr: up == 0
for: 3m
labels: {severitat: pagina}
annotations:
resum: "{{ $labels.job }}/{{ $labels.instance }} no respon al scrape"
- name: seguretat_operativa
rules:
# blackbox_exporter publica la data de caducitat del certificat que veu en sondejar
- alert: CertificatCaducaAviat
expr: (probe_ssl_earliest_cert_expiry - time()) < 12 * 3600
labels: {severitat: pagina}
annotations:
resum: "El certificat de {{ $labels.instance }} caduca d'aquí a menys de 12 h"
# Cada servei exposa km0_vault_lease_segons_restants (gauge) per a les seves credencials dinàmiques
- alert: VaultLeaseNoRenovat
expr: km0_vault_lease_segons_restants < 900
for: 5m
labels: {severitat: pagina}
annotations:
resum: "{{ $labels.servei }} té un lease de Vault a menys de 15 min i no es renova"
- alert: AuditoriaCadenaTrencada
expr: increase(km0_auditoria_verificacions_fallides_total[15m]) > 0
labels: {severitat: pagina}
annotations:
resum: "La verificació de la cadena d'auditoria ha fallat (06-05)"Sobre aquestes regles:
- Les regles d'enregistrament (
record:) calculen un sol cop l'expressió cara i la desen com a mètrica nova; les alertes i els dashboards la reutilitzen. Amb finestres de 6 h sobre histogrames, fer-ho així estalvia molta feina a Prometheus. for:exigeix que la condició es mantingui aquest temps abans de disparar-se. És el primer filtre contra alertes per parpelleigs.- Els certificats de 24 h de Vault (06-04) fan que l'alerta a 12 h sigui el llindar natural: si a mitja vida no s'ha renovat, la renovació automàtica està trencada.
km0_vault_lease_segons_restantsikm0_auditoria_verificacions_fallides_totalsón mètriques que publiquen els mateixos serveis: la instrumentació de negoci i de seguretat fa servir el mateix mecanisme que la de latència.
7.4 Alertmanager: rutes, agrupació, silencis
Prometheus avalua i dispara; Alertmanager decideix a qui avisar, com agrupar i quan callar:
# km0/observabilitat/alertmanager.yml
global:
resolve_timeout: 5m
route:
receiver: operadors-ticket # destí per defecte
group_by: [alertname, servei] # una notificació per alerta+servei, no per instància
group_wait: 30s # espera per agrupar alertes que arriben juntes
group_interval: 5m
repeat_interval: 4h
routes:
- matchers: [severitat = pagina]
receiver: operadors-guardia
repeat_interval: 1h
- matchers: [servei = analitica]
receiver: equip-dades
receivers:
- name: operadors-guardia
slack_configs:
- api_url_file: /run/secrets/slack_webhook
channel: "#km0-operacions"
title: "[GUÀRDIA] {{ .CommonAnnotations.resum }}"
text: "{{ range .Alerts }}{{ .Annotations.descripcio }}\n{{ end }}"
# En producció, a més: pagerduty_configs / opsgenie_configs per trucar al mòbil
- name: operadors-ticket
email_configs:
- to: [email protected]
- name: equip-dades
slack_configs:
- api_url_file: /run/secrets/slack_webhook
channel: "#km0-dades"
inhibit_rules:
# Si un objectiu està caigut, no avisar a més de la seva latència o dels seus errors
- source_matchers: [alertname = ObjectiuCaigut]
target_matchers: [severitat = pagina]
equal: [servei]Silencis: quan en Jordi ha de reiniciar inventari-2 per a una migració, crea un silenci de 30 minuts amb un matcher (instance="inventari-2:9464") des de la interfície o amb amtool silence add instance=inventari-2:9464 --duration=30m --comment="migració". Sense silencis, l'alternativa és ignorar alertes, que és el principi de la fatiga d'alertes: quan la meitat de les notificacions són soroll, ningú no llegeix la que importa. Regles pràctiques: cada alerta de pàgina ha de tenir un runbook (07-03) i una acció possible; una alerta que salta i "s'arregla sola" tres cops seguits es degrada a ticket o s'elimina; el nombre de pàgines per setmana es revisa com una mètrica més.
- Dashboards a Grafana i monitoratge actiu
8.1 Què posar al panell de la Setmana de la Verema
Un dashboard no és una col·lecció de tot el que es pot graficar; és una narració de dalt a baix: primer si el negoci va bé, després si els serveis compleixen, després per què. Per a la campanya del Celler Roure Alt:
| Fila | Panells | Font |
|---|---|---|
| Negoci | Comandes/min (total i amb vi-crianca); taxa de pagament rebutjat; pressupost d'error restant del mes (%) |
km0_comandes_total, regles d'enregistrament |
SLO de comandes |
Taxa d'error 5m/1h amb línia al 0,1 %; p50/p95/p99 amb línia a 500 ms; burn rate actual | regles d'enregistrament, histogram_quantile |
| Dependències | p99 de ReservarEstoc; estoc de vi-crianca a inv-bcn/inv-vlc; hit ratio de Redis; lag de comandes.esdeveniments per grup |
inventari, km0_estoc_unitats, exporters |
| Saturació | km0_peticions_en_vol per servei; connexions de PostgreSQL; CPU dels brokers |
gauges, exporters |
| Vora | 429 per minut a Kong (06-05); sondes sintètiques verdes/vermelles |
Kong, blackbox |
Grafana es provisiona des de fitxers perquè el dashboard visqui al repositori, no en clics:
# km0/observabilitat/grafana/provisioning/datasources/prometheus.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
url: http://prometheus:9090
isDefault: true
# km0/observabilitat/grafana/provisioning/dashboards/km0.yml
apiVersion: 1
providers:
- name: km0
folder: Quilòmetre Zero
type: file
options:
path: /var/lib/grafana/dashboardsI un dashboard JSON reduït a dos panells, per veure'n l'estructura:
{
"title": "KM0 - Setmana de la Verema",
"uid": "km0-verema",
"refresh": "30s",
"panels": [
{
"type": "stat", "title": "Comandes / min", "gridPos": {"x": 0, "y": 0, "w": 6, "h": 4},
"targets": [{"expr": "sum(rate(km0_comandes_total{estat=\"confirmada\"}[5m])) * 60"}]
},
{
"type": "timeseries", "title": "POST /comandes p50 / p95 / p99", "gridPos": {"x": 6, "y": 0, "w": 18, "h": 8},
"fieldConfig": {"defaults": {"unit": "s", "thresholds": {"steps": [
{"color": "green", "value": null}, {"color": "red", "value": 0.5}]}}},
"targets": [
{"legendFormat": "p50", "expr": "histogram_quantile(0.50, sum by (le) (rate(km0_peticio_durada_segons_bucket{servei=\"comandes\",metode=\"POST /comandes\"}[5m])))"},
{"legendFormat": "p95", "expr": "histogram_quantile(0.95, sum by (le) (rate(km0_peticio_durada_segons_bucket{servei=\"comandes\",metode=\"POST /comandes\"}[5m])))"},
{"legendFormat": "p99", "expr": "histogram_quantile(0.99, sum by (le) (rate(km0_peticio_durada_segons_bucket{servei=\"comandes\",metode=\"POST /comandes\"}[5m])))"}
]
}
]
}8.2 Monitoratge passiu i actiu
Tot l'anterior és passiu: mesura el trànsit que hi ha. Si a les 04:00 no hi ha trànsit, la taxa d'error és 0/0 i no diu res; si Kong té malament una ruta que encara ningú no ha fet servir, tampoc. El monitoratge actiu (sondes sintètiques) genera trànsit artificial per comprovar el camí complet: el blackbox_exporter demana GET /api/v1/cataleg/mercats a través de Kong cada 15 s i publica probe_success, probe_duration_seconds i probe_ssl_earliest_cert_expiry. Una sonda més ambiciosa (un petit script que crea i cancel·la una comanda de prova amb l'usuari [email protected], marcada perquè no compti a analítica) verifica la saga sencera. Aquestes sondes són les que detecten el certificat caducat del diumenge abans que l'Anna.
8.3 OpenTelemetry Metrics
Tot el que hem vist fa servir prometheus_client directament. OpenTelemetry és l'estàndard obert que unifica mètriques, logs i traces en un SDK i un protocol (OTLP): amb ell, km0_peticio_durada_segons seria un Histogram del MeterProvider d'OTel, exportat a un Collector que el lliura a Prometheus (o a qualsevol altre backend). La semàntica (tipus, etiquetes, cardinalitat, PromQL) és la mateixa. A Quilòmetre Zero es manté prometheus_client per a mètriques perquè és més simple, i s'adopta OpenTelemetry per a les traces i la correlació amb logs a 07-02, que és on el seu valor és més gran.
Errors Comuns i Consells
- Mesurar només infraestructura. CPU i memòria no diuen si l'Anna pot comprar. Comença per RED a cada servei i per dues mètriques de negoci; afegeix USE de recursos com a suport al diagnòstic.
- Etiquetes d'alta cardinalitat.
comanda_id,usuari, IPs o rutes sense normalitzar multipliquen les sèries fins a tombar Prometheus. Si ho necessites per investigar, va a logs o traces. - Summaries en serveis replicats. Els quantils calculats al client no es poden agregar entre instàncies. Fes servir histogrames amb buckets que incloguin els llindars dels teus SLO.
- Alertar sobre mitjanes o sobre causes. La mitjana amaga la cua; la CPU al 80 % no és un problema en si mateixa. Alerta sobre SLI d'usuari i burn rate; la resta, a dashboards o tickets.
- Alertes sense
for:ni runbook. Cada parpelleig desperta algú i ningú no sap què fer. Tota alerta de pàgina té unfor, un responsable i un runbook enllaçat a l'anotació. - Oblidar les excepcions al middleware. Si la mètrica només es registra quan la petició acaba bé, la latència i els errors de les que fallen desapareixen. Registra al
finally. - Confiar només en el monitoratge passiu. Sense trànsit no hi ha senyal. Una sonda sintètica per camí crític (catàleg, crear comanda, login) cobreix les hores vall i els canvis de configuració no provats.
- Dashboards fets a mà a Grafana. Es perden, es dupliquen, ningú no sap quin és el bo. Provisiona'ls des del repositori, al costat del codi que instrumenten.
Exercicis
Exercici 1. Durant la Setmana del Formatge Artesà, la Formatgeria Montblanc pregunta quantes comandes amb formatge-curat s'han confirmat en la darrera hora, i la Marta vol saber si inventari està responent pitjor que ahir. (a) Es pot respondre la primera pregunta amb km0_comandes_total{estat="confirmada"} tal com està definida? Si no, què canviaries i quin risc de cardinalitat tindria? (b) Escriu la consulta PromQL que dona el p95 de ReservarEstoc en els darrers 5 minuts agregant les dues instàncies d'inventari, i explica per què no serveix avg(histogram_quantile(...)) per instància. (c) Quin gauge et diria si el problema és a les rèpliques inv-bcn/inv-vlc en lloc del servei?
Exercici 2. L'SLO de comandes és un 99,9 % d'èxit en 30 dies i el servei rep de mitjana 3 milions de peticions al mes. (a) Quants errors caben al pressupost? (b) A les 10:00 del dissabte comença un incident en què el 8 % de les peticions falla. Amb un trànsit de 100 peticions/s, en quant de temps s'esgota el pressupost sencer? (c) Quina de les dues alertes de burn rate (14,4 en 1 h/5 min; 6 en 6 h/30 min) saltarà primer, i aproximadament quan, tenint en compte els for:? (d) A les 10:20 l'incident es resol; per què es resol l'alerta poc després encara que la finestra d'1 h continuï contaminada?
Exercici 3. Un dilluns en Jordi troba 47 notificacions del cap de setmana a #km0-operacions: 30 de PostgresConnexionsAltes a la rèplica de comandes (que puja al 90 % cada nit durant el DAG km0_vendes_diaries i baixa sola), 12 d'ObjectiuCaigut per a repartiment durant un desplegament programat de 10 minuts, i 5 de KafkaLagAnalitica. Cap no corresponia a un problema real. Proposa, per a cada grup, el canvi concret (a alertes.yml, a alertmanager.yml o al procediment) que evita el soroll sense perdre la detecció d'un problema real, i explica quin senyal de símptoma cobriria el cas en què sí que ho fos.
Solucions
Exercici 1.
(a) No: km0_comandes_total només té l'etiqueta estat, no sap quins productes portava cada comanda. Dues opcions: un counter a part km0_linies_comanda_total{producte} incrementat per cada línia confirmada (cardinalitat = nombre de productes, acceptable si es limita als productes en campanya o si el catàleg és d'uns centenars; amb desenes de milers de productes caldria agregar per productor o respondre aquesta pregunta des d'analitica, no des de Prometheus), o afegir producte a km0_comandes_total, que és pitjor perquè una comanda amb tres productes es comptaria tres vegades i trencaria la mètrica de comandes. Mai comanda_id. (b) histogram_quantile(0.95, sum by (le) (rate(km0_peticio_durada_segons_bucket{servei="inventari", metode="ReservarEstoc"}[5m]))). Sumar primer els buckets de les dues instàncies per le reconstrueix la distribució conjunta i el percentil es calcula sobre ella; fer la mitjana de dos p95 calculats per separat no dona el p95 del conjunt (si una instància atén el 90 % del trànsit amb p95 = 40 ms i l'altra el 10 % amb p95 = 800 ms, la mitjana, 420 ms, no descriu ningú). (c) El retard de replicació que publica postgres_exporter (pg_replication_lag_seconds o l'equivalent sobre pg_stat_replication) per a inv-bcn i inv-vlc; si aquest gauge puja, les lectures a la rèplica retornen estoc vell i inventari pot estar reintentant o esperant; si és a zero, el problema és al servei o al primari.
Exercici 2.
(a) 0,1 % de 3 000 000 = 3 000 errors al mes. (b) 100 peticions/s × 8 % = 8 errors/s; 3 000 / 8 = 375 s ≈ 6 minuts i quart. Tot el pressupost del mes se'n va en poc més de sis minuts. (c) Burn rate = 0,08 / 0,001 = 80, molt per sobre de tots dos llindars. La finestra de 5 min supera l'1,44 % gairebé immediatament; la d'1 h necessita acumular: amb l'hora prèvia neta, la taxa d'error en 1 h arriba a l'1,44 % quan porten uns 11 minuts d'incident (0,08 × t / 60 min = 0,0144 → t ≈ 10,8 min); amb for: 2m, ComandesBurnRateRapid avisa cap a les 10:13. La de 6 h necessitaria 0,006 × 360 / 0,08 = 27 minuts més for: 15m, així que no arriba a saltar si l'incident dura 20 minuts. (d) Perquè la regla exigeix les dues finestres alhora (and): en resoldre's, la finestra de 5 min baixa per sota del llindar en uns minuts, i encara que la d'1 h continuï per sobre fins a les 11:20, la conjunció deixa de complir-se i Alertmanager marca l'alerta com a resolta. Aquesta és precisament la raó de ser de la finestra curta.
Exercici 3.
PostgresConnexionsAltes a la rèplica durant el DAG: és una causa, no un símptoma, i és un comportament esperat; opcions: pujar el llindar només per a la rèplica amb un unless/matcher per instance, afegir for: 30m (el DAG dura menys), o millor, degradar-la a severitat: ticket (ja ho és) i encaminar els tickets a un resum diari en lloc de a Slack en temps real (group_interval/repeat_interval llargs a la ruta per defecte); el símptoma real que sí que ha d'avisar és la latència p99 de comandes o el retard de replicació, que estan coberts per ComandesLatenciaP99. ObjectiuCaigut en desplegaments: el procediment de desplegament ha de crear un silenci (amtool silence add job=repartiment --duration=15m --comment="desplegament") com a pas del pipeline; a més, amb diverses rèpliques l'alerta hauria de ser "totes les instàncies del job caigudes" (absent(up{job="repartiment"} == 1) o count(up{job="repartiment"} == 1) == 0), no una instància; el símptoma que cobreix un desplegament realment trencat és el lag de repartiment.posicions (KafkaLagRepartiment) i la sonda sintètica del panell. KafkaLagAnalitica: 200 000 amb for: 30m és massa sensible per a un consumidor batch que es posa al dia cada nit; pujar el llindar, allargar for a diverses hores, o canviar-la per una alerta sobre el símptoma d'analítica: "el DAG km0_vendes_diaries no ha acabat a les 07:00" (mètrica d'Airflow) o "la darrera execució ha fallat". En tots els casos, el principi és el mateix: l'alerta de pàgina protegeix una experiència d'usuari concreta; la resta és informació per al diagnòstic.
Conclusió
Un sistema distribuït no avisa quan falla; cal construir els instruments. L'observabilitat es recolza en tres senyals i aquesta lliçó n'ha desenvolupat el primer: les mètriques, l'únic que escala amb el nombre de sèries i no amb el trànsit. Hem vist què mesurar en quatre capes (infraestructura amb USE, plataforma, aplicació amb RED i els senyals daurats, negoci), els quatre tipus de mètrica i per què l'histograma és el tipus adequat per a latències en serveis replicats, la regla de cardinalitat que prohibeix comanda_id com a etiqueta, el model pull de Prometheus amb els seus exporters i sondes, i el PromQL imprescindible (rate, histogram_quantile amb sum by (le), increase). Sobre aquesta base, els "nous" de 01-03 s'han convertit en SLI i SLO concrets per a comandes (99,9 % d'èxit, p99 < 500 ms), amb un pressupost d'error que es gasta i alertes de burn rate multifinestra que avisen per símptomes, encaminades per Alertmanager a qui pot actuar, i un dashboard provisionat que explica la Setmana de la Verema de dalt a baix.
Ara, quan ComandesBurnRateRapid salti un dissabte a les 10:13, en Jordi sabrà que el 8 % de les comandes falla. El que les mètriques no li diran és quines, ni per què: si és inventari que rebutja reserves de vi-crianca, la passarel·la de pagaments, o un traceparent que s'ha perdut en una capçalera de Kafka. Per a això calen els altres dos pilars: els logs estructurats i centralitzats, que expliquen què va passar amb la comanda P-2026-000125, i les traces distribuïdes, que mostren en quin servei se'n van anar els 480 ms. Aquesta és la lliçó següent.
Curs d'Arquitectures Distribuïdes
Mòdul 1: Introducció als Sistemes Distribuïts
- Conceptes Bàsics de Sistemes Distribuïts
- Models de Sistemes Distribuïts
- Avantatges i Desafiaments dels Sistemes Distribuïts
- Les Fal·làcies de la Computació Distribuïda
- Temps, Rellotges i Ordenació d'Esdeveniments
- Del Monòlit a la Plataforma Distribuïda: el Cas Quilòmetre Zero
Mòdul 2: Comunicació en Sistemes Distribuïts
- Protocols de Comunicació
- RPC i RMI
- gRPC i Serialització de Dades
- Missatgeria i Cues de Missatges
- Patrons de Comunicació Asíncrona
Mòdul 3: Consistència i Replicació
- Models de Consistència
- El Teorema CAP i PACELC
- Algorismes de Consens
- Replicació de Dades
- Transaccions Distribuïdes i Sagues
Mòdul 4: Emmagatzematge Distribuït
- Particionament de Dades i Hashing Consistent
- Sistemes de Fitxers Distribuïts
- Emmagatzematge d'Objectes
- Bases de Dades Distribuïdes
- Memòries Cau Distribuïdes
Mòdul 5: Computació Distribuïda
- Models de Computació Distribuïda
- MapReduce i Hadoop
- Spark i Computació en Memòria
- Processament de Fluxos de Dades
- Planificació de Treballs i Pipelines de Dades
Mòdul 6: Seguretat en Sistemes Distribuïts
- Autenticació i Autorització
- Xifratge i Protecció de Dades
- Gestió d'Identitats
- Seguretat entre Serveis: mTLS i Gestió de Secrets
- Passarel·les d'API, Limitació de Taxa i Auditoria
Mòdul 7: Monitoratge i Manteniment
- Monitoratge de Sistemes Distribuïts
- Logs Centralitzats i Traçabilitat Distribuïda
- Gestió de Fallades i Recuperació
- Patrons de Resiliència: Timeouts, Reintents i Circuit Breaker
- Automatització i Orquestració
- Proves en Sistemes Distribuïts i Enginyeria del Caos
