El balancejador d'AlpinaShop ja funciona: una IP anycast global, HTTPS, el MIG al darrere i les imatges servides directament des del bucket. Però queda la ineficiència amb què vam tancar la lliçó anterior. Cada vegada que un client de Lisboa obre la fitxa d'una motxilla, els mateixos bytes tornen a sortir d'europe-west1 i creuen la península. Amb 60 GB de catàleg, centenars de milers de peticions d'imatge al mes i clients repartits per Europa, això és latència que el client nota i sortida de dades que la factura acumula.
Cloud CDN resol exactament aquest problema, i ho fa sense que hagis de canviar una línia de l'aplicació ni moure un objecte de lloc. No és un producte separat amb la seva pròpia consola, la seva pròpia IP i el seu propi domini: és una casella que s'activa sobre el servei de backend o el backend de bucket que ja vas construir a 03-02. Aquest detall de disseny és el que fa que aquesta lliçó sigui curta en comandes i llarga en criteri: activar-lo costa un --enable-cdn; fer-ho bé consisteix a entendre què es desa a la memòria cau, amb quina clau, durant quant de temps i com es mesura.
En aquesta lliçó activaràs la memòria cau sobre bb-catalogo-imagenes i bs-catalogo-web, triaràs el mode de memòria cau correcte per a cadascun, evitaràs que els paràmetres utm_* de les campanyes de màrqueting destrossin la ràtio d'encerts, decidiràs una política de TTL coherent amb els noms immutables que ja vas adoptar a 02-02, aprendràs per què la invalidació no ha de ser la teva eina habitual, i mesuraràs el resultat en mil·lisegons i en euros.
Contingut
- Què és una CDN i quin problema resol exactament
- La xarxa de punts de presència de Google
- On viu Cloud CDN: sobre el balancejador, no al costat
- Activació sobre el backend de bucket i sobre el servei de backend
- Modes de memòria cau:
CACHE_ALL_STATIC,USE_ORIGIN_HEADERS,FORCE_CACHE_ALL - La clau de memòria cau i el desastre dels paràmetres
utm_* - TTL: qui decideix quant viu un objecte a la vora
- Noms immutables: l'estratègia que sosté tot l'anterior
- Invalidació de memòria cau: el botó d'emergència
- Contingut privat a la memòria cau: URL i galetes signades
- Memòria cau negativa, contingut obsolet i altres opcions útils
- Mesurament: ràtio d'encerts, capçaleres
AgeiVia, i depuració ambcurl - L'estalvi real: latència i euros
- Quan Cloud CDN no n'hi ha prou: Media CDN
- Què és una CDN i quin problema resol exactament
Una xarxa de distribució de contingut (CDN) és un conjunt de servidors de memòria cau repartits geogràficament que guarden còpies del teu contingut a prop dels usuaris. Quan un client demana un objecte, el serveix el node més proper en lloc del servidor d'origen.
Sona simple, però convé ser precís sobre els tres problemes diferents que resol, perquè no són el mateix i es mesuren de manera diferent:
| Problema | Sense CDN | Amb CDN |
|---|---|---|
| Latència | L'objecte viatja d'europe-west1 a Lisboa a cada petició |
Se serveix des del punt de presència de Lisboa: desenes de mil·lisegons menys |
| Cost de sortida | Cada byte servit es factura com a sortida a internet des del bucket | Els encerts de memòria cau es facturen a tarifa de CDN, més barata |
| Càrrega a l'origen | El MIG i el bucket atenen totes les peticions | L'origen només veu les fallades de memòria cau: menys instàncies, menys operacions |
Hi ha un quart efecte, menys obvi i molt valuós: absorció de pics. Quan AlpinaShop publiqui la campanya de tardor i la portada rebi deu vegades el trànsit habitual, si la portada i les seves imatges són a la memòria cau, el MIG gairebé no se n'assabenta. L'autoescalat que vas configurar a 02-01 continuarà existint com a xarxa de seguretat, però es dispararà molt menys.
I una limitació que cal tenir clara des del principi: una CDN només accelera el que es pot desar a la memòria cau. La pàgina de la cistella d'un client concret, el resultat d'un pagament o el tauler de la Lucía no es desen mai. La CDN no fa més ràpida la teva aplicació; fa que la major part del trànsit no arribi a la teva aplicació.
- La xarxa de punts de presència de Google
Google manté més de dues-centes ubicacions de memòria cau repartides pel món, a més de desenes de regions i punts de presència de xarxa. Moltes d'aquestes memòries cau són dins de les xarxes dels mateixos operadors (el que Google anomena Google Global Cache), és a dir, físicament a l'operador al qual està connectat el client.
L'important per al teu disseny no és el número, sinó la mecànica:
flowchart LR
C1([Client Lisboa])
C2([Client Hèlsinki])
P1["PoP Lisboa<br/>memòria cau"]
P2["PoP Hèlsinki<br/>memòria cau"]
LB["Balancejador global<br/>alpinashop-lb-ip"]
O1["Bucket alpinashop-catalogo<br/>europe-west1"]
O2["MIG alpinashop-web-mig<br/>europe-west1"]
C1 -->|1. GET /imagenes/...| P1
C2 -->|1. GET /imagenes/...| P2
P1 -->|2. fallada de memòria cau| LB
P2 -->|2. encert: no surt| P2
LB --> O1
LB --> O2
P1 -.->|3. resposta desada| C1
- El client resol
alpinashop.examplei arriba a la IP anycast. Aquesta IP s'anuncia des de tots els punts de presència, així que el client entra pel més proper. - En aquest punt de presència es consulta la memòria cau. Si hi ha encert, la resposta surt d'allà i no arriba mai a
europe-west1. - Si hi ha fallada, la petició viatja per la xarxa privada de Google fins a l'origen —no per internet pública—, la resposta es desa a la vora i es lliura al client.
Dues conseqüències pràctiques:
- La memòria cau no és una, són moltes. Un objecte pot estar calent a Madrid i fred a Varsòvia. Per això la ràtio d'encerts no és mai del 100 % ni ho ha de ser.
- Google agrupa les peticions concurrents al mateix objecte (request coalescing, activat per defecte): si mil clients demanen alhora una imatge que no és a la memòria cau, l'origen rep una petició, no mil. Això evita l'efecte estampida en els llançaments.
- On viu Cloud CDN: sobre el balancejador, no al costat
Aquesta és la idea que més confusió genera en arribar des d'altres plataformes. En molts proveïdors, la CDN és un servei a part: crees una "distribució", et donen un nom d'amfitrió propi i hi apuntes el teu DNS. A Google Cloud no.
Cloud CDN és una propietat d'un backend del balancejador d'aplicacions extern global. S'activa així:
- Sobre un backend de bucket (
bb-catalogo-imagenes) → desa a la memòria cau els objectes de Cloud Storage. - Sobre un servei de backend (
bs-catalogo-web) → desa el que retornin les instàncies del MIG, o demà el NEG sense servidor de Cloud Run (DA-001).
D'aquí se'n deriven coses molt còmodes:
- No canvies el DNS ni la IP. Continues amb
alpinashop-lb-ipi el mateix certificat. - El mateix mapa d'URL decideix què es desa a la memòria cau i què no, perquè cada ruta va a un backend diferent i cada backend té la seva pròpia configuració de memòria cau.
- Cloud Armor (03-05) s'avalua a la vora, de manera que les regles de seguretat s'apliquen també a les peticions que acaben servint-se des de la memòria cau. La memòria cau no és un forat pel qual es cola el trànsit sense filtrar.
- Els registres del balancejador que vas activar a 03-02 ja porten els camps de memòria cau. No hi ha un sistema d'observabilitat a part.
Recorda com va quedar el mapa alpinashop-url-map:
| Ruta | Backend | Desar a la memòria cau? |
|---|---|---|
/imagenes/* |
bb-catalogo-imagenes (bucket) |
Sí, agressivament: són fitxers immutables |
/estatico/* |
bb-catalogo-imagenes (bucket) |
Sí: CSS i JS versionats |
/ i fitxes de producte |
bs-catalogo-web (MIG) |
Sí, amb cura: HTML que canvia |
/carrito, /cuenta, /pago |
bs-catalogo-web (MIG) |
Mai: contingut per usuari |
Aquest quadre és la decisió de disseny de la lliçó. Tot la resta són paràmetres.
- Activació sobre el backend de bucket i sobre el servei de backend
Comencem pel fàcil i de més impacte: les imatges.
gcloud config set project alpinashop-prod
# CDN sobre el backend de bucket que serveix /imagenes/* i /estatico/*
gcloud compute backend-buckets update bb-catalogo-imagenes \
--enable-cdn \
--cache-mode=CACHE_ALL_STATIC \
--default-ttl=3600 \
--max-ttl=31536000 \
--client-ttl=3600 \
--negative-cachingQuè fa cada opció:
--enable-cdn: activa la memòria cau. És l'únic obligatori; la resta són ajustos.--cache-mode: la política general (apartat 5).--default-ttl: quant es desa una resposta si l'origen no diu res.--max-ttl: el sostre. Encara que l'origen demani un any, no se superarà aquest valor. Aquí el deixem en un any perquè confiem en els nostres propisCache-Control.--client-ttl: elmax-agemàxim que se li comunica al navegador. Permet desar molt a la vora i poc al client, cosa molt útil (apartat 7).--negative-caching: desa també els errors (apartat 11).
Ara el servei de backend del MIG. Aquí som més conservadors, perquè al darrere hi ha una aplicació que genera HTML:
gcloud compute backend-services update bs-catalogo-web \
--global \
--enable-cdn \
--cache-mode=CACHE_ALL_STATIC \
--default-ttl=300 \
--max-ttl=3600 \
--client-ttl=60 \
--serve-while-stale=86400 \
--compression-mode=AUTOMATIC--default-ttl=300: cinc minuts per a l'estàtic que serveixi l'aplicació i no porti capçaleres pròpies.--serve-while-stale=86400: si l'origen no respon, la vora pot continuar servint la còpia caducada fins a 24 hores mentre revalida. Aquesta opció converteix una caiguda del MIG en una degradació en lloc d'un error 502 per al contingut desat. És una de les millors relacions benefici/esforç de tot el mòdul.--compression-mode=AUTOMATIC: la vora comprimeix amb gzip o Brotli les respostes de text que l'origen enviï sense comprimir, estalviant amplada de banda sense tocar gunicorn.
Comprova l'estat amb:
gcloud compute backend-buckets describe bb-catalogo-imagenes \
--format="yaml(cdnPolicy, enableCdn)"
gcloud compute backend-services describe bs-catalogo-web --global \
--format="yaml(enableCDN, cdnPolicy)"Els canvis de configuració de CDN triguen uns minuts a propagar-se a tots els punts de presència. I compte: canviar la configuració no buida la memòria cau. Els objectes ja desats continuen allà amb el seu TTL original.
- Modes de memòria cau:
CACHE_ALL_STATIC, USE_ORIGIN_HEADERS, FORCE_CACHE_ALL
CACHE_ALL_STATIC, USE_ORIGIN_HEADERS, FORCE_CACHE_ALLEl mode de memòria cau respon a una pregunta: qui decideix què es desa, l'origen o el CDN?
| Mode | Què desa | Qui mana | Risc | Quan fer-lo servir |
|---|---|---|---|---|
USE_ORIGIN_HEADERS |
Només el que l'origen marca explícitament com a desable | L'origen, sempre | Cap; si t'equivoques, simplement no es desa | Aplicacions que ja emeten Cache-Control correctes i volen control total |
CACHE_ALL_STATIC |
Contingut estàtic per tipus de contingut i extensió, encara que l'origen calli; respecta l'origen per a la resta | Compartit | Baix: es respecten private i no-store |
L'opció per defecte assenyada. La que fa servir AlpinaShop |
FORCE_CACHE_ALL |
Totes les respostes correctes, ignorant private i no-store |
El CDN, ignorant l'origen | Alt: pot servir la pàgina d'un usuari a un altre | Només en backends que serveixen exclusivament contingut públic immutable |
Detalls que importen:
CACHE_ALL_STATICconsidera "estàtic" el contingut pel seu tipus MIME i la seva extensió: imatges, vídeo, CSS, JavaScript, fonts, PDF. L'HTML no entra en aquesta categoria, així que les fitxes de producte d'AlpinaShop no es desaran tret que l'aplicació ho demani amb el seu propiCache-Control. És just el que volem: control explícit sobre l'HTML.- En qualsevol mode, una resposta amb
Cache-Control: private,no-storeoSet-Cookieno es desa (tret deFORCE_CACHE_ALL). Aquesta és la xarxa de seguretat que impedeix el desastre clàssic. FORCE_CACHE_ALLsobre un backend que serveix pàgines de sessió és probablement el pitjor incident de seguretat que es pot provocar amb una casella. Si algun dia el necessites, aplica'l a un servei de backend dedicat al qual només arribin rutes públiques, mai al que serveix el lloc sencer.
Per a AlpinaShop la decisió és:
bb-catalogo-imagenes→CACHE_ALL_STATIC. Tot el que hi ha allà és estàtic per definició i ja ve ambCache-Controld'un any des de 02-02.bs-catalogo-web→CACHE_ALL_STATIC, i que sigui l'aplicació Flask la que decideixi ruta a ruta.
Així és com el Dani ho declara a l'aplicació:
# app.py — control explicit de desat a la memoria cau per ruta
@app.route("/producto/<sku>")
def fitxa_producte(sku):
producte = repositori.obtenir(sku)
resposta = make_response(render_template("producto.html", producto=producte))
# Public i desable 5 minuts a la vora, 1 minut al navegador.
# 's-maxage' el llegeix el CDN; 'max-age' el llegeix el navegador.
resposta.headers["Cache-Control"] = "public, max-age=60, s-maxage=300"
return resposta
@app.route("/carrito")
def cistella():
resposta = make_response(render_template("carrito.html"))
# Mai, enlloc, sota cap circumstancia.
resposta.headers["Cache-Control"] = "private, no-store"
return respostaRegla que convé gravar: si una resposta depèn de qui la demana, porta private, no-store. Posa-l'hi tu explícitament i no confiïs que el mode de memòria cau et salvi.
- La clau de memòria cau i el desastre dels paràmetres
utm_*
utm_*La clau de memòria cau és la cadena amb què el CDN identifica un objecte desat. Si dues peticions generen la mateixa clau, la segona és un encert. Si generen claus diferents, la segona és una fallada encara que el contingut sigui idèntic.
Per defecte la clau inclou:
- El protocol (
httpohttps). - L'amfitrió (
alpinashop.example). - La ruta (
/imagenes/productos/MOCH-2210/web/frontal-800.a3f9c1.webp). - La cadena de consulta completa (
?utm_source=newsletter&utm_campaign=otono26).
I aquí és on hi ha el problema. L'equip de màrqueting d'AlpinaShop llança una campanya i les URL de les imatges comencen a arribar així:
/imagenes/productos/MOCH-2210/web/frontal-800.a3f9c1.webp?utm_source=newsletter&utm_medium=email&utm_campaign=otono26 /imagenes/productos/MOCH-2210/web/frontal-800.a3f9c1.webp?utm_source=instagram&utm_medium=social&utm_campaign=otono26 /imagenes/productos/MOCH-2210/web/frontal-800.a3f9c1.webp?fbclid=IwAR3x9...
Són el mateix objecte amb tres claus diferents. Cada variant provoca una fallada de memòria cau, una lectura del bucket i un càrrec d'emplenament de memòria cau. Amb una campanya gran, la ràtio d'encerts s'enfonsa d'un 92 % a un 40 % i ningú no entén per què la factura ha pujat justament quan van arribar les visites.
La solució és excloure aquests paràmetres de la clau:
# Backend de bucket: les imatges no depenen de CAP parametre
gcloud compute backend-buckets update bb-catalogo-imagenes \
--cache-key-query-string-blacklist=utm_source,utm_medium,utm_campaign,utm_term,utm_content,gclid,fbclid,msclkid,ref
# Servei de backend: enfocament invers, llista blanca.
# Nomes aquests parametres formen part de la clau; la resta s'ignoren.
gcloud compute backend-services update bs-catalogo-web --global \
--cache-key-query-string-whitelist=pagina,orden,talla,colorDues estratègies, i convé entendre per què se'n fa servir una a cada lloc:
| Estratègia | Comanda | Avantatge | Risc |
|---|---|---|---|
| Llista negra (excloure) | --cache-key-query-string-blacklist |
Fàcil de començar; no trenca res existent | Cada paràmetre de seguiment nou cal afegir-lo a mà |
| Llista blanca (incloure només) | --cache-key-query-string-whitelist |
Immune a paràmetres nous; ràtio màxima | Si oblides un paràmetre que sí que canvia el contingut, serveixes la pàgina equivocada |
Per al bucket, la llista negra n'hi ha prou i de sobres; en realitat cap paràmetre no afecta una imatge. Per al servei de backend, la llista blanca és més potent però exigeix que el Dani revisi la llista cada vegada que afegeixi un filtre nou al catàleg. És una decisió que es documenta, no que s'improvisa.
Si les imatges de veritat no depenen de cap paràmetre, l'opció més neta és directament:
gcloud compute backend-buckets update bb-catalogo-imagenes \
--cache-key-query-string-blacklist="" # equival a ignorar tota la queryAltres components que es poden afegir a la clau:
--cache-key-include-http-header=X-Alpina-Region: útil si serveixes variants per capçalera. Cada valor diferent multiplica les entrades de memòria cau, així que fes-lo servir només amb capçaleres de cardinalitat molt baixa.--cache-key-include-named-cookie=idioma: variants per galeta concreta. Mateix avís.--no-cache-key-include-protocol: unifica HTTP i HTTPS a la mateixa entrada. Com que a AlpinaShop tot l'HTTP es redirigeix a HTTPS (03-02), tant és.
I un advertiment que va en l'altre sentit: si l'origen retorna una capçalera Vary amb valors que el CDN no admet, la resposta directament no es desa. Vary: Accept-Encoding és segur; Vary: User-Agent mata la memòria cau completament, perquè hi ha desenes de milers d'user agents diferents.
- TTL: qui decideix quant viu un objecte a la vora
Hi ha cinc valors implicats i es confonen constantment. Aquest és l'ordre de comandament:
| Valor | On es defineix | A qui afecta | Què fa |
|---|---|---|---|
Cache-Control: max-age |
Capçalera de l'origen | Navegador i CDN | TTL general |
Cache-Control: s-maxage |
Capçalera de l'origen | Només memòries cau compartides (el CDN) | Té prioritat sobre max-age a la vora |
--default-ttl |
Configuració del backend | CDN | TTL quan l'origen no diu res |
--max-ttl |
Configuració del backend | CDN | Sostre: retalla el que demani l'origen |
--client-ttl |
Configuració del backend | Navegador | Sostre del max-age que s'envia al client |
La seqüència real, per a una resposta que arriba des de l'origen amb Cache-Control: public, max-age=31536000, immutable i un backend configurat amb --max-ttl=86400 --client-ttl=3600:
- El CDN llegeix
max-age= 31 536 000 s. - Aplica
--max-ttl: desa l'objecte 24 hores, no un any. - Aplica
--client-ttl: reescriu la capçalera cap al client amax-age=3600, una hora.
La combinació s-maxage alt + max-age baix és la més útil i la menys usada:
Significa: "CDN, desa'l un dia; navegador, desa'l un minut". Avantatge: si necessites canviar el contingut, invalides la memòria cau del CDN (que sí que controles) i en 60 segons tots els navegadors del món veuen el que és nou. Mai no pots invalidar la memòria cau d'un navegador aliè; per això convé que el client en desi poc i la vora en desi molt — tret de quan el nom del fitxer és immutable, que és el cas següent.
- Noms immutables: l'estratègia que sosté tot l'anterior
A 02-02 vas prendre una decisió que ara cobra els seus interessos: les imatges d'AlpinaShop es desen amb un nom que no es reutilitza mai, amb un hash o versió incrustada:
productos/MOCH-2210/web/frontal-800.a3f9c1.webp productos/MOCH-2210/web/frontal-800.b7e204.webp ← la foto nova és un altre objecte
I es pugen amb Cache-Control: public, max-age=31536000, immutable.
La conseqüència és enorme i mereix enunciar-se explícitament:
Si el nom és immutable, el problema de la invalidació desapareix. Publicar una foto nova no consisteix a actualitzar un objecte desat, sinó a publicar un objecte que ningú no ha demanat mai. La fitxa HTML apunta al nom nou i la memòria cau serveix l'objecte nou des del primer moment, mentre el vell caduca sol.
Amb aquest esquema, la política correcta per al bucket és desar un any de veritat, també al client:
gcloud compute backend-buckets update bb-catalogo-imagenes \
--cache-mode=CACHE_ALL_STATIC \
--default-ttl=3600 \
--max-ttl=31536000 \
--client-ttl=31536000Aquí sí que volem un client-ttl llarg: el navegador d'un client recurrent no tornarà a demanar la imatge mai. Fixa't en el contrast amb l'apartat anterior: el TTL del client és llarg quan el nom és immutable i curt quan no ho és. Aquesta és tota la regla.
I el corol·lari per a l'HTML: la fitxa de producte no pot tenir un TTL llarg, perquè la seva URL sí que és estable (/producto/MOCH-2210). Per això porta s-maxage=300. Contingut amb URL estable → TTL curt. Contingut amb URL versionada → TTL d'un any.
- Invalidació de memòria cau: el botó d'emergència
De vegades cal esborrar alguna cosa de la vora abans que caduqui: s'ha publicat un preu equivocat, una imatge amb el logotip malament, un text legal incorrecte.
# Invalidar un objecte concret
gcloud compute url-maps invalidate-cdn-cache alpinashop-url-map \
--path="/imagenes/productos/MOCH-2210/web/frontal-800.a3f9c1.webp"
# Invalidar un prefix complet (el comodi nomes val al final)
gcloud compute url-maps invalidate-cdn-cache alpinashop-url-map \
--path="/imagenes/productos/MOCH-2210/*"
# Invalidar tot el lloc: el martell. Evita-ho.
gcloud compute url-maps invalidate-cdn-cache alpinashop-url-map \
--path="/*" --async
# Seguir l'operacio
gcloud compute operations list --global --filter="operationType~invalidate" --limit=5Observa que la invalidació s'executa sobre el mapa d'URL, no sobre el backend: es propaga a tots els backends que aquest mapa encamina.
Per què no ha de ser el teu mecanisme habitual:
- És lenta en termes d'incident. Cal propagar l'ordre a tots els punts de presència del món; l'ordre de magnitud són minuts, no segons. Durant una crisi de contingut, aquests minuts es fan llargs.
- Està subjecta a quota. Hi ha un límit d'invalidacions per projecte i unitat de temps. Un desplegament que invalida
/*a cada publicació xoca amb la quota abans del que et penses. - Un
/*destrueix la ràtio d'encerts. Després d'una invalidació total, tot el trànsit del món colpeja l'origen alhora. Has convertit una publicació rutinària en una prova de càrrega involuntària. - Requereix permisos elevats, típicament
roles/compute.loadBalancerAdmin, que no voldràs repartir alegrement (03-04).
L'alternativa correcta és la de l'apartat 8: noms versionats per als recursos, TTL curt per a l'HTML. La invalidació queda reservada als errors, que és per al que està.
- Contingut privat a la memòria cau: URL i galetes signades
El bucket alpinashop-catalogo és privat (el vas crear amb --public-access-prevention a 02-02) i el backend de bucket el llegeix amb permisos propis, així que les imatges de catàleg són públiques a través del balancejador però no directament. Perfecte per al catàleg.
Ara imagina el cas següent d'AlpinaShop: els vídeos de les guies tècniques d'escalada només els han de veure els clients que han comprat el curs presencial. Són fitxers grans, es beneficien molt de la memòria cau, però no poden ser públics.
Per a això existeixen les URL signades de Cloud CDN i les galetes signades. La diferència amb les URL signades de Cloud Storage (02-02) és important: aquelles les valida Cloud Storage i trenquen la memòria cau (cada signatura és una URL diferent); aquestes les valida la vora del CDN i permeten desar a la memòria cau.
# 1) Generar una clau de signatura de 16 bytes en base64url
head -c 16 /dev/urandom | base64 | tr +/ -_ > clave-cdn.txt
# 2) Registrar-la al backend
gcloud compute backend-buckets add-signed-url-key bb-catalogo-guias \
--key-name=clave-guias-2026-01 \
--key-file=clave-cdn.txt
# 3) Indicar quant es desa la resposta signada
gcloud compute backend-buckets update bb-catalogo-guias \
--signed-url-cache-max-age=3600
# 4) Desar la clau a Secret Manager (03-06) i esborrar el fitxer local
gcloud secrets create cdn-clave-guias --data-file=clave-cdn.txt
shred -u clave-cdn.txtGaleta signada davant d'URL signada:
| URL signada | Galeta signada | |
|---|---|---|
| Abast | Un objecte concret | Un prefix de rutes sencer |
| Com arriba | Dins de l'enllaç | Capçalera Set-Cookie amb el nom Cloud-CDN-Cookie |
| Bona per a | Una descàrrega puntual | Una galeria o un reproductor amb molts fitxers |
| Efecte a la memòria cau | Desa bé: la clau ignora els paràmetres de signatura | Desa bé, i una sola galeta cobreix tota la sessió |
Per als vídeos de les guies, la galeta és l'opció correcta: l'aplicació Flask valida la compra, emet una galeta signada vàlida per a /guias/ durant dues hores i el reproductor demana els fragments sense més autenticació. La vora valida la signatura i serveix des de la memòria cau.
Avís. Les claus de signatura són material criptogràfic: donen accés a contingut de pagament. Han de viure a Secret Manager (03-06), rotar-se periòdicament (es poden registrar diverses claus alhora al mateix backend precisament per rotar sense talls) i no aparèixer mai al repositori. Si el contingut protegit té implicacions contractuals o de drets, que un professional de seguretat revisi el disseny abans de producció.
- Memòria cau negativa, contingut obsolet i altres opcions útils
Memòria cau negativa. Sense ella, si un rastrejador demana deu mil URL inexistents, el teu origen genera deu mil 404. Amb ella, la vora recorda l'error:
gcloud compute backend-services update bs-catalogo-web --global \
--negative-caching \
--negative-caching-policy='404=120,410=300,500=0,502=0,503=0'L'interessant és el matís dels codis 5xx posats a 0: no volem desar els nostres propis errors de servidor. Si el MIG retorna un 502 durant un desplegament i el desem dos minuts, hem multiplicat l'impacte del desplegament. Els 404 sí: un producte que no existeix continuarà sense existir d'aquí a dos minuts.
Servir contingut obsolet (--serve-while-stale). Ja el vam activar: durant aquest marge, si l'origen no respon o retorna error, la vora serveix la còpia caducada i revalida en segon pla. És la diferència entre "el web va una mica desactualitzat" i "el web està caigut".
Saltar-se la memòria cau sota demanda. Molt útil perquè el Dani depuri en producció sense tocar la configuració:
gcloud compute backend-services update bs-catalogo-web --global \
--bypass-cache-on-request-headers=x-alpina-debugA partir d'aquí, qualsevol petició amb aquesta capçalera va directa a l'origen. Com que es pot saltar la memòria cau de tot el lloc, la capçalera ha de ser poc endevinable i no s'ha de documentar fora de l'equip.
- Mesurament: ràtio d'encerts, capçaleres
Age i Via, i depuració amb curl
Age i Via, i depuració amb curlSense mesurar, tot l'anterior és fe. Hi ha tres nivells d'observació.
Nivell 1: la resposta HTTP. La manera més ràpida de saber si alguna cosa s'està desant a la memòria cau:
HTTP/2 200 content-type: image/webp cache-control: public, max-age=31536000, immutable age: 4127 via: 1.1 google
Com es llegeix:
| Capçalera | Què significa |
|---|---|
Age: 4127 |
La còpia fa 4127 segons que és a la vora. Age present i més gran que zero = encert de memòria cau. |
Sense Age |
Fallada de memòria cau: la resposta ve de l'origen |
Via: 1.1 google |
Ha passat per la infraestructura de proxy de Google. Apareix hi hagi encert o no |
Cache-Control |
El que la vora li diu al navegador, ja retallat per --client-ttl |
Prova clàssica: demana-ho dues vegades seguides. La primera no porta Age; la segona, sí. Si la segona tampoc no en porta, hi ha un problema de desat a la memòria cau i toca el nivell 2.
Nivell 2: els registres del balancejador. Els camps de memòria cau ja hi són:
gcloud logging read '
resource.type="http_load_balancer"
AND httpRequest.requestUrl:"/imagenes/"
' --project=alpinashop-prod --limit=20 --freshness=1h \
--format="table(
httpRequest.requestUrl,
httpRequest.cacheLookup,
httpRequest.cacheHit,
httpRequest.cacheFillBytes,
jsonPayload.statusDetails
)"cacheLookup: false→ ni tan sols es va buscar a la memòria cau. Gairebé sempre significa que el CDN no està activat en aquest backend, o que la petició no és desable (mètode diferent deGET, capçaleraAuthorization…).cacheLookup: true, cacheHit: false→ es va buscar i no hi era: fallada legítima o clau de memòria cau fragmentada (apartat 6).cacheFillBytes→ els bytes que es van portar de l'origen per emplenar. Si això és alt de forma sostinguda, la teva ràtio és dolenta.statusDetails: response_from_cache→ confirmació d'encert.
Nivell 3: mètriques. La ràtio d'encerts s'obté de loadbalancing.googleapis.com/https/request_count agrupant per l'etiqueta cache_result, els valors de la qual són HIT, MISS, DISABLED i variants de revalidació. El tauler de la Marta (06-04) ha de portar:
| Mètrica / vista | Objectiu per a AlpinaShop |
|---|---|
Ràtio d'encerts a /imagenes/* |
> 90 % |
| Ràtio d'encerts global | > 60 % |
cacheFillBytes mensual |
Estable o a la baixa malgrat créixer el trànsit |
p95 de total_latencies d'imatges |
Molt per sota del p95 de l'HTML |
Guia de depuració d'una fallada persistent. Si un objecte no es desa mai, comprova en aquest ordre:
--enable-cdnés realment al backend correcte (describe, no memòria).- La resposta no porta
Cache-Control: private,no-storenino-cache. - La resposta no porta
Set-Cookie. És la causa més freqüent en aplicacions Flask: n'hi ha prou que es toquisessiona la vista perquè s'emeti una galeta i la resposta deixi de ser desable. - No hi ha un
Varyamb capçaleres d'alta cardinalitat (User-Agent,Cookie). - La petició és
GETi no portaAuthorization. - La clau de memòria cau no està fragmentada per paràmetres de consulta.
- La resposta porta
Content-Lengtho una longitud determinable.
El punt 3 s'arregla en dues línies i sol explicar la meitat dels casos:
@app.after_request
def netejar_galeta_en_estatics(resposta):
"""Evita que una galeta de sessio accidental faci no desable el cataleg."""
if request.path.startswith(("/estatico/", "/imagenes/")):
resposta.headers.pop("Set-Cookie", None)
return resposta
- L'estalvi real: latència i euros
Anem amb un càlcul orientatiu per a AlpinaShop. Suposem 2 TB al mes d'imatges servides a clients europeus i una ràtio d'encerts del 90 %.
| Concepte | Sense CDN | Amb CDN al 90 % |
|---|---|---|
| Sortida a internet des del bucket | 2 000 GB × ~0,11 $/GB ≈ 220 $ | 200 GB (les fallades) → emplenament de memòria cau ≈ 2–20 $ |
| Sortida des de la memòria cau del CDN | — | 1 800 GB × ~0,08 $/GB ≈ 144 $ |
| Cerques a la memòria cau | — | ~2 M peticions × 0,0075 $/10 000 ≈ 1,5 $ |
| Operacions de classe B al bucket | Milions de lectures | Només les d'emplenament: dos ordres de magnitud menys |
| Total aproximat | ≈ 220 $ | ≈ 150–170 $ |
Preus d'ordre de magnitud a tall il·lustratiu. Les tarifes de sortida, d'emplenament de memòria cau i de cerca varien per continent i per tram de volum, i canvien amb el temps: verifica-les sempre a la calculadora i a la documentació oficial de preus abans de presentar una xifra a ningú.
Conclusions honestes d'aquest quadre, que és més interessant que un "estalvies un 90 %" de fullet:
- L'estalvi en factura és real però moderat: al voltant d'un 25-30 %. La sortida des de la memòria cau també es paga.
- L'estalvi creix amb el trànsit, perquè l'origen deixa d'escalar amb les visites: menys instàncies al MIG, menys operacions al bucket, menys IOPS.
- El guany gran és la latència. Un client de Lisboa passa d'un temps fins al primer byte de l'ordre de 40-60 ms a menys de 15 ms; un de Hèlsinki, de més de 60 ms a xifres similars. En una fitxa de producte amb dotze imatges, això es tradueix en centenars de mil·lisegons de pàgina completa, que és exactament el que mesura Google en els senyals d'experiència de pàgina i el que es nota en la conversió.
- I hi ha un estalvi que no surt a cap factura: els pics de campanya deixen de ser un problema de capacitat.
- Quan Cloud CDN no n'hi ha prou: Media CDN
Google Cloud té un segon producte de distribució, Media CDN, construït sobre la mateixa infraestructura que serveix YouTube. No és "Cloud CDN millorat": és un altre producte, amb un altre model de configuració (EdgeCacheService, EdgeCacheOrigin, EdgeCacheKeyset) i un altre públic.
| Cloud CDN | Media CDN | |
|---|---|---|
| Es configura sobre | El balancejador global existent | Recursos propis, independents del balancejador |
| Pensat per a | Webs, APIs, imatges, estàtics | Vídeo sota demanda i en directe, descàrregues massives |
| Volum típic | Qualsevol | Desenes de TB al mes en endavant |
| Extres | Els vistos en aquesta lliçó | Regles d'encaminament avançades, millor economia a gran escala |
Per a AlpinaShop, Cloud CDN és l'elecció correcta i ho continuarà sent. Si algun dia la secció de guies tècniques es converteix en una plataforma de vídeo amb milers d'hores i desenes de TB mensuals, aleshores sí que tocaria avaluar Media CDN.
Errors habituals i consells
- Creure que Cloud CDN és un producte a part i buscar on crear la "distribució". És una casella del backend del balancejador que ja tens.
- Activar
FORCE_CACHE_ALLal backend que serveix el lloc sencer. És la via més ràpida per servir-li a un client la cistella d'un altre. Si el necessites, fes-ho en un backend dedicat a rutes públiques. - Ignorar els paràmetres de campanya. Els
utm_*ifbclidfragmenten la clau de memòria cau i enfonsen la ràtio just el dia que arriba el trànsit. Configura la llista negra abans de la campanya, no després. - Confondre
max-ageambs-maxage. El segon només el llegeixen les memòries cau compartides i té prioritat a la vora. La parellamax-agebaix +s-maxagealt és la que vols per a l'HTML. - Posar
--client-ttlllarg en contingut amb URL estable. No pots invalidar el navegador de ningú. TTL de client llarg només amb noms immutables. - Fer servir la invalidació com a part del desplegament. És lenta, té quota i un
/*deixa l'origen al descobert. Versiona els noms. - Oblidar-se de
Set-Cookie. Una sessió de Flask tocada per accident en una vista de catàleg fa no desable tota la ruta i ningú no entén la ràtio baixa. Vary: User-Agent. Anul·la la memòria cau completament. Revisa quinVaryemet la teva aplicació i el teu CMS.- Desar els 5xx. Amb memòria cau negativa mal configurada, un desplegament amb una fallada de trenta segons es converteix en cinc minuts d'error per a tothom. Posa els 5xx a
0. - No activar
--serve-while-stale. És pràcticament gratis i converteix caigudes en degradacions. - Mesurar "a ull" des del navegador. El navegador té la seva pròpia memòria cau i t'enganyarà. Depura amb
curl -sIi amb els campscacheHit/cacheLookupdel registre. - Consell: prova des de diverses ubicacions. Un encert a Madrid no diu res de Varsòvia. Cada punt de presència té la seva pròpia memòria cau i el seu propi escalfament.
- Consell: separa el desable del que no ho és al mapa d'URL. És més fàcil raonar sobre
/estatico/*amb memòria cau d'un any i/cuenta/*sense memòria cau, que sobre un únic backend amb regles subtils.
Exercicis
Exercici 1 — Diagnosticar una ràtio d'encerts del 38 %
La Marta obre el tauler una setmana després d'activar el CDN i veu una ràtio del 38 % a /imagenes/*, quan n'esperava més del 90 %. Recopila aquestes dades:
curl -sIsobre una imatge concreta, dues vegades seguides: la segona resposta sí que portaAge: 61.- El registre mostra moltes peticions amb
cacheLookup: true,cacheHit: falsei URL acabades en?utm_source=.... - Altres peticions mostren
cacheLookup: falsesobre rutes/imagenes/promo/*. - El
Cache-Controlde les imatges de/imagenes/promo/éspublic, max-age=300.
Explica cada símptoma per separat i escriu les comandes que corregeixen la situació.
Exercici 2 — Política de memòria cau per a tres rutes noves
AlpinaShop afegeix tres rutes. Defineix per a cadascuna: mode de memòria cau, Cache-Control que ha d'emetre l'aplicació, TTL de client i de vora, i quins components ha de portar la clau de memòria cau.
/api/stock/<sku>— retorna JSON amb les unitats disponibles. Canvia cada pocs minuts i el consulten milers de visitants./estatico/app.4f2b9c.js— el bundle de JavaScript, amb hash al nom./cuenta/pedidos— l'històric de comandes del client autenticat.
Exercici 3 — Càlcul d'impacte d'una campanya
La campanya de tardor multiplicarà per cinc el trànsit d'imatges durant dues setmanes: de 2 TB/mes es passarà a un ritme equivalent a 10 TB/mes. Estima el cost mensual de sortida amb i sense CDN suposant una ràtio del 90 %, i respon: quin altre efecte, més important que el cost, té el CDN en aquesta campanya? Què configuraries abans que comenci?
Solucions
Solució 1
Són tres problemes diferents disfressats d'un:
a) La imatge concreta sí que es desa. L'Age: 61 de la segona petició ho demostra. Per tant la configuració base del backend de bucket és correcta i no cal tocar-la.
b) Els utm_* fragmenten la clau. Cada variant de campanya genera una entrada nova: cacheLookup: true, cacheHit: false és exactament la signatura d'aquest problema. Es corregeix excloent els paràmetres de la clau:
gcloud compute backend-buckets update bb-catalogo-imagenes \
--cache-key-query-string-blacklist=utm_source,utm_medium,utm_campaign,utm_term,utm_content,gclid,fbclid,msclkidc) /imagenes/promo/* ni tan sols es busca a la memòria cau. cacheLookup: false significa que la petició no va arribar a consultar-se. Si aquestes rutes estan encaminades al mapa d'URL cap a un backend diferent (per exemple un bs-promociones que es va crear per a la campanya), aquest backend no té --enable-cdn. Es comprova i es corregeix així:
gcloud compute url-maps describe alpinashop-url-map \
--format="yaml(pathMatchers)"
gcloud compute backend-services update bs-promociones --global \
--enable-cdn --cache-mode=CACHE_ALL_STATIC \
--default-ttl=3600 --max-ttl=86400d) El max-age=300 de les promocions no és un error, però és incoherent: si les imatges de promoció també tenen nom immutable, haurien de portar un any. Si no el tenen, la correcció de fons és adoptar el mateix conveni de noms de la resta del catàleg, no apujar el TTL a cegues.
Solució 2
| Ruta | Mode | Cache-Control de l'aplicació |
Vora | Client | Clau de memòria cau |
|---|---|---|---|---|---|
/api/stock/<sku> |
CACHE_ALL_STATIC (amb capçalera explícita) |
public, max-age=0, s-maxage=60 |
60 s | 0 s | Protocol + amfitrió + ruta. Sense paràmetres de consulta |
/estatico/app.4f2b9c.js |
CACHE_ALL_STATIC |
public, max-age=31536000, immutable |
1 any | 1 any | Només ruta; ignorar tota la consulta |
/cuenta/pedidos |
Irrellevant | private, no-store |
Mai | Mai | No aplica |
Raonament:
- Estoc. És JSON, no el desaria
CACHE_ALL_STATICper si sol, així que la capçalera explícita és obligatòria.s-maxage=60ambmax-age=0és la combinació clau: la vora absorbeix milers de peticions per minut i el navegador sempre pregunta, de manera que un canvi d'estoc es veu com a molt un minut tard. Seixanta segons de desfasament en un comptador d'unitats és acceptable; un minut de dades obsoletes al pas de pagament no ho seria, i per això la comprovació definitiva d'estoc es fa en el moment de confirmar la comanda, no llegint aquesta API. - Bundle JS. Nom amb hash → cas de llibre: un any a tot arreu. Publicar una versió nova és publicar un altre nom.
- Històric de comandes.
private, no-storeexplícit. I molt important: mai en un backend ambFORCE_CACHE_ALL. A més portaràSet-Cookiede sessió, cosa que reforça que no es pugui desar a la memòria cau, però no cal dependre d'aquest efecte col·lateral: la capçalera es posa a propòsit.
Solució 3
Cost orientatiu amb 10 TB/mes:
- Sense CDN: 10 000 GB × ~0,11 $/GB ≈ 1 100 $/mes.
- Amb CDN al 90 %: 9 000 GB des de la memòria cau × ~0,08 $/GB ≈ 720 $, més 1 000 GB d'emplenament (≈10-40 $) i les cerques (uns pocs dòlars) ≈ 740-770 $/mes. Estalvi aproximat del 30 %.
L'efecte més important no és el cost, és la càrrega a l'origen. Sense CDN, aquest trànsit multiplicat per cinc l'absorbeixen el MIG i el bucket: l'autoescalat pujaria cap al límit de 10 instàncies, el cost de còmput es dispararia i la latència empitjoraria justament el dia de més vendes. Amb un 90 % d'encerts, l'origen només veu el 10 % del trànsit d'imatges; el MIG es manté a prop de la seva mida habitual i l'experiència del client és millor precisament quan més importa.
Què configurar abans que comenci la campanya:
- La llista negra de paràmetres
utm_*,gclidifbclid. Sense això, la campanya se saboteja a si mateixa. --serve-while-stale=86400abs-catalogo-web, perquè un problema de l'origen sota càrrega degradi en lloc de tombar la botiga.- Memòria cau negativa amb els 5xx a
0i els 404 a un parell de minuts. - Verificar amb
curl -sIque les imatges de la campanya retornenAgea la segona petició, abans del llançament. - Un tauler amb la ràtio d'encerts i
cacheFillBytesvisible durant la campanya, i una alerta si la ràtio cau per sota del 80 %. - No invalidar la memòria cau durant la campanya tret d'emergència real: seria regalar el pic de trànsit a l'origen.
Conclusió
AlpinaShop ja no envia els mateixos bytes a Lisboa una vegada i una altra. Has entès que Cloud CDN no és un producte a part sinó una propietat del balancejador que vas construir a 03-02: s'activa amb --enable-cdn sobre bb-catalogo-imagenes i sobre bs-catalogo-web, sense tocar el DNS, la IP, el certificat ni l'aplicació, i aprofitant els mateixos registres i les mateixes mètriques.
Saps triar el mode de memòria cau amb criteri —CACHE_ALL_STATIC com a opció assenyada per defecte, USE_ORIGIN_HEADERS quan l'aplicació ja mana de veritat, i FORCE_CACHE_ALL només en backends dedicats a contingut públic—, i per què una resposta amb private, no-store o Set-Cookie no s'ha de desar mai. Domines la clau de memòria cau, que és on es guanya o es perd la ràtio: has exclòs utm_*, gclid i fbclid perquè la campanya de tardor no destrueixi la feina, i coneixes l'equilibri entre llista negra i llista blanca. Has ordenat els cinc valors de TTL —max-age, s-maxage, --default-ttl, --max-ttl, --client-ttl— i has vist per què els noms immutables adoptats a 02-02 són la peça que fa innecessària la invalidació, aquesta operació lenta, amb quota i capaç de deixar l'origen a la intempèrie. Saps servir contingut de pagament des de la memòria cau amb galetes signades, desar els 404 però mai els 500, continuar servint durant una caiguda amb --serve-while-stale, i depurar una fallada de memòria cau llegint Age, Via i els camps cacheHit/cacheLookup del registre. I tens un càlcul honest de l'estalvi: al voltant d'un 30 % de la factura de sortida, molt més en càrrega de l'origen, i una millora de latència que es nota a cada fitxa de producte.
Amb això, la vora d'AlpinaShop està completa pel que fa al rendiment: xarxa, balanceig i memòria cau. Però hi ha un problema que hem anat ajornant lliçó rere lliçó. A 03-01 vam dir que el tallafoc no distingeix persones i que "només la Lucía i la Marta poden entrar per SSH" es resol en un altre lloc. A 02-02 vam limitar la Lucía a un prefix del bucket amb una condició que no vam explicar. En aquesta mateixa lliçó hem dit que invalidar la memòria cau requereix un rol elevat que no convé repartir. Tot això apunta al mateix lloc: qui pot fer què, sobre quin recurs. A la lliçó següent, 03-04, Gestió d'identitat i accés (IAM), construïm el model de permisos real d'AlpinaShop: grups en lloc de persones, rols predefinits en lloc d'Editor, un rol personalitzat a mida per a la Lucía, comptes de servei sense claus descarregades, suplantació, condicions, i les eines per respondre a la pregunta més freqüent de qualsevol administrador del núvol: "per què aquest usuari no pot fer això?".
Curs de Google Cloud Platform (GCP)
Mòdul 1: Introducció a Google Cloud Platform
- Què és Google Cloud Platform?
- Configuració del teu compte de GCP
- Descripció general de la consola de GCP
- Projectes, jerarquia de recursos i facturació
- Regions, zones i model de responsabilitat compartida
- Cloud Shell i la CLI de gcloud
Mòdul 2: Serveis principals de GCP
- Compute Engine: màquines virtuals a Google Cloud
- Cloud Storage: emmagatzematge d'objectes
- Cloud SQL: bases de dades relacionals gestionades
- App Engine: plataforma com a servei
- Google Kubernetes Engine (GKE)
- Bases de dades NoSQL: Firestore, Bigtable i Spanner
- Com triar el servei de còmput adequat
Mòdul 3: Xarxes i seguretat
- Xarxes VPC
- Balanceig de càrrega al núvol
- Cloud CDN
- Gestió d'identitat i accés (IAM)
- Cloud Armor
- Secrets i xifratge: Secret Manager i Cloud KMS
- Cloud DNS, certificats TLS i publicació segura de serveis
Mòdul 4: Dades i anàlisi
- BigQuery: el magatzem de dades analític
- Cloud Dataflow: processament de dades per lots i en temps real
- Cloud Dataproc: Spark i Hadoop gestionats
- Cloud Pub/Sub: missatgeria asíncrona
- Cloud Data Fusion: integració de dades sense codi
- Orquestració de pipelines amb Cloud Composer i Workflows
- Govern de les dades i taulers amb Dataplex i Looker Studio
Mòdul 5: Aprenentatge automàtic i IA
- Vertex AI: la plataforma d'aprenentatge automàtic de GCP
- AutoML: models a mida sense escriure codi
- TensorFlow a GCP: entrenament i servei de models
- API de llenguatge natural
- API de visió
- IA generativa a Vertex AI: models Gemini i incrustacions
- MLOps: del model al producte amb Vertex AI Pipelines
Mòdul 6: DevOps i monitoratge
- Cloud Build: integració contínua a GCP
- Cloud Source Repositories i gestió del codi font
- Cloud Functions: funcions sense servidor
- Cloud Monitoring (abans Stackdriver): mètriques, taulers i alertes
- Cloud Deployment Manager i infraestructura com a codi nativa
- Cloud Logging i Cloud Trace: registres, traces i diagnòstic
- Terraform a GCP: infraestructura com a codi a la pràctica
Mòdul 7: Temes avançats de GCP
- Híbrid i multinúvol amb Anthos
- Computació sense servidor amb Cloud Run
- Xarxes avançades: VPC compartida, aparellament i connectivitat híbrida
- Bones pràctiques de seguretat
- Gestió i optimització de costos
- Fiabilitat: SLO, alta disponibilitat i recuperació de desastres
- Govern a escala: organització, polítiques i auditoria
