La lliçó anterior va acabar amb una pregunta incòmoda: el pipeline de Quilòmetre Zero construeix, signa i desplega la versió 1.15.0 de comandes, executa pytest, verifica contractes i observa els SLO durant el canary, però quina prova demostra que la saga de 03-05 compensa correctament quan inventari cau a mig camí? Com sabem que el circuit breaker de 07-04 s'obre a temps, o que Patroni commuta en menys de 30 segons, abans que passi un dissabte a la tarda en plena Setmana de la Verema? En un programa que corre en un sol procés, una prova unitària ben escrita respon gairebé a tot. En un sistema distribuït no: el no-determinisme, les fallades parcials i el temps fan que moltes propietats importants només es puguin comprovar exercitant dependències reals, provocant fallades a propòsit i observant el sistema complet. Aquesta lliçó tanca el Mòdul 7 amb les dues disciplines que converteixen la confiança en evidència: les proves adaptades a allò distribuït (integració amb contenidors, contractes, càrrega, resiliència i proves en producció) i l'enginyeria del caos, que injecta a la plataforma les mateixes fallades que hem estudiat a 07-03 i 07-04, amb hipòtesis explícites i radi d'explosió controlat.
Contingut
- Per què provar un sistema distribuït és més difícil
- La piràmide de proves adaptada
- Proves d'integració amb dependències reals: Testcontainers i la saga
- Proves de contracte: consumidors, productors i esquemes
- Proves de càrrega: la Setmana de la Verema amb k6
- Proves de resiliència i proves en producció
- Simulació determinista i verificació formal: visió general
- Enginyeria del caos: principis, eines i cicle d'un experiment
- Experiments de caos a Quilòmetre Zero
- Errors Comuns i Consells
- Exercicis
- Conclusió
- Per què provar un sistema distribuït és més difícil
Una prova clàssica es recolza en tres supòsits: la mateixa entrada produeix la mateixa sortida, l'entorn de la prova s'assembla al real i, si alguna cosa falla, falla del tot. Els sistemes distribuïts trenquen els tres.
| Dificultat | Què vol dir a Quilòmetre Zero | Conseqüència per a les proves |
|---|---|---|
| No-determinisme | Dos consumidors del tòpic comandes.esdeveniments poden processar els missatges en un ordre diferent a cada execució; el planificador i la xarxa decideixen. |
Una prova que passa 99 vegades i falla 1 (flaky) pot estar revelant una carrera real, no un problema de la prova. |
| Entorns | En local no hi ha tres nodes de Patroni, ni particions de Kafka, ni Kong al davant. Un mock d'inventari mai no retorna UNAVAILABLE amb 800 ms de latència. |
Les proves unitàries amb dobles són necessàries però insuficients: el comportament que importa és a la integració. |
| Fallades parcials | pagaments respon, inventari no, i comandes ha de decidir què fer amb P-2026-000125 a mitges. |
Cal provar estats intermedis (ESTOC_RESERVAT sense pagament), no només el camí feliç i l'error total. |
| Temps | Timeouts, reintents amb backoff, finestres del circuit breaker, TTL de Redis, heartbeats del DetectorPhi. |
El resultat depèn de quan passa cada cosa; provar "esperant 5 segons" és lent i fràgil. |
| Escala | Un error que apareix amb 500 comandes per segon no apareix amb 5. | Algunes propietats només es verifiquen amb càrrega realista. |
D'aquí surt la idea central de la lliçó: en un sistema distribuït, la pregunta "funciona?" es descompon en diverses preguntes diferents, cadascuna amb el seu tipus de prova, i cap d'elles no és suficient per si sola.
- La piràmide de proves adaptada
La piràmide clàssica (moltes unitàries, algunes d'integració, poques d'extrem a extrem) continua sent vàlida, però en distribuït s'hi afegeixen capes que no existien i canvia el pes d'altres.
| Nivell | Què verifica | Dependències | Velocitat | Quantitat recomanada | Exemple a Quilòmetre Zero |
|---|---|---|---|---|---|
| Unitàries | Lògica pura d'un servei | Cap (dobles) | ms | Moltes | Càlcul del total d'una comanda, transició d'estats de SagaComanda amb repositori en memòria |
| Integració amb dependències reals | El servei contra la seva base de dades, el seu broker, la seva memòria cau | Contenidors efímers | segons | Força | La saga contra PostgreSQL i Kafka reals (apartat 3) |
| Contracte | Que productor i consumidor d'una API o esdeveniment continuen d'acord | Només el contracte | ms-s | Una per parella | comandes ↔ inventari sobre inventari.proto (apartat 4) |
| Extrem a extrem | Un flux complet de negoci a través de tots els serveis | Entorn complet | minuts | Molt poques | Crear una comanda de l'Anna des de Kong fins que analitica la compta |
| Rendiment i càrrega | Que el sistema compleix els SLO sota la demanda prevista | Entorn semblant a producció | minuts-hores | Per versió i campanya | Setmana de la Verema amb k6 (apartat 5) |
| Resiliència | Que els patrons de 07-04 actuen com es van dissenyar davant de fallades injectades | Entorn + eina d'injecció | minuts | Per patró crític | Toxiproxy entre comandes i inventari (apartats 6 i 9) |
| En producció | Que la versió real, amb dades reals, es comporta | Producció | continu | Sempre actives | Canary de 07-05, sondes sintètiques de 07-01 |
Dues observacions pràctiques:
- Les proves d'extrem a extrem són poques i cares a propòsit. Cadascuna necessita tot l'entorn aixecat, triga minuts i, quan falla, assenyala "alguna cosa entre set serveis". Es reserven per a dos o tres fluxos que, si es trenquen, enfonsen el negoci: crear comanda, pagar, lliurar.
- Les de contracte substitueixen moltes d'extrem a extrem: si
comandesiinventaridemostren per separat que respecten el mateix contracte, no cal desplegar-los junts per saber que s'entendran.
- Proves d'integració amb dependències reals: Testcontainers i la saga
Un mock de PostgreSQL no fa SELECT ... FOR UPDATE, no té deadlocks i no rebutja una transacció per violació de clau única. Un mock de Kafka no reparteix particions ni relliura missatges. Testcontainers ho resol arrencant, des de la mateixa prova, contenidors Docker efímers amb les dependències reals, i destruint-los en acabar. Cada execució parteix d'un estat net i la prova pot córrer igual al portàtil de la Marta i al pipeline de 07-05.
Provarem el més delicat de 03-05: que la saga de comanda, quan pagaments rebutja el cobrament després d'haver reservat estoc, compensa (allibera la reserva) i acaba en CANCELLADA, deixant a més l'esdeveniment corresponent a l'outbox de 02-05.
# km0/tests/test_saga_comanda.py
import json
import pytest
import psycopg
from testcontainers.postgres import PostgresContainer
from testcontainers.kafka import KafkaContainer
from kafka import KafkaConsumer
from serveis.comandes.saga_comanda import SagaComanda, EstatSaga
from serveis.comandes.repositori import RepositoriSagues
from serveis.comandes.outbox import PublicadorOutbox
@pytest.fixture(scope="session")
def postgres():
# Un sol contenidor de PostgreSQL per a tota la sessió de proves:
# arrencar-lo costa ~3 s, i cada test neteja les seves taules.
with PostgresContainer("postgres:16") as pg:
yield pg
@pytest.fixture(scope="session")
def kafka():
with KafkaContainer("confluentinc/cp-kafka:7.6.0") as kf:
yield kf
@pytest.fixture
def connexio(postgres):
# Connexió real; es crea l'esquema mínim que la saga necessita.
with psycopg.connect(postgres.get_connection_url().replace("+psycopg2", "")) as con:
with open("sql/comandes_sagues.sql") as f:
con.execute(f.read()) # taules sagas, outbox, missatges_processats
yield con
con.execute("TRUNCATE sagas, outbox, missatges_processats")
con.commit()
class InventariFals:
"""Doble del client gRPC d'inventari. Registra les crides per
poder afirmar que la compensació s'ha executat."""
def __init__(self):
self.reserves = []
self.alliberaments = []
def reservar_estoc(self, comanda_id, producte, unitats):
self.reserves.append((comanda_id, producte, unitats))
return {"reserva_id": f"R-{comanda_id}"}
def alliberar_reserva(self, reserva_id):
self.alliberaments.append(reserva_id)
class PagamentsRebutja:
"""Doble de pagaments que sempre rebutja el cobrament."""
def cobrar(self, comanda_id, import_eur):
raise RuntimeError("targeta rebutjada")
def test_saga_compensa_si_pagaments_falla(connexio, kafka):
inventari = InventariFals()
outbox = PublicadorOutbox(connexio, bootstrap=kafka.get_bootstrap_server())
saga = SagaComanda(
repo=RepositoriSagues(connexio),
inventari=inventari,
pagaments=PagamentsRebutja(),
outbox=outbox,
)
saga.executar(comanda_id="P-2026-000125", client="llucia",
linies=[("formatge-curat", 2)], import_eur=18.40)
# 1. L'estat final persistit a la taula `sagas` és CANCELLADA
fila = connexio.execute(
"SELECT estat FROM sagas WHERE comanda_id = %s", ("P-2026-000125",)
).fetchone()
assert fila[0] == EstatSaga.CANCELLADA.value
# 2. La compensació ha alliberat exactament la reserva que s'havia fet
assert inventari.reserves == [("P-2026-000125", "formatge-curat", 2)]
assert inventari.alliberaments == ["R-P-2026-000125"]
# 3. L'esdeveniment ComandaCancellada ha sortit per l'outbox cap a un Kafka de debò
outbox.publicar_pendents()
consumidor = KafkaConsumer(
"comandes.esdeveniments",
bootstrap_servers=kafka.get_bootstrap_server(),
auto_offset_reset="earliest",
consumer_timeout_ms=5000,
)
tipus = [json.loads(m.value)["tipus"] for m in consumidor]
assert "ComandaCancellada" in tipusQuè cal entendre d'aquesta prova, línia a línia:
- Les fixtures
postgresikafkatenenscope="session": els contenidors s'arrenquen un cop i es comparteixen. Arrencar Kafka per cada test faria la suite inutilitzable. - La fixture
connexioexecuta el mateixsql/comandes_sagues.sqlque fa servir el desplegament real. Si algú canvia l'esquema i oblida la migració, aquesta prova ho detecta. ElTRUNCATEfinal garanteix aïllament entre tests. InventariFalsiPagamentsRebutjasón dobles dels altres serveis, no de la infraestructura. Aquest és el criteri de la piràmide: en una prova d'integració decomandeses fan servir de debò les seves dependències (la seva base de dades, el seu broker) i se substitueixen els serveis aliens, el comportament dels quals es garanteix a part amb contractes.- Les tres asercions cobreixen les tres cares de la compensació: estat persistit, acció compensatòria executada i esdeveniment publicat. Si la saga marqués
CANCELLADAperò oblidés alliberar l'estoc, la segona aserció fallaria, que és exactament l'error silenciós que deixaria "Formatgeria Montblanc" amb unitats bloquejades. - Per verificar l'esdeveniment es consumeix del Kafka real del contenidor. Això prova el patró Outbox complet de 02-05, inclosa la serialització de l'embolcall
id_esdeveniment/tipus/versio/data_ms/origen/dades.
Amb la mateixa estructura s'escriuen els altres casos que importen: inventari retorna UNAVAILABLE a la primera reserva (ha de reintentar segons 07-04 i no crear dues reserves), el procés mor entre ESTOC_RESERVAT i PAGAMENT_CONFIRMAT (en rearrencar, la saga s'ha de reprendre des de la taula sagas), i el mateix esdeveniment arriba dues vegades (la taula missatges_processats ha de descartar el duplicat).
- Proves de contracte: consumidors, productors i esquemes
Quan comandes (consumidor) crida ReservarEstoc d'inventari (productor), tots dos depenen d'un acord: camps, tipus, codis d'error. Una prova de contracte fixa aquest acord en un artefacte verificable pels dos costats, sense desplegar-los junts.
Contractes dirigits pel consumidor (Pact)
En l'enfocament consumer-driven, és el consumidor qui declara què necessita. comandes escriu: "quan crido ReservarEstoc amb producte=formatge-curat, unitats=2, espero una resposta amb reserva_id de tipus cadena i estat=RESERVAT". Aquest pact es publica en un broker de contractes i el pipeline d'inventari el verifica contra la seva implementació real a cada canvi. Si inventari reanomena reserva_id a id_reserva, el seu propi pipeline falla abans de desplegar, i el missatge d'error diu quin consumidor es trencaria.
Compatibilitat d'esquemes protobuf i d'esdeveniments
Per a gRPC i Kafka, bona part del contracte ja és als esquemes de 02-03 (contractes/inventari.proto) i 02-05 (l'embolcall d'esdeveniments). El que cal verificar és que un canvi d'esquema sigui compatible cap enrere: els consumidors vells han de continuar entenent els missatges nous.
# km0/tests/contracte/comandes_inventari.py
"""Contracte consumidor-productor entre comandes i inventari.
Dues verificacions:
1. comandes declara els camps que fa servir; inventari els ha de continuar servint.
2. La versió nova d'inventari.proto és compatible amb la publicada.
"""
import subprocess
import pytest
from google.protobuf import descriptor_pb2
from contractes import inventari_pb2, inventari_pb2_grpc
# --- 1. Expectatives del consumidor -------------------------------------
# El que `comandes` fa servir realment de la resposta (grep del codi + revisió).
CAMPS_QUE_USA_COMANDES = {
"ReservarEstocResposta": {"reserva_id", "estat", "unitats_reservades"},
"ConsultarEstocResposta": {"producte", "disponible"},
}
# Codis gRPC davant dels quals `comandes` té lògica de reintent/compensació.
CODIS_ESPERATS = {"UNAVAILABLE", "FAILED_PRECONDITION", "DEADLINE_EXCEEDED"}
@pytest.mark.parametrize("missatge,camps", CAMPS_QUE_USA_COMANDES.items())
def test_inventari_continua_servint_els_camps_que_comandes_usa(missatge, camps):
descriptor = getattr(inventari_pb2, missatge).DESCRIPTOR
camps_reals = {f.name for f in descriptor.fields}
falten = camps - camps_reals
assert not falten, f"inventari.proto ja no defineix {falten} a {missatge}"
def test_servei_exposa_els_metodes_del_contracte():
metodes = {m.name for m in inventari_pb2.DESCRIPTOR.services_by_name["Inventari"].methods}
assert {"ReservarEstoc", "ConsultarEstoc"} <= metodes
# --- 2. Compatibilitat cap enrere del .proto ----------------------------
def _descriptor_set(ruta_proto: str) -> descriptor_pb2.FileDescriptorSet:
"""Compila un .proto a un FileDescriptorSet binari amb protoc."""
sortida = subprocess.check_output([
"protoc", "--include_imports", "-I", "contractes",
"--descriptor_set_out=/dev/stdout", ruta_proto,
])
fds = descriptor_pb2.FileDescriptorSet()
fds.ParseFromString(sortida)
return fds
def _camps_per_missatge(fds):
resultat = {}
for fitxer in fds.file:
for msg in fitxer.message_type:
resultat[msg.name] = {f.number: (f.name, f.type, f.label) for f in msg.field}
return resultat
def test_proto_nou_es_compatible_amb_el_publicat():
publicat = _camps_per_missatge(_descriptor_set("contractes/publicat/inventari.proto"))
nou = _camps_per_missatge(_descriptor_set("contractes/inventari.proto"))
for nom, camps_vells in publicat.items():
assert nom in nou, f"S'ha eliminat el missatge {nom}"
for numero, (nom_camp, tipus, etiqueta) in camps_vells.items():
assert numero in nou[nom], (
f"{nom}: s'ha eliminat el camp {numero} ({nom_camp}); "
"a protobuf els camps es marquen `reserved`, no s'esborren"
)
assert nou[nom][numero][1] == tipus, (
f"{nom}.{nom_camp}: ha canviat el tipus del camp {numero}"
)Explicació:
- La primera part codifica el que el consumidor necessita com a dades (
CAMPS_QUE_USA_COMANDES) i ho comprova contra el descriptor del.protocompilat. És la versió lleugera de l'enfocament Pact: no simula crides, però atrapa el 90 % de les ruptures (camps reanomenats o eliminats, mètodes desapareguts). - La segona part compara el
.protonou amb la còpia publicada (l'última versió desplegada, desada acontractes/publicat/). Les regles de compatibilitat de protobuf són concretes: no reutilitzar ni eliminar números de camp, no canviar tipus, afegir camps només com a opcionals. Un consumidor compilat amb l'esquema vell ignora els camps nous i continua funcionant. - Per als esdeveniments de Kafka, la mateixa idea s'aplica sobre l'embolcall de 02-05: el camp
versioexisteix precisament per a això, i un test anàleg verifica que lesdadesde la versió N+1 contenen tots els camps de la versió N. Amb un schema registry la verificació pot ser automàtica en registrar l'esquema.
- Proves de càrrega: la Setmana de la Verema amb k6
A la Setmana de la Verema, "Celler Roure Alt" llança el vi-crianca amb descompte i el trànsit de comandes es multiplica per vuit respecte d'un dimarts normal. La prova de càrrega respon a una pregunta molt concreta: compleix la versió 1.15.0 l'SLO de 07-01 (99,9 % d'èxit i p99 < 500 ms) sota aquest trànsit? Sense llindars lligats a l'SLO, una prova de càrrega només genera gràfiques boniques.
k6 descriu la càrrega en JavaScript i avalua llindars en acabar, retornant un codi de sortida diferent de zero si no es compleixen, cosa que permet integrar-la com una etapa més del pipeline.
// km0/tests/carrega/verema.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
// Mètriques pròpies, amb noms alineats amb les de Prometheus de 07-01
const errors = new Rate('km0_comandes_error_rate');
const duradaComanda = new Trend('km0_crear_comanda_ms', true);
export const options = {
// Perfil de la campanya: rampa d'entrada, altiplà de 20 min al x8, baixada.
stages: [
{ duration: '3m', target: 50 }, // 50 usuaris virtuals: trànsit base
{ duration: '5m', target: 400 }, // pujada fins al pic (x8)
{ duration: '20m', target: 400 }, // altiplà: aquí es mesura de debò
{ duration: '3m', target: 0 }, // drenatge
],
thresholds: {
// Llindars = SLO de 07-01. Si s'incompleixen, k6 surt amb error.
'km0_comandes_error_rate': ['rate<0.001'], // 99,9 % d'èxit
'km0_crear_comanda_ms': ['p(99)<500', 'p(95)<250'], // p99 < 500 ms
'http_req_failed': ['rate<0.001'],
},
};
const BASE = __ENV.KM0_URL || 'https://staging.km0.example/api/v1';
const TOKEN = __ENV.KM0_TOKEN; // JWT de Keycloak per a un client de proves
const productes = ['vi-crianca', 'vi-crianca', 'vi-crianca', 'formatge-curat', 'tomaquet-rosa'];
export default function () {
// 1. Consulta del catàleg (lectura barata, molt més freqüent que la comanda)
const cat = http.get(`${BASE}/cataleg/productes?mercat=girona`);
check(cat, { 'cataleg 200': (r) => r.status === 200 });
// 2. Crear comanda: l'operació que governa l'SLO
const producte = productes[Math.floor(Math.random() * productes.length)];
const cos = JSON.stringify({
client: `carrega-${__VU}`,
linies: [{ producte, unitats: 1 + Math.floor(Math.random() * 3) }],
});
const res = http.post(`${BASE}/comandes`, cos, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${TOKEN}`,
'X-Request-Id': `k6-${__VU}-${__ITER}`, // Kong el propaga: traçable a Tempo
},
tags: { operacio: 'crear_comanda' },
});
const ok = check(res, {
'comanda 201': (r) => r.status === 201,
'retorna id': (r) => r.status === 201 && r.json('comanda_id') !== undefined,
});
errors.add(!ok);
duradaComanda.add(res.timings.duration);
sleep(1 + Math.random() * 2); // temps de "pensar" de l'usuari
}Com llegir el guió:
stagesreprodueix la forma del trànsit, no només el seu volum. El pic arriba després d'una rampa, com passa quan s'envia el butlletí de la campanya, i hi ha 20 minuts d'altiplà perquè els problemes de memòria, connexions esgotades o lag de Kafka apareixen amb el temps, no al primer minut.thresholdstradueix l'SLO a condicions executables. Que el llindar tingui els mateixos números quealertes.ymlde 07-01 no és casualitat: si la prova passa a staging però l'alerta salta a producció, la diferència és a l'entorn, no al criteri.- L'
X-Request-Idamb prefixk6-permet filtrar a Loki i Tempo (07-02) les traces de la prova, i també excloure-les de les mètriques de negoci si la prova s'executa contra producció. - Es barregen lectures de catàleg i escriptures de comandes en una proporció realista. Una prova que només crea comandes sobrecarrega
km0_comandesperò deixa Redis icatalegfreds, i no descobriria que la memòria cau s'invalida massa sovint. - Durant l'altiplà s'observen a més les mètriques del costat del servidor: l'HPA de 07-05 ha d'escalar
comandes, el lag del consumidor d'analiticano ha de créixer sense límit ikm0_circuit_estatha de romandre en tancat. La prova de càrrega és també una prova de l'automatització.
Locust és l'alternativa en Python, amb la mateixa filosofia (usuaris virtuals amb comportament programat) i una interfície web per seguir la prova en directe; la tria entre tots dos és de preferència de llenguatge.
- Proves de resiliència i proves en producció
Proves de resiliència
Els patrons de 07-04 tenen una propietat incòmoda: només actuen quan alguna cosa va malament, així que en el camí feliç mai no s'executen. Una suite que no injecta fallades pot tenir el 100 % de cobertura de línies i no haver exercitat mai l'obertura d'un circuit breaker. Les proves de resiliència injecten tres famílies de fallada:
| Fallada injectada | Eina típica | Què s'ha d'observar (07-04) |
|---|---|---|
Latència (p. ex. +800 ms a inventari) |
Toxiproxy, tc netem |
amb_timeout talla l'espera; el PressupostReintents no s'esgota; el p99 puja però no explota |
Errors (5xx, UNAVAILABLE) |
Doble del servei, Toxiproxy reset_peer |
reintentar aplica backoff amb jitter; després de N fallades el CircuitBreaker obre (km0_circuit_estat=2) i km0_circuit_rebutjos_total creix |
| Partició (tall total entre dos serveis) | Toxiproxy timeout, Chaos Mesh NetworkChaos |
El fallback respon amb dades de la memòria cau (km0_fallback_total); la saga entra en COMPENSANT si correspon; res no es queda penjat |
L'apartat 9 recull una d'aquestes proves amb codi.
Proves en producció
Per molt fidel que sigui staging, hi ha coses que només existeixen a producció: les dades reals, el trànsit real i les integracions reals (la passarel·la de pagaments, la flota de repartiment). "Provar en producció" no vol dir saltar-se les proves anteriors, sinó afegir tècniques que limiten el dany d'allò que encara no se sap:
- Canary (07-05): la versió nova rep un 5 % del trànsit i es compara automàticament contra l'SLO. És una prova en producció amb rollback automàtic.
- Feature flags: la funcionalitat es desplega apagada i s'encén per segments (primer per a
jsalaimpuig, després per al mercat de Lleida, després per a tothom). Separa el desplegament del llançament i permet apagar sense redesplegar. - Shadow traffic (trànsit ombra): Kong duplica les peticions reals cap a la versió nova, la resposta de la qual es descarta però es mesura. Permet veure com es comporta
comandes 1.16.0amb el trànsit d'un dissabte sense que cap client ho pateixi. Exigeix cura amb els efectes secundaris: el trànsit ombra no ha de cobrar ni reservar estoc de debò. - Synthetic monitoring (07-01): les sondes blackbox executen contínuament un flux de compra de prova amb un client fictici. Són la prova d'extrem a extrem que mai no deixa d'executar-se.
- Simulació determinista i verificació formal: visió general
Hi ha una família de tècniques que ataquen directament el no-determinisme de l'apartat 1. Convé conèixer-les encara que a Quilòmetre Zero no s'apliquin de manera exhaustiva:
- Jepsen sotmet bases de dades i sistemes de coordinació (Cassandra, etcd, Kafka, PostgreSQL amb Patroni...) a particions, rellotges desviats i caigudes, mentre un verificador comprova si l'historial d'operacions observat respecta les garanties promeses (linealitzabilitat, aïllament de transaccions). Els seus informes públics són la millor lectura per entendre què volen dir de debò les promeses de consistència de 03-01, i bona part de les "sorpreses" que documenten són el que un hauria d'esperar de la tecnologia que triï.
- TLA+ és un llenguatge d'especificació amb què es descriu l'algorisme (per exemple, la saga de 03-05 amb els seus estats i compensacions) i un comprovador de models explora tots els entrellaçats possibles buscant estats que violin un invariant ("mai no hi ha una comanda COMPLETADA sense PAGAMENT_CONFIRMAT"). Troba errors de disseny que cap prova no trobaria, perquè no depenen que l'execució concreta passi pel cas dolent.
- Simulació determinista (l'enfocament de FoundationDB): el sistema sencer s'executa en un únic fil amb un planificador i una xarxa simulats que es controlen amb una llavor. Les fallades s'injecten sistemàticament i, quan alguna cosa es trenca, la mateixa llavor reprodueix la fallada exactament. És la resposta més radical al problema del flaky test: si és reproduïble, deixa de ser flaky.
Aquestes tècniques són costoses i exigeixen que el sistema s'hagi dissenyat per a elles. Per a la majoria de plataformes, inclosa Quilòmetre Zero, el camí realista és combinar proves d'integració i contracte, càrrega amb llindars d'SLO, i la disciplina de l'apartat següent.
- Enginyeria del caos: principis, eines i cicle d'un experiment
L'enginyeria del caos és la pràctica de provocar fallades a propòsit en un sistema per descobrir-ne les febleses abans que les descobreixi un incident. No és "trencar coses": és experimentació amb mètode científic. Els seus principis:
- Definir l'estat estable en termes de mètriques de negoci i d'SLO, no de recursos interns: "es creen 40 comandes/min amb un 99,9 % d'èxit", no "la CPU és al 40 %".
- Formular una hipòtesi sobre què passarà: "si matem el líder de Patroni, l'estat estable es manté llevat d'un sotrac d'escriptures de menys de 30 s".
- Variar esdeveniments del món real: les fallades que s'injecten són les que passen (caiguda de node, latència, partició, disc ple, rellotge desviat), no fallades exòtiques.
- Executar en producció, o tan a prop com sigui possible, perquè és l'únic entorn el comportament del qual importa; però amb radi d'explosió controlat: un pod, un mercat, un 5 % del trànsit, amb botó d'aturada i en horari amb l'equip present.
- Automatitzar els experiments que ja s'han passat un cop, perquè continuïn passant quan la plataforma canviï. Un experiment que només es va executar a mà al març no diu res sobre la versió de setembre.
Eines
| Eina | On actua | Què injecta | Ús a Quilòmetre Zero |
|---|---|---|---|
| Chaos Mesh / Litmus | Kubernetes (CRD) | Mort de pods, NetworkChaos (latència, pèrdua, partició), IOChaos, StressChaos, TimeChaos |
Experiments declaratius sobre k8s/, programables com a cron |
| Toxiproxy | Proxy TCP entre dos serveis | Latència, jitter, límit d'amplada de banda, talls, reset de connexió | Proves de resiliència reproduïbles en local i a CI |
tc netem |
Kernel Linux (una màquina) | Latència, pèrdua, reordenació, corrupció de paquets | Nodes de Kafka/Patroni fora de Kubernetes, via Ansible |
| Scripts propis | Qualsevol | kill -9, omplir disc (fallocate), desviar el rellotge |
Casos que les eines no cobreixen |
Cicle d'un experiment
flowchart TD
A[Definir estat estable<br/>SLO i mètriques de negoci] --> B[Formular hipòtesi<br/>què esperem que passi]
B --> C[Acotar radi d'explosió<br/>un pod, un mercat, 5 % trànsit]
C --> D[Executar injecció<br/>Chaos Mesh / Toxiproxy / tc]
D --> E{Es manté<br/>l'estat estable?}
E -- Sí --> F[Hipòtesi confirmada<br/>automatitzar i ampliar radi]
E -- No --> G[Avortar: aturar injecció<br/>i restaurar]
G --> H[Analitzar amb logs i traces<br/>07-02 · corregir disseny 07-03/07-04]
H --> B
F --> I[Registrar resultat<br/>i experiment següent]
La fletxa de G a H és on hi ha el valor: cada hipòtesi refutada és un incident que no passarà un dissabte. I el botó d'avortar no és opcional: abans d'executar cal saber exactament com s'atura la injecció i quant triga el sistema a tornar a l'estat estable.
Game days
Un game day és una sessió planificada (mig dia, equip complet, amb en Jordi i la Marta de guàrdia simulada) en què s'executen diversos experiments seguits i es practica també la part humana: ha saltat l'alerta correcta de 07-01? El runbook de 07-03 ha servit? Quant s'ha trigat a localitzar la causa amb les traces de 07-02? El resultat és un postmortem sense víctimes.
- Experiments de caos a Quilòmetre Zero
La taula següent és el pla del primer game day de la plataforma. Cada fila connecta una fallada real amb el mecanisme de 07-03 o 07-04 que l'hauria d'absorbir.
| Experiment | Injecció | Hipòtesi | Mètriques a observar | Resultat esperat gràcies a... |
|---|---|---|---|---|
Matar el líder de Patroni de km0_inventari |
Chaos Mesh PodChaos sobre el pod líder del statefulset |
Failover automàtic; les escriptures d'inventari fallen < 30 s; cap reserva no es perd ni es duplica |
patroni_master, errors 5xx d'inventari, km0_estoc_unitats{producte} abans/després, latència de ReservarEstoc |
Patroni + etcd amb fencing (07-03); reintents amb backoff a comandes (07-04) |
Partició entre comandes i inventari |
Toxiproxy timeout / NetworkChaos partition |
El circuit breaker obre en < 10 s; les comandes noves fallen ràpid amb un missatge clar; les sagues en curs compensen; en restaurar, semiobert → tancat | km0_circuit_estat, km0_circuit_rebutjos_total, km0_fallback_total, estats a la taula sagas, p99 de /api/v1/comandes |
CircuitBreaker i amb_timeout (07-04); saga COMPENSANT → CANCELLADA (03-05) |
| Latència de 300 ms a Redis | Toxiproxy latency sobre el proxy de Redis |
El catàleg es degrada (el p99 puja) però no falla; la memòria cau se salta per timeout i es llegeix de cataleg |
km0_peticio_durada_segons{servei="cataleg"}, taxa d'encerts de la memòria cau, km0_fallback_total{operacio="redis"} |
Fallback i degradació controlada (07-04) |
| Omplir el disc d'un broker de Kafka | fallocate al volum del broker 2 via Ansible |
El broker surt de l'ISR; els productors continuen escrivint als altres dos amb acks=all; cap esdeveniment de comandes.esdeveniments no es perd; l'alerta de disc salta |
kafka_under_replicated_partitions, kafka_isr_shrinks_total, lag d'analitica, alerta KafkaDiscPle |
ISR i factor de replicació 3 (07-03); alertes de 07-01 |
Rellotge desviat +5 min en un pod de comandes |
Chaos Mesh TimeChaos |
Els JWT de Keycloak es rebutgen com a "encara no vàlids" en aquest pod; el /salut/preparat el treu del balanceig |
Errors 401 per pod, kube_pod_status_ready |
Sondes de 07-03 i 07-05; validació de tokens de 06-01 |
Partició entre comandes i inventari amb Toxiproxy
Toxiproxy es col·loca entre comandes i el port gRPC d'inventari; comandes es configura perquè apunti al proxy (inventari-proxy:50051) en lloc del servei directe. Des de la prova es manipula el proxy mitjançant la seva API HTTP i s'observen les mètriques de Prometheus que comandes exposa a /metrics (07-01).
# km0/tests/caos/particio_inventari.py
"""Experiment: partició entre comandes i inventari.
Hipòtesi: amb inventari inabastable, el circuit breaker de comandes obre
en menys de 10 s, les peticions fallen ràpid (< 100 ms) i, en restaurar
la xarxa, el circuit torna a tancat en menys de 60 s sense intervenció.
"""
import time
import requests
from toxiproxy import Toxiproxy
from prometheus_client.parser import text_string_to_metric_families
TOXIPROXY = Toxiproxy(server_host="toxiproxy", server_port=8474)
COMANDES_METRICS = "http://comandes:8000/metrics"
COMANDES_API = "http://comandes:8000/api/v1/comandes"
CIRCUIT = "inventari" # etiqueta `dependencia` de km0_circuit_estat
def metrica(nom: str, etiquetes: dict) -> float:
"""Llegeix un valor concret de l'endpoint /metrics de comandes."""
text = requests.get(COMANDES_METRICS, timeout=2).text
for familia in text_string_to_metric_families(text):
if familia.name == nom:
for mostra in familia.samples:
if all(mostra.labels.get(k) == v for k, v in etiquetes.items()):
return mostra.value
raise KeyError(f"{nom}{etiquetes} no trobada")
def esperar_fins(condicio, timeout_s: float, cada_s: float = 0.5) -> float:
"""Retorna els segons que ha trigat a complir-se la condició o llança AssertionError."""
inici = time.monotonic()
while time.monotonic() - inici < timeout_s:
if condicio():
return time.monotonic() - inici
time.sleep(cada_s)
raise AssertionError(f"la condició no s'ha complert en {timeout_s} s")
def crear_comanda_de_prova() -> tuple[int, float]:
inici = time.monotonic()
r = requests.post(COMANDES_API, json={
"client": "caos", "linies": [{"producte": "carbasso", "unitats": 1}]
}, headers={"X-Request-Id": "caos-particio"}, timeout=5)
return r.status_code, (time.monotonic() - inici) * 1000
def test_particio_inventari():
proxy = TOXIPROXY.get_proxy("inventari")
# --- Estat estable: circuit tancat i una comanda de control funciona
assert metrica("km0_circuit_estat", {"dependencia": CIRCUIT}) == 0
estat, _ = crear_comanda_de_prova()
assert estat == 201
rebutjos_abans = metrica("km0_circuit_rebutjos_total", {"dependencia": CIRCUIT})
# --- Fase 1: latència de 800 ms (per sobre del timeout de 300 ms de 07-04)
proxy.add_toxic(name="lent", type="latency", attributes={"latency": 800, "jitter": 100})
try:
for _ in range(5):
crear_comanda_de_prova() # han d'esgotar el timeout, no penjar-se
# --- Fase 2: tall total (partició)
proxy.add_toxic(name="tall", type="timeout", attributes={"timeout": 0})
# Hipòtesi 1: el circuit obre en < 10 s
t_obertura = esperar_fins(
lambda: metrica("km0_circuit_estat", {"dependencia": CIRCUIT}) == 2,
timeout_s=10,
)
print(f"circuit obert en {t_obertura:.1f} s")
# Hipòtesi 2: amb el circuit obert es falla ràpid
estat, ms = crear_comanda_de_prova()
assert estat == 503, "amb inventari caigut s'espera un 503 explícit"
assert ms < 100, f"fallada lenta ({ms:.0f} ms): el circuit no està curtcircuitant"
assert metrica("km0_circuit_rebutjos_total", {"dependencia": CIRCUIT}) > rebutjos_abans
finally:
# Botó d'avortar: sempre es restaura la xarxa, passi el que passi
proxy.destroy_toxics()
# Hipòtesi 3: recuperació automàtica (semiobert -> tancat) en < 60 s
t_tancament = esperar_fins(
lambda: metrica("km0_circuit_estat", {"dependencia": CIRCUIT}) == 0,
timeout_s=60,
)
print(f"circuit tancat de nou en {t_tancament:.1f} s")
estat, _ = crear_comanda_de_prova()
assert estat == 201Punts clau:
- L'experiment comença comprovant l'estat estable (circuit tancat, una comanda de control funciona). Si el sistema ja estava malament abans d'injectar res, el resultat no diria res.
- S'injecta en dues fases perquè són fallades diferents de 07-04: la latència exercita
amb_timeouti el pressupost de reintents; el tall exercita el circuit breaker. Un tall net és en realitat el cas fàcil; la latència "gairebé acceptable" és la que esgota fils. - Les hipòtesis són numèriques i observables des de fora (
km0_circuit_estat, temps de resposta), les mateixes mètriques que veu Grafana. Si la prova passa, el panell de 07-01 mostrarà exactament aquesta seqüència durant un incident real. - El
finallyambdestroy_toxics()és el botó d'avortar. Sense ell, una fallada en una aserció deixaria la partició activa. - El bloc final verifica el que més s'oblida: que el sistema torna sol. Un circuit breaker que obre bé però necessita un reinici per tancar converteix cada fallada transitòria en un incident.
El mateix experiment com a manifest de Chaos Mesh
Un cop validat amb Toxiproxy, l'experiment es declara a Kubernetes per executar-lo periòdicament contra l'entorn real:
# km0/k8s/caos/particio-comandes-inventari.yaml
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: particio-comandes-inventari
namespace: km0-prod
labels:
experiment: "07-06"
spec:
action: partition # tall bidireccional; altres: delay, loss, duplicate, corrupt
mode: one # radi d'explosió: UN sol pod de comandes, no tots
selector:
namespaces: [km0]
labelSelectors:
app: comandes
direction: both
target:
mode: all
selector:
namespaces: [km0]
labelSelectors:
app: inventari
duration: "90s" # es restaura sol en expirar: avortament automàticmode: onelimita el dany a un únic pod decomandes: la resta continua atenent, així que l'SLO global amb prou feines se'n ressent encara que la hipòtesi falli. Ampliar el radi (mode: fixed-percental 50 %) és el pas següent, només després que l'experiment petit hagi passat diverses vegades.durationés l'avortament automàtic: passi el que passi, als 90 segons la partició desapareix. Per a ungame days'hi afegeix a més l'anotació de pausa (experiment.chaos-mesh.org/pause: "true") com a aturada manual.- El manifest es versiona a
k8s/caos/i s'aplica des del pipeline GitOps de 07-05 amb unSchedulede Chaos Mesh, per exemple cada dimarts a les 11:00, en horari d'oficina. Les hipòtesis es comproven amb la mateixa classe d'asercions del guió anterior, ara executades contra Prometheus.
Errors Comuns i Consells
- Simular la infraestructura en lloc de fer-la servir. Un mock de PostgreSQL o de Kafka valida la lògica del servei contra una idea de com es comporta la infraestructura, no contra la infraestructura. Amb Testcontainers el cost és d'uns segons per sessió; el benefici és detectar el
deadlock, la migració oblidada o l'esdeveniment mal serialitzat. - Tractar els tests intermitents com a soroll. Un test que falla una de cada cinquanta vegades en un sistema distribuït sol estar mostrant una carrera real. Reintentar-lo automàticament fins que passi amaga el problema; el correcte és capturar els logs i les traces de l'execució fallida (07-02) i reproduir-la.
- Llindars de càrrega sense relació amb l'SLO. "La prova de càrrega ha passat" no vol dir res si el llindar era p95 < 2 s i l'SLO és p99 < 500 ms. Els llindars de k6 i els d'
alertes.ymlhan de sortir del mateix document. - Provar només el tall net. Un servei caigut del tot és la fallada més fàcil de gestionar: es detecta ràpid. La latència alta però per sota del timeout, o els errors al 10 % de les peticions, són els que esgoten fils i pressupostos. Injectar sempre les tres famílies: latència, errors i partició.
- Caos sense hipòtesi ni botó d'avortar. "Matarem un node a veure què passa" no és un experiment, és un incident autoinfligit. Sense estat estable definit no es pot saber si el resultat és bo; sense avortament, no es pot aturar quan és dolent.
- Executar l'experiment una sola vegada. Cada versió nova de
comandeso cada canvi aresiliencia.pypot desfer el que es va verificar. Els experiments que ja passen s'automatitzen (Chaos MeshSchedule, etapa del pipeline) perquè continuïn passant. - Contractes escrits pel productor. Si
inventaridocumenta la seva API però ningú no verifica què fa servir realmentcomandes, un camp "que ningú no usa" pot eliminar-se i trencar la saga. El contracte el declara el consumidor i el verifica el productor. - Confondre el radi d'explosió amb la durada. Un experiment curt sobre tots els pods pot fer més mal que un de llarg sobre un pod. El radi s'acota primer per abast (un pod, un mercat, un percentatge de trànsit) i després per temps.
Exercicis
Exercici 1: prova d'integració de la idempotència
A 02-05 es va dissenyar el consumidor idempotent d'analitica amb la taula missatges_processats. Escriu, amb la mateixa estructura de fixtures que tests/test_saga_comanda.py, una prova d'integració que publiqui dues vegades el mateix esdeveniment ComandaCompletada (mateix id_esdeveniment) a comandes.esdeveniments i verifiqui que km0_analitica comptabilitza una sola venda. Indica què es fa servir real i què se substitueix per un doble, i per què.
Exercici 2: llindars i perfil de càrrega per a la Setmana del Formatge Artesà
La Setmana del Formatge Artesà multiplica el trànsit per quatre (no per vuit) però concentra les compres en dues hores a la tarda, amb un pic molt abrupte. Adapta tests/carrega/verema.js: modifica stages per representar aquest perfil, ajusta la barreja de productes i afegeix un llindar perquè la prova falli si l'HPA de 07-05 no ha escalat comandes a almenys 4 rèpliques durant l'altiplà (pista: k6 pot consultar un endpoint HTTP; suposa que existeix GET /internal/repliques a staging).
Exercici 3: dissenyar un experiment de caos
Dissenya, en el format de la taula de l'apartat 9, un experiment per a la fallada real següent: el consumidor de repartiment.posicions a repartiment es queda encallat (el procés viu però no consumeix missatges, per exemple per un bloqueig intern). Defineix estat estable, injecció (amb quina eina?), hipòtesi, mètriques a observar i quin mecanisme de 07-01 a 07-05 l'hauria d'absorbir. Indica també el radi d'explosió i el botó d'avortar.
Solucions
Solució 1. Es fan servir reals PostgreSQL (la taula missatges_processats i la de vendes són el nucli del que es prova) i Kafka (el relliurament és precisament el que cal reproduir); se substitueix per un doble qualsevol servei extern que analitica cridi (cap, en aquest cas). Esquelet:
def test_esdeveniment_duplicat_es_compta_un_cop(connexio, kafka):
productor = KafkaProducer(bootstrap_servers=kafka.get_bootstrap_server())
esdeveniment = embolcallar(tipus="ComandaCompletada", origen="comandes",
dades={"comanda_id": "P-2026-000126", "import_eur": 42.0})
for _ in range(2): # mateix id_esdeveniment les dues vegades
productor.send("comandes.esdeveniments", json.dumps(esdeveniment).encode())
productor.flush()
consumidor = ConsumidorAnalitica(connexio, bootstrap=kafka.get_bootstrap_server())
consumidor.processar_fins_buit(timeout_s=5)
vendes = connexio.execute("SELECT count(*) FROM vendes WHERE comanda_id = %s",
("P-2026-000126",)).fetchone()[0]
processats = connexio.execute("SELECT count(*) FROM missatges_processats WHERE id_esdeveniment = %s",
(esdeveniment["id_esdeveniment"],)).fetchone()[0]
assert vendes == 1 and processats == 1L'aserció sobre missatges_processats comprova el mecanisme, i la de vendes l'efecte de negoci; totes dues fan falta, perquè un consumidor podria inserir a missatges_processats i tot i així duplicar la venda si no ho fa a la mateixa transacció.
Solució 2. Perfil amb pic abrupte i altiplà curt, llindar de rèpliques comprovat dins de la iteració amb una mètrica Gauge:
import { Gauge } from 'k6/metrics';
const repliques = new Gauge('km0_comandes_repliques');
export const options = {
stages: [
{ duration: '2m', target: 50 },
{ duration: '1m', target: 200 }, // pic abrupte: x4 en un minut
{ duration: '10m', target: 200 },
{ duration: '2m', target: 0 },
],
thresholds: {
'km0_comandes_error_rate': ['rate<0.001'],
'km0_crear_comanda_ms': ['p(99)<500'],
'km0_comandes_repliques': ['value>=4'], // s'avalua sobre l'últim valor
},
};
const productes = ['formatge-curat', 'formatge-curat', 'formatge-fresc', 'formatge-fresc', 'tomaquet-rosa'];
// Dins de default(), una de cada 50 iteracions:
// if (__ITER % 50 === 0) repliques.add(http.get(`${BASE}/internal/repliques`).json('comandes'));La rampa d'un minut és la part interessant: l'HPA de 07-05 reacciona amb retard (finestra d'estabilització) i és probable que el p99 s'incompleixi durant els primers dos minuts del pic. Això no és una fallada de la prova, és una troballa: o es preescalfa comandes abans de la campanya (rèpliques mínimes més altes aquell dia), o s'ajusta l'HPA.
Solució 3. Estat estable: repartiment.panell rep posicions actualitzades de furgoneta-3 cada 10 s i el lag del grup de consumidors de repartiment.posicions és < 50 missatges. Injecció: Chaos Mesh StressChaos no serveix (el procés no està saturat, està bloquejat); la manera més fidel és un feature flag o variable d'entorn de prova que fa que el consumidor deixi de fer poll sense morir, o kill -STOP al procés (el congela sense tancar la connexió). Radi: un sol pod del consumidor, mode: one. Hipòtesi: el pod continua passant /salut/viu (el procés viu) però ha de fallar /salut/preparat en menys de 30 s perquè la sonda de preparat comprova l'antiguitat de l'últim missatge consumit (07-03); Kubernetes el reinicia o el treu del grup, Kafka reassigna la partició (rebalance) i el lag torna a baixar; l'alerta de lag de 07-01 salta si dura més de dos minuts. Mètriques: kafka_consumergroup_lag{group="repartiment"}, kube_pod_status_ready, antiguitat de l'última posició a repartiment.panell. Avortar: kill -CONT o eliminar el flag; amb duration de Chaos Mesh si es fa servir PodChaos. Si la hipòtesi falla (el pod continua "preparat" mentre no consumeix), la correcció és de 07-03: la sonda de preparat ha de mesurar progrés, no només que el procés respongui.
Conclusió
Aquesta lliçó ha completat la resposta a la pregunta amb què va acabar 07-05. La confiança que la versió 1.15.0 funciona no ve d'una sola prova, sinó d'una piràmide adaptada a allò distribuït: unitàries per a la lògica, integració amb dependències reals en contenidors per a la saga i l'outbox, contractes perquè comandes i inventari continuïn entenent-se sense desplegar-los junts, càrrega amb llindars copiats de l'SLO per a la Setmana de la Verema, resiliència amb fallades injectades perquè els patrons de 07-04 s'executin alguna vegada abans de producció, i tècniques de producció (canary, feature flags, trànsit ombra, sondes sintètiques) per a allò que només existeix allà. Per sobre de tot això, l'enginyeria del caos converteix les fallades de 07-03 en experiments amb hipòtesi, radi d'explosió acotat i botó d'avortar, i els game days entrenen també les persones.
Amb ella es tanca el Mòdul 7, el fil del qual ha estat una sola pregunta desdoblada en cinc: com sabem que la plataforma funciona, i què fem quan no.
| Capa | Pregunta que respon | Mecanismes principals | Lliçó |
|---|---|---|---|
| Observabilitat | Què està passant ara i es compleix l'SLO? | Mètriques Prometheus, alertes per burn rate, sondes sintètiques, Grafana; logs JSON i traces OpenTelemetry amb trace_id |
07-01, 07-02 |
| Recuperació | Quan alguna cosa cau, com es detecta i es torna? | Health checks, DetectorPhi, failover amb fencing (Patroni, ISR), checkpoints, backups PITR, RPO/RTO, runbooks i postmortems |
07-03 |
| Resiliència | Com evitem que una fallada parcial es converteixi en total? | Timeouts, reintents amb pressupost, CircuitBreaker, Bulkhead, fallback i degradació, load shedding |
07-04 |
| Automatització | Com es desplega i s'escala sense intervenció manual ni errors humans? | Ansible, imatges immutables, Kubernetes amb sondes i HPA, canary contra SLO, service mesh, GitOps | 07-05 |
| Proves i caos | Com demostrem tot l'anterior abans que passi? | Testcontainers, contractes, k6 amb llindars d'SLO, Toxiproxy, Chaos Mesh, game days | 07-06 |
Les cinc capes es recolzen entre si: les proves de càrrega s'avaluen amb les mètriques de 07-01, els experiments de caos verifiquen els mecanismes de 07-03 i 07-04, i l'automatització de 07-05 és el que permet que tot això s'executi a cada versió i no només un cop.
Amb aquest mòdul, Quilòmetre Zero ja disposa de totes les peces: comunicació, consistència, emmagatzematge, computació, seguretat i operació. Fins ara cada lliçó ha mirat una peça; el Mòdul 8 canvia l'escala i mira la plataforma sencera com a arquitectura, a través de casos d'estudi. La primera pregunta és la que hi ha al fons des del Mòdul 1, quan el monòlit Python+PostgreSQL es va partir en cataleg, comandes, inventari, pagaments, repartiment i analitica: què fa que una arquitectura de microserveis funcioni com a sistema i no com una col·lecció de serveis, on es tallen les fronteres, com es comparteixen (o no) les dades i quin preu es paga per cada decisió. Amb això comença l'últim mòdul.
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
