La lliçó anterior va deixar fora d'HDFS, deliberadament, les fotos dels productes: milers de fitxers petits que la web ha de servir per HTTP, amb metadades, versions i una durabilitat que no depengui d'un NameNode. L'emmagatzematge d'objectes va néixer exactament per a això. Renuncia als directoris, a les escriptures parcials i al rename, i a canvi ofereix un model pla de buckets i claus, una API HTTP que Amazon S3 va convertir en estàndard de facto, durabilitat d'"onze nous" mitjançant replicació i codificació d'esborrament, i un cost per gigabyte que cap disc de xarxa no iguala. Avui és el magatzem per defecte de fotos, vídeos, còpies de seguretat, documents i, cada cop més, del mateix llac de dades. En aquesta lliçó entendrem el seu model i la seva API, veurem com s'aconsegueix la durabilitat (amb un exemple numèric de Reed-Solomon), aprendrem a reconèixer quan no és l'eina adequada, i el posarem a funcionar a Quilòmetre Zero amb MinIO a docker-compose.yml i un mòdul boto3 amb què la Formatgeria Montblanc pujarà la foto del seu formatge-curat i la web la mostrarà mitjançant URLs presignades.
Contingut
- Objecte, fitxer i bloc
- El model d'objectes: buckets, claus, metadades i versions
- L'API S3 com a estàndard de facto
- Durabilitat: replicació i codificació d'esborrament (Reed-Solomon)
- Consistència als magatzems d'objectes
- Quan fer servir objectes i quan no
- Cost: emmagatzematge, operacions i egress
- Objectes a Quilòmetre Zero: fotos, factures i backups
- Pràctica: MinIO,
mciserveis/cataleg/fotos.pyambboto3 - Errors Comuns i Consells
- Exercicis
- Conclusió
- Objecte, fitxer i bloc
Hi ha tres maneres de presentar l'emmagatzematge a un programa, i convé distingir-les abans de res:
| Bloc | Fitxer | Objecte | |
|---|---|---|---|
| Unitat | Blocs de mida fixa (512 B–4 KB) numerats | Fitxers en un arbre de directoris | Objectes identificats per una clau en un bucket pla |
| Interfície | Lectura/escriptura de blocs (SCSI, NVMe, iSCSI, RBD de Ceph) | POSIX: open, read, write, seek, rename |
HTTP: PUT, GET, DELETE, LIST |
| Modificació | Qualsevol bloc, en qualsevol moment | Qualsevol byte, append, truncament |
Cap: l'objecte es reemplaça sencer |
| Metadades | Cap (les posa el sistema de fitxers de sobre) | Fixes: permisos, dates, mida | Fixes + definides per l'usuari (x-amz-meta-*) |
| Qui el fa servir | Un sistema operatiu (disc d'una VM, base de dades) | Aplicacions que necessiten semàntica de fitxers | Aplicacions web, backups, llacs de dades, contingut estàtic |
| Escala típica | Un disc lògic per consumidor | Terabytes a petabytes (04-02) | Petabytes a exabytes, bilions d'objectes |
| Latència | Microsegons a mil·lisegons | Mil·lisegons | Desenes de mil·lisegons per operació |
| Exemples | EBS, Ceph RBD, iSCSI | NFS, HDFS, CephFS | S3, MinIO, Ceph RGW, Azure Blob, GCS |
La columna de "modificació" és la que ho canvia tot: un objecte és immutable. Per canviar un byte es puja un objecte nou amb la mateixa clau. Aquesta immutabilitat simplifica la replicació (dues còpies d'un objecte són idèntiques o una d'elles és d'una altra versió, no hi ha estats intermedis) i és la raó que els magatzems d'objectes escalin tant i costin tan poc. També és la raó de gairebé totes les seves limitacions (apartat 6).
- El model d'objectes: buckets, claus, metadades i versions
- Bucket: contenidor de nivell superior, amb nom únic (a S3, únic a nivell global; a MinIO, per desplegament). Un bucket té regió, política d'accés, configuració de versionat i regles de cicle de vida. Quilòmetre Zero farà servir
km0-fotos,km0-facturesikm0-backups. - Clau (key): el nom de l'objecte dins del bucket, una cadena de fins a 1024 bytes. No hi ha directoris:
fotos/formatge-curat/original.jpgés una única cadena, i la barra no significa res per al sistema. Les consoles i eines mostren "carpetes" agrupant per prefix, però és una il·lusió: llistar "la carpetafotos/formatge-curat/" és una operacióLISTambprefix=fotos/formatge-curat/. - Dades: de 0 bytes a 5 TB (S3). Opaques: el sistema no interpreta el contingut.
- Metadades: del sistema (
Content-Type,Content-Length,Last-Modified,ETag) i de l'usuari (x-amz-meta-productor: formatgeria-montblanc, fins a 2 KB en total). Les metadades viatgen amb l'objecte i es recuperen ambHEADsense descarregar les dades. No es poden modificar sense reescriure l'objecte (es fa amb una còpia sobre si mateix). - Versionat: si el bucket l'activa, cada
PUTsobre una clau existent no la sobreescriu sinó que crea una nova versió amb el seuVersionId, iDELETEno esborra sinó que hi col·loca un marcador d'esborrament a sobre. Les versions anteriors continuen allà i es poden recuperar: és la protecció més senzilla contra l'esborrat accidental i contra un procés que puja fitxers corruptes. - Cicle de vida (lifecycle): regles declaratives per prefix o etiqueta: "els objectes amb l'etiqueta
tipus=miniaturai més de 90 dies passen a classe d'emmagatzematge freda", "les versions no actuals s'esborren als 30 dies", "les pujades multipart incompletes s'avorten als 7 dies". El sistema les aplica sol, sense codi. - Classes d'emmagatzematge: el mateix objecte pot viure en nivells amb cost i temps d'accés diferents: estàndard (mil·lisegons), accés infreqüent (més barat per GB, més car per lectura), arxiu (S3 Glacier: hores per recuperar, cèntims per TB). MinIO implementa tiering cap a un altre magatzem remot.
- Etiquetes (tags): parells clau-valor per classificar objectes (
campanya=setmana-del-formatge-artesa) i dirigir les regles de cicle de vida i les polítiques d'accés.
flowchart LR
subgraph B["bucket km0-fotos (versionat activat)"]
K1["fotos/formatge-curat/original.jpg<br/>v3 (actual) · v2 · v1<br/>meta: productor=formatgeria-montblanc"]
K2["fotos/formatge-curat/miniatura-300.jpg<br/>v1"]
K3["fotos/tomaquet-rosa/original.jpg<br/>v1<br/>meta: productor=horta-la-vega"]
K4["fotos/vi-crianca/original.jpg<br/>marcador d'esborrament · v2 · v1"]
end
LC["Regla de cicle de vida:<br/>miniatures, tag tipus=miniatura, > 90 dies → classe freda<br/>versions no actuals > 30 dies → esborrar"] -.-> B
- L'API S3 com a estàndard de facto
Amazon va llançar S3 el 2006 amb una API REST senzilla, i avui pràcticament tots els magatzems d'objectes la implementen: MinIO, Ceph RGW, Google Cloud Storage (mode interoperable), Backblaze B2, Cloudflare R2, Wasabi… Aprendre S3 és aprendre'ls tots. Les operacions bàsiques:
| Operació | HTTP | Què fa |
|---|---|---|
PutObject |
PUT /bucket/clau |
Puja un objecte complet (fins a 5 GB en una sola crida); accepta metadades a les capçaleres |
GetObject |
GET /bucket/clau |
Descarrega; admet Range per llegir un tros i condicions (If-None-Match) |
HeadObject |
HEAD /bucket/clau |
Només metadades i ETag: existeix, quant fa, de quin tipus és |
DeleteObject(s) |
DELETE |
Esborra (o posa un marcador si hi ha versionat); fins a 1000 claus per crida |
ListObjectsV2 |
GET /bucket?list-type=2&prefix=… |
Llista claus per prefix, paginat de 1000 en 1000, amb delimiter=/ per simular carpetes |
CopyObject |
PUT amb x-amz-copy-source |
Copia dins del servidor sense descarregar; és com es "reanomena" i com es canvien metadades |
| Multipart upload | CreateMultipartUpload, UploadPart, CompleteMultipartUpload |
Pujada per parts (5 MB–5 GB cadascuna, fins a 10 000) en paral·lel i reprenible; obligatòria per sobre de 5 GB |
| URL presignada | Cap: és una signatura | Un URL amb la signatura del servidor i caducitat que permet a un tercer fer una operació concreta sense credencials |
Dos conceptes que mereixen detall:
ETag. Cada objecte té una etiqueta d'entitat que canvia si canvia el contingut. Per a pujades simples és l'MD5 del contingut ("9e107d9d372bb6826bd81d3542a419d6"), cosa que permet verificar la integritat d'una pujada comparant l'MD5 local amb l'ETag retornat. Per a pujades multipart és l'MD5 dels MD5 de les parts seguit de -N (nombre de parts), així que no serveix com a suma del fitxer sencer. Es fa servir també en lectures condicionals: la web demana GET amb If-None-Match: "<etag>" i rep 304 Not Modified si la foto no ha canviat.
URLs presignades. El magatzem d'objectes no hauria de ser accessible públicament, i la web de Quilòmetre Zero no vol fer de proxy de cada foto (pagaria l'amplada de banda dues vegades i afegiria latència). La solució és que el servei cataleg, que sí que té credencials, signi un URL de descàrrega amb una caducitat (per exemple 15 minuts) i el doni al navegador; el navegador descarrega directament del magatzem, que verifica la signatura i la data. El mateix mecanisme funciona per a pujades: la Formatgeria Montblanc rep un URL presignat de PUT i puja la seva foto directament al bucket sense passar per cataleg. La signatura (Signature V4) és un HMAC-SHA256 de la petició canònica amb la clau secreta; el servidor la recalcula i compara. És un exemple perfecte de capacitat (capability): un permís portàtil i limitat, sense comptes ni sessions, tema que reapareixerà a 06-01.
- Durabilitat: replicació i codificació d'esborrament (Reed-Solomon)
S3 anuncia una durabilitat del 99,999999999 % anual ("onze nous"): si desees 10 milions d'objectes, l'expectativa és perdre'n un cada 10 000 anys. No és una xifra de disponibilitat (aquesta és del 99,9 %–99,99 %); és la probabilitat que els bytes continuïn existint. S'aconsegueix amb dues tècniques.
Replicació. N còpies completes en N dominis de fallada diferents (discos, servidors, racks, centres de dades). Amb 3 còpies es tolera la pèrdua de 2 i el sobrecost és del 200 % (3 bytes desats per cada byte útil). És el que fa HDFS (04-02) i és simple i ràpida de llegir (qualsevol còpia serveix).
Codificació d'esborrament (erasure coding). En lloc de copiar, es divideix l'objecte en k fragments de dades i es calculen m fragments de paritat, de manera que qualssevol k dels k+m fragments són suficients per reconstruir l'objecte. Es toleren m pèrdues amb un sobrecost de només m/k. L'algorisme habitual és Reed-Solomon, la mateixa família de codis que fan servir els CD, els QR i les comunicacions espacials.
La intuïció, sense àlgebra de cossos finits: pensa en k = 2 dades, d1 i d2, i m = 2 paritats:
Desem [d1, d2, p1, p2] en quatre discos. Si perdem d1 i d2 (els dos de dades), tenim dues equacions i dues incògnites: d2 = p2 − p1, d1 = p1 − d2. Si perdem d1 i p2: d1 = p1 − d2. Qualsevol parella de supervivents és suficient. Reed-Solomon generalitza això a qualsevol k i m amb coeficients triats perquè tot subconjunt de k fragments sigui resoluble, operant en un cos finit (GF(2⁸)) perquè les sumes i multiplicacions es facin sobre bytes sense desbordar. Un exemple numèric amb enters per veure que funciona:
# Reed-Solomon "de joguina" amb k=2, m=2 i aritmètica entera (a la realitat: GF(2^8))
d1, d2 = 7, 12
p1 = d1 + d2 # 19
p2 = d1 + 2 * d2 # 31
# Es perden d1 i d2; sobreviuen p1 i p2:
d2_rec = p2 - p1 # 12
d1_rec = p1 - d2_rec # 7
assert (d1_rec, d2_rec) == (d1, d2)Comparem configuracions reals per a 1 TB de dades útils:
| Esquema | Fragments | Pèrdues tolerades | Bytes desats | Sobrecost | Qui el fa servir |
|---|---|---|---|---|---|
| 3 rèpliques | 3 | 2 | 3 TB | 200 % | HDFS per defecte, Cassandra |
| RS(4,2) | 4+2 | 2 | 1,5 TB | 50 % | MinIO amb 6 discos (per defecte la meitat de paritat: RS(3,3) en 6 discos) |
| RS(8,4) | 8+4 | 4 | 1,5 TB | 50 % | MinIO amb 12 discos, Ceph |
| RS(10,4) | 10+4 | 4 | 1,4 TB | 40 % | Facebook f4, HDFS 3 (RS-10-4) |
| RS(12,4) | 12+4 | 4 | 1,33 TB | 33 % | Backblaze (17+3), Azure LRC variants |
El preu de la codificació d'esborrament és la reconstrucció: per llegir un objecte cal reunir k fragments de k nodes (més latència que llegir una còpia), i per reparar un disc perdut cal llegir k fragments de cada objecte afectat (molt trànsit de xarxa). Per això els sistemes la fan servir per a dades grans i fredes, i mantenen replicació (o memòries cau, 04-05) per al petit i calent; molts fan totes dues coses: replicació per a objectes petits i erasure coding a partir d'una certa mida.
Els onze nous es calculen combinant la probabilitat de fallada anual d'un disc (1–3 %), el temps de reconstrucció després d'una fallada (hores) i el nombre de fallades simultànies que calen per perdre dades (m+1 al mateix grup dins d'aquesta finestra). Amb RS(8,4) i reparació en hores, aquesta coincidència és astronòmicament improbable. A això s'hi sumen les sumes de comprovació per fragment i l'scrubbing: un procés de fons rellegeix tot periòdicament per detectar corrupció silenciosa (bit rot) i reparar-la abans que s'acumulin fallades.
- Consistència als magatzems d'objectes
Durant 14 anys, S3 va ser eventualment consistent per a sobreescriptures i esborrats: després d'un PUT sobre una clau existent, un GET podia retornar la versió anterior durant uns segons; després d'un DELETE, un LIST podia continuar mostrant la clau. El desembre de 2020 Amazon va anunciar consistència forta read-after-write per a totes les operacions: qualsevol GET, HEAD o LIST posterior a un PUT o DELETE confirmat veu el nou estat. En el vocabulari de 03-01, cada objecte és linealitzable, i en el de 03-02, S3 és CP (una escriptura no es confirma fins que el quòrum de rèpliques la té). MinIO ofereix la mateixa garantia dins d'un desplegament, i Ceph RGW també.
No tots els proveïdors ho fan: alguns magatzems compatibles amb S3 i les configuracions de replicació entre regions són eventuals (la còpia a l'altra regió triga segons o minuts). I hi ha un matís important fins i tot amb consistència forta: no hi ha transaccions entre objectes. Pujar original.jpg i després miniatura-300.jpg són dues operacions independents; un lector pot veure la primera i no la segona. Si l'aplicació necessita que apareguin juntes, puja primer les miniatures i al final l'objecte "índex" que les referencia, o desa la referència a la base de dades només quan totes les pujades han acabat.
Finalment, les memòries cau davant del magatzem (una CDN, tema de 08-04, o un navegador amb Cache-Control) reintrodueixen la consistència eventual pel seu compte: reemplaçar original.jpg no buida la CDN. Per això la pràctica comuna és no sobreescriure claus de contingut: es puja original-v3.jpg (o es fa servir un hash del contingut com a part de la clau) i s'actualitza la referència. La immutabilitat de l'objecte s'estén així a la clau.
- Quan fer servir objectes i quan no
Un magatzem d'objectes encaixa quan les dades són:
- Immutables o de substitució completa: fotos, vídeos, PDF, backups, fitxers d'esdeveniments tancats, artefactes de compilació, models entrenats.
- Grans en volum total, amb accés per clau coneguda o per prefix.
- Servides per HTTP a navegadors, apps o a altres serveis, sovint amb CDN al davant.
- Tolerants a desenes de mil·lisegons per operació.
I no encaixa quan cal:
- Semàntica de fitxers:
seeki escriptura parcial,append, bloqueigs, directoris reals. Els sistemes que munten S3 com a disc (s3fs, Mountpoint) funcionen per a lectures, però cada escriptura reescriu l'objecte sencer. - Reanomenament atòmic.
mv fotos/tmp/x.jpg fotos/formatge-curat/original.jpgen un sistema de fitxers és una operació atòmica de metadades; a S3 ésCopyObject+DeleteObject, dues operacions, sense atomicitat i amb cost proporcional a la mida. Moure un "directori" d'un milió d'objectes és un milió de còpies. Molts motors de processament (Mòdul 5) van tenir problemes per això en passar d'HDFS a S3, i per això existeixen formats de taula (Iceberg, Delta) que eviten reanomenar. - Latència baixa i moltes operacions petites: llegir 10 000 objectes d'1 KB són 10 000 peticions HTTP; una base de dades (04-04) o una memòria cau (04-05) ho fa mil vegades més ràpid.
- Llistats freqüents i grans:
LISTestà paginat a 1000 i és l'operació més lenta; l'aplicació ha de desar a la seva base de dades quins objectes existeixen, no descobrir-los llistant. - Dades que canvien sovint: cada canvi és una versió nova de l'objecte complet.
- Cost: emmagatzematge, operacions i egress
Els magatzems d'objectes al núvol cobren per tres conceptes, i el tercer sorprèn molts equips:
| Concepte | Ordre de magnitud (S3 estàndard, 2026) | Comentari |
|---|---|---|
| Emmagatzematge | 0,02 €/GB·mes | 1 TB de fotos ≈ 20 €/mes; les classes fredes baixen a 0,004 €/GB·mes |
| Operacions | 5 €/milió de PUT/LIST; 0,4 €/milió de GET |
Un milió de fotos pujades ≈ 5 €; els LIST són cars i lents |
| Sortida de dades (egress) | 0,05–0,09 €/GB cap a internet | Servir 1 TB de fotos al mes des del bucket ≈ 50–90 €, més que desar-lo |
L'egress és la raó principal de posar una CDN al davant (08-04): la CDN guarda les fotos a la memòria cau de les seves vores i el bucket només paga el primer enviament a cada vora. També és la raó que moure dades entre núvols sigui car i que el llac de dades (04-02) i el còmput (Mòdul 5) hagin de ser a la mateixa regió. Alguns proveïdors (R2, B2) no cobren egress, i un MinIO propi canvia el model: no hi ha cost per GB servit, però hi ha discos, servidors i personal que els operi.
- Objectes a Quilòmetre Zero: fotos, factures i backups
Tres tipus de dades de la plataforma van al magatzem d'objectes:
| Dada | Bucket | Clau | Qui escriu | Qui llegeix | Versionat | Cicle de vida |
|---|---|---|---|---|---|---|
| Fotos de productes (original + miniatures 300 i 800 px) | km0-fotos |
fotos/<slug>/original.jpg, fotos/<slug>/miniatura-300.jpg |
Productors (via URL presignat de PUT), un procés de miniatures |
La web (via URL presignat de GET, després CDN) |
Sí | Versions no actuals > 30 dies → esborrar; miniatures antigues → classe freda |
| Factures en PDF | km0-factures |
factures/2026/09/P-2026-000123.pdf |
comandes, en confirmar |
El client des de "les meves comandes" (URL presignat) | No (immutables per llei) | Retenció legal: bloqueig d'objectes (object lock) 5 anys |
| Backups de PostgreSQL | km0-backups |
postgres/km0_inventari/2026-09-14T02:00.dump |
Feina nocturna (07-03 veurà com) | Restauracions | No | > 30 dies → classe freda; > 1 any → esborrar |
Les fotos són el cas principal. El flux complet, des que la Formatgeria Montblanc tria una foto al seu panell fins que l'Anna la veu a la web:
sequenceDiagram
participant Q as Panell de la<br/>Formatgeria Montblanc
participant C as cataleg
participant M as MinIO<br/>(km0-fotos)
participant T as Generador de<br/>miniatures
participant W as Web (navegador de l'Anna)
Q->>C: vull pujar foto de formatge-curat (jpeg, 1,8 MB)
C->>C: valida producte i productor; genera clau fotos/formatge-curat/original.jpg
C-->>Q: URL presignat de PUT (10 min)
Q->>M: PUT directe amb la foto i metadades
M-->>Q: 200 + ETag + VersionId
Q->>C: pujada completada (ETag)
C->>C: desa a km0_cataleg la clau i l'ETag; publica foto.pujada
T->>M: GET original.jpg
T->>M: PUT miniatura-300.jpg, miniatura-800.jpg
W->>C: GET /producte/formatge-curat
C->>C: signa URL de GET de miniatura-800.jpg (15 min)
C-->>W: HTML amb l'URL presignat
W->>M: GET miniatura-800.jpg (signatura vàlida)
M-->>W: 200, imatge (Cache-Control)
cataleg mai no toca els bytes de la foto: signa, valida i desa referències. És el patró habitual i el que permet que el magatzem, no el servei, absorbeixi l'amplada de banda.
- Pràctica: MinIO,
mc i serveis/cataleg/fotos.py amb boto3
mc i serveis/cataleg/fotos.py amb boto3MinIO és un magatzem d'objectes de codi obert compatible amb S3, escrit en Go, que es desplega en un sol binari. En producció s'executa en mode distribuït (diversos nodes, diversos discos per node, codificació d'esborrament automàtica); per a km0/ n'hi ha prou amb un node amb un volum. Afegim al docker-compose.yml:
# km0/docker-compose.yml (fragment)
services:
minio:
image: minio/minio:RELEASE.2026-06-01T00-00-00Z
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: km0admin
MINIO_ROOT_PASSWORD: km0secret123 # a 06-04 el mourem a un gestor de secrets
ports:
- "9000:9000" # API S3
- "9001:9001" # consola web
volumes:
- minio-data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 5s
minio-init: # crea el bucket i les regles una sola vegada
image: minio/mc:latest
depends_on:
minio: { condition: service_healthy }
entrypoint: >
/bin/sh -c "
mc alias set km0 http://minio:9000 km0admin km0secret123 &&
mc mb --ignore-existing km0/km0-fotos km0/km0-factures km0/km0-backups &&
mc version enable km0/km0-fotos &&
mc ilm rule add km0/km0-fotos --noncurrent-expire-days 30 &&
mc anonymous set none km0/km0-fotos &&
echo a punt"
volumes:
minio-data:mc (MinIO Client) és la CLI, que funciona amb qualsevol S3. Les ordres útils des de la teva màquina:
mc alias set km0 http://localhost:9000 km0admin km0secret123
mc ls km0 # buckets
mc cp formatge-curat.jpg km0/km0-fotos/fotos/formatge-curat/original.jpg \
--attr "productor=formatgeria-montblanc;producte=formatge-curat"
mc ls --versions km0/km0-fotos/fotos/formatge-curat/ # versions de cada clau
mc stat km0/km0-fotos/fotos/formatge-curat/original.jpg # metadades, ETag, VersionId
mc ilm rule ls km0/km0-fotos # regles de cicle de vida
mc share download --expire 15m km0/km0-fotos/fotos/formatge-curat/original.jpg # URL presignat
mc admin info km0 # discos, ús, estat del clústerboto3 és l'SDK oficial d'AWS per a Python i, gràcies a la compatibilitat de MinIO, funciona amb només canviar endpoint_url. El mòdul serveis/cataleg/fotos.py concentra tot el que cataleg necessita:
# km0/serveis/cataleg/fotos.py
"""Gestió de fotos de productes al magatzem d'objectes (MinIO / S3)."""
import hashlib
import os
import boto3
from botocore.config import Config
BUCKET = "km0-fotos"
s3 = boto3.client(
"s3",
endpoint_url=os.getenv("S3_ENDPOINT", "http://localhost:9000"),
aws_access_key_id=os.getenv("S3_ACCESS_KEY", "km0admin"),
aws_secret_access_key=os.getenv("S3_SECRET_KEY", "km0secret123"),
region_name="eu-west-1", # MinIO ho ignora; boto3 ho exigeix per signar
config=Config(signature_version="s3v4", s3={"addressing_style": "path"}),
)
def clau_foto(slug: str, variant: str = "original", ext: str = "jpg") -> str:
return f"fotos/{slug}/{variant}.{ext}"
def pujar_foto(slug: str, ruta_local: str, productor: str, variant: str = "original") -> dict:
"""Puja una foto amb metadades i verifica la integritat comparant l'ETag amb l'MD5."""
with open(ruta_local, "rb") as f:
dades = f.read()
md5 = hashlib.md5(dades).hexdigest()
clau = clau_foto(slug, variant)
resp = s3.put_object(
Bucket=BUCKET, Key=clau, Body=dades,
ContentType="image/jpeg",
CacheControl="public, max-age=31536000, immutable", # la CDN i el navegador la podran guardar un any
Metadata={"productor": productor, "producte": slug, "variant": variant},
Tagging="tipus=original" if variant == "original" else "tipus=miniatura", # la regla de cicle de vida filtra per aquest tag
)
etag = resp["ETag"].strip('"')
if etag != md5: # pujada simple: ETag == MD5 del contingut
raise RuntimeError(f"integritat: etag {etag} != md5 {md5}")
return {"clau": clau, "etag": etag, "version_id": resp.get("VersionId")}
def url_descarrega(slug: str, variant: str = "miniatura-800", minuts: int = 15) -> str:
"""URL presignat de GET que la web incrusta a l'HTML; caduca en `minuts`."""
return s3.generate_presigned_url(
"get_object",
Params={"Bucket": BUCKET, "Key": clau_foto(slug, variant)},
ExpiresIn=minuts * 60,
)
def url_pujada(slug: str, productor: str, minuts: int = 10) -> str:
"""URL presignat de PUT perquè el productor pugi directament, sense passar per cataleg."""
return s3.generate_presigned_url(
"put_object",
Params={"Bucket": BUCKET, "Key": clau_foto(slug), "ContentType": "image/jpeg",
"Metadata": {"productor": productor, "producte": slug}},
ExpiresIn=minuts * 60,
)
def llistar_fotos(slug: str) -> list[dict]:
"""Llista les variants d'un producte (LIST per prefix, paginat)."""
paginador = s3.get_paginator("list_objects_v2")
resultat = []
for pagina in paginador.paginate(Bucket=BUCKET, Prefix=f"fotos/{slug}/"):
for obj in pagina.get("Contents", []):
resultat.append({"clau": obj["Key"], "bytes": obj["Size"], "etag": obj["ETag"].strip('"')})
return resultat
def versions(slug: str, variant: str = "original") -> list[dict]:
resp = s3.list_object_versions(Bucket=BUCKET, Prefix=clau_foto(slug, variant))
return [{"version_id": v["VersionId"], "actual": v["IsLatest"], "data": v["LastModified"], "bytes": v["Size"]}
for v in resp.get("Versions", [])]
def restaurar_versio(slug: str, version_id: str, variant: str = "original") -> str:
"""Recupera una versió anterior copiant-la sobre si mateixa: es converteix en l'actual."""
clau = clau_foto(slug, variant)
resp = s3.copy_object(Bucket=BUCKET, Key=clau,
CopySource={"Bucket": BUCKET, "Key": clau, "VersionId": version_id},
MetadataDirective="COPY")
return resp["VersionId"]
def configurar_cicle_vida() -> None:
"""Miniatures de més de 90 dies a classe freda; versions no actuals esborrades als 30 dies;
pujades multipart abandonades avortades als 7 dies."""
s3.put_bucket_lifecycle_configuration(
Bucket=BUCKET,
LifecycleConfiguration={"Rules": [
{"ID": "miniatures-fredes", "Status": "Enabled",
"Filter": {"Tag": {"Key": "tipus", "Value": "miniatura"}}, # només miniatures: els originals no es mouen
"Transitions": [{"Days": 90, "StorageClass": "FRED"}]},
{"ID": "versions-antigues", "Status": "Enabled", "Filter": {"Prefix": ""},
"NoncurrentVersionExpiration": {"NoncurrentDays": 30}},
{"ID": "multipart-abandonades", "Status": "Enabled", "Filter": {"Prefix": ""},
"AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 7}},
]},
)
if __name__ == "__main__":
r = pujar_foto("formatge-curat", "mostres/formatge-curat.jpg", productor="formatgeria-montblanc")
print("pujada:", r)
print("descàrrega (15 min):", url_descarrega("formatge-curat", "original"))
print("fotos de formatge-curat:", llistar_fotos("formatge-curat"))
r2 = pujar_foto("formatge-curat", "mostres/formatge-curat-v2.jpg", productor="formatgeria-montblanc")
for v in versions("formatge-curat"):
print(" versió", v)
print("restaurada com a:", restaurar_versio("formatge-curat", r["version_id"]))Comentaris sobre les parts menys evidents:
Config(signature_version="s3v4", s3={"addressing_style": "path"}): MinIO local no té DNS amb comodins, així que el bucket va al path (/km0-fotos/…) i no al host (km0-fotos.localhost). Signature V4 és la que fan servir els URLs presignats moderns.pujar_fotocomprova l'ETagcontra l'MD5 local: si la xarxa ha corromput alguna cosa, es detecta a l'instant. Ambput_object(pujada simple) aquesta igualtat es compleix; amb multipart no, com hem vist a l'apartat 3. Per a fotos de més de 100 MB, fes servirs3.upload_file, que fa multipart automàticament amb reintents per part.CacheControl: immutable: com que hem promès no sobreescriure contingut que la CDN hagi de guardar en memòria cau, podem deixar-la guardar un any. Quan la formatgeria canviï la foto, la nova versió tindrà un altreVersionIdi, a la pràctica de Quilòmetre Zero, la web referenciaràoriginal.jpg?v=<etag>perquè l'URL canviï.url_descarregano parla amb el servidor:generate_presigned_urlcalcula la signatura localment amb la clau secreta. És instantani i no consumeix cap operació del bucket. Fixa't que l'ExpiresInno pot superar 7 dies amb Signature V4.url_pujadainclouContentTypeiMetadataals paràmetres signats: el productor **ha d'**enviar exactament aquestes capçaleres, o la signatura no quadra. És la manera de forçar que la pujada porti les metadades quecatalegha decidit.restaurar_versiomostra el truc de "modificar" un objecte immutable: copiar-lo sobre si mateix des de la versió desitjada, cosa que crea una versió nova amb el contingut antic.configurar_cicle_vida:StorageClassa MinIO es refereix a un nivell de tiering definit ambmc ilm tier add(per exemple, un altre MinIO amb discos lents o un S3 Glacier); a S3 es faria servirSTANDARD_IAoGLACIER. La regla de multipart abandonades evita pagar per parts òrfenes que ningú no veu als llistats.
Prova ràpida en dues línies:
pip install boto3
python -m serveis.cataleg.fotos # puja, llista, versiona, restaura
curl -sI "$(python -c 'from serveis.cataleg.fotos import url_descarrega; print(url_descarrega("formatge-curat","original"))')" | head -5El curl -I retorna 200 OK amb Content-Type: image/jpeg, ETag, x-amz-meta-productor: formatgeria-montblanc i Cache-Control. Espera 15 minuts i repeteix: 403 Forbidden amb Request has expired. Això és l'URL presignat funcionant.
Errors Comuns i Consells
- Pensar en carpetes. No existeixen.
LISTper prefix és lent i paginat; desa a la base de dades quines claus té cada producte en lloc de llistar el bucket a cada pàgina vista. - Sobreescriure claus que una CDN o un navegador guarden en memòria cau. El bucket serà consistent; les memòries cau no. Canvia la clau (hash o versió al nom) i actualitza la referència.
- Exposar credencials al navegador o al productor. Mai. Signa URLs des del servei, amb caducitat curta i els paràmetres que vols forçar inclosos a la signatura.
- Buckets públics "perquè funcioni". Un bucket públic amb
LISThabilitat és la fuita de dades més comuna del núvol.mc anonymous set nonei URLs presignats o CDN amb signatura. - Fer servir
renameo moure prefixos grans. No hi ha reanomenament: és còpia + esborrat per objecte. Dissenya les claus per no haver de moure-les. - Confiar en l'ETag com a MD5 de pujades multipart. No ho és. Si necessites un hash del fitxer complet, calcula'l i desa'l com a metadada (
x-amz-meta-sha256) o fes servir les sumes de comprovació d'objectes complets (ChecksumAlgorithm). - Oblidar l'egress i les operacions al pressupost. Servir fotos directament des del bucket a milers d'usuaris costa més que emmagatzemar-les; la CDN (08-04) és una decisió econòmica abans que de latència.
- Sense versionat ni cicle de vida. El versionat és una assegurança barata contra esborrats accidentals; el cicle de vida evita que les versions i les multipart abandonades creixin sense límit. Activa'ls el primer dia.
Exercicis
Exercici 1. Quilòmetre Zero té 4 200 productes amb 3 variants de foto (original 1,8 MB, miniatura-800 180 KB, miniatura-300 35 KB) i 2 000 productors. Durant la Setmana del Formatge Artesà la web serveix 6 milions de vistes de producte al dia, cadascuna amb una miniatura-800. (a) Calcula l'emmagatzematge total al bucket i el seu cost mensual a 0,02 €/GB. (b) Calcula l'egress diari i mensual a 0,08 €/GB si se serveix directament des del bucket. (c) Quin percentatge del trànsit caldria servir des d'una CDN perquè l'egress del bucket baixi al 5 % del càlcul anterior, i quin canvi a les claus ho fa possible sense risc de servir fotos obsoletes?
Exercici 2. Implementa generar_miniatures(slug) a fotos.py: descarrega original.jpg en memòria amb get_object, genera les variants de 800 i 300 px amb Pillow i puja-les amb pujar_foto reutilitzant les metadades de l'original (llegeix-les amb head_object). Després descriu què pot veure la web si un usuari carrega la pàgina de formatge-curat just entre la pujada de l'original i la de les miniatures, i com evitaries mostrar una imatge trencada.
Exercici 3. Amb RS(k=4, m=2) sobre 6 discos, un objecte de 24 MB es divideix en 4 fragments de dades de 6 MB i 2 de paritat. (a) Quants bytes ocupa en total i quin és el sobrecost enfront de 3 rèpliques? (b) Si fallen dos discos, quants MB cal llegir per reconstruir l'objecte en una lectura, i quants per reparar un sol fragment perdut d'aquest objecte? (c) Explica per què MinIO, amb 6 discos, fa servir per defecte RS(3,3) (paritat = meitat) en lloc de RS(4,2) i què es guanya i què es perd.
Solucions
Solució 1:
(a) Per producte: 1,8 MB + 0,18 MB + 0,035 MB ≈ 2,0 MB. Total: 4 200 × 2,0 MB ≈ 8,5 GB (més versions no actuals durant 30 dies; suposem un 20 % extra: ~10 GB). Cost: 10 GB × 0,02 € ≈ 0,20 €/mes. L'emmagatzematge és irrellevant.
(b) Egress diari: 6 000 000 × 180 KB ≈ 1,08 TB/dia; mensual ≈ 32 TB. A 0,08 €/GB: ≈ 86 €/dia, 2 600 €/mes. Tretze mil vegades el cost d'emmagatzemar. Les operacions GET (6 M/dia × 0,4 €/M ≈ 2,4 €/dia) també superen l'emmagatzematge.
(c) Perquè el bucket serveixi només el 5 %, la CDN ha d'atendre el 95 % de les peticions des de la memòria cau (hit ratio ≥ 95 %), cosa que és realista amb 4 200 objectes petits i Cache-Control: max-age=31536000, immutable. La condició perquè aquest max-age d'un any sigui segur és que la clau (o l'URL) canviï quan canviï el contingut: fotos/formatge-curat/miniatura-800.jpg?v=<etag> o fotos/formatge-curat/<etag>/miniatura-800.jpg. Amb claus immutables no cal invalidar la CDN mai; es detallarà a 08-04.
Solució 2:
from io import BytesIO
from PIL import Image
def generar_miniatures(slug: str, amplades=(800, 300)) -> list[dict]:
clau = clau_foto(slug, "original")
capcalera = s3.head_object(Bucket=BUCKET, Key=clau)
meta = capcalera["Metadata"] # productor, producte, variant
original = s3.get_object(Bucket=BUCKET, Key=clau)["Body"].read()
imatge = Image.open(BytesIO(original)).convert("RGB")
resultats = []
for amplada in amplades:
copia = imatge.copy()
copia.thumbnail((amplada, amplada * 10)) # conserva la proporció, limita l'amplada
buf = BytesIO(); copia.save(buf, "JPEG", quality=85, optimize=True)
ruta_tmp = f"/tmp/{slug}-{amplada}.jpg"
with open(ruta_tmp, "wb") as f: f.write(buf.getvalue())
resultats.append(pujar_foto(slug, ruta_tmp, productor=meta["productor"], variant=f"miniatura-{amplada}"))
return resultats(Es reutilitza pujar_foto, que ja fixa variant a les metadades.) Entre la pujada de l'original i la de les miniatures, el bucket conté original.jpg però no miniatura-800.jpg: la web, que demana un URL presignat de la miniatura, obtindria un URL vàlid però el GET retornaria 404 NoSuchKey i el navegador mostraria la imatge trencada. El magatzem és consistent per objecte, però no hi ha transacció entre objectes (apartat 5). Solució: cataleg no marca la foto com a "disponible" a km0_cataleg (ni publica foto.pujada per a la web) fins que el generador confirma les miniatures; mentrestant la web mostra la foto anterior o una imatge de farciment. Alternativa més robusta: el generador puja les miniatures abans que s'actualitzi la referència, i la referència (a la base de dades) és l'únic "commit" visible.
Solució 3:
(a) 4 × 6 MB + 2 × 6 MB = 36 MB per a 24 MB útils: sobrecost del 50 %. Amb 3 rèpliques serien 72 MB (200 %): la codificació d'esborrament estalvia la meitat del disc tolerant les mateixes 2 pèrdues.
(b) Per llegir l'objecte calen 4 fragments qualssevol: 24 MB llegits de 4 discos (amb les dues fallades, exactament els 4 supervivents). Per reparar un sol fragment perdut de 6 MB cal llegir 4 fragments (24 MB) i recalcular: el cost de reparació és k vegades la mida del fragment, que és la penalització característica de l'erasure coding enfront de la rèplica (on reparar 6 MB costa llegir 6 MB).
(c) Amb RS(3,3), cada objecte tolera la pèrdua de 3 discos de 6 (la meitat), amb sobrecost del 100 %; amb RS(4,2) en tolera 2, amb sobrecost del 50 %. MinIO tria per defecte la configuració més segura (i amb 6 discos, dues fallades simultànies no són tan rares durant una finestra de reparació llarga), i deixa que l'operador abaixi la paritat (MINIO_STORAGE_CLASS_STANDARD=EC:2) quan prefereix capacitat. Es guanya tolerància a fallades i lectures que necessiten només 3 fragments; es perd el 50 % addicional de disc.
Conclusió
L'emmagatzematge d'objectes canvia el contracte de l'emmagatzematge: buckets i claus planes en lloc de directoris, objectes immutables que es reemplacen sencers, metadades que viatgen amb les dades, versionat i regles de cicle de vida que el sistema aplica sol. L'API S3 (PUT/GET/HEAD/LIST, multipart, ETag, URLs presignats) és l'estàndard que MinIO, Ceph i la resta implementen. La durabilitat d'onze nous s'aconsegueix combinant replicació amb codificació d'esborrament, on Reed-Solomon reconstrueix un objecte a partir de qualssevol k dels seus k+m fragments amb un sobrecost del 33–50 % en lloc del 200 % de tres còpies, a canvi de lectures i reparacions més costoses. S3 i MinIO són avui fortament consistents per objecte, però no hi ha transaccions entre objectes ni reanomenament atòmic, i les memòries cau davant del bucket retornen la consistència eventual, per la qual cosa la bona pràctica és no sobreescriure claus. L'egress, no l'emmagatzematge, domina la factura, i aquesta és la raó econòmica de la CDN que veurem a 08-04. Quilòmetre Zero ha destinat a objectes les seves fotos (km0-fotos, amb fotos.py, pujada directa de la Formatgeria Montblanc per URL presignat i servei a la web amb URLs de 15 minuts), les seves factures i els seus backups, i ho ha muntat amb MinIO i mc a docker-compose.yml.
Ja tenim on desar fitxers massius (HDFS) i fitxers que se serveixen (objectes). Falta l'emmagatzematge que mou la plataforma cada segon: registres petits, consultats i actualitzats amb baixa latència, amb transaccions i amb les garanties de consistència que vam triar a 03-02. Aquest és el terreny de les bases de dades distribuïdes, on comandes migrarà a Cassandra, inventari es quedarà a PostgreSQL, i l'anell de hashing consistent de 04-01 reapareixerà amb rèpliques i nivells de consistència per operació.
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
