Tutti gli articoli
18 min di lettura

Evita lo sharding prematuro: pattern Kubernetes per app web scalabili

Due casse in un supermercato degli anni Ottanta, con la lampada ambrata della seconda che si accende proprio mentre apre.

Le app web scalabili mantengono latenza e disponibilità costanti con il crescere del carico, combinando l'architettura giusta con autoscaling, caching, partizionamento dei dati e test guidati dagli SLO. Le leve principali sono la scelta architetturale, i pattern di scalabilità del database, il design di cache e CDN, l'autoscaling e un'attività continua di test e monitoraggio. Se ti serve un'azione immediata: individua il collo di bottiglia più evidente (di solito il database o un percorso critico senza cache) e risolvi quello prima di investire in autoscaling o in modifiche all'infrastruttura.


In breve:

  • Concentrati prima sul collo di bottiglia più critico, di solito il database o un percorso critico senza cache, prima di investire in autoscaling o in revisioni dell'infrastruttura.

  • La scalabilità orizzontale richiede servizi stateless, idempotenti e debolmente accoppiati, mentre quella verticale è limitata dai tetti dei tipi di istanza e dai vincoli dei servizi stateful.

  • Dai priorità all'ottimizzazione delle query e all'aggiunta di repliche di lettura per aumentare la capacità del database, prima di valutare uno sharding complesso, che porta con sé una complessità notevole.

  • Usa test di carico a ciclo aperto con volumi di traffico elevati per misurare con precisione i limiti del sistema, guardando ai percentili di latenza e al degrado controllato in sovraccarico.

  • Applica un rate limiting rigoroso, una configurazione sicura della cache e un'invalidazione attenta: scalare amplifica l'esposizione e i rischi di sicurezza.


Tipi di scalabilità e i principi che li rendono possibili

Scalare in verticale significa dare a un singolo server più CPU, memoria o disco. È semplice e spesso è la soluzione più rapida, ma ogni tipo di istanza cloud ha un tetto, e i servizi stateful che dipendono dal disco locale o da dati di sessione in memoria sbattono contro quel tetto molto presto. Scalare in orizzontale significa aggiungere macchine o istanze dietro un load balancer. Non ha un tetto pratico, ma richiede che il livello applicativo possa girare in molte copie senza pestarsi i piedi con il proprio stato. La scalabilità diagonale mescola le due cose: potenzi un po' ogni nodo e aumenti il numero di nodi, ed è così che nella realtà cresce la maggior parte dei sistemi in produzione.

La scelta dipende dai tuoi vincoli. Un job di analisi single-tenant che ha bisogno di molta memoria per una singola elaborazione batch è un buon candidato alla scalabilità verticale. Un'API pubblica che gestisce picchi di traffico imprevedibili ha bisogno di scalabilità orizzontale. I servizi cloud gestiti spesso fissano un tetto alla scalabilità verticale a un determinato livello di istanza, e questo ti spinge verso la scalabilità orizzontale prima di quanto ti aspetteresti.

La scalabilità orizzontale funziona solo se i tuoi servizi rispettano alcuni principi di design:

  • Assenza di stato: nessuna richiesta deve dipendere da dati salvati solo sull'istanza che ha gestito la richiesta precedente.

  • Idempotenza: le richieste ripetute (frequenti con load balancing e autoscaling) non devono produrre effetti collaterali duplicati.

  • Contesti delimitati: ogni servizio possiede una fetta chiara di dati e di logica, il che mantiene indipendenti tra loro le repliche orizzontali.

Le linee guida architetturali del Google Cloud’s reliability framework raccomandano di riprogettare verso la scalabilità orizzontale e il design stateless proprio perché così si elimina il tetto della singola istanza contro cui la scalabilità verticale prima o poi finisce per sbattere.

Scalare il livello dati senza riscrivere tutto

Il database è il punto in cui la maggior parte dei piani di scalabilità si inceppa, soprattutto perché i team ricorrono allo sharding prima di provare soluzioni meno costose. Inizia individuando le query lente con il query planner del tuo database o con uno strumento come pg_stat_statements, poi aggiungi indici che corrispondano ai tuoi reali pattern di query invece di andare a intuito. Un numero sorprendente di "problemi di scalabilità" sparisce non appena esiste l'indice composito giusto.

Una volta esaurite l'indicizzazione e l'ottimizzazione delle query, la leva successiva sono le repliche di lettura. Copiano i dati in modo asincrono su uno o più nodi secondari: il traffico a forte prevalenza di letture (dashboard, reportistica, ricerca) viene indirizzato alle repliche, mentre le scritture restano sul primario. Il punto delicato è il ritardo di replica: le repliche non sono immediatamente coerenti, quindi ogni funzionalità che deve leggere subito ciò che ha appena scritto (un utente che pubblica un commento e si aspetta di vederlo) deve leggere dal primario oppure tollerare un breve ritardo.

Lo sharding, invece, divide i dati in orizzontale su più database indipendenti, ciascuno con un sottoinsieme di righe (per ID cliente, per regione o per un'altra chiave). Risolve i limiti di throughput in scrittura che la replica non può toccare, ma aggiunge complessità reale: query tra shard, ribilanciamenti e logica di migrazione. Ricorri allo sharding solo quando replica e indicizzazione non bastano più, non come prima mossa.

Passi pratici validi a qualsiasi scala:

  • Profila prima di ottimizzare: usa i log delle query lente per trovare il collo di bottiglia reale invece di fare supposizioni.

  • Aggiungi repliche di lettura per il traffico di reportistica e ricerca prima di pensare allo sharding.

  • Monitora il ritardo di replica in continuo: una replica in ritardo che serve silenziosamente dati obsoleti è peggio di nessuna replica.

  • Usa un connection pooler come pgbouncer per evitare l'esaurimento delle connessioni sotto carico: la maggior parte dei database ha limiti rigidi sulle connessioni simultanee e un'applicazione troppo loquace può raggiungere quel tetto molto prima di CPU o memoria.

Consiglio: prima di aggiungere una replica o uno shard, verifica se basta un connection pooler a risolvere i sintomi: molti incidenti del tipo "il database è sovraccarico" sono in realtà incidenti del tipo "troppe connessioni inattive".

I servizi di database gestiti (Amazon RDS, Cloud SQL, PlanetScale) sacrificano un po' di controllo in cambio di scalabilità prevedibile, backup automatici e meno carico operativo. Hanno senso quando il team è piccolo e la crescita è prevedibile. L'autogestione ha senso quando ti serve un'estensione specifica, un controllo stretto dei costi a volumi molto alti o una configurazione che un servizio gestito non espone. Il passaggio dall'uno all'altro diventa più difficile quanto più aspetti, quindi decidi presto e non a crisi in corso.

Caching, CDN e un design della cache sicuro

Il caching è la leva con il rapporto sforzo/beneficio migliore di questo elenco, perché elimina del tutto il lavoro invece di distribuirlo. Le risorse statiche (bundle JavaScript, immagini, font) stanno bene sull'edge della CDN con durate di cache lunghe. Le risposte dinamiche e specifiche per utente richiedono cache di breve durata o nessuna cache, e tutto ciò che contiene dati sensibili dal punto di vista delle autorizzazioni dovrebbe avere come impostazione predefinita Cache-Control: private o no-store, a meno che tu non abbia verificato esplicitamente che sia sicuro condividerlo.

Il design delle chiavi di cache è il punto da cui nasce la maggior parte dei problemi di sicurezza legati alla cache. La OWASP Web Cache Security Cheat Sheet tratta il caching come parte del modello di autorizzazione: se un elemento della richiesta (un header, un cookie, un parametro di query) può cambiare la risposta ma non è incluso nella chiave di cache, un attaccante può avvelenare la cache, oppure un utente può ricevere la risposta memorizzata di un altro utente. Ogni elemento che influisce sulla risposta deve stare nella chiave, in un header Vary, oppure quella rotta non va messa in cache.

  • Metti in cache le risorse statiche sull'edge con max-age lungo e URL versionati.

  • Tieni le risposte dinamiche e personalizzate fuori dalle cache condivise, a meno che la chiave non catturi del tutto ciò che le rende personali.

  • Usa gli header Vary in modo mirato, non come impostazione predefinita: un Vary troppo ampio può annullare completamente il caching.

  • Svuota la cache per pattern di chiave, non a tentativi, e testa i percorsi di invalidazione in un ambiente di staging che rispecchi il traffico di produzione.

Trattare la configurazione della cache come un confine di sicurezza, e non solo come una manopola per le prestazioni, è il principio attorno a cui ruota la cheat sheet di OWASP, dato che una cache mal configurata è una causa ricorrente di esposizione di dati tra tenant diversi.

Anche la scelta della CDN è una decisione di fiducia: stai estendendo il tuo perimetro di sicurezza a terze parti. La Subresource Integrity (SRI) permette ai browser di verificare che uno script o un foglio di stile scaricato corrisponda a un hash atteso, e gli strumenti dell'ecosistema Sigstore estendono questa verifica agli artefatti software firmati, limitando i danni in caso di compromissione di una CDN o di una dipendenza.

Le basi architetturali che rendono possibile scalare

Nessuna dose di autoscaling può rimediare a un'architettura che non è stata pensata per girare in parallelo. Sono i servizi front-end stateless, dove qualsiasi istanza può gestire qualsiasi richiesta, a far funzionare davvero scalabilità orizzontale e load balancing. Nel momento in cui un servizio conserva lo stato di sessione nella memoria locale o scrive su disco locale, hai creato un punto che non può essere replicato liberamente, e prima o poi ti serviranno sticky session o un archivio di sessione condiviso (Redis è la scelta più comune) per aggirarlo.

I confini tra i servizi contano quanto l'assenza di stato. Un monolite modulare, cioè una singola applicazione distribuibile organizzata internamente in moduli ben separati, può scalare molto a lungo se ogni modulo è pulito e l'insieme è stateless. È più semplice da gestire, testare e rilasciare rispetto a un'architettura a microservizi, ed è il punto di partenza giusto per la maggior parte dei team. I microservizi si guadagnano la loro complessità quando parti diverse del sistema hanno davvero bisogno di scalare, essere rilasciate o essere gestite in modo indipendente, non perché il diagramma architetturale fa più bella figura. Il percorso di migrazione concreto è estrarre un servizio solo quando un modulo specifico ha superato i limiti di scalabilità o di proprietà del team del monolite, non tutto in una volta.

I pattern event-driven disaccoppiano le parti del sistema che viaggiano a velocità diverse. Una scrittura su una coda (RabbitMQ, Amazon SQS) o su uno stream (Kafka, Kinesis) permette a una richiesta dell'utente di tornare subito, mentre i worker in background gestiscono in modo asincrono l'elaborazione più lenta. Conta soprattutto per i carichi con molte scritture: un checkout di e-commerce, per esempio, può accettare l'ordine e mettere in coda aggiornamenti di magazzino, email di conferma ed eventi di analisi, invece di far aspettare il cliente finché tutto è completato in modo sincrono.

  • Mantieni stateless i servizi front-end e le API, così ogni istanza può servire qualsiasi richiesta.

  • Parti da un monolite modulare ed estrai servizi solo quando un modulo lo ha superato.

  • Usa code o stream per disaccoppiare il lavoro lento in background dal percorso della richiesta su cui gli utenti aspettano.

Messe insieme, queste scelte determinano se l'autoscaling serve davvero a qualcosa. Aggiungere istanze a un servizio stateful e fortemente accoppiato spesso aggiunge solo altri posti in cui i bug possono nascondersi, non capacità.

L'autoscaling nella pratica

L'autoscaling ha tre modalità, e sceglierne una sbagliata è un punto di fallimento frequente. L'autoscaling basato su metriche reagisce a segnali in tempo reale come le richieste al secondo o la profondità delle code. L'autoscaling pianificato anticipa pattern noti, per esempio l'impennata di traffico serale di un rivenditore. L'autoscaling predittivo prevede la domanda in anticipo ed è più utile quando il provisioning è lento, come nell'avvio di nodi GPU, dove reagire a picco già arrivato è troppo tardi.

Su Kubernetes, quattro componenti coprono la maggior parte dei casi:

  1. Horizontal Pod Autoscaler (HPA) aggiunge o rimuove repliche di pod in base a CPU, memoria o metriche personalizzate come le richieste al secondo.

  2. Vertical Pod Autoscaler (VPA) regola le richieste di CPU e memoria dei pod esistenti in modo che siano dimensionati correttamente senza interventi manuali.

  3. Cluster Autoscaler o Karpenter aggiunge o rimuove nodi in modo che ci sia capacità sufficiente per i pod che l'HPA vuole eseguire; Karpenter crea i nodi just-in-time invece di partire da template fissi di gruppi di nodi.

  4. KEDA estende l'autoscaling a trigger event-driven, scalando i pod in base alla lunghezza di una coda o al ritardo di uno stream invece che alla sola CPU.

Una CNCF overview of autoscaling trade-offs descrive KEDA e Karpenter come complementari: KEDA decide quanti pod servono sulla base di trigger personalizzati, Karpenter si assicura che esistano i nodi per eseguirli, e insieme bilanciano costi, affidabilità e prestazioni meglio del solo scaling basato su CPU.

L'autoscaling predittivo giustifica la propria complessità proprio per i carichi con provisioning lento. Un caso di studio CNCF sull'autoscaling delle GPU descrive un controller che combina un modello di previsione a breve termine con rilevamento dei picchi e scaling graduale, espandendosi a ondate invece che tutto in una volta, che ha raggiunto l'85% di accuratezza entro più o meno il 10% con dieci minuti di anticipo e ha evitato guasti a cascata nei test. Lo scaling graduale conta anche al di là delle GPU: far partire centinaia di pod contemporaneamente può sovraccaricare lo scheduler e provocare esattamente l'instabilità che l'autoscaling doveva prevenire.

Consiglio: testa il comportamento dell'autoscaling in un ambiente di staging con carico sintetico prima di fidartene in produzione, e imposta un numero massimo rigido di repliche, così una metrica impazzita non può scalare il sistema fino a una fattura a sorpresa.

Test di carico che producono numeri affidabili

I test di carico a ciclo chiuso, in cui ogni utente virtuale aspetta la risposta prima di inviare la richiesta successiva, sottostimano il comportamento del traffico reale, perché gli utenti veri non aspettano educatamente il proprio turno. I test a ciclo aperto, che inviano richieste a un ritmo fisso indipendentemente dal tempo di risposta, mostrano in modo più accurato come si comporta un sistema quando si avvicina ai propri limiti. Le Google Cloud’s load-testing guidance raccomandano pattern a ciclo aperto e indicano le metriche che vale la pena registrare: throughput, latenza al 99° percentile, concorrenza e tasso di errore, invece di affidarsi a un singolo valore medio di tempo di risposta che nasconde le esperienze peggiori.

  • Usa strumenti a ciclo aperto come k6 o Nighthawk per simulare traffico realistico, senza attese.

  • Registra la latenza al 99° percentile, non solo la media: le medie nascondono le richieste lente che generano le lamentele.

  • Esegui test di sovraccarico deliberati per vedere come il sistema cede, e introduci il load shedding (rifiutare in modo pulito le richieste in eccesso) così da degradare in modo controllato invece di crollare del tutto.

  • Riporta i risultati direttamente nelle soglie dell'autoscaler e negli SLO: un dato di capacità misurato è molto più affidabile di una stima.

Monitoraggio, SLO e come trasformare la telemetria in azione

Gli Service Level Objective traducono "il sistema deve essere veloce e affidabile" in numeri su cui puoi impostare allarmi, come "il 99% delle richieste si completa in meno di 300 millisecondi". Un error budget, cioè la quantità di violazioni dello SLO ammessa prima di sospendere lo sviluppo di funzionalità per lavorare sull'affidabilità, trasforma quel numero in uno strumento decisionale e non in una dashboard che nessuno guarda.

  • Tieni d'occhio i percentili di latenza (p50, p95, p99), il tasso di richieste, il tasso di errore e la profondità delle code come segnali principali.

  • Usa Prometheus per la raccolta delle metriche e il tracing distribuito per collegare una richiesta lenta lato utente al servizio o alla query che la causa.

  • Automatizza le correzioni ovvie, come riavviare un pod bloccato o scalare quando la profondità della coda supera una soglia nota, così le persone si occupano solo degli incidenti che richiedono davvero un giudizio.

Scalare lo storage: OLTP, OLAP e dati event-driven

I carichi transazionali e quelli analitici si contendono le stesse risorse del database se li lasci convivere nello stesso sistema. Mantieni il database OLTP (quello che gestisce le transazioni degli utenti in tempo reale) concentrato su letture e scritture piccole e veloci, e sposta reportistica, dashboard e analisi su uno store OLAP separato o su un data warehouse ottimizzato per scansionare grandi volumi di dati storici. Lanciare una query analitica pesante sul database transazionale di produzione è una causa comune, e del tutto evitabile, di picchi di latenza.

Le risorse di grandi dimensioni, immagini, video, file caricati, vanno nell'object storage (Amazon S3, Cloud Storage) dietro una CDN, non nel database principale. Servile tramite URL immutabili e versionati (un nome di file che include un hash del contenuto), così la CDN può tenerle in cache a tempo indeterminato senza mai dover verificare se sono cambiate.

  • Separa i carichi OLTP e OLAP in store diversi, così le query di reportistica non possono togliere risorse al traffico transazionale.

  • Conserva i file binari di grandi dimensioni nell'object storage, non nel database, e servili tramite CDN.

  • Usa URL versionati e immutabili per le risorse memorizzabili in cache, così l'invalidazione diventa superflua.

Per i sistemi con molte scritture, l'event sourcing e il CQRS (separare il modello di scrittura da quello di lettura) permettono di scalare scritture e letture in modo indipendente, con processi in background che costruiscono viste di lettura ottimizzate a partire da un flusso di eventi, invece di interrogare direttamente lo store transazionale in uso.

Una checklist di costi e compromessi prima di investire

Non tutte le leve valgono la pena per ogni team. Caching e correzioni delle query sono vittorie rapide con un ritorno alto. Sharding, distribuzione multi-regione e autoscaling predittivo sono investimenti a lungo termine che hanno senso solo quando le soluzioni più semplici sono esaurite e la crescita è costante.

  • Sistema prima caching e query lente: costano poco e spesso eliminano gran parte del problema.

  • Aggiungi repliche di lettura prima dello sharding: lo sharding è l'ultima risorsa, non il punto di partenza.

  • Investi nell'autoscaling predittivo solo per carichi prevedibili con provisioning lento, come i servizi basati su GPU.

  • Fai un giro di SLO e test di carico prima di ogni modifica all'infrastruttura, così sai davvero che cosa stai risolvendo.

Azione Costo tipico Quando conviene
Aggiungere indici al database Basso Subito, per la maggior parte dei sistemi con molte query
Aggiungere una CDN e il caching statico Basso Appena servi risorse statiche a utenti reali
Aggiungere repliche di lettura Medio Quando il traffico di lettura supera le capacità di un singolo primario
Introdurre lo sharding Alto Solo dopo aver esaurito la replica
Autoscaling predittivo Alto Carichi con provisioning lento e picchi prevedibili

Come si costruiscono davvero questi sistemi nella pratica

Il lavoro sulla scalabilità raramente fallisce perché un team ha scelto lo strumento sbagliato. Fallisce perché i requisiti cambiano a progetto in corso, la responsabilità passa di mano, oppure chi aveva capito l'architettura originale se ne va prima che il collo di bottiglia venga risolto. Un modello guidato da figure senior tiene gli stessi ingegneri coinvolti dalla prima consulenza fino alla consegna, proprio per evitare le riscritture a metà progetto e le trappole di dipendenza che altrove fanno deragliare il lavoro sulla scalabilità.

Tra i casi di studio di Ampersand ci sono progetti come la gestione di una piattaforma multisito per un partito politico svizzero da un unico sistema, dove la sfida di scalabilità riguardava meno il traffico puro e più la gestione affidabile di molti siti quasi identici su un'infrastruttura condivisa, uno scenario che premia proprio i principi di assenza di stato e di confini netti tra i servizi di cui sopra. Il passo concreto per un team che vuole valutare la propria prontezza a scalare è un audit o un progetto pilota circoscritto, prima di impegnarsi in una ricostruzione più ampia, invece di tirare a indovinare sull'architettura giusta.

Le implicazioni di sicurezza che la scalabilità porta con sé

Scalare un sistema significa anche scalarne l'esposizione. Ogni nuova istanza, livello di cache ed endpoint pubblico è un'altra superficie che un attaccante può sondare, e le difese che bastavano con poco traffico (una singola regola di firewall, una richiesta controllata a mano) smettono di funzionare quando il traffico è distribuito su decine di istanze create dall'autoscaling.

Il rate limiting è la prima linea di difesa, e deve intervenire prima che una richiesta raggiunga la tua logica applicativa costosa, idealmente sul load balancer o sull'API gateway, non in profondità dentro un servizio che ha già consumato cicli di CPU per analizzare la richiesta. I limiti per utente e per IP intercettano problemi diversi: quelli per utente bloccano gli abusi da un singolo account compromesso, mentre quelli per IP intercettano scraping più ampio o tentativi di forza bruta, anche se possono colpire utenti legittimi dietro una traduzione di indirizzi di rete condivisa.

La protezione dai DDoS su larga scala si basa in genere su una CDN o su un servizio dedicato che assorbe gli attacchi volumetrici prima che raggiungano i server di origine, dato che provare a uscire da un attacco distribuito con l'autoscaling è lento e costoso rispetto al filtraggio sull'edge.

Caching e scalabilità interagiscono con la sicurezza anche in modi facili da trascurare: una cache con chiavi impostate male può servire a un utente la pagina di errore per limite superato di un altro oppure, peggio, servire una risposta personalizzata e autenticata a un visitatore non autenticato, che è esattamente il rischio di avvelenamento della cache di cui parla la cheat sheet di OWASP. Man mano che distribuisci un sistema su più edge CDN, più istanze create dall'autoscaling e più livelli di cache, tratta ognuna di queste aggiunte come un nuovo punto che richiede controlli di accesso propri, non solo come capacità in più.

Come Ampersand aiuta i team a scalare senza tirare a indovinare

Scalare bene un'applicazione web significa azzeccare allo stesso tempo architettura, dati, caching e operatività, e farlo è difficile con un via vai di collaboratori esterni. Ampersand tiene gli stessi ingegneri senior sul progetto dalla prima consulenza alla consegna, che si tratti di costruire una nuova applicazione web o piattaforma, di realizzare integrazioni di sistema o di fornire supporto tecnico continuativo con un contratto mensile. Se il tuo sistema ha bisogno di una valutazione della capacità o di un pilota circoscritto prima di un investimento più grande, dai un'occhiata ai modelli di collaborazione e ai prezzi di Ampersand e scrivici per definire il lavoro.

Domande frequenti

Che cos'è la scalabilità in un'applicazione web?

La scalabilità è la capacità di un sistema di reggere un carico crescente, più utenti, più dati, più richieste, senza una perdita proporzionale di prestazioni o affidabilità. Di solito si ottiene con una combinazione di scalabilità orizzontale, caching, ottimizzazione del database e autoscaling, non con un singolo intervento.

Come si scala da 0 a un milione di utenti?

La maggior parte dei sistemi scala per tappe: si parte da un database ben indicizzato e da un livello applicativo stateless, si aggiungono caching e una CDN quando i contenuti statici iniziano a pesare, poi si introducono repliche di lettura, code e autoscaling man mano che il traffico cresce in modo irregolare. Sharding e autoscaling predittivo servono di norma solo quando le soluzioni più semplici come indicizzazione, replica e caching sono esaurite.

Come rendo scalabile un'applicazione?

Progetta fin dall'inizio per l'assenza di stato, così qualsiasi istanza può gestire qualsiasi richiesta, poi aggiungi scalabilità orizzontale, caching e repliche di lettura del database man mano che il carico cresce. Test di carico continui con metodi a ciclo aperto e un monitoraggio guidato dagli SLO ti dicono quale di queste leve ti serve davvero come prossima mossa.

Quali sono alcuni esempi di scalabilità?

Un esempio è una CDN che tiene in cache le risorse statiche sull'edge, così i server di origine gestiscono molto meno traffico; un altro è un cluster Kubernetes che usa l'Horizontal Pod Autoscaler per aggiungere repliche di pod quando il volume di richieste cresce. Anche le repliche di lettura del database che assorbono il traffico di reportistica, lasciando il database primario veloce per le transazioni, sono un pattern comune.

Fonti

Aggiornato

Parliamone

Hai un progetto che tocca questo tema?

Una chiamata gratuita di 10 minuti è il modo più rapido per capire se siamo lo studio giusto.

Prenota una chiamata gratuita di 10 min