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

  1. Què és una CDN i quin problema resol exactament
  2. La xarxa de punts de presència de Google
  3. On viu Cloud CDN: sobre el balancejador, no al costat
  4. Activació sobre el backend de bucket i sobre el servei de backend
  5. Modes de memòria cau: CACHE_ALL_STATIC, USE_ORIGIN_HEADERS, FORCE_CACHE_ALL
  6. La clau de memòria cau i el desastre dels paràmetres utm_*
  7. TTL: qui decideix quant viu un objecte a la vora
  8. Noms immutables: l'estratègia que sosté tot l'anterior
  9. Invalidació de memòria cau: el botó d'emergència
  10. Contingut privat a la memòria cau: URL i galetes signades
  11. Memòria cau negativa, contingut obsolet i altres opcions útils
  12. Mesurament: ràtio d'encerts, capçaleres Age i Via, i depuració amb curl
  13. L'estalvi real: latència i euros
  14. Quan Cloud CDN no n'hi ha prou: Media CDN

  1. 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ó.

  1. 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
  1. El client resol alpinashop.example i 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.
  2. 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.
  3. 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.

  1. 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-ip i 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.

  1. 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-caching

Què 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 propis Cache-Control.
  • --client-ttl: el max-age mà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.

  1. Modes de memòria cau: CACHE_ALL_STATIC, USE_ORIGIN_HEADERS, FORCE_CACHE_ALL

El 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_STATIC considera "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 propi Cache-Control. És just el que volem: control explícit sobre l'HTML.
  • En qualsevol mode, una resposta amb Cache-Control: private, no-store o Set-Cookie no es desa (tret de FORCE_CACHE_ALL). Aquesta és la xarxa de seguretat que impedeix el desastre clàssic.
  • FORCE_CACHE_ALL sobre 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 amb Cache-Control d'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 resposta

Regla 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.

  1. La clau de memòria cau i el desastre dels paràmetres 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 (http o https).
  • 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,color

Dues 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 query

Altres 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.

  1. 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:

  1. El CDN llegeix max-age = 31 536 000 s.
  2. Aplica --max-ttl: desa l'objecte 24 hores, no un any.
  3. Aplica --client-ttl: reescriu la capçalera cap al client a max-age=3600, una hora.

La combinació s-maxage alt + max-age baix és la més útil i la menys usada:

Cache-Control: public, max-age=60, s-maxage=86400

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.

  1. 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=31536000

Aquí 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.

  1. 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=5

Observa 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à.

  1. 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.txt

Galeta 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ó.

  1. 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-debug

A 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.

  1. Mesurament: ràtio d'encerts, capçaleres Age i Via, i depuració amb curl

Sense 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:

curl -sI https://alpinashop.example/imagenes/productos/MOCH-2210/web/frontal-800.a3f9c1.webp
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 de GET, capçalera Authorization…).
  • 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:

  1. --enable-cdn és realment al backend correcte (describe, no memòria).
  2. La resposta no porta Cache-Control: private, no-store ni no-cache.
  3. La resposta no porta Set-Cookie. És la causa més freqüent en aplicacions Flask: n'hi ha prou que es toqui session a la vista perquè s'emeti una galeta i la resposta deixi de ser desable.
  4. No hi ha un Vary amb capçaleres d'alta cardinalitat (User-Agent, Cookie).
  5. La petició és GET i no porta Authorization.
  6. La clau de memòria cau no està fragmentada per paràmetres de consulta.
  7. La resposta porta Content-Length o 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

  1. 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.

  1. 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_ALL al 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_* i fbclid fragmenten 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-age amb s-maxage. El segon només el llegeixen les memòries cau compartides i té prioritat a la vora. La parella max-age baix + s-maxage alt és la que vols per a l'HTML.
  • Posar --client-ttl llarg 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 quin Vary emet 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 -sI i amb els camps cacheHit/cacheLookup del 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 -sI sobre una imatge concreta, dues vegades seguides: la segona resposta sí que porta Age: 61.
  • El registre mostra moltes peticions amb cacheLookup: true, cacheHit: false i URL acabades en ?utm_source=....
  • Altres peticions mostren cacheLookup: false sobre rutes /imagenes/promo/*.
  • El Cache-Control de les imatges de /imagenes/promo/ és public, 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.

  1. /api/stock/<sku> — retorna JSON amb les unitats disponibles. Canvia cada pocs minuts i el consulten milers de visitants.
  2. /estatico/app.4f2b9c.js — el bundle de JavaScript, amb hash al nom.
  3. /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,msclkid

c) /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=86400

d) 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:

  1. Estoc. És JSON, no el desaria CACHE_ALL_STATIC per si sol, així que la capçalera explícita és obligatòria. s-maxage=60 amb max-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.
  2. Bundle JS. Nom amb hash → cas de llibre: un any a tot arreu. Publicar una versió nova és publicar un altre nom.
  3. Històric de comandes. private, no-store explícit. I molt important: mai en un backend amb FORCE_CACHE_ALL. A més portarà Set-Cookie de 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:

  1. La llista negra de paràmetres utm_*, gclid i fbclid. Sense això, la campanya se saboteja a si mateixa.
  2. --serve-while-stale=86400 a bs-catalogo-web, perquè un problema de l'origen sota càrrega degradi en lloc de tombar la botiga.
  3. Memòria cau negativa amb els 5xx a 0 i els 404 a un parell de minuts.
  4. Verificar amb curl -sI que les imatges de la campanya retornen Age a la segona petició, abans del llançament.
  5. Un tauler amb la ràtio d'encerts i cacheFillBytes visible durant la campanya, i una alerta si la ràtio cau per sota del 80 %.
  6. 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

Mòdul 2: Serveis principals de GCP

Mòdul 3: Xarxes i seguretat

Mòdul 4: Dades i anàlisi

Mòdul 5: Aprenentatge automàtic i IA

Mòdul 6: DevOps i monitoratge

Mòdul 7: Temes avançats de GCP

Mòdul 8: Projecte final

© Copyright 2026. Tots els drets reservats