Scalabilità del database: evitare il collo di bottiglia
Il database è sempre il singolo punto di errore nella scalabilità. Strategie per l'ottimizzazione di Postgres, repliche di lettura e partizionamento orizzontale.
L’anello più debole del monolite
Puoi scalare i tuoi server frontend all’infinito (aggiunge semplicemente più nodi dietro il bilanciatore di carico). Puoi scalare i tuoi server API all’infinito (funzioni Serverless). Non puoi ridimensionare facilmente il tuo database primario. C’è (di solito) solo una fonte di verità. Un disco. Un processo principale. Quando il traffico aumenta, la CPU raggiunge il 100% e la latenza delle query va da 10 ms a 10.000 ms. Il sito muore. Scalare il database è il problema più difficile nell’ingegneria del backend.
Perché Maison Code ne parla
A Maison Code, vediamo startup valide morire a causa di SQL.
Scrivono codice come SELECT * FROM Orders.
Funziona bene con 100 ordini.
Blocca il server con 1.000.000 di ordini.
Ottimizziamo i database per Alta cardinalità e Alto throughput.
Sappiamo che “Aggiungere indici” non è sufficiente. Sono necessarie modifiche all’architettura (caching, replica, partizionamento).
Strategia 1: indicizzazione (il frutto a portata di mano)
Nella maggior parte dei problemi di prestazioni mancano gli indici.
Scenario: ricerca di un utente tramite email.
Query: SELECT * FROM utenti WHERE email = 'alex@example.com'.
Senza indice: il DB esegue la scansione di 1.000.000 di righe (scansione sequenziale). SU).
Con indice (B-Tree): il DB passa direttamente all’e-mail. O(log N).
Costo: gli indici rallentano le scritture (INSERT/UPDATE), perché l’albero degli indici deve essere aggiornato.
Regola: colonne dell’indice utilizzate in “WHERE”, “JOIN” e “ORDER BY”.
Strategia 2: memorizzazione nella cache (Redis)
La query più veloce è quella che non fai. SQL è lento (I/O del disco, matematica della CPU). Redis è veloce (in memoria, valore-chiave). Modello: Cache Look-Aside.
funzione asincrona getProdotto(id) {
// 1. Controlla la cache
const cached = attendono redis.get(`product:${id}`);
if (memorizzato nella cache) restituisce JSON.parse(memorizzato nella cache);
// 2. Interrogazione DB (lenta)
const prodotto = attendono db.query('SELECT * FROM prodotti WHERE id = ?', [id]);
// 3. Salva nella cache (TTL 5 minuti)
attendono redis.set(`product:${id}`, JSON.stringify(product), 'EX', 300);
restituire il prodotto;
}
Ciò scarica il 90% del traffico di lettura dal DB.
Strategia 3: leggere le repliche
Anche con la memorizzazione nella cache, le letture possono sovraccaricare la CPU. Soluzione: creare copie del database (repliche).
- Nodo primario: gestisce le scritture (INSERT, UPDATE, DELETE). Sincronizza le modifiche alle repliche (asincrono).
- Nodi di replica: gestire le letture (SELECT). Compromesso: Ritardo di replica. L’utente aggiorna il proprio profilo. Aggiornano immediatamente la pagina. Hanno colpito una replica che non ha ancora ricevuto l’aggiornamento. Vedono il vecchio profilo. Correzione: “Leggi le tue scritture”. Per l’utente corrente, forza la lettura da Primario. Per i dati pubblici, leggere dalla replica.
Strategia 4: pooling delle connessioni (PgBouncer)
Postgres ha un limite sulle connessioni simultanee (ad esempio, mantenerne 100). Le funzioni serverless (Lambda) generano 1.000 istanze. Se 1.000 istanze tentano di aprire una connessione, Postgres si blocca. Soluzione: un proxy (PgBouncer / Supabase Pooler). Il proxy mantiene 100 connessioni aperte al DB. Le 1.000 Lambda parlano con il Procuratore. Il proxy mette in coda le query e le esegue utilizzando le 100 connessioni. Questo è obbligatorio per le architetture Serverless.
Strategia 5: partizionamento verticale (divisione delle tabelle)
La tabella “Utenti” ha “bio” (testo, enorme) e “last_login” (data, piccolo).
Se interroghi frequentemente “last_login”, ma “bio” rende enorme la dimensione della riga, il DB legge troppi dati dal disco.
Dividilo:
Tabella User_Core (id, email, password, last_login).
Tabella Profilo_utente (id, biografia, avatar).
Ora SELECT last_login FROM User_Core è incredibilmente veloce perché più righe possono essere inserite nelle pagine RAM.
6. Viste materializzate: pre-calcolo del successo
“Mostrami le entrate totali per il 2024.”
SELECT SUM(importo) FROM ordini WHERE anno = 2024.
Questo esegue la scansione di 10 milioni di righe. Ci vogliono 5 secondi.
Soluzione: vista materializzata.
“CREA VISTA MATERIALIZZATA entrate_annuali AS SELECT …”.
Postgres calcola il risultato e lo salva su disco come tabella fisica.
L’interrogazione richiede 1 ms.
Compromesso: i dati non sono aggiornati. È necessario “AGGIORNARE VISTA MATERIALIZZATA” periodicamente (ad esempio, ogni ora).
Perfetto per dashboard in cui il “tempo reale” non è strettamente richiesto.
7. Scalabilità automatica del database (Serverless Aurora)
Cosa succede se non vuoi gestire le repliche? Amazon Aurora Serverless v2. Aggiunge automaticamente CPU/RAM quando il traffico aumenta e la riduce quando il traffico diminuisce. Si ridimensiona in incrementi frazionari (ACU). Ti offre effettivamente la promessa “Infinite Scaling” di NoSQL, ma con piena compatibilità SQL. È costoso, ma più economico che assumere un DBA per eseguire manualmente il provisioning delle repliche di lettura alle 3 del mattino.
9. Dati delle serie temporali (TimescaleDB)
“Registra ogni visualizzazione di pagina.”
“Registra ogni lettura del termometro.”
Postgres standard si blocca quando si inseriscono 10.000 righe al secondo in una singola tabella.
Gli indici diventano frammentati.
Soluzione: TimescaleDB (estensione Postgres).
Partiziona automaticamente i dati in base all’ora (“Hypertables”).
ordini_2024_01, ordini_2024_02.
Lo interroghi come una tabella “ordini”, ma fisicamente sono piccoli pezzi.
L’eliminazione dei vecchi dati è immediata (DROP TABLE Orders_2020). “DELETE FROM ordini WHERE anno=2020” richiede ore.
10. Strategie di partizionamento avanzate
Il partizionamento verticale divide le colonne.
Partizionamento orizzontale divide le righe.
Per regione: users_eu, users_us.
Questo aiuta con GDPR (Residenza dei dati).
“I dati europei non lasciano mai il server dell’UE.”
Il partizionamento dichiarativo di Postgres rende tutto questo gestibile.
Questo è il modo in cui adattiamo le app SaaS a milioni di tenant senza acquistare un mainframe.
11. Il punto di vista dello scettico
“Usa semplicemente DynamoDB/NoSQL. È scalabile all’infinito.” Contropunto: NoSQL ridimensiona le scritture, ma non riesce in Dati relazionali. “Mostrami tutti gli ordini degli utenti di Parigi che hanno acquistato scarpe rosse.” In SQL: una query. In NoSQL: un incubo di join lato applicazione e recuperi multipli. La maggior parte dei dati dell’e-commerce sono relazionali. Attenersi a SQL (Postgres) fino a raggiungere la scala di Google.
Domande frequenti
D: Quando utilizzare lo Sharding? R: Quasi mai. Lo sharding (la suddivisione dei dati su più server in base all’ID utente) è estremamente complesso. Perdi le transazioni ACID tra gli shard. Non effettuare lo sharding finché non hai > 10 TB di dati. Il ridimensionamento verticale (server più grande) funziona sorprendentemente bene fino a quel punto.
D: ORM e SQL grezzo? R: Prisma/Drizzle (ORM) per la produttività. SQL grezzo per report complessi o query super ottimizzate. Gli ORM moderni sono sufficienti per il 99% delle query.
Conclusione
Il database è il cuore del tuo stack. Se smette di battere, l’app muore. Trattalo con rispetto. Indicizza le tue chiavi esterne. Memorizza nella cache i tuoi percorsi più attivi. E mai e poi mai eseguire “DROP TABLE” di venerdì.
Database soffocato?
Se le tue query stanno scadendo o la tua CPU è in fase di redlining, Maison Code può ottimizzare il tuo schema. Analizziamo piani di query, implementiamo strategie di memorizzazione nella cache e progettiamo set di repliche.
Le query sono troppo lente?
Ottimizziamo l’architettura del database utilizzando l’indicizzazione, la memorizzazione nella cache e le repliche di lettura per gestire su larga scala. Assumi i nostri architetti.