Les cinc lliçons anteriors han donat els fonaments: què és un sistema distribuït, amb quins models es descriu, què guanya i què perd qui distribueix, quines fal·làcies cal desterrar i per què el temps és un problema. Aquesta lliçó de tancament els posa a treballar sobre el cas que ens acompanyarà la resta del curs. Examinarem amb detall el monòlit de Quilòmetre Zero, els símptomes concrets que l'empenyen a canviar i l'arquitectura objectiu que anirem construint mòdul a mòdul.
És important entendre el caràcter d'aquesta lliçó: és un mapa, no el viatge. Aquí no s'explica cap tecnologia concreta; es decideix quines peces necessita la plataforma, per què i en quina lliçó del curs es desenvolupa cadascuna. També prepararem l'entorn de treball mínim (l'estructura de carpetes del projecte km0/ i un docker-compose.yml només amb PostgreSQL i l'aplicació Python) que serà el punt de partida sobre el qual s'aniran afegint serveis.
Contingut
- El monòlit de Quilòmetre Zero per dins
- Els símptomes que forcen l'evolució
- Principis de disseny per a la transició
- L'arquitectura objectiu
- Mapa de l'arquitectura al curs
- Un camí gradual, no un "big bang"
- Preparació de l'entorn de treball: el projecte
km0/ - Errors comuns i consells
- Exercicis de disseny
- Conclusió
- El monòlit de Quilòmetre Zero per dins
Recordem el punt de partida (lliçó 01-01): una aplicació Python en un únic procés, una base de dades PostgreSQL i un únic servidor. Mirem-ho ara amb més detall, perquè per decidir com partir-lo cal entendre com està fet.
Mòduls interns
El codi està organitzat en sis mòduls Python, que es corresponen amb els dominis funcionals del negoci:
| Mòdul | Responsabilitat | Taules principals | Qui el fa servir |
|---|---|---|---|
cataleg |
Productes, productors, categories, fotos, cerca | productes, productors, categories |
Clients (moltes lectures), productors (poques escriptures) |
comandes |
Cistell, creació i cicle de vida de la comanda | comandes, linies_comanda |
Clients, atenció al client |
inventari |
Estoc per producte i per productor, reserves | estoc, moviments_estoc |
comandes, productors |
pagaments |
Cobraments amb la passarel·la externa, reemborsaments | pagaments, reemborsaments |
comandes |
repartiment |
Assignació de repartidors, rutes, posició en temps real | repartiments, repartidors, posicions |
Repartidors (app mòbil), clients (seguiment) |
analitica |
Informes de vendes, productes més venuts, previsió de demanda | Consultes sobre totes les anteriors | Direcció, productors |
Com interactuen
Les interaccions entre mòduls són crides a funcions Python, i les operacions que toquen diversos mòduls es recolzen en una única transacció de base de dades. El flux de confirmar una comanda n'és l'exemple més clar:
sequenceDiagram
participant C as Client (Anna)
participant P as comandes
participant I as inventari
participant PA as pagaments
participant DB as PostgreSQL
C->>P: confirmar_comanda(cistell)
P->>DB: BEGIN
P->>I: reservar_estoc(linies)
I->>DB: UPDATE estoc ...
P->>PA: cobrar(import, targeta)
PA->>PA: crida a passarella externa (HTTPS)
PA->>DB: INSERT pagament
P->>DB: INSERT comanda
P->>DB: COMMIT
P-->>C: comanda confirmada
Fixa't en un detall que serà decisiu: la crida a la passarel·la de pagaments externa passa dins de la transacció de base de dades. Si la passarel·la triga 8 segons, la transacció (i els bloqueigs sobre les files d'estoc) duren 8 segons. I si la passarel·la falla, la transacció es desfà sencera, cosa que és correcta, però mentrestant ha estat bloquejant l'estoc per a tots els altres clients.
Desplegament
Un únic artefacte (un paquet Python) que s'instal·la al servidor amb un script. El desplegament implica aturar el procés, instal·lar i arrencar: uns 40 segons d'indisponibilitat total. Es desplega els dimarts i dijous a la nit, amb tots els canvis de tots els equips acumulats des del desplegament anterior.
- Els símptomes que forcen l'evolució
No hi ha una única raó per transformar Quilòmetre Zero, sinó cinc símptomes diferents, i cadascun empeny cap a una peça diferent de l'arquitectura objectiu. Convé identificar-los amb precisió perquè cada símptoma justifica una decisió concreta, i les decisions que no responen a cap símptoma són complexitat gratuïta.
Símptoma 1: els pics de campanya ho tomben tot
Ja ho vam veure: la Setmana de la Verema va multiplicar per 15 el trànsit i el servidor va caure del tot. L'anàlisi posterior va mostrar que el 92 % de les peticions eren lectures del catàleg (llistats, cerques, fitxes de producte amb fotos). La resta de la plataforma va caure per compartir procés i base de dades amb el catàleg.
Què demana: poder escalar el catàleg de manera independent, servir les lectures des d'una memòria cau i les fotos des d'un emmagatzematge a part, i aïllar les fallades perquè una sobrecàrrega del catàleg no afecti les comandes ni el repartiment.
Símptoma 2: una fallada de pagaments que ho va tombar tot
Un dimarts, la passarel·la de pagaments externa va començar a respondre amb 30 segons de retard. Cada confirmació de comanda mantenia una transacció oberta (i bloqueigs d'estoc) durant aquests 30 segons. En quatre minuts, PostgreSQL va esgotar el seu límit de connexions i tota la plataforma, incloses les pàgines del catàleg que no tenen res a veure amb pagaments, va deixar de respondre. Una fallada d'un proveïdor extern es va convertir en una caiguda total.
Què demana: que pagaments sigui un component aïllat amb els seus propis recursos, que la seva lentitud o la seva fallada es contingui (timeouts, degradació controlada), i que la confirmació de comanda no depengui d'una transacció única que abasta sistemes externs.
Símptoma 3: els equips es trepitgen en desplegar
Quilòmetre Zero ja té quatre equips (catàleg i productors, comandes i pagaments, repartiment, dades). Cada desplegament porta els canvis de tots, així que un error al mòdul de repartiment obliga a revertir també les millores del catàleg. L'equip de repartiment vol desplegar diverses vegades al dia; el de pagaments, subjecte a auditoria, vol cicles llargs i controlats. El resultat és que ningú no desplega amb la freqüència que necessita, i cada desplegament de dimarts és un esdeveniment tens.
Què demana: unitats de desplegament independents per domini, amb els seus propis cicles, proves i responsables, i una plataforma de desplegament que permeti fer-ho sense aturada.
Símptoma 4: la telemetria de repartidors en temps real
Quilòmetre Zero ha passat de 12 a 140 repartidors, i cadascun envia la seva posició cada 5 segons: 28 posicions per segon, 2,4 milions al dia, que s'insereixen a la taula posicions de la mateixa base de dades que gestiona les comandes. La taula creix 800 MB al dia i les insercions competeixen amb les transaccions de les comandes. A més, els clients volen veure el repartidor moure's pel mapa en temps real, cosa que amb el model petició-resposta obliga que l'app pregunti cada pocs segons, multiplicant la càrrega.
Què demana: un canal de comunicació diferent per a fluxos d'esdeveniments d'alta freqüència (no una taula relacional), comunicació asíncrona i bidireccional amb les apps, i un emmagatzematge adequat per a sèries temporals de gran volum.
Símptoma 5: l'analítica de vendes ofega la producció
Cada nit, el mòdul analitica executa consultes pesades sobre les taules de comandes i estoc per generar informes. Les consultes triguen 3 hores i, durant aquest temps, la base de dades va lenta per a tothom. Els productors demanen informes més rics (previsió de demanda per temporada, comparatives entre ciutats) que l'equip de dades no s'atreveix a implementar perquè cada consulta nova posa en risc la producció.
Què demana: separar la càrrega analítica de la transaccional, amb una còpia de les dades en un sistema dissenyat per al processament massiu, alimentada pels esdeveniments que produeixen els altres serveis.
- Principis de disseny per a la transició
Abans de dibuixar l'arquitectura objectiu, fixem els principis que es deriven del mòdul i que guiaran totes les decisions:
- Cada servei és propietari de les seves dades. Cap servei no accedeix directament a les taules d'un altre. Si
analiticanecessita les comandes, les rep com a esdeveniments o a través de l'API decomandes. És el que fa possible el desplegament i l'escalat independents (símptomes 1 i 3), i el que obliga a renunciar a la transacció única (que se substitueix per les tècniques del Mòdul 3). - Aïllament de fallades. Una fallada o una sobrecàrrega en un servei no s'ha de propagar als altres (símptomes 1 i 2). Això exigeix recursos separats, timeouts a tota crida remota (lliçó 01-04) i degradació controlada: si
repartimentcau, es pot continuar comprant; sipagamentscau, es pot continuar mirant el catàleg. - Síncron només quan la resposta fa falta ara. Comprovar l'estoc abans de confirmar una comanda necessita resposta immediata: síncron. Avisar
analiticaque s'ha creat una comanda no la necessita: asíncron, mitjançant esdeveniments. Cada crida síncrona que s'evita és una frontera de xarxa menys a la cadena de disponibilitat (lliçó 01-03). - Assumir les fal·làcies com a certes a l'inrevés. La xarxa fallarà, tindrà latència, no serà segura, la topologia canviarà. Tota comunicació entre serveis porta timeout, reintents idempotents, xifratge i descobriment dinàmic.
- Observabilitat des del principi. Amb sis serveis, sense mètriques, logs correlacionats i traces distribuïdes no hi ha manera de saber què passa. No és un afegit posterior: és part del disseny.
- Evolució gradual. No es reescriu tot alhora. S'extreu un servei, es comprova, s'extreu el següent. El monòlit continua funcionant durant tota la transició.
- L'arquitectura objectiu
Amb els símptomes i els principis sobre la taula, aquesta és la plataforma que el curs construirà:
flowchart TB
subgraph Clients
WEB[Navegador web]
APP[App mobil clients]
REP[App repartidors]
end
GW[API Gateway<br/>autenticació, limitació de taxa]
WEB --> GW
APP --> GW
REP -->|MQTT / WebSocket| RT[Servidor temps real]
subgraph Serveis["Serveis (Kubernetes)"]
CAT[cataleg]
PED[comandes]
INV[inventari]
PAG[pagaments]
REPA[repartiment]
ANA[analitica]
end
GW --> CAT
GW --> PED
GW --> REPA
PED -->|gRPC síncron| INV
PED -->|gRPC síncron| PAG
RT --> REPA
subgraph Bus["Bus d'esdeveniments (Kafka)"]
T1[(comandes.esdeveniments)]
T2[(repartiment.posicions)]
T3[(inventari.esdeveniments)]
end
PED -.->|ComandaCreada, ComandaPagada| T1
INV -.->|EstocActualitzat| T3
REPA -.->|PosicioActualitzada| T2
T1 -.-> ANA
T2 -.-> ANA
T3 -.-> CAT
T1 -.-> REPA
subgraph Dades["Emmagatzematge per servei"]
PGC[(PostgreSQL<br/>cataleg)]
RED[(Redis<br/>memòria cau catàleg)]
OBJ[(Magatzem d'objectes<br/>fotos)]
CAS[(Cassandra<br/>comandes)]
PGI[(PostgreSQL replicat<br/>inventari)]
PGP[(PostgreSQL<br/>pagaments)]
TS[(Cassandra<br/>posicions)]
DL[(Data lake + Spark/Flink<br/>analitica)]
end
CAT --> PGC
CAT --> RED
CAT --> OBJ
PED --> CAS
INV --> PGI
PAG --> PGP
PAG -->|HTTPS| EXT[Passarella de pagaments externa]
REPA --> TS
ANA --> DL
subgraph Transversal["Transversal"]
OBS[Observabilitat<br/>Prometheus, Grafana, OpenTelemetry]
SEC[Seguretat<br/>OIDC, mTLS, secrets]
end
Recorrem les decisions, cadascuna vinculada al seu símptoma:
Els sis serveis
Els límits dels serveis coincideixen amb els mòduls del monòlit, i no és casualitat: els mòduls ja reflectien els dominis del negoci i els equips. Un bon monòlit modular és la millor preparació per a una arquitectura de serveis. Cada servei té el seu propi desplegament, el seu propi escalat i el seu propi emmagatzematge (símptomes 1, 2 i 3).
Comunicació síncrona: gRPC
Per a les interaccions que necessiten resposta immediata (comandes → inventari per reservar estoc; comandes → pagaments per cobrar), es farà servir RPC amb gRPC: crides tipades, amb contracte explícit, eficients en serialització. Tota crida porta timeout i les operacions amb efectes són idempotents (Mòdul 2).
Comunicació asíncrona: esdeveniments per Kafka
Per a tot el que no necessita resposta immediata, els serveis publiquen esdeveniments en un bus (Kafka): ComandaCreada, ComandaPagada, EstocActualitzat, PosicioActualitzada. Qui hi estigui interessat s'hi subscriu: analitica ho consumeix tot; cataleg consumeix EstocActualitzat per mostrar disponibilitat; repartiment consumeix ComandaPagada per planificar el lliurament. Això desacobla els serveis en el temps (si analitica està caiguda, els esdeveniments esperen al bus) i resol el símptoma 5 (les dades arriben a l'analítica sense consultar la base de dades de producció) i part del 4 (les posicions són un flux d'esdeveniments, no files d'una taula transaccional).
Emmagatzematge per servei
Cada servei tria l'emmagatzematge que encaixa millor amb la seva càrrega:
| Servei | Emmagatzematge | Per què |
|---|---|---|
cataleg |
PostgreSQL + Redis com a memòria cau + magatzem d'objectes per a fotos | Moltes lectures repetides (memòria cau), dades relacionals senzilles, fotos grans fora de la base de dades (símptoma 1) |
comandes |
Cassandra | Volum alt d'escriptures, consultes per client i per data, necessitat d'escalar horitzontalment i de disponibilitat molt alta |
inventari |
PostgreSQL replicat | Necessita consistència forta (no vendre dues vegades l'última unitat) i transaccions; la replicació dona disponibilitat |
pagaments |
PostgreSQL | Transaccions, auditoria, volum moderat |
repartiment |
Cassandra (posicions, sèrie temporal) + PostgreSQL (assignacions) | 2,4 milions de posicions al dia; escriptures massives per clau de repartidor i temps (símptoma 4) |
analitica |
Data lake sobre magatzem d'objectes + processament per lots (Spark) i en flux (Flink/Kafka Streams) | Consultes massives separades de producció (símptoma 5) |
La conseqüència d'aquesta decisió és que la transacció única de confirmar comanda desapareix: reservar estoc (PostgreSQL d'inventari), cobrar (PostgreSQL de pagaments) i crear la comanda (Cassandra) són tres operacions en tres sistemes. Coordinar-les sense transacció global és el problema de les sagues (03-05).
Temps real per als repartidors
Les apps dels repartidors envien posicions per MQTT (protocol lleuger per a xarxes mòbils) i els clients reben actualitzacions per WebSocket, a través d'un servidor de temps real que publica al bus i se n'alimenta (símptoma 4; lliçons 02-01 i 08-02).
Analítica: lots i flux
analitica deixa de consultar producció. Els esdeveniments del bus es desen en un data lake, sobre el qual corren treballs per lots (informes nocturns, amb Spark) i processament en flux (mètriques en temps real de la campanya, amb Flink o Kafka Streams), orquestrats amb un planificador (símptoma 5; Mòdul 5).
Seguretat, observabilitat, desplegament
Una passarel·la autentica els usuaris (OIDC) i limita el trànsit; els serveis s'autentiquen entre si amb mTLS; els secrets es gestionen de manera centralitzada (Mòdul 6). Mètriques, logs i traces distribuïdes es recullen de tots els serveis (Mòdul 7). Tot es desplega com a contenidors a Kubernetes, en un proveïdor de núvol, amb infraestructura definida com a codi (07-05, 08-03).
- Mapa de l'arquitectura al curs
Aquesta taula és la brúixola del curs: cada component de l'arquitectura objectiu, i la lliçó en què es construeix o s'explica.
| Component / decisió | Mòdul | Lliçó |
|---|---|---|
| Sockets, TCP/UDP, HTTP, MQTT per a repartidors | 2 | 02-01 |
| Crides síncrones entre serveis (RPC) | 2 | 02-02 |
gRPC i serialització amb contractes (comandes → inventari, pagaments) |
2 | 02-03 |
| Bus d'esdeveniments Kafka, cues RabbitMQ | 2 | 02-04 |
| Idempotència, outbox, cues de missatges morts, consumidors en competència | 2 | 02-05 |
| Quines garanties dona cada emmagatzematge (models de consistència) | 3 | 03-01 |
Per què inventari (consistència) i comandes (disponibilitat) trien diferent: CAP i PACELC |
3 | 03-02 |
| Com es tria el líder de PostgreSQL replicat i com es coordina Kafka: consens (Raft) | 3 | 03-03 |
Replicació de PostgreSQL d'inventari; detecció de conflictes |
3 | 03-04 |
| Confirmar comanda sense transacció global: sagues | 3 | 03-05 |
| Com Cassandra reparteix les comandes i les posicions: particionament i hashing consistent | 4 | 04-01 |
| Sistemes de fitxers distribuïts per al data lake | 4 | 04-02 |
| Fotos de productes al magatzem d'objectes (S3/MinIO) | 4 | 04-03 |
Cassandra per a comandes i per a les posicions de repartiment |
4 | 04-04 |
| Redis com a memòria cau del catàleg | 4 | 04-05 |
Models de computació distribuïda per a analitica |
5 | 05-01 |
| Informes per lots: MapReduce i Hadoop | 5 | 05-02 |
| Previsió de demanda amb Spark | 5 | 05-03 |
| Mètriques de campanya en temps real: Flink / Kafka Streams | 5 | 05-04 |
| Orquestració de treballs nocturns amb Airflow | 5 | 05-05 |
| Autenticació de clients i rols (JWT) | 6 | 06-01 |
| Xifratge de dades de pagament i de posicions | 6 | 06-02 |
| Identitat d'usuaris i productors (OAuth2/OIDC) | 6 | 06-03 |
| mTLS entre serveis, gestió de secrets | 6 | 06-04 |
| Passarel·la, limitació de taxa, auditoria | 6 | 06-05 |
| Mètriques amb Prometheus i Grafana | 7 | 07-01 |
| Logs centralitzats i traces amb OpenTelemetry | 7 | 07-02 |
| Failover de PostgreSQL, checkpoints de Flink | 7 | 07-03 |
Timeouts, reintents, circuit breaker a comandes → pagaments |
7 | 07-04 |
| Desplegament a Kubernetes, Ansible | 7 | 07-05 |
| Proves i enginyeria del caos | 7 | 07-06 |
| Microserveis: límits, contractes, organització | 8 | 08-01 |
| WebSockets i MQTT per al seguiment en temps real | 8 | 08-02 |
| Desplegament a AWS/GCP | 8 | 08-03 |
| Funcions serverless per al redimensionament de fotos; edge per als mercats | 8 | 08-04 |
| Quilòmetre Zero d'extrem a extrem | 8 | 08-05 |
- Un camí gradual, no un "big bang"
L'arquitectura objectiu és la destinació, no el primer pas. Reescriure-ho tot de cop és la manera més segura de fracassar: durant mesos no hi hauria res a desplegar, i al final caldria migrar-ho tot alhora. En lloc d'això, Quilòmetre Zero seguirà el patró conegut com a estrangulament (strangler fig): s'extreu un servei del monòlit, s'hi redirigeix el trànsit i el monòlit perd aquesta responsabilitat; es repeteix amb el següent. A cada pas, el sistema funciona i es pot desplegar.
| Fase | Què s'extreu | Símptoma que resol | Mòduls del curs |
|---|---|---|---|
| 1 | cataleg (amb Redis i magatzem de fotos) |
1: pics de campanya | 2, 4 |
| 2 | pagaments (aïllat, amb timeouts i circuit breaker) |
2: fallada de pagaments que ho tomba tot | 2, 7 |
| 3 | repartiment amb el bus d'esdeveniments i el canal en temps real |
4: telemetria | 2, 4, 8 |
| 4 | comandes i inventari (amb sagues i replicació) |
3: desplegaments independents | 3, 4 |
| 5 | analitica sobre el bus i el data lake |
5: analítica | 5 |
| Transversal | Seguretat, observabilitat, Kubernetes | Tots | 6, 7 |
Es comença pel catàleg perquè és el que causa el símptoma més greu, el que té menys dependències (només lectures) i el que té menys risc: si el servei nou falla, es torna a apuntar al monòlit.
- Preparació de l'entorn de treball: el projecte
km0/
km0/Per seguir el curs de manera pràctica, crearem un projecte que anirem ampliant. En aquest mòdul només conté el punt de partida: el monòlit i la seva base de dades. No es pretén que el monòlit estigui implementat en detall; n'hi ha prou amb un esquelet que arrenqui, perquè als mòduls següents tinguem on afegir els serveis.
Estructura de carpetes
km0/
├── docker-compose.yml # Infraestructura local: creixerà mòdul a mòdul
├── README.md
├── monolit/ # El punt de partida (s'anirà buidant)
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── app.py # Punt d'entrada de l'aplicació
│ ├── cataleg/ # Un paquet Python per mòdul de domini
│ ├── comandes/
│ ├── inventari/
│ ├── pagaments/
│ ├── repartiment/
│ └── analitica/
├── serveis/ # Buit de moment. Mòdul 2: cataleg/, comandes/ ...
├── infra/ # Buit de moment. Mòdul 7: kubernetes/, ansible/
└── sql/
└── 001_esquema_inicial.sql # Taules del monòlitLes carpetes serveis/ i infra/ són buides a propòsit: existeixen perquè l'evolució sigui visible. A mesura que s'extregui cada servei, apareixerà a serveis/ i desapareixerà de monolit/.
docker-compose.yml mínim
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: km0
POSTGRES_USER: km0
POSTGRES_PASSWORD: km0_dev # només per a desenvolupament local
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./sql:/docker-entrypoint-initdb.d # executa els .sql en crear la BD
healthcheck:
test: ["CMD-SHELL", "pg_isready -U km0 -d km0"]
interval: 5s
timeout: 3s
retries: 10
monolit:
build: ./monolit
environment:
DATABASE_URL: postgresql://km0:km0_dev@postgres:5432/km0
ports:
- "8000:8000"
depends_on:
postgres:
condition: service_healthy # espera que PostgreSQL respongui
volumes:
postgres_data:Explicació de les parts rellevants per al curs:
servicesdeclara dos contenidors:postgresimonolit. Docker Compose crea una xarxa interna en què cada servei és accessible pel seu nom: per això la URL de connexió fa servirpostgrescom a host, i no una IP (recorda la fal·làcia 5: la topologia canvia; els noms són estables, les adreces no).healthcheckidepends_on ... condition: service_healthysón el primer exemple pràctic de "no assumeixis que l'altre node està a punt": el monòlit no arrenca fins que PostgreSQL respon apg_isready. Sense això, el monòlit arrencaria, intentaria connectar-se, fallaria i moriria. Aquest patró es repetirà amb cada servei que afegim.volumespersisteix les dades de PostgreSQL entre reinicis del contenidor (model de fallada crash-recovery: el procés mor, les dades al disc sobreviuen)../sql:/docker-entrypoint-initdb.dexecuta l'esquema inicial la primera vegada que es crea la base de dades.
Esquelet del monòlit
monolit/app.py és deliberadament mínim: comprova que pot parlar amb la base de dades i exposa un punt de salut. Fa servir només la biblioteca estàndard més psycopg per no introduir dependències que es triaran en mòduls posteriors.
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
import psycopg
DATABASE_URL = os.environ["DATABASE_URL"]
def comptar_productes() -> int:
"""Consulta mínima per comprovar que la base de dades respon."""
with psycopg.connect(DATABASE_URL) as conn:
with conn.cursor() as cur:
cur.execute("SELECT count(*) FROM productes")
return cur.fetchone()[0]
class Gestor(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/salut":
cos = {"estat": "ok", "productes": comptar_productes()}
self._respondre(200, cos)
else:
self._respondre(404, {"error": "no trobat"})
def _respondre(self, codi: int, cos: dict) -> None:
dades = json.dumps(cos).encode()
self.send_response(codi)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(dades)))
self.end_headers()
self.wfile.write(dades)
if __name__ == "__main__":
print("Quilòmetre Zero (monòlit) escoltant a :8000")
HTTPServer(("0.0.0.0", 8000), Gestor).serve_forever()monolit/requirements.txt conté una única línia, psycopg[binary]>=3.1, i monolit/Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]I sql/001_esquema_inicial.sql amb un esquema mínim i els productors del curs:
CREATE TABLE productors (
id SERIAL PRIMARY KEY,
nom TEXT NOT NULL,
ciutat TEXT NOT NULL
);
CREATE TABLE productes (
id SERIAL PRIMARY KEY,
productor_id INTEGER NOT NULL REFERENCES productors(id),
nom TEXT NOT NULL,
preu NUMERIC(10, 2) NOT NULL, -- mai float per a diners (fal·làcia 8)
estoc INTEGER NOT NULL DEFAULT 0
);
INSERT INTO productors (nom, ciutat) VALUES
('Horta La Vega', 'Lleida'),
('Formatgeria Montblanc', 'Girona'),
('Celler Roure Alt', 'Tarragona');
INSERT INTO productes (productor_id, nom, preu, estoc) VALUES
(1, 'Tomàquet rosa', 3.20, 120),
(1, 'Carbassó', 2.10, 80),
(2, 'Formatge curat d''ovella', 14.50, 5),
(2, 'Formatge fresc', 6.80, 30),
(3, 'Vi negre criança', 9.90, 200);Per arrencar l'entorn:
I, en una altra terminal, comprovar que el monòlit respon:
La resposta ha de ser {"estat": "ok", "productes": 5}.
Amb això queda muntat el punt de partida: exactament el monòlit de la lliçó 01-01, ara executable en contenidors. Al Mòdul 2 apareixerà serveis/cataleg/ i una segona entrada a docker-compose.yml.
Errors Comuns i Consells
- Partir el monòlit per capes tècniques en lloc de per dominis. Crear un "servei de base de dades", un "servei de lògica" i un "servei d'API" no aïlla res: cada operació continua travessant els tres. Els límits correctes són els de negoci (
cataleg,comandes...), que és on l'acoblament és menor. - Extreure serveis que comparteixen base de dades. Si
comandesiinventaries despleguen per separat però llegeixen i escriuen les mateixes taules, no són dos serveis: són un monòlit distribuït, amb tots els costos de la distribució i cap dels seus avantatges. - Fer totes les comunicacions síncrones. Converteix cada flux en una cadena de disponibilitats multiplicades i de latències sumades. La pregunta per a cada interacció ha de ser "necessito la resposta ara?"; si no, és un esdeveniment.
- Fer totes les comunicacions asíncrones. L'extrem oposat també falla: comprovar l'estoc per esdeveniments, esperant que "eventualment" arribi la resposta, fa impossible donar una confirmació immediata al client. El síncron existeix per una raó.
- Triar tecnologies abans que problemes. "Farem servir Kafka i Cassandra" no és una decisió d'arquitectura si no respon a un símptoma. Cada peça de l'arquitectura objectiu està justificada per un dels cinc símptomes; si no pots anomenar el símptoma, treu la peça.
- Deixar l'observabilitat i la seguretat per al final. Quan hi ha sis serveis i alguna cosa falla, ja és tard per preguntar-se com correlacionar logs. Totes dues són part del primer servei que s'extreu.
- Consell: mantén un document de decisions d'arquitectura (un ADR per decisió: context, opcions, decisió, conseqüències). D'aquí a un any, quan algú pregunti "per què Cassandra per a comandes?", la resposta estarà escrita.
Exercicis
Exercici 1: Identificar límits de serveis
L'equip proposa afegir dues funcionalitats: (a) valoracions i comentaris dels clients sobre els productes, i (b) cupons de descompte per campanya, que s'apliquen a l'import de la comanda i tenen un nombre màxim d'usos. Per a cadascuna, decideix si ha de ser un servei nou o part d'un d'existent, indica de quines dades seria propietària i quines comunicacions (síncrones o asíncrones) necessitaria amb la resta. Justifica cada decisió amb els principis de l'apartat 3.
Exercici 2: Triar el tipus de comunicació
Per a cada interacció, indica si ha de ser síncrona (RPC) o asíncrona (esdeveniment), i raona què passaria amb l'alternativa:
comandesnecessita saber si hi ha estoc abans de confirmar una comanda.catalegvol mostrar "queden poques unitats" quan l'estoc baixa de 5.- L'app de l'Anna vol mostrar la confirmació del pagament immediatament després de prémer "pagar".
analiticavol comptabilitzar cada venda per a l'informe de campanya.repartimentnecessita conèixer l'adreça de lliurament de cada comanda pagada per planificar rutes.
Exercici 3: Degradació controlada
Dissenya, per a cadascuna de les fallades següents, què ha de continuar funcionant a Quilòmetre Zero i quin missatge o comportament veurà l'usuari. Indica quin principi de disseny ho fa possible:
- Cau
pagaments(la passarel·la externa no respon). - Cau Redis (la memòria cau del catàleg).
- Cau el bus d'esdeveniments (Kafka) durant 10 minuts.
- Cau
repartimentdel tot.
Solucions
Solució 1:
(a) Valoracions: és un bon candidat a servei nou (valoracions). Té dades pròpies (valoració, comentari, client, producte, data), un cicle de vida independent (moderació, respostes del productor), un patró de càrrega diferent (moltes lectures en veure un producte, poques escriptures) i es podria escalar per separat. Comunicació: asíncrona amb comandes (consumeix ComandaLliurada per permetre valorar només productes comprats) i asíncrona cap a cataleg (publica ValoracioCreada perquè el catàleg mantingui la nota mitjana desnormalitzada, sense crides síncrones en mostrar la fitxa). El principi: cada servei propietari de les seves dades; síncron només quan la resposta fa falta ara (mostrar la nota mitjana no necessita consultar valoracions a cada petició).
(b) Cupons: és més discutible. El nombre màxim d'usos exigeix una comprovació amb consistència forta en el moment de confirmar la comanda (no es pot aplicar el cupó 101 si el màxim és 100), cosa que l'acosta al problema de l'estoc. Dues opcions raonables: incloure'l a comandes (que ja gestiona l'import i la confirmació) o crear promocions com a servei propi amb una crida síncrona reservar_us(cupo) des de comandes, anàloga a la reserva d'estoc. Un servei propi es justifica si les campanyes creixeran en complexitat (regles, segmentació, equip de màrqueting amb el seu propi ritme de desplegament); si només són cupons simples, posar-ho a comandes evita una frontera de xarxa al flux més crític. L'important és reconèixer que la resposta depèn del símptoma que es vulgui resoldre, no d'una regla fixa.
Solució 2:
- Síncrona. El client espera la confirmació; sense resposta immediata no es pot decidir. Amb esdeveniments,
comandeshauria d'esperar "una estona" queinventarirespongués per un altre canal, i no podria donar una resposta al client a la mateixa petició. - Asíncrona.
catalegconsumeixEstocActualitzati manté un indicador local. Amb crides síncrones, cada visualització de producte faria una crida ainventari, multiplicant la càrrega i afegint una dependència al flux més massiu (símptoma 1). - Síncrona a la interacció
comandes→pagaments(el client espera), però amb timeout curt i un pla de degradació: sipagamentsno respon en 3 segons, la comanda queda en "pagament pendent" i es resol després mitjançant esdeveniments (PagamentConfirmat), avisant la clienta. És una barreja: síncrona al camí feliç, asíncrona com a xarxa de seguretat. - Asíncrona.
analiticaconsumeixComandaPagada. Una crida síncrona aanaliticades decomandesacoblaria el flux de compra a un sistema analític que no necessita estar disponible per vendre (símptoma 5). - Asíncrona.
repartimentconsumeixComandaPagada, que inclou l'adreça. Planificar rutes no és immediat; i sirepartimentestà caigut, l'esdeveniment espera al bus i es processa després.
Solució 3:
- Cau
pagaments. Catàleg, cistell, seguiment de repartiment i analítica continuen funcionant. En confirmar una comanda, després del timeout, el client veu: "No hem pogut processar el pagament ara mateix; la teva comanda queda desada i t'avisarem tan bon punt es completi" (comanda en estat "pagament pendent"; l'estoc es reserva amb caducitat). Principi: aïllament de fallades + timeouts + asincronia com a xarxa de seguretat. Compara-ho amb el símptoma 2, on aquesta mateixa fallada tombava tota la plataforma. - Cau Redis.
catalegcontinua funcionant llegint del seu PostgreSQL, més lent i amb menys capacitat; durant una campanya podria ser necessari limitar el trànsit. L'usuari no veu cap error, només més latència. Principi: la memòria cau és una optimització, no una font de veritat; el servei ha de funcionar sense ella. - Cau Kafka 10 minuts. Les operacions síncrones (comprar, pagar, reservar estoc) continuen funcionant. Els esdeveniments s'acumulen a cada servei productor (patró outbox, lliçó 02-05) i es publiquen quan el bus torna. Efectes visibles: el mapa de repartidors es congela, l'indicador "queden poques unitats" es retarda, els informes de campanya van 10 minuts endarrerits. Principi: síncron només quan cal; l'asíncron tolera retards per definició.
- Cau
repartiment. Es pot comprar i pagar amb normalitat. El seguiment al mapa mostra "Seguiment no disponible temporalment; la teva comanda està confirmada". Les posicions dels repartidors s'acumulen al bus i es processen en recuperar-se. Els esdevenimentsComandaPagadaesperen; les rutes es planifiquen amb retard. Principi: aïllament de fallades i desacoblament temporal mitjançant esdeveniments.
Conclusió
Aquesta lliçó ha convertit els fonaments del mòdul en un pla. Hem obert el monòlit de Quilòmetre Zero —sis mòduls, una base de dades, una transacció que abasta fins a la passarel·la de pagaments externa, un desplegament de dimarts i dijous— i hem identificat els cinc símptomes que en forcen l'evolució: els pics de campanya, la fallada de pagaments que ho tomba tot, els equips que es trepitgen en desplegar, la telemetria de repartidors en temps real i l'analítica que ofega la producció. Cada símptoma justifica una peça de l'arquitectura objectiu: sis serveis propietaris de les seves dades, comunicació síncrona per gRPC on cal resposta immediata i asíncrona per esdeveniments a Kafka en tota la resta, emmagatzematge adaptat a cada càrrega (PostgreSQL replicat, Cassandra, Redis, magatzem d'objectes), processament per lots i en flux per a l'analítica, i seguretat, observabilitat i desplegament a Kubernetes com a fonaments transversals. La taula de l'apartat 5 és el mapa que enllaça cada component amb la lliçó que el desenvolupa, i el projecte km0/ amb el seu docker-compose.yml mínim és el punt de partida sobre el qual anirem construint. Res d'això no s'ha implementat encara: és un full de ruta, i el camí serà gradual, extraient un servei cada vegada. El primer pas és fer que dos processos parlin entre si de manera fiable, i això és exactament el que comença al Mòdul 2, Comunicació en Sistemes Distribuïts, amb els protocols de xarxa sobre els quals es recolza tota la resta.
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
