La lliçó anterior va acabar amb un coordinador de quaranta línies que repartia trossos de comandes.jsonl entre processos, sumava parcials i tornava a encuar la tasca d'un treballador mort. Funcionava en un portàtil; en un clúster de mil nodes caldria resoldre, per a cada treball, la localitat de dades, el shuffle entre nodes, la detecció de fallades, l'execució especulativa i la sortida atòmica. MapReduce és el model de programació amb què Google (Dean i Ghemawat, 2004) va resoldre tot això una sola vegada, de manera que el programador escriu únicament dues funcions, map i reduce, i el sistema s'encarrega de la resta. Hadoop n'és la implementació de codi obert, juntament amb HDFS (04-02) i el gestor de recursos YARN, i durant una dècada va ser sinònim de "big data". Avui ja gairebé ningú no escriu treballs MapReduce a mà, però tot el que va venir després (Spark, Flink, els motors SQL distribuïts) fa servir el seu vocabulari i les seves fases, i les seves decisions de disseny (escriure sempre a disc, tasques independents, un mestre que reassigna) continuen sent la referència contra la qual s'expliquen les millores. Aquesta lliçó presenta el model amb el càlcul de vendes per productor i per mercat de Quilòmetre Zero, recorre l'execució d'un treball dins de YARN, i l'implementa tres vegades: en Python amb Hadoop Streaming, en Java com a exemple canònic, i com a simulació local del shuffle per veure amb les mans el que el framework amaga.
Contingut
- El model de programació: map, shuffle & sort, reduce
- Combiner i partitioner
- Flux d'execució i tolerància a fallades
- Hadoop: HDFS, YARN i MapReduce v2
- Anatomia d'un job: de
submita_SUCCESS - Formats d'entrada i sortida
- Per què MapReduce és lent i quin és el seu lloc avui
- Pràctica: vendes per productor i mercat en Streaming, en Java i en simulació
- Errors Comuns i Consells
- Exercicis
- Conclusió
- El model de programació: map, shuffle & sort, reduce
MapReduce manlleva dos noms de la programació funcional i els dona un significat precís per a dades distribuïdes. Tot treball processa parelles clau/valor i passa per tres fases:
- Map. El sistema divideix l'entrada en trossos (splits) i executa, per a cada registre de cada tros, la funció
map(k1, v1) → llista de (k2, v2). El programador decideix què és la clau intermèdiak2: és la clau per la qual vol agrupar. Per a "vendes per productor",maprep una línia decomandes.jsonli emet, per cada línia de la comanda,(productor, quantitat × preu). - Shuffle & sort. El sistema recull totes les parelles
(k2, v2)de tots els mappers, les envia al reducer responsable de cadak2, i les lliura agrupades per clau i ordenades:(k2, [v2, v2, v2, ...]). És la fase que el programador no escriu i la que més costa (05-01, apartat 5). - Reduce. Per a cada clau,
reduce(k2, llista de v2) → llista de (k3, v3). Per a les vendes, suma la llista i emet(productor, total).
flowchart LR
subgraph Entrada[HDFS: comandes.jsonl]
B1[bloc 1]
B2[bloc 2]
B3[bloc 3]
end
B1 --> M1[map 1<br/>montblanc 12.50<br/>la-vega 7.80<br/>roure-alt 58.80]
B2 --> M2[map 2<br/>montblanc 12.60<br/>la-vega 6.40]
B3 --> M3[map 3<br/>roure-alt 29.40<br/>montblanc 25.00]
M1 --> SH[[shuffle & sort<br/>agrupar per clau]]
M2 --> SH
M3 --> SH
SH --> R1[reduce A<br/>la-vega: 7.80, 6.40 → 14.20<br/>montblanc: 12.50, 12.60, 25.00 → 50.10]
SH --> R2[reduce B<br/>roure-alt: 58.80, 29.40 → 88.20]
R1 --> O1[part-r-00000]
R2 --> O2[part-r-00001]
L'exemple amb què sempre es presenta MapReduce és el comptador de paraules: map emet (paraula, 1) per cada paraula d'una línia i reduce suma els uns. És el mateix esquelet que el nostre canviant "paraula" per "productor" i "1" per "import", i per això es diu que MapReduce és un comptador de paraules generalitzat: qualsevol càlcul que es pugui expressar com "extreure una clau de cada registre i agregar per clau" hi encaixa directament; els que necessiten diverses agrupacions encadenades (vendes per productor i, després, el productor amb més vendes per mercat) s'expressen com a diversos treballs en cadena, cadascun llegint la sortida de l'anterior des d'HDFS, que és l'origen de la lentitud de l'apartat 7.
Tres propietats del model n'expliquen l'èxit:
- Les funcions són locals.
mapveu un registre;reduceveu una clau i els seus valors. Cap de les dues no necessita saber quants nodes hi ha ni on són les dades. El programador escriu lògica de negoci, no distribució. - Les tasques són independents. Cada
mapsobre un split i cadareducesobre una partició de claus es poden executar en qualsevol node, en qualsevol ordre, i repetir: és la reexecució determinista de 05-01. - El shuffle és genèric. Un únic mecanisme (particionar per clau, ordenar, transferir, barrejar) serveix per a qualsevol treball, i el sistema el pot optimitzar per a tots.
- Combiner i partitioner
El model bàsic té dos ganxos que el programador pot substituir:
Combiner. Un mapper que processa un bloc de 128 MB de comandes.jsonl emet unes 400 000 parelles, però només hi ha tres productors diferents: 400 000 parelles viatjaran per la xarxa perquè el reducer sumi 133 000 valors per clau. El combiner és una funció reduce local al mapper que s'executa sobre la sortida de cada map abans del shuffle: agrupa les parelles del mapper per clau i les redueix a una per clau. Amb ell, el mapper envia 3 parelles en lloc de 400 000. És la "prereducció abans de moure" de 05-01. Només és vàlid quan la reducció és associativa i commutativa, i amb el mateix tipus d'entrada i sortida: sumar sí; una mitjana no, tret que es porti (suma, compte). Hadoop no garanteix que el combiner s'executi (pot córrer zero, una o diverses vegades sobre les mateixes dades), així que el resultat ha de ser idèntic amb ell o sense.
Partitioner. Decideix a quin reducer va cada clau: particio(k2) = hash(k2) mod nombre_reducers per defecte. Se substitueix quan es vol controlar el repartiment: enviar totes les claus d'un rang al mateix reducer (per obtenir una sortida globalment ordenada), o separar dos tipus de clau en dos fitxers de sortida. A la pràctica de l'apartat 8 emetem dues famílies de claus en el mateix treball (p:<productor> i m:<mercat>) i un partitioner les envia a reducers diferents, de manera que part-r-00000 contingui les vendes per productor i part-r-00001 les vendes per mercat. I el partitioner és també el lloc on s'ataca el biaix: un partitioner que conegui les claus calentes pot repartir-les entre diversos reducers, amb una segona passada per combinar.
| Component | Qui l'escriu | On s'executa | Per a què |
|---|---|---|---|
map |
Programador | Al node del split (localitat) | Extreure clau i valor de cada registre |
| Combiner | Programador (opcional; sovint el mateix reduce) |
Al node del mapper, sobre la seva sortida | Reduir el volum del shuffle |
| Partitioner | Programador (opcional; hash per defecte) | Al mapper, en escriure la sortida | Decidir quin reducer rep cada clau |
| Shuffle & sort | Framework | Mappers (ordenar i servir) i reducers (recollir i barrejar) | Agrupar per clau |
reduce |
Programador | En qualsevol node amb contenidor lliure | Agregar els valors de cada clau |
- Flux d'execució i tolerància a fallades
L'article original descriu una arquitectura amb un mestre i molts treballadors, que Hadoop conserva amb altres noms (apartat 4):
- El client envia el treball: el codi (
jaro scripts), la configuració i les rutes d'entrada i sortida. - El mestre demana a HDFS els blocs de l'entrada i crea una tasca map per split, anotant en quins nodes viu cada bloc. Crea també R tasques reduce, amb R configurat per l'usuari.
- Assigna tasques map a treballadors lliures, preferint el que té el bloc al seu disc; si no pot, un del mateix rack; si no, qualsevol.
- Cada map escriu la seva sortida, particionada i ordenada, al disc local del treballador, i informa el mestre d'on és.
- Quan tots els maps han acabat, el mestre assigna les tasques reduce; cada reducer recull per xarxa la seva partició de la sortida de tots els maps, la barreja (merge) mantenint l'ordre, i executa
reduceclau a clau. - Cada reducer escriu el seu fitxer de sortida a HDFS. Quan tots acaben, el treball està complet.
La tolerància a fallades es recolza en el que ja sabem:
- Fallada d'un treballador. El mestre la detecta per heartbeats perduts. Les tasques map completades en aquell node es reexecuten encara que haguessin acabat, perquè la seva sortida era al disc local del node caigut i els reducers que encara no l'havien recollit la necessiten. Les tasques reduce completades no es reexecuten: la seva sortida és a HDFS. Les tasques en curs tornen a la cua. Exactament el que feia
cua_treball.py, amb la subtilesa afegida de la sortida intermèdia local. - Fallada d'una tasca (excepció al codi, registre corrupte). Es reintenta fins a quatre vegades (
mapreduce.map.maxattempts); si continua fallant, el treball falla (o, si es configura, es tolera un percentatge de tasques fallides per saltar-se registres enverinats, la DLQ de 02-05 en versió batch). - Ressagats. Execució especulativa (05-01): quan la fase és a prop d'acabar, es llança una còpia de les tasques lentes.
- Sortida atòmica. Cada tasca escriu en un directori temporal (
_temporary/attempt_.../) i només quan la tasca acaba el framework fa el commit: reanomena el seu fitxer apart-r-00001al directori de sortida. Si dos intents de la mateixa tasca (reexecució, especulació) acaben, només el primer fa commit. En acabar el treball es crea un fitxer buit_SUCCESScom a senyal per a qui consumeixi la sortida (el sensor de 05-05 l'esperarà). És l'os.replacede 05-01 institucionalitzat. - El mestre com a punt únic. En el disseny original, si el mestre queia, el treball sencer s'avortava i el client el rellançava: es va acceptar com un compromís raonable perquè un mestre és una màquina entre milers i un treball es pot rellançar. Hadoop 2 ho va millorar creant un mestre per treball (l'ApplicationMaster de l'apartat següent) que YARN pot reiniciar, i el ResourceManager té alta disponibilitat amb ZooKeeper (03-03).
- Hadoop: HDFS, YARN i MapReduce v2
Hadoop és, des de la versió 2, tres projectes apilats:
| Capa | Projecte | Què fa | Lliçó |
|---|---|---|---|
| Emmagatzematge | HDFS | Fitxers en blocs de 128 MB replicats; NameNode amb metadades, DataNodes amb blocs; exposa la localitat de cada bloc | 04-02 |
| Gestió de recursos | YARN (Yet Another Resource Negotiator) | Reparteix CPU i memòria del clúster entre aplicacions en forma de contenidors; independent de MapReduce | Aquesta lliçó |
| Còmput | MapReduce v2 | Una aplicació YARN que implementa el model de l'apartat 1. Spark, Flink o Tez són altres aplicacions YARN | Aquesta lliçó, 05-03 |
A Hadoop 1, el mestre de MapReduce (el JobTracker) feia dues coses alhora: gestionar els recursos del clúster i coordinar cada treball. Això el feia un coll d'ampolla (uns 4 000 nodes de límit) i lligava el clúster a MapReduce: no s'hi podia executar res més. YARN va separar les dues funcions:
- ResourceManager (RM). Un per clúster (amb alta disponibilitat). Coneix els recursos de cada node i arbitra entre aplicacions amb un scheduler (Capacity o Fair Scheduler, amb cues per equip:
analiticaté la seva cua amb el 40 % del clúster garantit). No sap res de maps ni de reduces. - NodeManager (NM). Un per node. Informa l'RM de la seva CPU i memòria disponibles, llança i supervisa contenidors (un procés amb una quota de CPU i memòria, avui implementat amb cgroups) i serveix la sortida intermèdia dels maps als reducers (el shuffle service).
- ApplicationMaster (AM). Un per aplicació (per treball MapReduce), que corre en un contenidor normal. És el mestre de l'apartat 3: negocia contenidors amb l'RM, demana als NM que hi llancin tasques, en segueix el progrés i reexecuta les fallides. Si l'AM mor, l'RM el reinicia (fins a
yarn.resourcemanager.am.max-attemptsvegades) i el nou AM recupera el progrés des del registre de tasques completades. - Contenidor. La unitat d'assignació: "1 nucli i 2 GB al node
dn-07". Cada tasca map o reduce s'executa com una JVM dins d'un contenidor.
sequenceDiagram
participant C as Client (hadoop jar)
participant RM as ResourceManager
participant NM1 as NodeManager dn-01
participant AM as ApplicationMaster
participant NM2 as NodeManager dn-07
C->>RM: submitApplication(jar, conf, splits)
RM->>NM1: llança contenidor per a l'AM
NM1->>AM: arrenca MRAppMaster
AM->>RM: registre; demano 3 contenidors map (preferència: nodes amb els blocs)
RM-->>AM: contenidors assignats (dn-07, dn-12, dn-03)
AM->>NM2: llança tasca map sobre l'split 1
NM2-->>AM: progrés, fi del map (sortida a disc local)
AM->>RM: demano 2 contenidors reduce
RM-->>AM: contenidors
AM->>NM2: llança reduce; recull sortides via shuffle service
NM2-->>AM: reduce completat, commit a HDFS
AM->>RM: aplicació acabada; allibero contenidors
RM-->>C: estat FINISHED / SUCCEEDED
La conseqüència arquitectònica de YARN és que el clúster és un recurs compartit en què conviuen aplicacions diferents (un MapReduce d'analitica, un Spark de l'equip de recomanacions, un servei de llarga durada), i que el model de còmput és intercanviable: quan a 05-03 llancem Spark sobre YARN, el driver de Spark serà l'ApplicationMaster i els executors correran en contenidors, amb el mateix RM arbitrant. Kubernetes juga avui aquest mateix paper de gestor de recursos genèric (07-05).
- Anatomia d'un job: de
submit a _SUCCESS
submit a _SUCCESSVal la pena seguir amb detall el que passa dins d'una tasca, perquè els noms reapareixen a la interfície web de Hadoop, als comptadors del treball i a les explicacions de per què un job és lent.
Costat del map.
- L'
InputFormat(apartat 6) calcula els splits: per defecte, un per bloc HDFS, ajustat al final de línia com feiatrossos_per_bytesa 05-01. Ambcomandes.jsonlde 150 MB, dos splits, dos maps. - El
RecordReaderlliura registres almap: per a text,(offset del byte, línia). mapemet parelles a un buffer circular en memòria (100 MB per defecte,mapreduce.task.io.sort.mb). Quan s'omple al 80 %, un fil el buida a disc (spill): particiona per reducer, ordena per clau dins de cada partició, aplica el combiner si n'hi ha, i escriu un fitxer d'spill.- En acabar el map, els fitxers d'spill es barregen en un de sol, particionat i ordenat, amb un índex que diu on comença cada partició. Si hi va haver diversos spills, el combiner es torna a aplicar en la barreja.
Costat del reduce.
- Còpia (fetch). Tan bon punt acaba un map, cada reducer demana a l'shuffle service del NodeManager d'aquell map la seva partició, per HTTP, amb diversos fils en paral·lel. No espera que acabin tots els maps per començar a copiar (però sí per començar a reduir).
- Barreja i ordenació. Els fragments rebuts, cadascun ja ordenat, es barregen (en memòria si hi caben, a disc en rondes si no) en una única seqüència ordenada per clau.
- Reduce. Es recorre la seqüència; cada vegada que canvia la clau, es crida
reduce(clau, iterador de valors). Per això el reducer rep un iterador, no una llista: els valors d'una clau poden no cabre en memòria (els 133 000 imports de Formatgeria Montblanc sense combiner). - Commit. La sortida va a
_temporary/, i en acabar es reanomena apart-r-0000N. Quan l'AM confirma que tots els reduces han fet commit, escriu_SUCCESS.
Els comptadors del treball resumeixen tot això. Per a un job real d'un dia de campanya (250 000 esdeveniments, 150 MB, 2 maps, 2 reduces), amb combiner i sense:
| Comptador | Significat | Sense combiner | Amb combiner |
|---|---|---|---|
Map input records |
Línies llegides pels maps | 250 000 | 250 000 |
Map output records |
Parelles emeses per map |
810 000 (dues claus per línia de comanda) | 810 000 |
Map output bytes |
Mida d'aquestes parelles | 19,4 MB | 19,4 MB |
Combine input records |
Parelles que van entrar al combiner | 0 | 810 000 |
Combine output records |
Parelles que en van sortir | 0 | 14 (7 claus × 2 maps) |
Reduce shuffle bytes |
Bytes copiats per xarxa als reducers | 21,1 MB | 612 B |
Reduce input groups |
Claus diferents que van arribar a reduce |
7 | 7 |
Reduce input records |
Valors que va recórrer reduce |
810 000 | 14 |
Spilled records |
Parelles escrites a disc en spills (map + reduce) | 1 620 000 | 810 014 |
GC time elapsed (ms) |
Temps en recollida d'escombraries de les JVM | 4 100 | 900 |
CPU time spent (ms) |
CPU total de totes les tasques | 38 000 | 29 000 |
| Durada del job | 71 s | 52 s |
Dues lectures: els bytes de shuffle baixen quatre ordres de magnitud amb el combiner, i tot i així el treball triga 52 s per al que vendes_scatter_gather.py feia en 1 s. Aquest minut és el cost fix del framework: arrencar l'AM, demanar contenidors, llançar quatre JVM, escriure spills i barrejar, fer commit a HDFS. Un treball MapReduce no té sentit per sota dels gigabytes, i aquest és el punt que Spark ataca.
- Formats d'entrada i sortida
L'InputFormat decideix dues coses: com es divideix l'entrada en splits i com es llegeixen els registres de cada split. L'OutputFormat, com s'escriuen els resultats.
| Format | Split | Registre | Ús |
|---|---|---|---|
TextInputFormat (per defecte) |
Per bloc, ajustat a línia | (offset, línia) |
JSONL, CSV, logs: el nostre comandes.jsonl |
KeyValueTextInputFormat |
Per bloc | (text fins al tabulador, resta) |
Sortida d'un altre job de Streaming |
NLineInputFormat |
Cada N línies | (offset, línia) |
Quan cada línia és costosa (una URL a descarregar) |
SequenceFileInputFormat |
Per bloc (marques de sincronització) | (clau, valor) binaris |
Sortida intermèdia entre jobs encadenats |
| Avro, Parquet, ORC (llibreries) | Per bloc de fitxer | Registres amb esquema; Parquet i ORC són columnars | Llacs de dades moderns; Spark els prefereix (05-03) |
CombineFileInputFormat |
Agrupa molts fitxers petits en un split | Segons el format intern | Els fitxers per hora de /km0/clics/ |
Dos advertiments pràctics. Primer: un format és divisible només si es pot començar a llegir des de la meitat, i això depèn també de la compressió: gzip no és divisible (un fitxer d'1 GB en gzip és un únic split i un únic map, per gran que sigui), mentre que bzip2, LZO amb índex, o els formats de contenidor (Avro, Parquet, ORC, SequenceFile) sí que ho són. Segon: HDFS i MapReduce pateixen amb els fitxers petits (04-02): 10 000 fitxers de 50 KB són 10 000 maps d'un instant cadascun, i el cost de planificació domina; cal consolidar-los (CombineFileInputFormat, o millor, un pas previ de compactació).
La sortida segueix la mateixa lògica: TextOutputFormat escriu clau<TAB>valor per línia en un part-r-NNNNN per reducer; amb LazyOutputFormat no es creen fitxers buits; MultipleOutputs permet que un reducer escrigui en diversos fitxers amb nom (vendes per productor a productors-r-00000, per mercat a mercats-r-00000). El nombre de fitxers de sortida és sempre el nombre de reducers, i aquesta és la raó de triar-lo amb cura: pocs reducers fan fitxers grans i tasques llargues; molts, fitxers petits que seran un problema per al treball següent.
- Per què MapReduce és lent i quin és el seu lloc avui
Les decisions que van fer robust MapReduce són les que el fan lent:
- Tot passa per disc. La sortida de cada map s'escriu a disc local; el reducer la copia i la torna a escriure a disc en barrejar-la; la sortida del reduce va a HDFS amb tres rèpliques. Un treball amb un shuffle són com a mínim quatre escriptures del volum de dades intermedi. Es va fer així perquè qualsevol tasca es pogués reexecutar llegint la seva entrada de disc sense dependre de la memòria d'un node que pot morir.
- Els treballs s'encadenen per HDFS. Un càlcul amb diverses agrupacions (vendes per productor, després rànquing per mercat, després unir amb el catàleg) són tres jobs, i entre l'un i l'altre la sortida completa va a HDFS amb replicació i el següent la torna a llegir. Els algorismes iteratius (recomanacions per descens de gradient, PageRank, el BSP de 05-01) són deu o cent jobs encadenats, cadascun rellegint el conjunt sencer.
- Cost fix per treball i per tasca. Arrencar una JVM per tasca, negociar contenidors, el commit. Desenes de segons que en un treball de tres hores no importen i en una consulta interactiva ho són tot.
- Model rígid. Només map i reduce; una unió (join) entre dos conjunts cal expressar-la emetent tots dos costats amb la mateixa clau i distingint-los al reducer (reduce-side join), o carregant el petit en memòria de cada mapper (map-side join, l'antecedent del broadcast join de 05-03). Res d'això no ho fa el framework per tu.
El seu lloc avui és el de base històrica i model mental: el vocabulari (map, shuffle, reduce, combiner, partitioner, split, comptadors) és el que fan servir Spark i Flink; YARN i HDFS continuen en producció a moltes empreses com a capa de recursos i d'emmagatzematge, amb Spark a sobre; i Hive, el magatzem de dades SQL sobre Hadoop que Facebook va crear per no escriure MapReduce a mà, continua existint però executant les seves consultes sobre Tez o Spark en lloc de sobre MapReduce, conservant el seu catàleg de taules (el metastore) com a peça central de molts llacs de dades. Veure un job MapReduce nou el 2026 és estrany; entendre com funcionava és el que permet llegir un pla de Spark o el panell de Flink sense sorpreses.
- Pràctica: vendes per productor i mercat en Streaming, en Java i en simulació
El treball és el mateix en les tres versions: llegir /km0/esdeveniments/2026-09-14/comandes.jsonl (el fitxer que pujar_esdeveniments_hdfs.py va deixar a HDFS a 04-02, o el generat per vendes_scatter_gather.py a 05-01), i produir les vendes totals per productor i per mercat. Per fer-ho en un sol job, el mapper emet dues claus per línia de comanda, amb prefix: p:formatgeria-montblanc i m:girona. Un partitioner envia les p: al reducer 0 i les m: al reducer 1, així que la sortida queda en dos fitxers nets.
8.1 Hadoop Streaming en Python
Hadoop Streaming permet escriure mapper i reducer en qualsevol llenguatge que llegeixi de l'entrada estàndard i escrigui a la sortida estàndard. Hadoop llança el procés, li passa els registres línia a línia i recull el que emet; la clau i el valor se separen amb un tabulador.
#!/usr/bin/env python3
# km0/serveis/analitica/mapreduce/mapper.py
"""Mapper de Hadoop Streaming: per cada línia de comanda emet (p:<productor>, import) i (m:<mercat>, import)."""
import json, sys
for linia in sys.stdin: # Hadoop lliura l'split línia a línia
linia = linia.strip()
if not linia:
continue
try:
ev = json.loads(linia)
except json.JSONDecodeError:
sys.stderr.write("reporter:counter:km0,linies_corruptes,1\n") # comptador propi, visible a la UI
continue
if ev.get("tipus") != "comanda.creada":
continue
mercat = ev["dades"]["mercat"]
for ln in ev["dades"]["linies"]:
import_eur = ln["quantitat"] * ln["preu"]
print(f"p:{ln['productor']}\t{import_eur:.2f}") # clau <TAB> valor
print(f"m:{mercat}\t{import_eur:.2f}")#!/usr/bin/env python3
# km0/serveis/analitica/mapreduce/reducer.py
"""Reducer (i combiner) de Hadoop Streaming: suma els valors de cada clau.
Hadoop lliura les línies ORDENADES per clau, així que n'hi ha prou de detectar el canvi de clau.
"""
import sys
clau_actual, suma = None, 0.0
for linia in sys.stdin:
clau, valor = linia.rstrip("\n").split("\t", 1)
if clau != clau_actual: # canvi de clau: emetre l'anterior
if clau_actual is not None:
print(f"{clau_actual}\t{suma:.2f}")
clau_actual, suma = clau, 0.0
suma += float(valor)
if clau_actual is not None: # no oblidar l'última clau
print(f"{clau_actual}\t{suma:.2f}")El reducer de Streaming no rep (clau, llista de valors) com en Java, sinó la seqüència ordenada de parelles: és el mateix script qui detecta el canvi de clau. Aquesta és la raó que el shuffle ordeni i no només agrupi: amb l'entrada ordenada, agrupar és comparar amb la línia anterior, sense memòria. I com que el reducer suma, serveix també de combiner sense canvis.
Abans de tocar el clúster, la canonada d'Unix reprodueix el treball sencer, amb sort en el paper del shuffle:
$ cat esdeveniments/2026-09-14/comandes.jsonl | python3 mapper.py | sort -k1,1 | python3 reducer.py
m:girona 20.30
m:lleida 58.80
m:valencia 19.00
p:celler-roure-alt 58.80
p:formatgeria-montblanc 25.10
p:horta-la-vega 14.20(Sobre les tres línies d'exemple de 05-01: l'Anna a Girona va comprar 2 × 3,90 + 12,50 = 20,30; en Marc a Lleida 6 × 9,80 = 58,80; la Llúcia a València 3 × 4,20 + 4 × 1,60 = 19,00; i per productor, Horta La Vega 7,80 + 6,40 = 14,20, Formatgeria Montblanc 12,50 + 12,60 = 25,10, Celler Roure Alt 58,80.) Aquesta prova local val or: la majoria dels errors d'un job de Streaming (un split que falla, un valor no numèric) es veuen aquí en un segon en lloc d'en un minut de job fallit.
El llançament al clúster del docker-compose.yml de 04-02 (amb YARN afegit: un resourcemanager i un nodemanager de la imatge apache/hadoop):
docker compose exec resourcemanager hadoop jar \
$HADOOP_HOME/share/hadoop/tools/lib/hadoop-streaming-*.jar \
-D mapreduce.job.name="km0 vendes 2026-09-14" \
-D mapreduce.job.reduces=2 \
-D stream.map.output.field.separator='\t' \
-files mapper.py,reducer.py \
-mapper "python3 mapper.py" \
-combiner "python3 reducer.py" \
-reducer "python3 reducer.py" \
-partitioner org.apache.hadoop.mapred.lib.KeyFieldBasedPartitioner \
-D mapreduce.partition.keypartitioner.options=-k1.1,1.1 \
-input /km0/esdeveniments/2026-09-14/comandes.jsonl \
-output /km0/agregats/2026-09-14/vendesLínia a línia: -files copia els scripts a cada contenidor (la memòria cau distribuïda: el codi viatja a les dades); -combiner reutilitza el reducer localment a cada map; -partitioner amb KeyFieldBasedPartitioner i l'opció -k1.1,1.1 particiona pel primer caràcter de la clau (p o m), de manera que amb dos reducers cada família va a un (amb el hash de p i m mòdul 2 cauen en reducers diferents; si no fos així, n'hi hauria prou amb un partitioner propi, que a Streaming només es pot escriure en Java). El directori de sortida no ha d'existir; MapReduce es nega a sobreescriure, precisament per protegir la sortida atòmica. El resultat:
$ docker compose exec namenode hdfs dfs -ls /km0/agregats/2026-09-14/vendes
-rw-r--r-- 2 hadoop supergroup 0 _SUCCESS
-rw-r--r-- 2 hadoop supergroup 71 part-00000
-rw-r--r-- 2 hadoop supergroup 54 part-00001
$ docker compose exec namenode hdfs dfs -cat /km0/agregats/2026-09-14/vendes/part-00000
p:celler-roure-alt 58.80
p:formatgeria-montblanc 25.10
p:horta-la-vega 14.20I l'estat del treball, amb els seus comptadors, es consulta amb yarn application -list -appStates ALL, mapred job -status <job_id> o a la interfície web del ResourceManager (http://localhost:8088).
8.2 El mateix job en Java: VendesPerProductor.java
Java és el llenguatge natiu de Hadoop, i un job en Java evita el cost de llançar un intèrpret per tasca i de serialitzar-ho tot com a text. Aquest és l'exemple canònic, línia a línia:
// km0/serveis/analitica/mapreduce/VendesPerProductor.java
package km0.analitica;
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.DoubleWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Partitioner;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class VendesPerProductor {
/** Mapper<clau entrada, valor entrada, clau sortida, valor sortida>.
* TextInputFormat lliura (offset en bytes: LongWritable, línia: Text). */
public static class VendesMapper extends Mapper<LongWritable, Text, Text, DoubleWritable> {
private static final ObjectMapper JSON = new ObjectMapper();
private final Text clau = new Text(); // es reutilitzen: eviten crear
private final DoubleWritable importEur = new DoubleWritable(); // milions d'objectes
@Override
protected void map(LongWritable offset, Text linia, Context ctx)
throws IOException, InterruptedException {
JsonNode ev;
try {
ev = JSON.readTree(linia.toString());
} catch (IOException e) {
ctx.getCounter("km0", "linies_corruptes").increment(1);
return;
}
if (!"comanda.creada".equals(ev.path("tipus").asText())) return;
JsonNode dades = ev.get("dades");
String mercat = dades.get("mercat").asText();
for (JsonNode ln : dades.get("linies")) {
importEur.set(ln.get("quantitat").asDouble() * ln.get("preu").asDouble());
clau.set("p:" + ln.get("productor").asText());
ctx.write(clau, importEur); // (p:<productor>, import)
clau.set("m:" + mercat);
ctx.write(clau, importEur); // (m:<mercat>, import)
}
}
}
/** Reducer<clau entrada, valor entrada, clau sortida, valor sortida>.
* Rep cada clau amb un Iterable de TOTS els seus valors, ja agrupats pel shuffle. */
public static class SumaReducer extends Reducer<Text, DoubleWritable, Text, DoubleWritable> {
private final DoubleWritable total = new DoubleWritable();
@Override
protected void reduce(Text clau, Iterable<DoubleWritable> valors, Context ctx)
throws IOException, InterruptedException {
double suma = 0.0;
for (DoubleWritable v : valors) suma += v.get(); // iterador: els valors poden no cabre en memòria
total.set(Math.round(suma * 100.0) / 100.0);
ctx.write(clau, total);
}
}
/** Partitioner: les claus "p:" van al reducer 0 i les "m:" al reducer 1. */
public static class PrefixPartitioner extends Partitioner<Text, DoubleWritable> {
@Override
public int getPartition(Text clau, DoubleWritable valor, int numReducers) {
if (numReducers == 1) return 0;
return clau.charAt(0) == 'p' ? 0 : 1;
}
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration(); // llegeix core-site.xml, yarn-site.xml, etc.
Job job = Job.getInstance(conf, "km0 vendes per productor i mercat");
job.setJarByClass(VendesPerProductor.class); // quin jar enviar als contenidors
job.setMapperClass(VendesMapper.class);
job.setCombinerClass(SumaReducer.class); // sumar és associatiu: el reducer serveix de combiner
job.setPartitionerClass(PrefixPartitioner.class);
job.setReducerClass(SumaReducer.class);
job.setNumReduceTasks(2);
job.setMapOutputKeyClass(Text.class); // tipus intermedis (k2, v2)
job.setMapOutputValueClass(DoubleWritable.class);
job.setOutputKeyClass(Text.class); // tipus finals (k3, v3)
job.setOutputValueClass(DoubleWritable.class);
FileInputFormat.addInputPath(job, new Path(args[0])); // /km0/esdeveniments/2026-09-14/comandes.jsonl
FileOutputFormat.setOutputPath(job, new Path(args[1])); // /km0/agregats/2026-09-14/vendes-java (no ha d'existir)
System.exit(job.waitForCompletion(true) ? 0 : 1); // true: imprimeix progrés i comptadors
}
}El que cal entendre de cada bloc:
- Els tipus
Writable. Hadoop no fa servirStringnidoublea les interfícies, sinóText,DoubleWritable,LongWritable: tipus serialitzables de manera compacta i comparables byte a byte, que el shuffle pot ordenar sense deserialitzar. Reutilitzar les instàncies (clau.set(...)en comptes denew Text(...)) és l'optimització més citada de MapReduce: un mapper emet milions de parelles i la creació d'objectes dispararia la recollida d'escombraries. Mapper<K1, V1, K2, V2>iReducer<K2, V2, K3, V3>. Els genèrics documenten el contracte de l'apartat 1.Contextés el canal pel qual la tasca emet parelles (ctx.write) i incrementa comptadors.- L'
Iterabledel reducer es pot recórrer una sola vegada: Hadoop l'alimenta des de la seqüència ordenada a disc, i reutilitza l'objecteDoubleWritablea cada iteració (desar referències als valors és un error clàssic: tots apunten al mateix objecte). Jobés la descripció declarativa: quines classes, quants reducers, quins tipus, quines rutes.waitForCompletionl'envia al ResourceManager i bloqueja fins que acaba.setJarByClassdiu a Hadoop quin jar conté el codi, que YARN copiarà a cada contenidor.setCombinerClass(SumaReducer.class)és vàlid perquè entrada i sortida del reducer són del mateix tipus(Text, DoubleWritable)i la suma és associativa. Si el reducer emetés una altra cosa (un rànquing, una mitjana) caldria un combiner diferent o cap.
Compilació i llançament:
mvn package -q # produeix target/km0-analitica.jar (Hadoop com a dependència 'provided')
docker compose cp target/km0-analitica.jar resourcemanager:/tmp/
docker compose exec resourcemanager hadoop jar /tmp/km0-analitica.jar km0.analitica.VendesPerProductor \
/km0/esdeveniments/2026-09-14/comandes.jsonl /km0/agregats/2026-09-14/vendes-javaLa sortida és idèntica a la de Streaming (amb part-r-00000 i part-r-00001, la r indicant que la va escriure un reducer) i triga uns segons menys per l'absència d'intèrprets de Python i de text intermedi. En un treball de terabytes, aquesta diferència és de desenes de minuts.
8.3 Simulació local del shuffle
Per veure el que el framework amaga, simulacions/mapreduce_local.py implementa les tres fases en un sol procés, amb el shuffle explícit: particionar, ordenar per clau, agrupar.
# km0/simulacions/mapreduce_local.py
"""MapReduce en un procés: fa visible el shuffle (particionar, ordenar, agrupar) entre map i reduce."""
import json, sys
from itertools import groupby
from collections import defaultdict
def mapar(linia: str):
"""map(k1, v1) -> [(k2, v2)]. Mateixa lògica que mapper.py."""
ev = json.loads(linia)
if ev["tipus"] != "comanda.creada":
return
for ln in ev["dades"]["linies"]:
import_eur = round(ln["quantitat"] * ln["preu"], 2)
yield f"p:{ln['productor']}", import_eur
yield f"m:{ev['dades']['mercat']}", import_eur
def particio(clau: str, n_reducers: int) -> int:
"""Partitioner: prefix 'p' al reducer 0, 'm' a l'1 (amb n=2)."""
return 0 if clau[0] == "p" else n_reducers - 1
def reduir(clau: str, valors):
"""reduce(k2, [v2]) -> (k3, v3)."""
return clau, round(sum(valors), 2)
def executar(ruta: str, n_maps: int = 3, n_reducers: int = 2, amb_combiner: bool = True):
linies = open(ruta, encoding="utf-8").read().splitlines()
splits = [linies[i::n_maps] for i in range(n_maps)] # repartiment de línies entre mappers
# --- Fase map: cada mapper produeix la seva sortida particionada i ordenada (els 'spills') ---
sortides_map = [] # [ mapper ][ partició ] -> llista ordenada
for i, split in enumerate(splits):
buffer = defaultdict(list)
for linia in split:
for k, v in mapar(linia):
buffer[particio(k, n_reducers)].append((k, v))
per_particio = []
for p in range(n_reducers):
parelles = sorted(buffer[p], key=lambda kv: kv[0]) # sort per clau DINS de la partició
if amb_combiner: # combiner: reduce local per clau
parelles = [reduir(k, (v for _, v in grup)) for k, grup in groupby(parelles, key=lambda kv: kv[0])]
per_particio.append(parelles)
sortides_map.append(per_particio)
print(f"map {i}: {len(split)} línies -> " + ", ".join(f"partició {p}: {len(per_particio[p])} parelles" for p in range(n_reducers)))
# --- Fase shuffle: cada reducer recull LA SEVA partició de TOTS els mappers i les barreja ordenades ---
resultats = {}
for r in range(n_reducers):
fragments = [sortides_map[i][r] for i in range(n_maps)]
entrada = sorted((kv for frag in fragments for kv in frag), key=lambda kv: kv[0]) # merge (aquí, un sort)
print(f"reduce {r}: rep {sum(len(f) for f in fragments)} parelles de {n_maps} mappers")
# --- Fase reduce: iterar en ordre, agrupant per canvi de clau ---
resultats[r] = [reduir(k, (v for _, v in grup)) for k, grup in groupby(entrada, key=lambda kv: kv[0])]
return resultats
if __name__ == "__main__":
for r, files in executar(sys.argv[1], amb_combiner="--sense-combiner" not in sys.argv).items():
print(f"--- part-r-0000{r} ---")
for k, v in files:
print(f"{k}\t{v}")$ python mapreduce_local.py esdeveniments/2026-09-14/comandes.jsonl --sense-combiner map 0: 1 línies -> partició 0: 2 parelles, partició 1: 2 parelles map 1: 1 línies -> partició 0: 1 parelles, partició 1: 1 parelles map 2: 1 línies -> partició 0: 2 parelles, partició 1: 2 parelles reduce 0: rep 5 parelles de 3 mappers reduce 1: rep 5 parelles de 3 mappers --- part-r-00000 --- p:celler-roure-alt 58.8 p:formatgeria-montblanc 25.1 p:horta-la-vega 14.2 --- part-r-00001 --- m:girona 20.3 m:lleida 58.8 m:valencia 19.0 $ python mapreduce_local.py esdeveniments/2026-09-14/comandes.jsonl | head -5 map 0: 1 línies -> partició 0: 2 parelles, partició 1: 1 parelles ...
Amb el fitxer de 400 000 comandes de 05-01 i --sense-combiner, cada reducer rep centenars de milers de parelles; amb combiner, en rep 3 × n_maps per als productors i 4 × n_maps per als mercats. El groupby d'itertools sobre la seqüència ordenada és literalment el que fa el reducer de Streaming en detectar el canvi de clau, i sorted sobre els fragments és el merge de l'apartat 5 (a Hadoop, una barreja de seqüències ja ordenades, més barata que un sort complet).
Errors Comuns i Consells
- Provar directament al clúster. Un job de Streaming es prova amb
cat | mapper | sort | reduceren un segon; al clúster, cada intent fallit costa un minut i un log de YARN que cal anar a buscar ambyarn logs -applicationId. - Un combiner que no és associatiu. Calcular mitjanes, o "el primer", o emetre un tipus diferent al combiner, produeix resultats que canvien segons quantes vegades s'hagi executat. Regla: el combiner ha de ser una funció tal que executar-la 0, 1 o N vegades doni el mateix resultat final.
- Desar referències als valors de l'
Iterable. Hadoop reutilitza l'objecte; si el reducer fallista.add(v)per ordenar després, la llista acaba amb N còpies de l'últim valor. Cal copiar (new DoubleWritable(v.get())). - Directori de sortida existent. El job falla abans de començar. És intencionat: la sortida atòmica exigeix un directori net. Esborrar-lo forma part del pipeline (05-05), no del job.
gzipa l'entrada. Uncomandes.jsonl.gzde 2 GB és un split i un map de vint minuts. Fes servirbzip2, LZO indexat, o millor Parquet.- Massa reducers o massa pocs. Un de sol converteix el reduce en seqüencial (Amdahl); mil sobre 20 MB de dades creen mil fitxers minúsculs. Orientació: que cada reducer processi entre 1 i 5 GB de shuffle, i mai més reducers que claus diferents útils.
- Ignorar els comptadors.
Reduce shuffle bytesiSpilled recordssón el termòmetre del treball. Si el shuffle és de la mida de l'entrada, falta el combiner; si els spills són diverses vegades la sortida del map, falta memòria al buffer d'ordenació. - Barrejar encadenament de jobs amb lògica. Tres jobs encadenats a mà amb rutes intermèdies a HDFS es converteixen en un pipeline fràgil. És feina del planificador de 05-05, i una raó de pes per passar a Spark, on les tres fases són un sol programa.
Exercicis
Exercici 1: Productor amb més vendes per mercat
Dissenya un treball (o cadena de treballs) MapReduce que produeixi, per a cada mercat, el productor amb més vendes i el seu import. Indica les claus i valors intermedis de cada fase, si pots fer servir combiner, i quants jobs calen. Després escriu el mapper.py i el reducer.py de Streaming del primer job.
Exercici 2: Fallades a mig job
El job de l'apartat 8.1 té 2 maps i 2 reduces. Descriu què fa l'ApplicationMaster en cadascun d'aquests casos i quines tasques es reexecuten: (a) el NodeManager dn-07 mor quan el map 1, que s'hi executava, ja havia acabat i el reduce 0 havia copiat la seva partició però el reduce 1 encara no; (b) el reduce 1 llança una excepció per un valor no numèric a la seva tercera línia; (c) el reduce 0 triga cinc vegades la mediana i l'AM llança una còpia especulativa que acaba primer. Quins fitxers hi ha a /km0/agregats/2026-09-14/vendes/ durant i després de cada cas?
Exercici 3: Biaix amb partitioner
Durant la Setmana del Formatge Artesà, p:formatgeria-montblanc concentra el 50 % de les parelles. Amb el combiner activat, és un problema? I sense combiner, o si el reduce fos "llista de les 100 comandes més grans de cada productor" (on el combiner no redueix tant el volum)? Proposa un partitioner de Java i la lògica d'un segon job per repartir la clau calenta entre quatre reducers i combinar després, seguint el salting de 05-01.
Solucions
Exercici 1.
Calen dos jobs, perquè hi ha dues agrupacions encadenades: primer sumar per (mercat, productor) i després, per mercat, triar el màxim.
- Job 1.
map: per cada línia de comanda,(mercat|productor, import). Combiner: suma (associativa).reduce: suma. Sortida:girona|formatgeria-montblanc<TAB>18240.50. - Job 2.
map: llegeix la sortida del job 1 i emet(mercat, productor|total). Combiner: sí, "quedar-se amb el màxim" és associatiu i commutatiu i no canvia el tipus.reduce: recórrer els valors de cada mercat i emetre el productor amb el total més gran.
Job 1 en Streaming:
# mapper1.py
import json, sys
for linia in sys.stdin:
ev = json.loads(linia)
if ev.get("tipus") != "comanda.creada": continue
m = ev["dades"]["mercat"]
for ln in ev["dades"]["linies"]:
print(f"{m}|{ln['productor']}\t{ln['quantitat'] * ln['preu']:.2f}")
# reducer1.py: idèntic a reducer.py de l'apartat 8.1 (suma per clau).Un sol job seria possible amb una clau composta mercat i valors productor|import, sumant per productor dins del reducer amb un diccionari en memòria: funciona si els productors d'un mercat caben en memòria (aquí sí, són tres), però perd el combiner i carrega tot el volum al reduce. Amb Spark serà un groupBy seguit d'una finestra, en un sol programa (05-03).
Exercici 2.
(a) L'AM deixa de rebre heartbeats de dn-07 i marca com a perdudes les seves tasques. El map 1 estava completat, però la seva sortida vivia al disc local de dn-07, i el reduce 1 encara no l'havia copiat, així que l'AM reexecuta el map 1 en un altre node (demanant un contenidor a l'RM, preferiblement en un node amb rèplica del bloc). El reduce 0 conserva la seva còpia i no se'n veu afectat; el reduce 1 espera i copia de la nova ubicació. Durant aquest temps, al directori de sortida només existeix _temporary/ amb els intents en curs.
(b) La tasca reduce 1 falla amb excepció; l'AM la reintenta en un altre contenidor (fins a 4 intents per defecte). Com que l'error és determinista (el mateix valor corrupte), fallarà les quatre vegades i el job sencer falla; no s'escriu _SUCCESS, i _temporary/ es neteja. Solució: que el reducer toleri el valor (try/except amb comptador km0,valors_no_numerics) o configurar mapreduce.reduce.failures.maxpercent. El reduce 0, que havia acabat i fet commit, deixa el seu part-00000 al directori, però sense _SUCCESS cap consumidor no l'ha de llegir.
(c) Les dues còpies del reduce 0 escriuen a _temporary/attempt_..._r_000000_0/ i _temporary/attempt_..._r_000000_1/. La còpia especulativa acaba primer i demana el commit; l'AM el concedeix i reanomena el seu fitxer a part-00000; després mata l'intent original i descarta el seu directori temporal. Al final: _SUCCESS, part-00000 (de l'intent 1) i part-00001. Mai no hi ha dos part-00000 perquè el commit és exclusiu per tasca.
Exercici 3.
Amb combiner i una suma, no és un problema: cada mapper redueix les seves 200 000 parelles de Montblanc a una, i el reducer en rep una per mapper. El biaix és a l'entrada dels mappers, que ja està repartida per blocs, no per clau. Sense combiner, el reducer 0 rep la meitat de les parelles del treball i triga el doble que el reducer dels mercats; amb un "top 100 per productor", el combiner només redueix cada mapper a 100 parelles per productor, que continua sent poc, així que tampoc no és greu; el biaix importa de debò quan el reducer necessita tots els valors (llista completa de comandes del productor, mediana exacta).
Partitioner de salting:
public static class SaltPartitioner extends Partitioner<Text, DoubleWritable> {
public int getPartition(Text clau, DoubleWritable v, int n) {
String k = clau.toString();
if (k.startsWith("p:formatgeria-montblanc#")) // el mapper emet p:formatgeria-montblanc#0..#3
return Integer.parseInt(k.substring(k.indexOf('#') + 1)) % n;
return (k.hashCode() & Integer.MAX_VALUE) % n;
}
}El mapper afegeix el sufix #(hash(comanda_id) % 4) només a la clau calenta (determinista: reexecutable). El job 1 produeix quatre parcials p:formatgeria-montblanc#0..3; un job 2 (o un pas final lleuger fora de MapReduce, perquè són quatre números) treu el sufix i suma. Per al "top 100", el job 2 barreja quatre llistes de 100 i es queda amb les 100 millors: correcte perquè el top-K global està contingut en la unió dels top-K parcials.
Conclusió
MapReduce va convertir els patrons de 05-01 en un contracte de dues funcions: el programador escriu map, que extreu una clau de cada registre, i reduce, que agrega els valors d'una clau, i el sistema aporta la localitat (un map per bloc, executat on viu el bloc), el shuffle & sort que agrupa per clau, la reexecució de tasques fallides, l'execució especulativa i la sortida atòmica amb _temporary i _SUCCESS. El combiner és la prereducció que estalvia quatre ordres de magnitud de shuffle al job de vendes, i el partitioner és la palanca per separar famílies de claus o trencar una clau calenta. Hadoop ho implementa sobre HDFS amb YARN com a gestor de recursos genèric, amb un ResourceManager que arbitra, NodeManagers que llancen contenidors i un ApplicationMaster per treball que fa de mestre reiniciable. Ho hem executat tres vegades: en Python amb Hadoop Streaming, provat abans amb cat | mapper | sort | reducer; en Java, l'exemple canònic amb els seus Writable, el seu Job i el seu iterador d'un sol ús; i en una simulació que fa visibles les particions, el sort i el groupby.
També n'hem vist el preu: cada fase escriu a disc, cada agrupació addicional és un altre job que passa per HDFS, cada tasca arrenca una JVM, i un càlcul que un procés de Python resol en un segon triga un minut al clúster. Per a un lot nocturn de terabytes és un preu acceptable; per encadenar la suma per productor amb la unió al catàleg i el rànquing per mercat, o per entrenar les recomanacions en cent iteracions sobre els clics, no ho és. La lliçó següent presenta Spark, que conserva el model (particions, shuffle, tasques reexecutables) però l'expressa com un DAG d'operadors que s'executa en memòria, amb un optimitzador que decideix les fases: el vendes_diaries.py d'analitica passarà de dos scripts i un hadoop jar a un programa de trenta línies amb DataFrames.
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
