A 01-06 vam obrir un monòlit Python amb una base de dades PostgreSQL, un desplegament de dimarts i dijous i cinc símptomes que l'empenyien a canviar, i vam dibuixar una arquitectura objectiu amb una taula que remetia cada component a una lliçó futura. Trenta-vuit lliçons després, totes aquelles caselles estan construïdes: gRPC i Kafka, sagues i rèpliques, Cassandra i Redis, Spark i Flink, Keycloak i Vault, Prometheus i Kubernetes, MQTT i WebSockets, Terraform i Lambda, la CDN i la cua SQLite de la furgoneta. Aquesta última lliçó no afegeix cap peça nova: ajunta les que hi ha. Primer l'arquitectura completa en un sol diagrama i la taula de 01-06 tancada; després el recorregut d'una comanda de l'Anna des del seu navegador fins a la factura a S3, amb tot el que deixa al seu pas en traces, mètriques i auditoria; després les sis decisions d'arquitectura com a ADR, amb les seves alternatives rebutjades i les seves conseqüències; els cinc símptomes de 01-06 amb la seva resolució; l'avaluació de la plataforma amb els criteris del mateix curs (fal·làcies, consistència per dada, SLO, RPO/RTO, controls de seguretat); el que queda fora; el projecte que l'alumne pot construir al seu portàtil amb fites verificables; i una guia per continuar aprofundint. Els exercicis són d'integració: incorporar un domini nou, analitzar un incident d'extrem a extrem i retallar l'arquitectura per a tres persones. La conclusió tanca el curs.
Contingut
- L'arquitectura final de Quilòmetre Zero
- La taula de 01-06, tancada
- El recorregut d'una comanda de l'Anna d'extrem a extrem
- El que la comanda deixa al seu pas: traces, mètriques i auditoria
- Les decisions d'arquitectura com a ADR
- Els cinc símptomes de 01-06, revisitats
- Avaluació amb els criteris del curs
- El que queda fora i passos següents
- El projecte: Quilòmetre Zero reduït en
docker-compose - Guia d'estudi
- Errors Comuns i Consells
- Exercicis
- Conclusió
- L'arquitectura final de Quilòmetre Zero
flowchart TB
subgraph Clients
ANA[Navegador / mòbil de l'Anna]
APPR[App repartidors]
PANP[Panell productors]
FURG[furgoneta-3<br/>agregador SQLite]
end
subgraph Vora[Vora de xarxa]
CDN[CDN + Worker<br/>memòria cau per mercat, JWT]
end
subgraph Regio[Regió eu-west-1, 3 AZ]
ALB[ALB] --> KONG[Kong<br/>jwt, rate-limiting, X-Request-Id]
KONG --> BFF[BFF app repartidors]
subgraph EKS[EKS + service mesh mTLS]
CAT[cataleg]
PED[comandes<br/>saga, outbox]
INV[inventari<br/>gRPC]
PAG[pagaments<br/>ACL passarella]
REP[repartiment<br/>ws_servidor, pont MQTT]
ANA2[analitica]
MQ[Mosquitto]
OBS[Prometheus · Loki · Tempo · Grafana]
KC[Keycloak realm km0]
VLT[Vault / Secrets Manager]
end
KONG --> CAT & PED & REP
BFF --> REP & PED & CAT
PED -- gRPC mTLS --> INV
PED -- gRPC mTLS --> PAG
PAG --> PAS[(Passarella de pagaments)]
subgraph Dades[Dades]
RDS[(RDS PostgreSQL Multi-AZ<br/>km0_inventari)]
CAS[(Cassandra 3 nodes<br/>km0_comandes)]
RED[(ElastiCache Redis<br/>memòria cau, limitador, pub/sub)]
S3[(S3 km0-fotos, factures,<br/>backups, auditoria)]
LLAC[(Llac S3 /km0/esdeveniments)]
ANADB[(PostgreSQL km0_analitica)]
end
INV --> RDS
PED --> CAS
CAT --> RED
CAT --> S3
subgraph Missatgeria[Missatgeria]
MSK[(MSK Kafka<br/>comandes.esdeveniments, inventari.alertes,<br/>repartiment.posicions, repartiment.panell,<br/>auditoria.esdeveniments)]
end
PED -. outbox .-> MSK
INV -. outbox .-> MSK
MSK -.-> CAT & REP & ANA2
MQ --> REP --> MSK
subgraph Comput[Computació]
FLK[Flink alertes_estoc, panell]
SPK[Spark vendes_diaries, EMR spot]
AIR[Airflow km0_vendes_diaries]
end
MSK --> FLK --> MSK
MSK --> LLAC --> SPK --> ANADB
AIR --> SPK
subgraph Serverless
LMIN[Lambda miniatures]
LFAC[Lambda factures]
end
S3 -- s3:ObjectCreated --> LMIN --> S3
MSK -- pagament.confirmat --> LFAC --> S3
end
subgraph DR[Regió eu-central-1, pilot light]
RDSR[(Rèplica RDS)]
S3R[(S3 replicat)]
MSKR[(MSK mínim, MirrorMaker)]
end
RDS -.-> RDSR
S3 -.-> S3R
MSK -.-> MSKR
ANA --> CDN --> ALB
PANP --> CDN
APPR --> ALB
FURG -- MQTT QoS 1 --> MQ
REP -- WebSocket --> ANA
KC -. OIDC .- ANA & APPR & PANP
El diagrama té una peça que fins ara només estava promesa: el BFF de l'app de repartidors, anunciat a 06-05 i situat a 08-01. És un servei petit, propietat de l'equip de repartiment, darrere de Kong, que compon la pantalla d'inici de l'app (ruta del dia de repartiment, estat de les comandes d'aquesta ruta de comandes, noms i fotos dels productes de cataleg) en una sola resposta, amb crides en paral·lel, timeouts curts i degradació (si cataleg no respon, la ruta es mostra sense fotos). No té base de dades: és composició a l'API (08-01, apartat 5), justificada perquè l'app fa poques crides, les dades són petites i necessita frescor.
- La taula de 01-06, tancada
La taula de 01-06 era una promesa: cada component, la lliçó on es construiria. Aquesta és la mateixa taula, tancada, amb el que finalment es va construir i el fitxer o el nom que ho materialitza a km0/.
| Component | Lliçó | Què es va construir |
|---|---|---|
| Protocols: TCP/UDP/HTTP; MQTT i WebSockets presentats | 02-01 | Client TCP amb timeout; mesura de capçaleres |
| RPC síncron entre serveis | 02-02 | Stubs, marshalling, semàntiques de fallada |
| gRPC amb contractes | 02-03 | contractes/inventari.proto (ReservarEstoc, ConsultarEstoc, ObservarCanvis) |
| Bus d'esdeveniments i cues | 02-04 | Kafka comandes.esdeveniments (6 particions, clau = comanda), embolcall id_esdeveniment/tipus/versio/data_ms/origen/dades |
| Idempotència, outbox, DLQ, consumidors en competència | 02-05 | Taula outbox, missatges_processats, tòpics .retry i DLQ |
| Models de consistència; CRDT | 03-01 | Taula de consistència per dada; G-Counter |
CAP i PACELC: inventari (C) i comandes (A) |
03-02 | Decisió per domini |
| Consens: Raft, etcd, elecció de líder | 03-03 | etcd per a Patroni; controlador de Kafka |
| Replicació: primari/rèplica, multilíder, quòrums | 03-04 | inv-bcn/inv-vlc; Patroni |
| Sagues | 03-05 | saga_comanda.py, taula sagas, orquestració i coreografia |
| Particionament i hashing consistent | 04-01 | Anell, particions de Kafka i Cassandra |
| Sistemes de fitxers distribuïts | 04-02 | Llac /km0/esdeveniments/... (HDFS, després S3) |
| Emmagatzematge d'objectes | 04-03 | MinIO/S3 km0-fotos, km0-factures, km0-backups, km0-auditoria; fotos.py; URL presignades |
| Bases de dades distribuïdes | 04-04 | Cassandra km0_comandes (comandes_per_client, comandes_per_id), repositori amb nivells de consistència |
| Memòries cau | 04-05 | Redis cache-aside per a cataleg, invalidació per estoc.actualitzat, Redis Cluster |
| Models de computació | 05-01 | BSP, dataflow, actors |
| MapReduce i Hadoop | 05-02 | Vendes per mercat sobre el llac |
| Spark | 05-03 | vendes_diaries.py |
| Fluxos | 05-04 | Flink alertes_estoc.py (inventari.alertes), panell repartiment.posicions → repartiment.panell |
| Pipelines | 05-05 | Airflow DAG km0_vendes_diaries |
| AuthN/AuthZ | 06-01 | JWT (sub, roles, jti), RBAC/ABAC als serveis |
| Xifratge | 06-02 | TLS, xifratge en repòs amb claus pròpies, xifratge de camp per a pagaments i posicions |
| Identitats | 06-03 | Keycloak realm km0, clients web-km0, app-repartidors, comandes-servei; OIDC |
| mTLS i secrets | 06-04 | km0-ca, AuthInterceptor/ServeiInterceptor, Vault (AppRole, credencials dinàmiques) |
| Gateway i auditoria | 06-05 | Kong (/api/v1/cataleg, /api/v1/comandes, jwt, rate-limiting, X-Request-Id), auditoria.esdeveniments, km0-auditoria |
| Mètriques i SLO | 07-01 | serveis/comu/metriques.py, km0_comandes_total, SLO 99,9 % i p99 < 500 ms, alertes per taxa de consum de pressupost d'error |
| Logs i traces | 07-02 | serveis/comu/{logs,traces}.py, Loki, Tempo, OpenTelemetry, X-Request-Id |
| Fallades i recuperació | 07-03 | Patroni + etcd, ISR, checkpoints, còpies de seguretat i PITR a km0-backups, RPO/RTO, DR pilot light, runbooks |
| Resiliència | 07-04 | serveis/comu/resiliencia.py: timeouts, reintents amb jitter, circuit breaker, bulkhead, load shedding |
| Automatització i orquestració | 07-05 | Ansible, k8s/comandes-deployment.yaml, HPA, canary, GitOps, mesh |
| Proves i caos | 07-06 | Testcontainers, contractes, k6 tests/carrega/verema.js, Toxiproxy, Chaos Mesh |
| Microserveis | 08-01 | Bounded contexts, serveis/inventari/api.py, productes_vista (CQRS), versionat /v1→/v2, ADR-001 |
| Temps real | 08-02 | Mosquitto amb ACL, furgoneta_mqtt.py, pont_mqtt_kafka.py, ws_servidor.py, seguiment.js, SSE d'alertes |
| Núvol | 08-03 | infra/aws/main.tf (VPC 3 AZ, EKS, RDS Multi-AZ, MSK, ElastiCache, S3, IRSA), FinOps |
| Serverless i edge | 08-04 | serverless/miniatures, serverless/factures, template.yaml, saga en Step Functions, Worker de CDN, vora/furgoneta/agregador.py |
| Extrem a extrem | 08-05 | Aquesta lliçó |
- El recorregut d'una comanda de l'Anna d'extrem a extrem
L'Anna, a València, compra dos formatge-curat de la Formatgeria Montblanc i un vi-crianca del Celler Roure Alt. La comanda és P-2026-000125. Aquest és el recorregut complet, amb la lliçó en què es va construir cada pas.
sequenceDiagram
autonumber
participant A as Navegador de l'Anna
participant CDN as CDN + Worker
participant K as Kong
participant P as comandes
participant I as inventari
participant PG as pagaments
participant KF as Kafka
participant C as cataleg
participant AN as analitica (Flink, llac)
participant R as repartiment
participant L as Lambda factures
A->>CDN: GET /api/v1/cataleg/productes/formatge-curat (JWT)
CDN-->>A: HIT: fitxa per mercat valencia (08-04)
A->>CDN: POST /api/v1/comandes (JWT, Idempotency-Key)
CDN->>K: verifica signatura JWT, reenvia (no desable)
K->>K: plugin jwt, rate-limiting, X-Request-Id (06-05)
K->>P: POST /comandes + X-Request-Id
P->>P: Idempotency-Key ja vista? no → crear comanda (creada) a Cassandra (04-04)
P->>P: iniciar saga: fila a sagas (03-05)
P->>I: gRPC ReservarEstoc (mTLS, ServeiInterceptor, timeout 300 ms, circuit breaker)
I->>I: SELECT FOR UPDATE a RDS, reserva 15 min; outbox: estoc.reservat, estoc.actualitzat
I-->>P: Reserva OK
P->>PG: gRPC Cobrar (Idempotency-Key cobrament-P-2026-000125)
PG->>PG: passarella externa (ACL), 1,2 s
PG-->>P: pagament autoritzat
P->>P: estat pagada; outbox: comanda.creada, pagament.confirmat (mateixa transacció)
P-->>K: 201 Created {id, estat: pagada}
K-->>CDN: 201 Created
CDN-->>A: 201 (p99 < 500 ms sense comptar la passarella: SLO 07-01)
Note over P,KF: relay d'outbox → comandes.esdeveniments (partició per id de comanda)
KF-->>C: estoc.actualitzat → productes_vista + invalidar Redis (08-01, 04-05)
KF-->>AN: comanda.creada, pagament.confirmat → llac /km0/esdeveniments; Flink alertes_estoc (05-04)
KF-->>R: pagament.confirmat → assignar furgoneta-3, publicar repartiment.assignat
KF-->>L: pagament.confirmat (grup factures-lambda) → PDF a km0-factures (08-04)
A->>R: wss://.../ws {jwt}, {subscriure: P-2026-000125}
R->>R: autoritzar sub = client; tema furgoneta:furgoneta-3
loop cada 5 s
Note over R: furgoneta-3 → MQTT → pont → repartiment.posicions → Redis pub/sub
R-->>A: {tipus: posicio, seq, lat, lon}
end
Cinc observacions sobre el diagrama que resumeixen el curs:
- Només dues crides síncrones entre serveis (passos 9 i 12), totes dues amb timeout, circuit breaker i mTLS, i totes dues perquè la seva resposta governa la decisió següent (08-01). Tota la resta són esdeveniments.
- Cap transacció no creua serveis. La comanda es crea a Cassandra, la reserva a RDS i el cobrament a la passarel·la com a transaccions locals; la saga les encadena i compensa (03-05); l'outbox garanteix que els esdeveniments surten si i només si la transacció local es confirma (02-05).
- La idempotència apareix quatre vegades: la
Idempotency-Keyde l'Anna a Kong/comandes(un doble clic no crea dues comandes), l'id_reservaainventari, la claucobrament-<comanda>cap a la passarel·la, i l'id_esdevenimenta cada consumidor. Un reintent en qualsevol punt és segur. - L'Anna rep la resposta al pas 17, abans que existeixi la vista del catàleg, l'assignació de repartiment o la factura. El que veu després (posició de la furgoneta, factura a la seva àrea personal) arriba per camins asíncrons amb segons de retard, que és l'estat normal (08-01).
- La vora i el gateway fan la seva feina abans que res arribi als serveis: el JWT es verifica tres vegades (Worker, Kong, servei), el rate limiting protegeix
comandes, i la fitxa del producte ni tan sols ha arribat a la regió.
- El que la comanda deixa al seu pas: traces, mètriques i auditoria
Una comanda no només produeix estat; produeix evidència. El que queda després de P-2026-000125:
| Senyal | On | Contingut | Lliçó |
|---|---|---|---|
| Traça | Tempo | Un trace_id generat a Kong (o propagat des del navegador), amb spans: kong → comandes POST /comandes → inventari.ReservarEstoc (12 ms) → pagaments.Cobrar (1 210 ms, amb span fill passarella) → cassandra.insert → outbox.insert. Després, spans enllaçats (no fills) als consumidors: cataleg.projeccio, repartiment.assignar, factures.handler, units per l'id_esdeveniment propagat a les capçaleres de Kafka |
07-02 |
| Logs | Loki | Línies JSON amb request_id, trace_id, comanda_id, sub=u-anna, nivell i missatge, a cada servei; consultables amb {servei="comandes"} | json | comanda_id="P-2026-000125" |
07-02 |
| Mètriques | Prometheus | km0_comandes_total{estat="confirmada"} +1; histograma de latència de POST /comandes (bucket 0,5 s); km0_grpc_client_durada_segons{metode="ReservarEstoc"}; km0_circuit_estat{dependencia="pagaments"}=0; lag del grup cataleg-projeccio; km0_ws_connexions +1 |
07-01 |
| Auditoria | auditoria.esdeveniments → km0-auditoria (immutable, amb retenció legal) |
{"sub": "u-anna", "accio": "comandes.crear", "recurs": "P-2026-000125", "des_de": ..., "request_id": ..., "resultat": "ok"}; i l'accés de comandes-servei a inventari registrat pel ServeiInterceptor |
06-05 |
| Esdeveniments de negoci | comandes.esdeveniments (retenció 7 dies) i llac /km0/esdeveniments/comandes.esdeveniments/data=2026-09-15/ (per sempre) |
comanda.creada, estoc.reservat, estoc.actualitzat, pagament.confirmat, repartiment.assignat |
02-04, 04-02 |
| Estat | Cassandra (comandes_per_client, comandes_per_id, sagas), RDS (referencies, reserves, outbox), productes_vista, S3 (factures/u-anna/P-2026-000125.pdf) |
La comanda, la reserva, la saga completada, la vista i la factura | Mòduls 3 i 4 |
| Cost | Factura del proveïdor, per etiqueta | Unes fraccions de cèntim: invocacions, transicions, GB-s, egress de la factura | 08-03 |
La prova que l'observabilitat està ben feta és que amb el comanda_id es pot reconstruir tot: dels logs al trace_id, de la traça a cada servei i la seva latència, de la mètrica a si aquesta comanda va ser una de les que van trencar l'SLO, de l'esdeveniment al llac al que analitica va calcular, de l'auditoria a qui ho va fer. És el que l'exercici 2 posa a prova.
- Les decisions d'arquitectura com a ADR
A 08-01 es va escriure l'ADR-001 complet. Aquestes són les sis decisions estructurals de Quilòmetre Zero en format resumit: cadascuna amb l'alternativa que es va rebutjar i la conseqüència negativa que es va acceptar, que és la part que d'aquí a un any permetrà saber si continua sent vàlida.
| ADR | Decisió | Alternativa rebutjada i per què | Conseqüència acceptada | Lliçó |
|---|---|---|---|---|
| ADR-001 | Extreure inventari del monòlit com a bounded context amb km0_inventari (PostgreSQL amb Patroni / RDS Multi-AZ) i API gRPC + esdeveniments |
Mantenir l'estoc dins de comandes (acoblaria regles i desplegaments); estoc a Cassandra (transaccions lleugeres cares per comparar-i-actualitzar); 2PC (bloquejos i passarel·la externa fora del protocol) |
Consistència eventual entre estoc real i catàleg (finestra de segons); una crida síncrona més al camí crític (+12 ms p99); un component amb estat a operar | 08-01 |
| ADR-002 | Cassandra (3 nodes, RF=3, QUORUM) per a km0_comandes, amb taules per consulta |
PostgreSQL amb Citus (coordinador i transaccions distribuïdes que el patró d'accés no necessita); MongoDB (primari únic amb failover); PostgreSQL i acceptar 30 s d'indisponibilitat en campanya | Sense JOIN ni consultes ad hoc: tota pregunta nova és una taula o una vista CQRS; nivell de consistència triat a cada operació (un bug silenciós si es tria malament); operació de compactacions, reparacions i snapshots |
03-02, 04-04 |
| ADR-003 | Sagues orquestrades des de comandes (saga_comanda.py, taula sagas) per confirmar comanda |
Coreografia pura (cada servei reacciona a esdeveniments: més desacoblada, però el flux queda implícit i difícil de seguir i de compensar en ordre); Step Functions (latència i cost per transició en un camí crític d'1,2 M/mes; lock-in) | comandes coneix inventari i pagaments (acoblament acceptat); l'orquestrador és un component amb estat que cal recuperar en arrencar; absència d'aïllament entre sagues (contramesures de 03-05: reserves amb caducitat, estats pendents) |
03-05, 08-04 |
| ADR-004 | Kafka com a bus d'esdeveniments únic (comandes.esdeveniments amb clau = comanda, repartiment.*, inventari.alertes, auditoria.esdeveniments), amb outbox als productors i consumidors idempotents |
RabbitMQ per a tot (sense retenció ni replay, imprescindibles per reconstruir vistes CQRS i alimentar el llac); crides HTTP entre serveis per notificar (acoblament temporal, símptoma 2); un ESB amb transformacions (la lliçó de la SOA) | Consistència eventual com a norma; ordre només per partició; esquemes d'esdeveniments com a contractes a versionar; un clúster crític (MSK) del qual depèn tot; retenció finita tret del llac | 02-04, 02-05, 08-01 |
| ADR-005 | Kubernetes (EKS) amb service mesh, GitOps i canary com a plataforma de desplegament dels serveis; serveis gestionats per a dades (RDS, MSK, ElastiCache, S3) | Màquines virtuals amb Ansible per a tot (sense autoescalat ni reconciliació, desplegaments manuals, símptoma 3); PaaS tipus Cloud Run/App Runner per servei (menys control de xarxa, mesh i operadors; encaixaria per a una versió petita); Kubernetes autogestionat (operar el pla de control sense benefici) | Complexitat d'operació (namespaces, RBAC, mesh, operadors) assumida per un equip de plataforma; 1-2 ms per salt del sidecar; lock-in selectiu als gestionats; cost base permanent (≈ 3 900 €/mes amb reserves) | 07-05, 08-03 |
| ADR-006 | Serverless (Lambda amb SAM) per a tasques d'esdeveniment esporàdiques: miniatures, factures, webhooks de la passarel·la, tasques programades; Step Functions només per a fluxos llargs i rars (baixa de productor) | Consumidors a Kubernetes per a tot (cost permanent per a tasques que corren segons al dia; sondes i guàrdia sense motiu); serverless per a comandes o el WebSocket (latència, estat, connexions); Step Functions per a la saga de comanda (ADR-003) |
Lock-in alt als adaptadors (limitat separant gestor i lògica); reintents automàtics que obliguen a idempotència estricta; arrencades en fred; observabilitat que cal unir a la del clúster; revisar el cost si la freqüència es multiplica per deu | 08-04 |
Cada ADR és a km0/docs/adr/ADR-00N-*.md, amb el format complet de 08-01. Fixeu-vos que tres de les sis decisions (002, 003, 006) rebutgen explícitament una opció "més moderna" o "més gestionada": l'arquitectura no és la suma de les millors tecnologies sinó de les decisions que responen a un símptoma amb un cost conegut.
- Els cinc símptomes de 01-06, revisitats
| Símptoma (01-06) | Què demanava | Com va quedar resolt | Com es verifica |
|---|---|---|---|
| 1. Els pics de campanya ho tomben tot (Verema ×15; 92 % lectures de catàleg) | Escalar el catàleg a part, servir lectures des de la memòria cau i fotos des d'un altre lloc, aïllar fallades | cataleg com a servei propi amb Redis cache-aside (04-05), fotos a S3 amb miniatures per Lambda (04-03, 08-04) i CDN amb immutable (08-04); HPA i nodes spot en campanya (07-05, 08-03); rate limiting a Kong (06-05); load shedding (07-04) |
k6 tests/carrega/verema.js a ×15 a staging abans de cada campanya (07-06): p99 de catàleg < 100 ms, hit ratio de CDN > 95 %, comandes sense degradació |
| 2. Una fallada de pagaments ho tomba tot (passarel·la a 30 s; connexions esgotades) | pagaments aïllat amb els seus recursos, lentitud continguda, confirmació sense transacció global |
pagaments amb ACL davant de la passarel·la (08-01); timeout, circuit breaker i bulkhead a comandes → pagaments (07-04); saga amb reserva d'estoc amb caducitat i estat "pagament pendent" (03-05); base de dades pròpia (ADR-001/002) |
Experiment de caos: Toxiproxy afegeix 30 s de latència a la passarel·la (07-06); el circuit s'obre en < 10 s, el catàleg i el seguiment continuen, les comandes queden pendents i es completen en tancar-se el circuit |
| 3. Els equips es trepitgen en desplegar (4 equips, desplegament de dimarts i dijous) | Unitats de desplegament independents per domini, sense aturada | Un Deployment per servei amb rolling i canary automàtic contra SLO (07-05); proves de contracte que protegeixen els consumidors (07-06); GitOps: desplegar és fer merge; equips propietaris de servei i guàrdia per servei (08-01) | Freqüència de desplegament per servei (mètrica DORA): repartiment diverses vegades al dia, pagaments una vegada per sprint, sense coordinació; zero desplegaments coordinats l'últim trimestre |
| 4. La telemetria de repartidors en temps real (140 repartidors, 2,4 M posicions/dia a la taula de comandes; clients preguntant) | Canal per a fluxos d'alta freqüència, comunicació bidireccional, emmagatzematge de sèries temporals | MQTT amb QoS 1, ACL i sessions (08-02); pont a repartiment.posicions amb clau (02-04); Cassandra per a posicions (04-04); Flink per al panell (05-04); WebSockets amb fan-out per Redis a l'Anna i als operadors (08-02); agregador a la furgoneta (08-04) |
Latència d'extrem a extrem (furgoneta → navegador) p95 < 3 s; cap escriptura de posicions a la base de comandes; km0_ws_descarts_total ≈ 0 fora d'incidents |
| 5. L'analítica ofega la producció (3 h de consultes nocturnes sobre producció) | Separar la càrrega analítica amb una còpia alimentada per esdeveniments | analitica consumeix comandes.esdeveniments i repartiment.* cap al llac (04-02); Spark vendes_diaries.py a EMR efímer amb spot (05-03, 08-03); Flink per al que no espera el lot (05-04); Airflow km0_vendes_diaries (05-05); km0_analitica separada |
Zero consultes d'analitica contra km0_inventari o km0_comandes (la política de xarxa i les credencials ho impedeixen: 06-04, 07-05); el DAG acaba abans de les 06:00 amb SLA a Airflow; els productors tenen previsions que abans no s'atrevien a demanar |
- Avaluació amb els criteris del curs
Un bon curs deixa criteris, no només tècniques. Aquests són els cinc amb què Quilòmetre Zero s'avalua a si mateixa, i amb què l'alumne pot avaluar qualsevol altra plataforma.
Les vuit fal·làcies (01-04)
| Fal·làcia | On la plataforma l'assumeix com a falsa |
|---|---|
| La xarxa és fiable | Timeouts a tota crida (07-04); reintents amb idempotència (02-05); QoS 1 i cua persistent a la furgoneta (08-02, 08-04); replicació i quòrums (03-04) |
| La latència és zero | Dues crides síncrones com a màxim al camí crític (08-01); memòria cau en tres capes (04-05); CDN i Worker (08-04); SLO de latència mesurat (07-01) |
| L'amplada de banda és infinita | gRPC binari (02-03); MQTT amb 2 bytes de capçalera (08-02); coalescència cap a clients lents (08-02); agregació de trams a la furgoneta i CDN per a l'egress (08-04) |
| La xarxa és segura | TLS a tot arreu (06-02); mTLS amb mesh (06-04); JWT verificat a la vora, al gateway i al servei (06-01, 06-05, 08-04); ACL al broker; grups de seguretat i subxarxes privades (08-03) |
| La topologia no canvia | Descobriment de serveis per Kubernetes i mesh (07-05); hashing consistent (04-01); reconnexió amb backoff als clients (08-02); failover d'RDS per DNS (08-03) |
| Hi ha un sol administrador | Equips propietaris de servei, plataforma interna, RBAC de Kubernetes i IAM per service account (08-01, 07-05, 08-03); contractes i ADR com a acords |
| El cost de transport és zero | Egress, trànsit entre AZ i NAT al pressupost; endpoints de VPC; client.rack; FinOps (08-03) |
| La xarxa és homogènia | Contractes protobuf i esquemes d'esdeveniments versionats (02-03, 02-05); l'últim tram dissenyat per a xarxes mòbils (08-02) |
Model de consistència per dada (03-01, 03-02)
| Dada | Model | Mecanisme | Qui la llegeix així |
|---|---|---|---|
| Unitats disponibles d'una referència per mercat | Forta (linealitzable per fila) | SELECT FOR UPDATE al primari d'RDS; rèplica síncrona |
inventari.ReservarEstoc |
| Estat del cobrament | Forta a pagaments; idempotent cap a la passarel·la |
Transacció local + Idempotency-Key |
La saga |
| Comanda de l'Anna (per id) | Quòrum (lectura de la teva pròpia escriptura garantida per QUORUM+QUORUM) |
Cassandra RF=3 | comandes en confirmar; l'Anna en consultar la seva comanda |
| Llistat de comandes d'un client | Eventual (ONE) |
Cassandra | La pantalla "les meves comandes" |
| Disponibilitat a la fitxa del catàleg | Eventual (segons) | productes_vista per esdeveniments + Redis + CDN 60 s |
L'Anna navegant |
| Panell del productor | Eventual (segons) | Projecció CQRS | Marta Puig |
| Posició de la furgoneta | Eventual, "l'últim guanya" per seq |
MQTT retained, Redis ultima:, deduplicació per seqüència |
L'Anna, els operadors |
| Estoc físic de la parada del mercat | Multilíder amb autoritat local; CRDT per a comptadors | Reconciliació en sincronitzar | El terminal de Lleida |
| Informes de vendes | Eventual (hores) | Llac + Spark nocturn | Productors, direcció |
| Auditoria | Forta en escriptura, immutable | Outbox → auditoria.esdeveniments → S3 amb bloqueig d'objectes |
Compliment |
La lliçó d'aquesta taula és que "forta" apareix dues vegades i "eventual" sis: la consistència forta es paga i es reserva per al que de debò la necessita.
SLO (07-01) i RPO/RTO (07-03)
| Servei / dada | SLO | RPO | RTO |
|---|---|---|---|
comandes (crear) |
99,9 % disponibilitat; p99 < 500 ms (sense la passarel·la) | km0_comandes: 0 amb QUORUM davant d'un node; minuts davant de pèrdua de regió (snapshots) |
Node: 0 (sense failover); regió: 1-2 h (runbook pilot light) |
inventari |
99,95 %; p99 < 100 ms a ReservarEstoc |
0 (rèplica síncrona Multi-AZ); minuts entre regions | ~60 s (failover d'RDS); regió: 30-60 min |
cataleg |
99,9 %; p99 < 200 ms (100 ms amb CDN) | N/A (reconstruïble des dels esdeveniments i S3) | Minuts (rellegir comandes.esdeveniments) |
repartiment temps real |
99 %; latència d'extrem a extrem p95 < 3 s | Posicions: es toleren pèrdues | Minuts |
| Kafka | 99,95 % | 0 amb acks=all, min.insync.replicas=2 |
Segons (elecció de líder) |
analitica |
Informe diari abans de les 06:00 (SLA d'Airflow) | Hores | Hores (reexecutar el DAG) |
| Fotos i factures | 99,99 % (S3) | ~0 (versionat + CRR) | Minuts (canviar de regió a la CDN) |
Controls de seguretat (Mòdul 6): qui pot fer què
| Actor | Identitat | Pot | No pot | Control |
|---|---|---|---|---|
| Anna (client) | JWT de Keycloak, roles: [client], client web-km0 |
Veure el catàleg, crear comandes pròpies, veure les seves comandes i factures, seguir els seus repartiments per WebSocket, xatejar amb els seus productors | Veure comandes del Marc; subscriure's a un mercat sencer; publicar productes | RBAC als serveis (06-01); autorització per subscripció (08-02); rate limit per consumidor (06-05) |
| Marta Puig (productora) | JWT, roles: [productor], claim productor: formatgeria-montblanc |
Publicar i editar els seus productes, pujar fotos per URL presignada, veure el seu panell de comandes, rebre alertes d'estoc per SSE, respondre al xat | Tocar productes de l'Horta La Vega; veure dades personals de clients més enllà del nom i el lliurament | ABAC "els seus productes" a cataleg (06-01); URL presignada amb prefix (04-03); projecció filtrada per productor (08-01) |
| Jordi Sala (operador) | JWT, roles: [operador] |
Panell de repartiment de tots els mercats, reassignar furgonetes, enviar ordres per MQTT (via repartiment), veure l'auditoria de repartiment |
Modificar preus; accedir a dades de pagament | RBAC; repartiment publica a ordres amb la identitat pont (08-02); auditoria de cada ordre |
furgoneta-3 (dispositiu) |
Usuari MQTT propi, credencial a Vault, revocable | Publicar a km0/repartiment/furgoneta-3/{posicio,estat,tram}, llegir les seves ordres |
Publicar com a furgoneta-7; llegir res més |
ACL de Mosquitto (08-02); validació al pont; xifratge local (08-04) |
comandes (servei) |
Certificat mTLS emès pel mesh (SPIFFE), client comandes-servei a Keycloak, service account amb rol IAM |
ReservarEstoc, ConsultarEstoc a inventari; Cobrar a pagaments; produir a comandes.esdeveniments; llegir els seus secrets |
Llegir km0_inventari directament; produir a repartiment.*; accedir a S3 |
ServeiInterceptor per mètode (06-04); polítiques d'autorització del mesh i de xarxa (07-05); IRSA (08-03); credencials dinàmiques de Vault |
| Lambda factures | Rol IAM de la funció | Consumir comandes.esdeveniments (grup propi), escriure a km0-factures, PutItem a km0-idempotencia |
Llegir km0-fotos; escriure a Kafka |
Polítiques de SAM (08-04) |
| Worker de CDN | Cap credencial secreta | Verificar signatures amb la clau pública, desar respostes public |
Accedir a bases de dades; veure la clau privada | Disseny "res de secret a la vora" (08-04) |
| Equip de plataforma | IAM amb MFA, RBAC de Kubernetes per namespace | Operar la infraestructura, aplicar Terraform des del pipeline | Llegir dades de pagament desxifrades; aplicar Terraform des d'un portàtil a producció | Mínim privilegi, GitOps, auditoria de CloudTrail (08-03) |
- El que queda fora i passos següents
Cap arquitectura no està acabada; aquesta té cinc fronts oberts, cadascun amb el mòdul del curs que dona les eines per abordar-lo:
- Multiregió activa-activa. Avui hi ha pilot light (07-03, 08-03): la regió secundària triga 1-2 hores a servir. Passar a activa-activa exigiria decidir el model d'escriptura per dada:
km0_comandesa Cassandra multi-DC (LOCAL_QUORUMper regió, 04-04) és natural; l'estoc (inventari) no ho és, i obligaria a particionar l'autoritat per mercat (Girona i Lleida escriuen aeu-west-1, València aeu-central-1) amb el model multilíder de 03-04 per als mercats repartits. És un projecte de mesos i l'ADR-007 pendent. - Event sourcing complet. Avui els esdeveniments són notificacions de canvis d'estat que es desen a part (i per sempre al llac). L'event sourcing faria dels esdeveniments la font de veritat de
comandes: l'estat es reconstrueix reproduintcomanda.creada,pagament.confirmat, ... Es guanya auditoria perfecta i reconstrucció de qualsevol vista; es paga amb snapshots, versionat d'esdeveniments històrics i consultes sempre per projecció. CQRS (08-01) ja deixa el terreny preparat. - Data mesh.
analiticaés avui un equip central que ho consumeix tot. A mesura que creixen els dominis, cada equip passaria a publicar les seves dades com a producte (amb esquema, qualitat, SLA i propietari) ianaliticaa ser plataforma. És la llei de Conway (08-01) aplicada a les dades. - Plataforma interna com a producte. L'equip de plataforma de 08-01 existeix; el que falta és tractar-lo com a producte: un portal on un equip crea un servei nou amb el seu Deployment, els seus dashboards, les seves alertes, el seu pipeline i el seu ADR de plantilla en minuts (l'exercici 1 ho posa a prova).
- Cost com a restricció de disseny. FinOps (08-03) mesura; el pas següent és que el cost entri a les decisions d'arquitectura com hi entra la latència: euros per comanda com a SLO, revisió trimestral dels serveis gestionats davant dels propis, i apagada automàtica de tot el que no serveixi trànsit.
- El projecte: Quilòmetre Zero reduït en
docker-compose
docker-composeEl curs ha mostrat codi a cada lliçó; el projecte proposa construir una versió reduïda i funcional al portàtil, amb el que cap a docker-compose i sense núvol: cataleg, comandes, inventari, Kafka, PostgreSQL, Redis i Prometheus. pagaments se simula, repartiment i analitica són opcionals, i la resta de la plataforma (Keycloak, Kubernetes, Terraform, Lambda, CDN) queda fora de l'abast mínim. L'important no és la mida sinó que cada fita sigui verificable amb una ordre.
Estructura final del repositori km0/
km0/
├── README.md
├── docker-compose.yml # PostgreSQL, Kafka (KRaft), Redis, Prometheus, Grafana, els serveis
├── docs/
│ └── adr/ # ADR-001 … ADR-006
├── contractes/
│ ├── inventari.proto # ReservarEstoc, ConsultarEstoc, ObservarCanvis (02-03)
│ └── esdeveniments/ # esquemes JSON de comanda.creada, estoc.actualitzat, ... (02-05)
├── serveis/
│ ├── comu/ # metriques.py, logs.py, traces.py, resiliencia.py, jwt.py
│ ├── cataleg/ # api.py, cache.py (04-05), projeccio_estoc.py (08-01)
│ ├── comandes/ # api_http.py (v1/v2), saga_comanda.py (03-05), outbox_relay.py, repositori.py
│ ├── inventari/ # api.py (08-01), servidor_grpc.py, _domini.py, _repositori.py, _esdeveniments.py
│ ├── pagaments/ # passarella_simulada.py, consumidor.py
│ ├── repartiment/ # furgoneta_mqtt.py, pont_mqtt_kafka.py, ws_servidor.py, ws_consumidor_kafka.py
│ └── analitica/ # consumidor_llac.py
├── sql/
│ ├── inventari/ # referencies, reserves, outbox, missatges_processats
│ ├── cataleg/ # productes, productes_vista.sql
│ └── comandes/ # sagas (a la versió reduïda, PostgreSQL en comptes de Cassandra)
├── simulacions/ # rellotges, particions, quòrums (Mòduls 1 i 3)
├── dags/ # km0_vendes_diaries.py (05-05)
├── serverless/ # miniatures/, factures/, template.yaml, saga/ (08-04)
├── vora/
│ ├── mosquitto/ # mosquitto.conf, acl, passwd (08-02)
│ ├── web/seguiment.js # client WebSocket (08-02)
│ ├── worker/cataleg.js # Worker de CDN (08-04)
│ └── furgoneta/agregador.py # (08-04)
├── certs/ # km0-ca i certificats de desenvolupament (06-04)
├── observabilitat/
│ ├── prometheus.yml, alertes.yml # (07-01)
│ └── grafana/dashboards/ # RED per servei, saga, lag de consumidors
├── k8s/ # comandes-deployment.yaml, HPA, PDB, NetworkPolicy, overlays/ (07-05)
├── ansible/ # inventari i playbooks de Kafka/Cassandra (07-05)
├── infra/aws/ # main.tf, variables.tf, outputs.tf, entorns/ (08-03)
└── tests/
├── integracio/ # Testcontainers: saga, idempotència, projecció (07-06)
├── contractes/ # Pact, compatibilitat protobuf i esdeveniments
├── carrega/verema.js # k6
└── caos/ # Toxiproxy, manifestos Chaos Meshdocker-compose.yml de la versió reduïda
# km0/docker-compose.yml — versió reduïda per al projecte final
services:
postgres:
image: postgres:16
environment: { POSTGRES_USER: km0, POSTGRES_PASSWORD: km0, POSTGRES_DB: km0 }
volumes:
- ./sql:/docker-entrypoint-initdb.d:ro # crea km0_inventari, km0_cataleg, km0_comandes i les seves taules
ports: ["5432:5432"]
healthcheck: { test: ["CMD-SHELL", "pg_isready -U km0"], interval: 5s, retries: 10 }
kafka:
image: apache/kafka:3.7.0 # KRaft: sense ZooKeeper (03-03)
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "false" # els tòpics es creen explícitament (02-04)
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
ports: ["9092:9092"]
kafka-init: # crea els tòpics i acaba
image: apache/kafka:3.7.0
depends_on: [kafka]
entrypoint: ["/bin/sh", "-c"]
command: |
"sleep 5 &&
/opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:9092 --create --if-not-exists --topic comandes.esdeveniments --partitions 6 --replication-factor 1 &&
/opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:9092 --create --if-not-exists --topic inventari.alertes --partitions 3 --replication-factor 1 &&
/opt/kafka/bin/kafka-topics.sh --bootstrap-server kafka:9092 --create --if-not-exists --topic comandes.esdeveniments.dlq --partitions 1 --replication-factor 1"
redis:
image: redis:7
ports: ["6379:6379"]
inventari:
build: ./serveis/inventari
environment: { PG_DSN: "postgresql://km0:km0@postgres/km0_inventari", KAFKA_BOOTSTRAP: "kafka:9092" }
depends_on: { postgres: { condition: service_healthy } }
ports: ["50051:50051", "9101:9100"] # gRPC i mètriques
comandes:
build: ./serveis/comandes
environment:
PG_DSN: "postgresql://km0:km0@postgres/km0_comandes"
KAFKA_BOOTSTRAP: "kafka:9092"
INVENTARI_GRPC: "inventari:50051"
PAGAMENTS_URL: "http://pagaments:8002"
depends_on: [inventari, kafka-init]
ports: ["8001:8000", "9102:9100"]
pagaments:
build: ./serveis/pagaments # passarel·la simulada: rebutja el 5 % i triga 200-1500 ms
ports: ["8002:8002"]
cataleg:
build: ./serveis/cataleg
environment:
PG_DSN: "postgresql://km0:km0@postgres/km0_cataleg"
KAFKA_BOOTSTRAP: "kafka:9092"
REDIS_URL: "redis://redis:6379"
depends_on: [redis, kafka-init]
ports: ["8003:8000", "9103:9100"]
prometheus:
image: prom/prometheus:v2.53.0
volumes: ["./observabilitat/prometheus.yml:/etc/prometheus/prometheus.yml:ro"]
ports: ["9090:9090"]
grafana:
image: grafana/grafana:11.1.0
volumes: ["./observabilitat/grafana:/etc/grafana/provisioning:ro"]
ports: ["3000:3000"]Fites verificables
| Fita | Què construir | Com comprovar-ho |
|---|---|---|
| F1. Arrencada | docker compose up ho aixeca tot; els tres serveis exposen /health i /metrics |
curl localhost:8001/health → {"estat":"ok"}; curl localhost:9090/api/v1/targets mostra els tres up |
| F2. Catàleg amb memòria cau | GET /productes/formatge-curat amb cache-aside a Redis (04-05) |
Primera petició > 5 ms i X-Cache: MISS; segona < 1 ms i HIT; redis-cli keys 'producte:*' mostra la clau amb TTL |
| F3. Reserva d'estoc per gRPC | inventari.ReservarEstoc amb SELECT FOR UPDATE, idempotent per id_reserva, publicant a outbox (02-03, 08-01) |
grpcurl -plaintext -d '{"id_reserva":"r1","comanda_id":"P-1","mercat":"girona","linies":[{"producte":"formatge-curat","unitats":2}]}' localhost:50051 km0.inventari.v1.Inventari/ReservarEstoc dues vegades → mateixa resposta, estoc descomptat una sola vegada; SELECT * FROM outbox amb estoc.reservat i estoc.actualitzat |
| F4. Outbox → Kafka | Relay que llegeix outbox i produeix a comandes.esdeveniments amb clau = comanda, marcant els publicats (02-05) |
kafka-console-consumer --topic comandes.esdeveniments --from-beginning --property print.key=true mostra els esdeveniments amb el seu embolcall; matar el relay a mitges i reiniciar-lo no duplica (id_esdeveniment únics) |
| F5. Crear comanda amb saga | POST /comandes amb Idempotency-Key; saga reservar → cobrar → confirmar; compensació si pagaments rebutja (03-05) |
curl -X POST -H 'Idempotency-Key: k1' ... dues vegades → una sola comanda; amb la passarel·la forçada a rebutjar (PAGAMENTS_MODE=rebutjar), la comanda queda rebutjada i l'estoc torna (SELECT disponibles FROM referencies); SELECT * FROM sagas mostra les transicions |
| F6. Projecció CQRS | cataleg consumeix estoc.actualitzat, actualitza productes_vista i invalida Redis (08-01) |
Després de F5, GET /productes/formatge-curat mostra disponible actualitzat en < 2 s; SELECT * FROM missatges_processats creix; reenviar un esdeveniment a mà amb el mateix id_esdeveniment no canvia res |
| F7. Resiliència | Timeouts, reintents amb jitter i circuit breaker a comandes → pagaments (07-04) |
Aturar pagaments (docker compose stop pagaments): les peticions fallen ràpid (< 1 s, no 30 s) després d'obrir-se el circuit; km0_circuit_estat{dependencia="pagaments"} = 2 a Prometheus; en arrencar pagaments, es tanca sol |
| F8. Observabilitat | RED per servei, latència de la saga, lag de consumidors; un dashboard a Grafana; una alerta d'SLO (07-01) | Dashboard amb rate(km0_comandes_total[1m]) per estat i p99 de POST /comandes; l'alerta es dispara en provocar un 5 % d'errors amb PAGAMENTS_MODE=errors |
| F9. Càrrega | k6 amb 50 usuaris virtuals creant comandes i llegint el catàleg durant 2 minuts (07-06) | p99 de catàleg < 50 ms; cap comanda duplicada (SELECT count(*), count(DISTINCT idempotency_key) iguals); estoc final = inicial − venut |
| F10. Prova d'integració | Testcontainers aixecant PostgreSQL i Kafka; prova de la saga amb pagaments que rebutja i de la idempotència del consumidor (07-06) |
pytest tests/integracio en verd en menys de 2 minuts |
| Opcional A. Temps real | Mosquitto + pont_mqtt_kafka.py + ws_servidor.py + seguiment.js (08-02) |
Publicar amb mosquitto_pub -t km0/repartiment/furgoneta-3/posicio -q 1 -m '{...}' i veure-ho al navegador; publicar un seq menor i comprovar que es descarta |
| Opcional B. Analítica | Consumidor que escriu esdeveniments en fitxers Parquet per data; un script de Spark local que calcula vendes per productor (05-03) | spark-submit vendes_diaries.py --data 2026-09-15 produeix la taula amb la Formatgeria Montblanc al capdavant |
Cada fita ha d'acabar amb un ADR curt si es pren una decisió (per exemple, "PostgreSQL en comptes de Cassandra per a comandes a la versió reduïda: context, conseqüències") i amb les proves que la verifiquen a tests/. El resultat és un repositori que es pot ensenyar i del qual es pot parlar en una entrevista amb la mateixa precisió amb què aquest curs ha parlat de Quilòmetre Zero.
- Guia d'estudi
El curs ha estat un mapa; aquests són els territoris per continuar. Se citen obres i articles per títol i autor, sense enllaços: tots són fàcils de localitzar pel seu nom.
Llibres de referència
- Martin Kleppmann, Designing Data-Intensive Applications (O'Reilly, 2017). El llibre que més s'assembla als Mòduls 2, 3 i 4 d'aquest curs, amb molta més profunditat: replicació, particionament, transaccions, consens, processament per lots i en flux. És la lectura següent natural.
- Andrew S. Tanenbaum i Maarten van Steen, Distributed Systems (3a ed., disponible gratuïtament pels autors). El text acadèmic clàssic: models, comunicació, sincronització, consistència i tolerància a fallades amb rigor formal. Complementa el Mòdul 1 i el 3.
- Sam Newman, Building Microservices (2a ed., O'Reilly, 2021) i Monolith to Microservices (2019). Tot el de 08-01 amb detall: fronteres, migració, proves, organització.
- Betsy Beyer et al. (eds.), Site Reliability Engineering: How Google Runs Production Systems i The Site Reliability Workbook. L'origen dels SLO, els pressupostos d'error, els postmortems i la guàrdia del Mòdul 7. Disponibles gratuïtament en línia per Google.
- Eric Evans, Domain-Driven Design (2003), i Vaughn Vernon, Implementing Domain-Driven Design (2013), per aprofundir en els bounded contexts de 08-01.
- Gregor Hohpe i Bobby Woolf, Enterprise Integration Patterns (2003): el catàleg de patrons de missatgeria dels quals 02-04 i 02-05 van prendre els noms.
- Neal Ford, Mark Richards et al., Software Architecture: The Hard Parts (2021): trade-offs de descomposició, dades distribuïdes i sagues, amb el mateix esperit d'ADR.
Articles fundacionals (tots localitzables per títol)
- Leslie Lamport, "Time, Clocks, and the Ordering of Events in a Distributed System" (1978): la base de 01-05. I "The Part-Time Parliament" (1998) i "Paxos Made Simple" (2001) per al consens.
- Diego Ongaro i John Ousterhout, "In Search of an Understandable Consensus Algorithm" (Raft, 2014): el que etcd i Kafka KRaft implementen (03-03).
- Giuseppe DeCandia et al., "Dynamo: Amazon's Highly Available Key-value Store" (2007): hashing consistent, quòrums, vector clocks i la tria de disponibilitat que Cassandra hereta (04-01, 04-04).
- Jeffrey Dean i Sanjay Ghemawat, "MapReduce: Simplified Data Processing on Large Clusters" (2004), i Sanjay Ghemawat et al., "The Google File System" (2003): 05-02 i 04-02.
- James C. Corbett et al., "Spanner: Google's Globally-Distributed Database" (2012): consistència forta a escala global amb rellotges acotats (TrueTime), el contrapunt a Dynamo.
- Jay Kreps, Neha Narkhede i Jun Rao, "Kafka: a Distributed Messaging System for Log Processing" (2011), i l'assaig de Jay Kreps "The Log: What every software engineer should know about real-time data's unifying abstraction" (2013): la idea del log com a columna vertebral (ADR-004).
- Hector Garcia-Molina i Kenneth Salem, "Sagas" (1987): l'article original de 03-05.
- Seth Gilbert i Nancy Lynch, "Brewer's Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services" (2002), i Daniel Abadi, "Consistency Tradeoffs in Modern Distributed Database System Design" (PACELC, 2012): 03-02.
- Marc Shapiro et al., "Conflict-free Replicated Data Types" (2011): els CRDT de 03-01 i 08-04.
- Peter Deutsch i James Gosling, les "Fallacies of Distributed Computing", i Peter Bailis et al., "Highly Available Transactions: Virtues and Limitations" (2013), per continuar amb les fal·làcies i els models d'aïllament.
- Tyler Akidau et al., "The Dataflow Model" (2015): temps d'esdeveniment, marques d'aigua i finestres de 05-04.
Pràctica
- Jepsen (Kyle Kingsbury): anàlisis públiques de com fallen bases de dades reals sota particions; la millor escola d'escepticisme sobre garanties anunciades.
- Els llibres de Michael Nygard, Release It! (2a ed., 2018), per als patrons d'estabilitat de 07-04 explicats a partir d'incidents reals.
- Construir el projecte de l'apartat 9 i, després, trencar-lo amb els experiments de 07-06.
Errors Comuns i Consells
- Presentar l'arquitectura com un diagrama sense decisions. El diagrama de l'apartat 1 val poc sense els ADR de l'apartat 5; el que s'aprèn d'una arquitectura són les seves alternatives rebutjades i les seves conseqüències acceptades.
- Avaluar per tecnologies i no per criteris. "Fa servir Kafka i Kubernetes" no diu si està ben dissenyada. Les taules de l'apartat 7 (fal·làcies, consistència per dada, SLO, RPO/RTO, qui pot fer què) sí.
- Començar el projecte per la infraestructura. Kubernetes, mesh i Terraform sense un servei a desplegar és l'error de "triar tecnologies abans que problemes" de 01-06. El projecte comença a F1 amb tres serveis i
docker-compose. - Creure que la plataforma està acabada. L'apartat 8 llista cinc fronts; una arquitectura sense llista de pendents és una que ningú no està mirant.
- Consell: en qualsevol sistema nou, comença per escriure els símptomes (01-06), la taula de consistència per dada (03-01) i els SLO (07-01) abans de dibuixar caixes. Són les tres pàgines que més decisions correctes produeixen.
- Consell: guarda aquest curs com a referència per lliçó: quan d'aquí a un any hagis de decidir entre orquestració i coreografia, o quant TTL posar en una CDN, la lliçó corresponent té la taula.
Exercicis
Exercici 1: incorporar el domini "valoracions"
A 08-01 es va decidir que les valoracions són un bounded context propi amb comunicació per esdeveniments. Ara dissenya'l d'extrem a extrem aplicant tot el curs: (a) contracte d'esdeveniments que consumeix i produeix (amb l'embolcall de 02-04) i model de dades amb el seu model de consistència (03-01) i el seu emmagatzematge (Mòdul 4); (b) API pública a Kong amb autenticació i autorització (qui pot valorar què; 06-01, 06-05) i com arriba la mitjana a la fitxa desada per la CDN (08-04); (c) SLO, mètriques i alertes (07-01), i quines traces i auditoria deixa una valoració; (d) desplegament (07-05), RPO/RTO (07-03) i què caldria afegir a main.tf (08-03); (e) l'ADR-007 en format resumit (decisió, alternativa rebutjada, conseqüència).
Exercici 2: incident d'extrem a extrem
Dissabte 19 de setembre, 18:42, Setmana del Formatge Artesà. Es dispara l'alerta "SLO de comandes: consum de pressupost d'error ×14 en 5 min". Dades disponibles:
- Prometheus:
rate(km0_comandes_total{estat="rebutjada_estoc"}[5m])passa de 0,3/s a 9/s a les 18:40; la latència p99 dePOST /comandesbaixa de 420 ms a 95 ms;km0_grpc_client_durada_segons{metode="ReservarEstoc"}p99 = 8 ms;km0_circuit_estat{dependencia="inventari"}= 0; lag del grupcataleg-projeccio= 0. - Logs d'
inventari(Loki): a partir de les 18:39:50, centenars de línies{"nivell":"warning","msg":"EstocInsuficient","producte":"formatge-curat","mercat":"girona","disponibles":0,"sollicitades":2,"request_id":...}. - Logs de
cataleg:{"nivell":"info","msg":"vista actualitzada","producte":"formatge-curat","mercat":"girona","aplicat":true}a les 18:39:48, i cap més després per a aquest producte. - Traces: les comandes rebutjades tenen spans
kong → comandes → inventari.ReservarEstoc (EstocInsuficient)de 90 ms; cap no arriba apagaments. productes_vistaacataleg:formatge-curat / girona: disponible = true, unitats_aprox = 0, versio_estoc = 1758300000123.- Auditoria: a les 18:39:45,
{"qui":"mpuig","accio":"estoc.ajustar","recurs":"formatge-curat/girona","de":140,"a":0,"origen":"panell-productor"}.
(a) Reconstrueix la cadena causal completa. (b) És un incident de la plataforma, del producte o d'operació? Quina part del comportament és correcta i quina és un defecte? (c) Localitza el defecte exacte al codi de 08-01 (projeccio_estoc.py o productes_vista.sql) i corregeix-lo. (d) Escriu les tres accions del postmortem (07-03) i quin senyal hauria detectat el problema abans que l'SLO.
Exercici 3: retallar l'arquitectura per a tres persones
Una startup de tres persones vol llançar un marketplace com Quilòmetre Zero en un sol mercat, amb 20 productors, 300 comandes al dia i 6 repartidors. Tenen tres mesos i no poden operar Kubernetes ni Kafka. Fent servir la taula de "quan no fer servir microserveis" de 08-01 i el lock-in selectiu de 08-03, dissenya l'arquitectura mínima: (a) què es queda com a monòlit modular i amb quina estructura interna; (b) quines tres decisions de Quilòmetre Zero conservaries intactes perquè són barates i eviten errors irreversibles; (c) com resoldries el seguiment en temps real i les fotos sense Kafka, MQTT ni Lambda; (d) quins senyals mesurables indicarien que ha arribat el moment d'extreure el primer servei, i quin seria.
Solucions
Exercici 1.
(a) Consumeix comanda.lliurada (publicat per repartiment a comandes.esdeveniments amb l'embolcall estàndard; dades: {comanda_id, client, linies: [{producte, productor}], lliurat_ms}) per saber què es pot valorar. Produeix en un tòpic nou valoracions.esdeveniments (3 particions, clau = producte, perquè les valoracions d'un producte arribin en ordre a la projecció): valoracio.creada {valoracio_id, client, comanda_id, producte, productor, punts, comentari, data_ms} i valoracio.resposta {valoracio_id, productor, resposta, data_ms}. Model: taula valorables (client, comanda_id, producte, lliurat_ms, valorat bool) i valoracions (id, client, producte, productor, punts, comentari, resposta, creada_en) en una base PostgreSQL pròpia km0_valoracions (RDS petita, sense Multi-AZ al principi): volum baix (una per línia lliurada), consultes relacionals (per producte, per productor, pendents de resposta), i la regla "una valoració per línia" exigeix una restricció UNIQUE (client, comanda_id, producte) amb consistència forta local. La mitjana a la fitxa és eventual (projecció a cataleg), i el panell del productor llegeix la seva pròpia base amb consistència forta.
(b) Rutes a Kong: POST /api/v1/valoracions (rol client), GET /api/v1/productes/{slug}/valoracions (pública, desable 60 s), POST /api/v1/valoracions/{id}/resposta (rol productor), GET /api/v1/productors/me/valoracions?pendents=true (rol productor). Autorització al servei: en crear, claims.sub ha de tenir una fila a valorables no valorada per a aquella comanda i producte (ABAC amb dada pròpia, sense cridar comandes); en respondre, claims.productor ha de coincidir amb valoracions.productor. Rate limiting específic (5 valoracions/minut per consumidor) contra l'abús. La mitjana arriba a la fitxa perquè cataleg consumeix valoracio.creada i manté suma_punts i num_valoracions a productes_vista (idempotent per id_esdeveniment); la fitxa continua sent public, max-age=60 per mercat, i la mitjana canvia amb fins a un minut de retard a la CDN, acceptable. Els tres últims comentaris se serveixen a la ruta pública de valoracions (desable, sense dades personals més enllà del nom de pila, que el client va consentir).
(c) SLO: 99,5 % de disponibilitat i p99 < 300 ms per crear (no és al camí de compra: SLO més lax que el de comandes). Mètriques: km0_valoracions_total{resultat}, latència per ruta, lag del consumidor de comanda.lliurada i del de cataleg sobre valoracions.esdeveniments. Alertes: consum de pressupost d'error, lag > 5 min (un lliurament que no es pot valorar), i taxa de no_autoritzat anòmala (abús). Traça: kong → valoracions POST → postgres, enllaçada al consumidor de cataleg per l'id_esdeveniment. Auditoria: valoracio.crear i valoracio.respondre a auditoria.esdeveniments (les respostes dels productors són contingut públic, i la moderació necessita saber qui ha escrit què).
(d) k8s/valoracions-deployment.yaml copiant la plantilla de comandes (2 rèpliques, sondes, resources, topologySpreadConstraints, HPA per CPU, NetworkPolicy que només permet entrada des de Kong i sortida al seu RDS i a Kafka, serviceAccountName amb IRSA sense permisos d'S3). RPO: minuts (còpies de seguretat d'RDS i PITR; la pèrdua de valoracions és tolerable però molesta); RTO: 1 hora (no bloqueja vendes). A main.tf: aws_db_instance.valoracions (db.t4g.small, sense multi_az), el seu grup de seguretat (5432 només des d'EKS), un mòdul IRSA sense polítiques d'S3, i el tòpic valoracions.esdeveniments gestionat amb el proveïdor kafka o creat pel pipeline.
(e) ADR-007: "valoracions com a bounded context propi amb PostgreSQL i comunicació exclusivament per esdeveniments". Alternativa rebutjada: dins de cataleg (acoblaria la moderació als desplegaments del catàleg en campanya i barrejaria dos llenguatges: "producte publicat" i "valoració"). Conseqüència acceptada: la fitxa mostra la mitjana amb fins a un minut de retard; un component més amb base de dades, dashboard i guàrdia (de l'equip de catàleg i productors); si repartiment deixa de publicar comanda.lliurada, ningú no pot valorar fins que es recuperi el lag (alerta).
Exercici 2.
(a) Cadena causal: 18:39:45, Marta Puig ajusta des del seu panell l'estoc de formatge-curat a Girona de 140 a 0 (probablement un error en introduir la dada, o una retirada real del producte). inventari aplica l'ajust i publica estoc.actualitzat {disponible: 0}. 18:39:48, cataleg rep l'esdeveniment i la projecció l'aplica (aplicat: true) → unitats_aprox = 0. Però disponible continua a true. A partir de les 18:39:50, els clients que veuen la fitxa (amb disponible = true) afegeixen formatge-curat al cistell i confirmen; comandes inicia la saga, inventari respon EstocInsuficient en 8 ms, la saga compensa i la comanda queda rebutjada_estoc. La latència p99 baixa (les comandes rebutjades no passen per la passarel·la), la taxa de rebuigs puja ×30 i el pressupost d'error es consumeix perquè rebutjada_estoc compta com a fallada de l'SLO de comandes.
(b) És un incident mixt. La saga, el circuit i la compensació funcionen correctament: cap comanda no s'ha cobrat sense estoc, cap reserva no ha quedat penjada. L'ajust de la Marta és una acció legítima de negoci (i l'auditoria la registra amb precisió). El defecte és a la plataforma: la fitxa del catàleg continua dient "disponible" amb 0 unitats, cosa que dirigeix centenars de clients a un rebuig segur. És un defecte de la projecció CQRS, no de la consistència eventual: el retard va ser de 3 segons, acceptable; el problema és que la dada projectada és incorrecta de manera permanent.
(c) A projeccio_estoc.py, aplicar calcula disp = d["disponible"] >= LLINDAR_DISPONIBLE amb LLINDAR_DISPONIBLE = 1: amb disponible = 0, disp = False, i l'UPDATE hauria d'haver posat disponible = FALSE... tret que la condició versio_estoc < %(v)s hagi fallat. Però el log diu aplicat: true, així que l'UPDATE va afectar la fila. Llavors el defecte no és aquí; mireu productes_vista.sql: disponible BOOLEAN NOT NULL DEFAULT FALSE i unitats_aprox s'actualitzen juntes al mateix UPDATE. La fila mostra unitats_aprox = 0 i disponible = true, cosa que només és possible si una altra ruta d'escriptura va posar disponible = true després: la ruta de publicació del producte pel productor (cataleg crea/actualitza la fila quan el productor edita la fitxa i, segons 08-01, "la fila la crea cataleg quan el productor publica el producte"). La Marta va editar la fitxa (potser el mateix ajust des del panell toca producte i estoc), i l'UPSERT de publicació sobreescriu disponible amb el seu valor per defecte o amb el valor que tenia la fitxa al formulari, sense respectar versio_estoc. El defecte és que dos camins escriuen la mateixa columna amb regles diferents: la correcció és que la publicació de la fitxa mai no escrigui disponible ni unitats_aprox (columnes propietat exclusiva de la projecció), és a dir, INSERT ... ON CONFLICT (slug, mercat) DO UPDATE SET nom = EXCLUDED.nom, preu_cents = EXCLUDED.preu_cents, productor = EXCLUDED.productor sense tocar les columnes d'estoc; i, com a xarxa de seguretat, un CHECK (NOT disponible OR unitats_aprox >= 1) a la taula que faria fallar l'escriptura incoherent en comptes de deixar-la passar. És el principi de 08-01 de "propietari de la dada" aplicat dins d'un servei, columna a columna.
(d) Postmortem sense culpa (07-03): 1. Corregir l'UPSERT de publicació i afegir el CHECK; prova d'integració (07-06) que publica la fitxa després d'un estoc.actualitzat a 0 i verifica disponible = false. 2. Afegir una confirmació al panell del productor per a ajustos d'estoc que canviïn més d'un 50 % ("Posar formatge-curat a Girona a 0 unitats? N'hi havia 140"), perquè l'auditoria mostra un salt improbable. 3. Separar a l'SLO de comandes els rebuigs per estoc (resultat de negoci correcte) dels errors de plataforma, o crear una alerta específica de "taxa de rebutjada_estoc per producte" amb llindar baix, perquè el símptoma s'atribueixi bé. El senyal que ho hauria detectat abans: una alerta de coherència a la projecció, count(productes_vista where disponible and unitats_aprox = 0) > 0, avaluada cada minut (una consulta barata), o la mètrica de Flink de 05-04 alertes_estoc per a formatge-curat a Girona, que es va disparar a les 18:39:50 cap a inventari.alertes i que la Marta va rebre per SSE (08-02) però que ningú no va correlacionar amb els rebuigs.
Exercici 3.
(a) Un monòlit modular en Python (FastAPI) amb una base de dades PostgreSQL gestionada (RDS o Cloud SQL amb còpies de seguretat automàtiques i PITR, sense Multi-AZ al principi), desplegat en un PaaS de contenidors (Cloud Run, App Runner, Fly.io, Render: 2 instàncies, autoescalat inclòs, TLS gestionat). Estructura interna: els sis paquets de 01-06 (cataleg, comandes, inventari, pagaments, repartiment, analitica) cadascun amb el seu api.py com a únic mòdul públic (08-01), esquemes de PostgreSQL separats per paquet (inventari.referencies, comandes.comandes) i una regla de lint (import-linter) que prohibeix importar mòduls privats d'un altre paquet i consultes SQL entre esquemes. La transacció de confirmar comanda és local (una sola base): reserva, comanda i registre del cobrament en una transacció, amb la crida a la passarel·la fora d'ella (cobrar primer amb Idempotency-Key, després confirmar en transacció; si falla la confirmació, reemborsar). Redis gestionat petit per a la memòria cau del catàleg i el limitador. Un sol repositori, un pipeline, docker-compose per a desenvolupament.
(b) Tres decisions barates que eviten errors irreversibles: 1. L'embolcall d'esdeveniments i una taula esdeveniments (outbox) des del primer dia, encara que el "bus" sigui la mateixa base de dades i el consumidor un procés del monòlit: quan arribi Kafka, els productors ja publiquen i els consumidors ja són idempotents; i el llac analític pot començar exportant aquesta taula a Parquet cada nit. 2. Idempotència a les API d'escriptura (Idempotency-Key en crear comanda, claus deterministes cap a la passarel·la): és el que a 02-05 i 08-04 evita cobraments i comandes duplicats, costa una taula, i no es pot afegir després sense migrar clients. 3. Observabilitat mínima amb SLO: logs JSON amb request_id, mètriques RED i un SLO escrit per crear comanda; sense això, no hi haurà dades per decidir quan extreure res (apartat d). I una quarta gratuïta: els ADR des del primer dia.
(c) Seguiment: 6 repartidors enviant posicions per HTTPS POST cada 10 s al monòlit (60 peticions/minut: res), desades en una taula repartiment.posicions amb retenció de 24 h (a aquest volum, PostgreSQL sobra), i els clients amb SSE o polling cada 5 s contra un endpoint que retorna l'última posició de la comanda (amb Cache-Control: max-age=4 perquè el PaaS o una CDN absorbeixi repeticions). Sense MQTT ni WebSockets: no hi ha volum que ho justifiqui, i SSE amb reconnexió automàtica cobreix l'experiència. Fotos: pujada per URL presignada a S3/GCS (04-03, es conserva) i miniatures generades al mateix monòlit en una tasca en segon pla (una cua simple a PostgreSQL amb SELECT ... FOR UPDATE SKIP LOCKED, processada per un worker del mateix desplegament), amb una CDN gratuïta o de baix cost davant del bucket amb claus immutables (08-04, es conserva perquè és gairebé gratis i evita l'egress).
(d) Senyals per extreure el primer servei, mesurables gràcies a (b)-3: el p99 de crear comanda es degrada quan puja el trànsit del catàleg (contenció a la mateixa base o CPU: símptoma 1); l'equip passa de 3 a 8-10 persones i els desplegaments del catàleg trenquen comandes (símptoma 3); el trànsit de lectura supera el que Redis + una rèplica de lectura de PostgreSQL absorbeixen; o la latència de la passarel·la comença a bloquejar connexions (símptoma 2). El primer a sortir, com a 01-06, és cataleg (només lectures, sense transaccions creuades, marxa enrere amb un canvi de ruta) o, si el símptoma dominant és el de la passarel·la, pagaments amb la seva ACL. Mai comandes i inventari primer: són els que exigeixen sagues, i les sagues es justifiquen només quan la frontera ja existeix per una altra raó. La regla de 08-01 es compleix a l'inrevés: tres persones sense símptomes és la fila exacta de la taula que diu "monòlit modular".
Conclusió
Quilòmetre Zero va començar com un monòlit Python amb una base de dades i cinc símptomes, i acaba com una plataforma que es pot dibuixar en un diagrama, recórrer amb una comanda, justificar amb sis ADR i avaluar amb els criteris que el mateix curs ha donat: les vuit fal·làcies assumides com a falses en punts concrets del disseny, una taula de consistència per dada on "forta" apareix dues vegades i "eventual" sis, SLO i RPO/RTO per component, i una taula de qui pot fer què des de l'Anna fins al Worker de la CDN. Els cinc símptomes de 01-06 tenen cadascun la seva resolució i la seva manera de verificar-la; les decisions tenen cadascuna la seva alternativa rebutjada i el seu preu; i el que queda fora està escrit, amb el mòdul que dona les eines per abordar-ho. El projecte proposat redueix tot això al que cap en un portàtil, amb deu fites que es comproven amb una ordre, i la guia d'estudi assenyala els llibres i articles on cada idea d'aquest curs té el seu origen i la seva profunditat.
El que l'alumne sap fer ara no és "fer servir Kafka" o "desplegar a Kubernetes", encara que també. És una cosa més duradora: davant d'un sistema que s'ha de repartir entre diverses màquines, sap escriure primer els símptomes i no les tecnologies; tallar fronteres on el llenguatge canvia de significat i no on hi ha una taula; triar per a cada dada el model de consistència que necessita i pagar només per aquest; fer que cada crida tingui timeout, cada missatge un identificador i cada consumidor sigui idempotent, perquè la xarxa fallarà; substituir la transacció global per una saga que compensa; posar la dada on la seva càrrega ho demana, de PostgreSQL a Cassandra, de Redis a S3, de la CDN a la cua SQLite d'una furgoneta; separar el que es calcula per lots del que no pot esperar; autenticar i autoritzar a cada frontera sense confiar en cap; mesurar amb SLO, observar amb traces, recuperar amb còpies de seguretat provades i assajar les fallades abans que passin; descriure la infraestructura com a codi i el cost com una mètrica; i deixar cada decisió escrita amb el seu context i les seves conseqüències, perquè qui vingui després pugui canviar-la amb coneixement.
Això és dissenyar arquitectures distribuïdes: no acumular peces, sinó saber en cadascuna què es guanya, què es perd i per què es va triar. Quilòmetre Zero ha estat l'excusa; el criteri és el que s'endú l'alumne. Gràcies per haver recorregut el curs fins aquí, i bona sort amb el següent sistema que s'hagi de repartir entre diverses màquines: ara ja sap per on començar.
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
