CodeGym /Corsi /ChatGPT Apps /Scalabilità e deploy: bilanciamento, cluster di servizi b...

Scalabilità e deploy: bilanciamento, cluster di servizi backend, blue/green e canary

ChatGPT Apps
Livello 16 , Lezione 3
Disponibile

1. Di cosa tratta questa lezione e perché è importante

Immaginate di aver lasciato GiftGenius allo stadio in cui vive in solitaria su Vercel: una sola istanza di MCP‑gateway (che espone MCP verso l’esterno e chiama i vostri servizi REST), un backend per gli agenti, tutto «funziona in qualche modo». È ancora accettabile per un pet project e i primi 100 utenti.

Ma non appena OpenAI aggiunge la vostra App allo Store e finisce all’improvviso in evidenza in home prima di Natale, «un solo gateway sulla porta 3000» diventa una storia molto triste: coda di invocazioni dei tool, timeout, errori 500, calo del rating nello Store e email dal marketing del tipo «perché era tutto giù nel picco delle vendite?».

Il nostro obiettivo in questa lezione è imparare a pensare a GiftGenius (e a qualsiasi ChatGPT App) come a un sistema composto da molte istanze identiche dietro un bilanciatore. Inoltre, capire strategie di rilascio accorte e uno schema chiaro di «come tornare indietro se qualcosa va storto».

2. Scalabilità orizzontale e design stateless

Partiamo dall’idea di base: se il vostro MCP Gateway o un servizio backend interno conserva stato importante nella memoria del singolo processo, è praticamente impossibile scalarlo correttamente in orizzontale.

Scalabilità verticale vs orizzontale

Per prima cosa, allineiamo la terminologia.

Scalabilità verticale è quando «potenziate» un singolo server: più CPU, più RAM. È rapido, talvolta economico all’inizio, ma ha un limite rigido e rende quell’istanza un single point of failure: se quel «mostro» cade, cade tutto.

Scalabilità orizzontale è quando avviate più istanze del servizio dietro un bilanciatore. Ogni istanza è relativamente piccola, non conserva niente di critico in memoria e lo stato risiede in storage esterni (Postgres, Redis, object storage). Si possono aggiungere o togliere istanze liberamente in base al carico.

Per MCP Gateway e i servizi backend (Gift REST API, Commerce REST API, Analytics Service / REST API, ecc.) la scalabilità orizzontale è di fatto obbligatoria: ChatGPT può improvvisamente indirizzare verso di voi molto più traffico (stagionalità, promozione nello Store, un TikTok virale), e dovete poter semplicemente aggiungere istanze, non «pregare che un solo server regga».

Che cos’è un servizio stateless nel contesto di MCP Gateway e dei backend

Perché la scalabilità orizzontale funzioni, il servizio deve essere il più possibile stateless.

Stateless nel nostro contesto significa:

  • il servizio non conserva in memoria stato utente unico e di lunga durata da cui dipende la logica di business;
  • qualsiasi stato importante risiede in un DB esterno, in una coda, in una cache, in uno storage tipo S3;
  • se una specifica istanza cade, un’altra può continuare a servire l’utente «riprendendo» il contesto dallo storage esterno.

Per GiftGenius ciò significa che:

  • la cronologia delle selezioni di regali dell’utente, i suoi like/dislike e il carrello stanno, ad esempio, in Postgres;
  • le code di task lunghi (generazione massiva di selezioni, invio di raccolte via email) stanno in un broker tipo Redis/Cloud Queue;
  • se esiste un servizio separato per workflow di agenti complessi, conserva checkpoint e memoria di lunga durata nel proprio store, non nella RAM di un singolo processo.

L’istanza del MCP Gateway o di qualsiasi servizio backend diventa «bestiame, non un animale domestico»: la si può terminare e ricreare senza pietà, senza perdere dati di business.

Mini‑esempio: spostare lo stato dalla memoria allo storage esterno

Immaginiamo che tempo fa abbiate implementato un MCP‑tool molto semplice add_to_cart, che tramite il gateway chiama la logica interna e quest’ultima conserva il carrello in memoria del processo (sì, a volte nelle demo si fa — ed è normale finché capite che in produzione non va bene):

// MALE: il carrello è in memoria del processo del servizio backend
const inMemoryCarts = new Map<string, string[]>();

export async function addToCart(userId: string, sku: string) {
  const cart = inMemoryCarts.get(userId) ?? [];
  cart.push(sku);
  inMemoryCarts.set(userId, cart);
  return cart;
}

Qui la scalabilità orizzontale è impossibile: una richiesta andrà all’istanza A, un’altra all’istanza B, e l’utente avrà carrelli diversi.

La variante corretta è spostare il carrello in un DB o in una cache esterna. In modo convenzionale (fortemente semplificato):

// BENE: carrello in uno storage esterno
import { db } from "./db";

export async function addToCart(userId: string, sku: string) {
  await db.cartItems.insert({ userId, sku }); // semplificato
  const cart = await db.cartItems.findMany({ where: { userId } });
  return cart;
}

Ora non importa quale istanza del servizio backend stia elaborando la richiesta arrivata tramite il gateway: il carrello è unico per tutti.

3. Bilanciamento del carico: come il traffico raggiunge i cluster dei servizi backend

Non appena avete più di un’istanza di un servizio, serve qualcuno che distribuisca le richieste tra di esse. È come il gestore degli ordini in una pizzeria affollata: molti corrieri, molti clienti, senza logica regna il caos.

L4 vs L7, e perché ci interessa soprattutto L7

Un bilanciatore può operare a livelli diversi:

  • L4 (TCP/UDP) inoltra semplicemente i byte dal client a uno dei backend, senza capire davvero quale protocollo stia passando;
  • L7 (HTTP) capisce che si tratta di una richiesta HTTP, sa guardare il path, gli header, i cookie, talvolta persino il body.

Per l’architettura delle ChatGPT App con MCP Gateway e servizi REST, quasi sempre serve un bilanciatore L7: tutto parla via HTTP/SSE, e vogliamo poter instradare per path, dominio, header (ad esempio, per i rilasci canary) e fare health check.

Health check e rimozione delle istanze «malate» dalla rotazione

Il bilanciatore dovrebbe verificare periodicamente che le istanze siano vive. Il modo più semplice è avere un endpoint GET /health o /readyz che restituisca 200 OK se tutto va bene.

In un servizio Node/TypeScript che funge da MCP Gateway o backend, l’health check può essere fatto così:

// apps/gateway/src/http/health.ts
import { type Request, type Response } from "express";

export function healthHandler(req: Request, res: Response) {
  res.json({
    status: "ok",
    version: process.env.RELEASE_ID ?? "dev",
  });
}

Il bilanciatore interroga /health ogni N secondi. Se le risposte iniziano a essere 5xx o a scadere per timeout, quell’istanza viene esclusa dalla rotazione e il nuovo traffico non ci arriva più.

Particolarità per lo streaming / SSE

Il MCP Gateway spesso lavora tramite SSE (Server‑Sent Events), soprattutto se usate lo streaming dei risultati parziali. Il bilanciatore deve:

  • supportare connessioni HTTP di lunga durata;
  • saper tenere conto di tali connessioni nella scelta dell’istanza (alcuni LB considerano il numero di connessioni attive, non solo gli RPS).

È importante perché una singola invocazione «loquace» di un tool che streamma testo per 2 minuti resta come connessione attiva. Se tali connessioni sono troppe sulla stessa istanza, questa va «alleggerita» temporaneamente — inviando le nuove connessioni alle altre.

4. Cluster dei servizi backend: separare per compiti, non tutto in un unico calderone

Il passo successivo logico è smettere di pensare a un unico «grande servizio backend» e dividere il sistema in più cluster in base alla natura del carico e alla criticità.

Esempio di architettura di GiftGenius per cluster

Tutto quanto raccolto nel modulo 16 suggerisce questo schema per GiftGenius:

Cluster Cosa fa Caratteristiche del carico Particolarità di scalabilità
A: Gift REST API / strumenti leggeri Ricerca prodotti, formattazione di liste, calcoli semplici RPS elevato, risposte brevi (< 500 ms), poca CPU Scaliamo su CPU/RPS, molte istanze piccole
B: Agents / servizio REST per job pesanti Chiamate LLM, workflow complessi, generazione di auguri RPS basso, risposte lunghe (10s–2min), IO‑heavy Scaliamo in base alla lunghezza della coda dei task, si possono usare worker
C: Commerce REST API / ACP Checkout, integrazione con il provider di pagamenti, ACP Affidabilità critica, SLO stringenti Deploy separato, modifiche lente e prudenti

In sostanza, è l’implementazione del pattern bulkheads («paratie»): se il cluster B all’improvviso inizia a «bruciare CPU a colpi di token» durante la generazione di testi complessi, il cluster C dei pagamenti continua a funzionare, perché ha il proprio pool di risorse e la propria scalabilità.

Come appare attraverso il Gateway

Il MCP Gateway, descritto nella prima lezione del modulo, vede tutto il traffico MCP in ingresso e lo instrada ai cluster backend. Approssimativamente così:

  • invocazioni dei tool list_gifts, suggest_gifts → cluster A (Gift REST API);
  • invocazioni dei tool generate_greeting_card o workflow di agenti complessi → cluster B (servizio REST degli agenti o worker);
  • strumenti create_order, confirm_payment → cluster C (Commerce REST API).

Dietro può esserci già un bilanciatore comune o più bilanciatori (ad esempio, un L7‑LB separato davanti al commerce, per isolare ulteriormente).

Possiamo rappresentare lo schema generale:

flowchart LR
    ChatGPT((ChatGPT))
    GW[MCP Gateway]
    LBA[LB Gift API Cluster A]
    LBB[LB Agents/Workers Cluster B]
    LBC[LB Commerce API Cluster C]

    A1[Gift REST API A-1]
    A2[Gift REST API A-2]
    B1[Agents Service B-1]
    B2[Agents Service B-2]
    C1[Commerce REST API C-1]
    C2[Commerce REST API C-2]

    ChatGPT --> GW
    GW -->|tools: gifts| LBA
    GW -->|agents workflows| LBB
    GW -->|commerce| LBC

    LBA --> A1
    LBA --> A2
    LBB --> B1
    LBB --> B2
    LBC --> C1
    LBC --> C2

Lo schema è leggermente idealizzato, ma riflette il principio chiave: tipi di carico diversi — cluster backend diversi dietro un unico MCP Gateway.

5. Strategie di deploy: perché servono blue/green e canary

Passiamo ora a come aggiornare il tutto in modo che gli utenti non se ne accorgano e voi possiate dormire sonni tranquilli.

Anti‑esempio: deploy «sopra l’ambiente di produzione»

La strategia più semplice e più pericolosa: prendete il cluster in produzione (ad esempio, il cluster Gift REST API A), avviate la nuova immagine sopra la vecchia, sostituite i container o riavviate i processi.

Quali problemi ci sono qui:

  • mentre alcune istanze sono già nuove e altre sono vecchie, il sistema può comportarsi in modo imprevedibile (soprattutto se lo schema del DB è cambiato);
  • se qualcosa va storto, il rollback è un nuovo deploy «com’era», che può richiedere minuti;
  • al momento del deploy è facile avere un breve downtime in cui nessuna istanza è ancora salita.

In Kubernetes e nei PaaS questo è mitigato in parte dai rolling update, ma l’idea resta: senza una strategia chiara c’è molta «zona grigia» in cui versioni diverse del codice elaborano il traffico contemporaneamente.

Deploy blue/green: due ambienti e switch istantaneo

Blue/Green è un approccio in cui coesistono due ambienti quasi identici: Blue (produzione attuale) e Green (nuova versione).

In sintesi il processo è questo:

  1. Distribuite la nuova versione (v2) nell’ambiente Green: è lo stesso insieme di gateway + cluster backend, solo senza traffico reale per ora.
  2. Eseguite su Green tutti i test necessari: test automatici, smoke test, verifiche manuali tramite ChatGPT Dev Mode.
  3. Al momento del rilascio cambiate la configurazione del bilanciatore/instradamento in modo che il 100% del traffico di produzione vada su Green.
  4. Blue continua a vivere accanto come «pista alternativa». Se qualcosa va storto, reindirizzate il traffico indietro in pochi secondi.

Per GiftGenius può essere così: avete mcp-gateway-blue.example.com e mcp-gateway-green.example.com. La ChatGPT App in produzione «punta» all’endpoint MCP ufficiale (gateway) e, al momento del rilascio, modificate la configurazione DNS/LB in modo che il nome di dominio mcp-gateway.example.com punti al green.

Pro:

  • switch istantaneo avanti/indietro;
  • qualsiasi problema si può risolvere dopo il rollback;
  • non esiste lo stato «metà cluster nuovo, metà cluster vecchio».

Contro:

Durante il rilascio dovete mantenere due ambienti completi, cioè pagare risorse ×2. Per questo questa strategia si applica più spesso ai servizi backend critici — ad esempio il cluster commerce C e lo stesso MCP Gateway, dove rompere il checkout e il punto d’ingresso non è mai accettabile.

Rilasci canary: la piccola «canarina» nella miniera di carbone

Un rilascio Canary è un’alternativa più economica: non avviate due produzioni complete, ma rilasciate la nuova versione gradualmente su una piccola porzione di traffico, osservandola con attenzione.

Scenario indicativo:

  1. Fate il deploy della versione v2 del cluster Gift REST API A nello stesso pool o in un piccolo pool canary separato.
  2. Configurate il bilanciatore o il MCP Gateway in modo che, per esempio, 1% delle invocazioni dei tool legati ai regali vada su v2, e il 99% su v1.
  3. Osservate le metriche: tasso di errore, latenza, metriche di business specifiche (conversion, checkout riusciti).
  4. Se tutto va bene, aumentate gradualmente la quota: 1% → 5% → 10% → 50% → 100%. Se va male — rollback immediato.

Nel contesto delle ChatGPT Apps il canary è utile non solo per il codice, ma anche per gli esperimenti con i prompt: una nuova versione del system prompt per il servizio degli agenti può cambiare radicalmente il comportamento, ed è meglio verificarla prima su un piccolo campione di utenti.

Il Gateway o il LB possono decidere quale richiesta sia «canary» in base a vari segnali:

  • in modo casuale (ad esempio, 1% di tutte le richieste);
  • per userId (una parte di utenti entra nell’esperimento in modo persistente);
  • tramite un header o un cookie speciale (per test interni).

Piccolo esempio di logica di instradamento in pseudo‑TypeScript (per illustrare l’idea nel gateway):

// Pseudocodice nel Gateway: canary casuale semplice 5%
function routeToGiftBackendCluster(ctx: { userId?: string | null }) {
  const rnd = Math.random();
  if (rnd < 0.05) {
    return "gift-api-v2"; // canary
  }
  return "gift-api-v1";   // stable
}

Nella realtà, ovviamente, non farete questo con Math.random() nel codice di runtime, ma esternalizzerete le regole in configurazioni/feature flag. La logica, però, è simile: una parte del traffico va alle versioni canary del servizio backend, il resto alla stabile.

6. Il rollback come parte obbligatoria della strategia

Tempo fa ho interiorizzato una buona regola: il rollback deve essere più rapido del fix.

Ciò significa che se dopo il rilascio iniziano a comparire errori e gli utenti scrivono «si rompe tutto», non serve fare gli eroi correggendo il bug in produzione. Bisogna premere il grande pulsante rosso «rollback».

Nel contesto di piattaforme come Vercel (su cui abbiamo già distribuito la parte Next.js di GiftGenius) questo è molto naturale: ogni deploy è un artefatto immutabile e Vercel consente un rollback rapido alla versione precedente.

Per MCP Gateway e i cluster backend distribuiti su Kubernetes o un altro orchestratore, questo ruolo è svolto da kubectl rollout undo: tornate al set precedente di pod e immagini.

La cosa principale è loggare e mostrare la versione che sta servendo il traffico. Ad esempio, potete:

  • aggiungere la version a /health e ad altri endpoint diagnostici (l’abbiamo già fatto sopra);
  • trasmettere l’identificatore di rilascio nei log tramite header (per esempio, X-Release-Id).

Mini‑esempio: una route API di Next.js che restituisce la versione della build per l’ispezione della ChatGPT App all’interno del widget:

// apps/web/app/api/version/route.ts
export async function GET() {
  return Response.json({
    version: process.env.RELEASE_ID ?? "dev",
    builtAt: process.env.BUILT_AT ?? "unknown",
  });
}

Un endpoint del genere è utile anche per il debug: potete chiedere all’istanza in produzione quale versione stia girando esattamente, senza domandarvi «ma è davvero uscito l’ultimo build?».

7. Pianificazione della capacità: quante istanze servono per GiftGenius

Abbiamo già discusso come rilasciare nuove versioni in sicurezza (blue/green, canary) e come fare rollback rapidamente in caso di problemi. Resta una domanda pratica: quante istanze e quali cluster tenere in produzione per reggere il traffico reale senza dissanguarvi?

Senza esagerare con le formule, ma un po’ serve. Lo scaling va legato a carico ed economia: quante richieste al giorno/secondo, quante chiamate LLM pesanti, quanto costa in denaro.

Per semplicità, pensiamo per ordini di grandezza:

  • con 10k richieste al giorno a GiftGenius (circa 0,1 RPS in media) ve la cavate con una‑due istanze di MCP Gateway e un paio di istanze di Gift REST API/worker degli agenti;
  • con 100k richieste al giorno (12 RPS medi, con picchi più alti) conviene avere 35 istanze del gateway + il cluster di Gift REST API, un cluster B separato per gli agenti pesanti e un cluster commerce dedicato;
  • con 1M richieste al giorno (decine di RPS, carichi di picco durante le feste) serviranno sicuramente cluster, risorse dedicate agli agenti LLM, cache aggressiva e uno strato edge (di cui c’è una lezione a parte).

Non sono numeri rigorosi, ma un modo per costringervi a stimare l’ordine di grandezza del carico e a pensare in anticipo: dove sono i colli di bottiglia, come scalerete e quanto costerà.

Per GiftGenius è particolarmente importante prepararsi alle festività: Capodanno, Natale, San Valentino, Black Friday. Il carico può aumentare di molte volte, e la vostra sistema dovrebbe reggerlo.

8. Mini‑esempio pratico: evoluzione del deploy di GiftGenius

Per mettere tutto insieme, disegniamo una semplice evoluzione del deploy di GiftGenius.
Applicheremo in sequenza tutto ciò di cui abbiamo parlato: design stateless di gateway e servizi backend, bilanciamento del carico, cluster separati e strategie di rilascio (blue/green, canary).

Livello base: un gateway + backend su Vercel/Kubernetes

A un certo punto del corso avete già fatto questo: un’applicazione Next.js con Apps SDK su Vercel, dentro la quale vivono sia l’endpoint MCP sia la logica backend semplice (Gift/Commerce) in un unico servizio. Abbastanza monolitico.

I pro sono chiari: semplice, economico, pochi punti in cui sbagliare.

Il contro è uno, ma critico: non scala per traffico serio e tollera male gli aggiornamenti.

Livello 2: gateway MCP separato + più cluster backend

Il passo successivo:

  • estraete il MCP Gateway in un servizio separato (Node/Go/NGINX+Lua, non importa);
  • avviate più istanze di Gift REST API (cluster A) e più worker/servizi per gli agenti (cluster B);
  • per il commerce dedicate un servizio separato (cluster C), possibilmente con un DB/infrastruttura a parte.

Già qui entrano in gioco bilanciamento L7, health check e, ove possibile, scalabilità orizzontale.

Livello 3: strategie di rilascio

A questo livello aggiungete:

  • Blue/Green per il cluster commerce C (e, se volete, per il MCP Gateway), così che checkout e autorizzazione siano il più stabili possibile;
  • rilasci Canary per i cluster Gift REST API e per il servizio degli agenti, per sperimentare serenamente con nuove versioni dei tool e degli agenti senza rischiare di abbattere tutta la produzione.

Schematicamente:

flowchart LR
    ChatGPT((ChatGPT))
    GWBlue[Gateway Blue]
    GWGreen[Gateway Green]
    LB[Traffic Switch]

    subgraph Prod
      LB --> GWBlue
      LB -.canary,% .-> GWGreen
    end

    ChatGPT --> LB

Nella realtà può essere un po’ più complesso (Blue/Green solo per commerce, canary solo per i cluster gift), ma l’idea è chiara: sapete sempre quale versione va dove, e per ChatGPT continua a sembrare un unico punto d’ingresso MCP (gateway).

9. Piccoli frammenti di codice per versioning e diagnostica

Abbiamo già visto l’health endpoint e /api/version. Aggiungiamo un altro esempio su come loggare versione e cluster nel handler di un MCP‑tool lato gateway, per poi «incrociare» facilmente le metriche.

Immaginiamo il tool suggest_gifts, implementato come endpoint REST nel Gift REST API e invocato tramite gateway:

import { type McpToolHandler } from "@modelcontextprotocol/sdk";

export const suggestGifts: McpToolHandler<{
  occasion: string;
  budget: number;
}> = async ({ input, meta }) => {
  const releaseId = process.env.RELEASE_ID ?? "dev";
  const clusterId = process.env.CLUSTER_ID ?? "gift-api-A";

  console.log("[suggest_gifts]", {
    releaseId,
    clusterId,
    userId: meta.userId,
    occasion: input.occasion,
  });

  // qui il MCP Gateway, in base alla tabella di instradamento, chiama il Gift REST API,
  // e lo strumento resta un sottile wrapper sopra la chiamata REST
  return {
    content: [{ type: "text", text: "Gift ideas..." }],
  };
};

Qui noi:

  • leggiamo RELEASE_ID e CLUSTER_ID dalle variabili d’ambiente;
  • li scriviamo nei log strutturati;
  • poi è facile usarli per l’analisi: «su quale versione/cluster stiamo avendo più errori?».

Dal punto di vista della ChatGPT App è tutto trasparente, ma per voi come sviluppatori è un grande vantaggio, soprattutto in combinazione con canary/blue‑green.

10. Errori tipici durante la scalabilità e il deploy di una ChatGPT App

Errore n. 1: conservare lo stato di sessione/utente in memoria nel processo del gateway o del backend.
Questo approccio uccide la scalabilità orizzontale: non appena avete una seconda istanza, lo stato si «stratifica» tra di esse. È particolarmente rischioso conservare in memoria il carrello, i risultati di ricerca o l’avanzamento del workflow. Tutto ciò deve vivere in uno storage esterno — DB, cache o store specializzato per lo stato dell’agente.

Errore n. 2: pensare che «un server potente» basti.
La scalabilità verticale è comoda all’inizio, ma funziona male con una crescita reale: c’è un limite fisico alla macchina, un processo diventa single point of failure, e ChatGPT può portare picchi di traffico imprevedibili. Per MCP Gateway e i cluster backend serve quasi sempre un design stateless e più istanze dietro un bilanciatore.

Errore n. 3: rilasciare nuove versioni «sopra la produzione» senza una strategia chiara.
Se aggiornate semplicemente container/processi nel cluster in produzione, ottenete uno stato intermedio in cui parte del traffico va alla vecchia versione e parte alla nuova, e in caso di errore il rollback diventa «ridistribuire di nuovo». Molto più affidabile mantenere due ambienti (blue/green) o almeno una versione canary del servizio backend, dove va una piccola porzione di traffico.

Errore n. 4: assenza di un piano di rollback rapido.
Scenario negativo: il rilascio avviene, le metriche sono rosse, gli utenti si lamentano e voi iniziate solo allora a pensare a come tornare indietro. Scenario corretto: possibilità di rollback immediato preparata in anticipo (switch blue/green, rollout undo, rollback di Vercel), identificatori di versione chiari nei log e negli health endpoint, e regola ferrea «prima fare rollback, poi indagare».

Errore n. 5: un unico cluster «per tutto» senza separazione per tipo di carico.
Se la generazione dei testi di auguri (agenti LLM) e il checkout vivono nello stesso cluster, qualsiasi problema lato modelli (latenze, timeout, crescita di token) può fermare anche i pagamenti. La separazione in cluster per tipo di attività (Gift REST API / strumenti leggeri, servizio Agents‑heavy, Commerce REST API) e limiti/risorse separati per ciascun cluster è un passo importante verso la resilienza.

Errore n. 6: assenza di legame tra architettura ed economia.
È facile farsi prendere la mano con «alziamo un paio di nodi in più», dimenticando che ogni chiamata LLM e ogni istanza costano. Senza una pianificazione della capacità almeno basilare (stima dei carichi e dei costi) si può sottoscalare e far cadere la produzione, o sovrascalare e perdere marginalità. È utile collegare numero di richieste, percentuale di operazioni LLM pesanti e costo dell’hosting con le metriche di business dell’applicazione.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION