„L’applicazione è lenta“ è una delle frasi più costose durante l’esercizio. Senza contesto non è chiaro se si tratti di vera latenza (tempo di risposta), di minore throughput (richieste al secondo), di un aumento degli errori (p.es. timeout) o semplicemente di un cambiamento nel comportamento degli utenti. Chi deve localizzare i colli di bottiglia di performance ha quindi bisogno di un metodo riproducibile: metriche end-to-end lungo il percorso della transazione, baseline solide (valori normali) e test di carico che portino il sistema controllatamente al limite.
Questo contributo è rivolto ad amministratori, system engineer, operatori e fornitori di servizi tecnici. Il focus non è sul codice sorgente, ma sulla misurabilità, sulla realtà operativa e sulla risoluzione dei problemi: quali metriche sono imprescindibili, come definire le baseline, come pianificare test di carico senza danni alla produzione — e come passare da „lento“ a una causa concreta con strategia di rollback.
Localizzare i colli di bottiglia di performance: perché le metriche End-to-End sono il punto di partenza
End-to-End (E2E) significa: si misura una transazione utente o di sistema completa dall’ingresso al risultato. Può essere un login, una ricerca, un processo d’ordine o una chiamata API. Il vantaggio: le metriche E2E sono interpretabili sia per l’operazione che per i decisori — e mostrano immediatamente se il problema ha impatto sul cliente o se è solo un aspetto parziale (p.es. un singolo host con CPU elevata) che risalta.
Importante è la netta distinzione dall’osservabilità: „monitoraggio“ è il controllo continuo di metriche note, „observabilità“ indica la capacità di derivare dagli segnali (metriche, log, trace) anche stati di errore non noti. Per localizzare i colli di bottiglia di performance servono entrambe le cose: metriche di base stabili più la possibilità di approfondire durante un incidente.
Le tre metriche core: latenza, throughput, tasso di errore
Per ogni transazione E2E dovrebbero esistere almeno queste misure:
- Latenza: tempo di risposta, idealmente come percentili (p50/p95/p99). I percentili mostrano le „code lunghe“: poche richieste molto lente che comunque impattano fortemente gli utenti.
- Throughput: numero di transazioni per unità di tempo (RPS, TPS, Jobs/min). Questo aiuta a separare il carico dal „lento“.
- Tasso di errore: HTTP 5xx, timeout, aborti, retry. Più retry aumentano il carico e mascherano le cause.
In aggiunta vanno considerati gli indicatori di saturazione (utilizzo CPU, I/O-wait, lunghezze delle code, utilizzo del connection pool del DB, thread pool, errori di rete). Questi mostrano dove le risorse scarseggiano. Senza metriche E2E però non saprete se ciò spiega davvero il problema utente.
Baseline nel monitoring: rendere misurabile lo stato normale
Una baseline non è un singolo valore numerico, ma un intervallo di valori previsto per periodo e contesto. „CPU 60 %“ può essere normale se il sistema è stabile – o critico se contemporaneamente aumenta la latenza p99. Le baseline prevengono due insidie tipiche: un diluvio di allarmi dovuto a soglie troppo strette e „punti ciechi“, perché nessuno si accorge che i valori si spostano lentamente nel corso delle settimane.
Definire correttamente le baseline: intervallo temporale, granularità, segmentazione
Si è dimostrato pratico:
- Intervallo: almeno 2–4 settimane di dati, meglio 6–8 settimane, per evidenziare i pattern settimanali.
- Granularità: latenza E2E non solo come media, ma p50/p95/p99 con risoluzione da 1–5 minuti.
- Segmentazione: per regione, tenant, endpoint, transazione critica, e separatamente per le fasi di „cold start“ (Deployments, Auto-Scaling).
Le baseline dovrebbero inoltre essere „Change-aware“: dopo release, modifiche agli indici DB, migrazioni di storage o nuovi gateway di sicurezza (z. B. WAF) lo stato normale si sposta. Per questo serve una correlazione dei change: Deployments, modifiche di configurazione e eventi infrastrutturali devono poter essere correlati temporalmente con le metriche (Change-Log/CMDB/Annotationen).
SLOs come strumento operativo: le baseline diventano azionabili
Un SLO (Service Level Objective) è un obiettivo misurabile, z. B. „p95 della transazione di login < 800 ms“ o „99,9 % di request riuscite“. La differenza rispetto a un SLA: lo SLA è tipicamente contrattuale, lo SLO è operativo. Gli SLO aiutano a non limitarsi a „vedere“ le baseline, ma a valutarle: da quando una deviazione è rilevante? Senza SLO durante un Incident si discute troppo a lungo sul „se sembra lento“.
Punti di misura lungo la catena: dal Client al database
I colli di bottiglia delle performance raramente si manifestano in un solo punto. Tipico è una catena di saturazioni parziali: un po‘ più di latenza nella rete porta a più connessioni aperte, questo riempie i pool, aumenta il queuing e amplifica la p99. Perciò conviene un modello di misurazione standardizzato lungo il percorso:
- Client/Synthetic: misurazione dalla prospettiva utente (z. B. HTTP-Checks da più sedi). Il Synthetic Monitoring è riproducibile, rileva i guasti precocemente, ma può solo approssimare i percorsi reali degli utenti.
- Edge/Ingress: Load Balancer/Reverse Proxy (TLS-Termination, Queues, 4xx/5xx, Upstream-Latenz). Una nuova Cipher-Suite o problemi OCSP possono aumentare la latenza senza che l’app sia „colpevole“.
- Applikation: durata della request, queue interne, utilizzo di Thread/Worker, Garbage Collection (nelle Managed Runtimes), Cache-Hits/Misses.
- Datenzugriff: tempi delle DB-Query, Lock-Waits, Connection-Pool, IOPS, Storage-Latenz.
- Plattform: CPU-Steal (Virtualisierung), Memory-Pressure, Netzwerk-Errors, Disk-Queue, Kernel-Limits.
Una causa frequente di errori diagnostici è la confusione tra sintomo e causa. Esempio: un’elevata occupazione della CPU può essere la conseguenza di un’ondata di retry, scatenata da un timeout in una dipendenza a valle. E2E più segmentazione (quale endpoint? quale tenant? quale regione?) prevengono questi vicoli ciechi.
Sequenza di verifica nell’incidente: un percorso diagnostico pratico
Quando è in corso un incidente di performance, serve una sequenza che funzioni anche sotto pressione. L’ordine seguente è volutamente operativo: prima l’impatto sugli utenti, poi l’isolamento, poi l’approfondimento.
1) Confermare e delimitare l’impatto sugli utenti
- Quale transazione è interessata (login, ricerca, export, endpoint API)?
- Da quando, in quali regioni/sedi, soltanto internamente o anche esternamente?
- Si tratta di latenza, tasso di errore o entrambi? Ci sono timeout o retry?
- Ci sono change paralleli (deployment, cambio di certificati, policy di firewall, manutenzione DB)?
Se usate Synthetic Checks: verificate che seguano lo stesso percorso degli utenti reali (DNS, WAF, IdP, Proxy). Un ostacolo frequente è che i synthetic abbreviano internamente e quindi non rilevano problemi all’Edge/Identity.
2) Controllare le metriche E2E sui percentili, non sulle medie
Molti sistemi appaiono a posto in media, mentre p95/p99 esplodono. Questo spesso indica queueing, locking o singoli hotspot (un tenant „rumoroso“, un endpoint con payload grande, un percorso di storage con latenza intermittente). Se p50 resta stabile ma p99 cresce, è un forte indicatore di saturazione sporadica o outlier.
3) Cercare pattern di saturazione: code, pool, limiti
Trigger tipici di colli di bottiglia in esercizio:
- Connection Pools: i pool di connessioni verso database o client HTTP al limite generano code. Sintomo: aumento della latenza delle richieste senza elevato utilizzo CPU.
- Thread/Worker Pools: web server o worker di job sono saturi, le richieste restano in attesa.
- Rate Limits: 429/throttling genera backoff e retry.
- Storage-Latenz: aumenta l’I/O-wait, il DB rallenta, l’app si blocca.
- DNS/Identity: una risoluzione DNS lenta o latenza dell’IdP rende „tutto“ lento.
Per gli admin è importante: i pool sono deliberatamente introdotti come „sicurezze“. Se semplicemente aumentate i pool, spostate spesso i colli di bottiglia più avanti (es. nel database) e rischiate un guasto più grave. Prima comprendere la causa, poi adeguare la capacità.
4) Controlli di sistema semplici: CPU, Memory, Disk, Netzwerk
Anche con un buon monitoring è utile un rapido controcontrollo a livello host/VM per vedere saturazioni evidenti o limiti del kernel. Esempio (Linux):
# Überblick über Load, CPU, Memory
uptime
free -h
vmstat 1 5
# Disk- und I/O-Indikatoren
iostat -xz 1 5
# Netzwerk: Drops/Errors
ip -s link
ss -s
# Offene Files / Limits (häufig bei hoher Parallelität)
ulimit -n
cat /proc/sys/fs/file-maxInterpretazione: un load elevato con bassa occupazione CPU spesso indica I/O-Wait o processi bloccati. Aumenti di „await“/“svctm“ (a seconda dello strumento) o code disco elevate indicano lo storage. Molti retransmits/errori sull’interfaccia indicano problemi di rete o mismatch di MTU.
Impostare metriche End-to-End tecnicamente: pratico e manutenibile
Affinché i dati E2E aiutino realmente durante un incidente, devono essere consistenti e utili per le operations. Tre punti sono decisivi: definizione univoca della transazione, label/dimensioni stabili e una chiara correlazione tra componenti.
Definire le transazioni: «Cosa misuriamo esattamente?»
Selezionate 5–15 transazioni critiche che rappresentano la creazione di valore (es. Login, Ricerca, Checkout, Esportazione documenti, API Create/Update). Troppe transazioni rendono le dashboard poco chiare; troppo poche nascondono i punti caldi. Per ciascuna transazione documentate: obiettivo (SLO), metodo di misurazione (Synthetic/RUM), dipendenze (IdP, DB, Storage, API esterne) e profili di carico attesi (ora del giorno, chiusura mensile).
Correlazione: Trace-ID, Request-ID e annotazioni di change
Anche senza un focus sugli sviluppatori conviene uno schema di correlazione standard. Una Request-ID è un identificatore univoco per request che viene propagato nei log e a monte/valle. Una Trace-ID è simile, ma pensata per sistemi distribuiti (Distributed Tracing). In esercizio il vantaggio è semplice: potete passare da una request E2E lenta ai log e alle dipendenze senza dover indovinare.
Parallelamente servono annotazioni di change: deploy, modifiche di configurazione, feature toggle, migrazioni DB. Senza questi indicatori temporali le baseline diventano inaffidabili, perché non sapete se uno spostamento è «normale» (change) o un «difetto lento».
Pianificare i test di carico: realistici, sicuri e significativi
I test di carico non sono un fine a sé. L’obiettivo non è celebrare la «massima RPS», ma individuare in modo riproducibile i colli di bottiglia prima che li incontrino gli utenti. Un test di carico ben eseguito risponde a: sotto quale carico quale risorsa cede per prima? Come si comportano i percentili di latenza? Quali meccanismi di backpressure entrano in funzione (code, rate limit, circuit breaker)?
Prerequisiti: ambiente di test, dati, dipendenze
I rischi tipici emergono quando i test di carico vengono eseguiti «di striscio»:
- Dati vicini alla produzione: dati fittizi puri sottostimano lock sul DB, uso degli indici e comportamento della cache. Contemporaneamente non dovete replicare dati personali reali negli ambienti di test senza misure legali e organizzative.
- Dipendenze: API esterne, gateway mail, identity provider. Nei test di carico controllate se state sollecitando sistemi reali o se usate mock/stub.
- Parità ambientale: differenze di generazione CPU, tier di storage o percorsi di rete falsano i risultati. Documentate apertamente le discrepanze.
Design del test: ramp-up, step, soak e criteri di interruzione
Per avere rilevanza operativa si è dimostrata efficace una combinazione:
- Ramp-up: aumentare il carico in passi per identificare le soglie (es. ogni 5 minuti +20%).
- Test a step: mantenere livelli di carico per valutare stabilmente p95/p99 (considerare il warmup delle cache).
- Soak-test: diverse ore a carico realistico per osservare leak, frammentazione, backpressure sui log o effetti di DB-autovacuum/maintenance.
Importante: senza criteri di interruzione i team tendono al «continuiamo a testare» e provocano danni secondari (code sovraffollate, dischi pieni, job batch interrotti). L’interruzione non è un fallimento, ma parte del design.
Esempio pratico: test di carico HTTP con k6 (minimale, copiabile)
L’esempio seguente mostra uno script k6 volutamente semplice per un endpoint API. k6 è uno strumento di load testing diffuso; il vantaggio è una curva di carico riproducibile e una valutazione precisa. Adattate URL, header e check al vostro ambiente.
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 20 },
{ duration: '5m', target: 20 },
{ duration: '2m', target: 60 },
{ duration: '5m', target: 60 },
{ duration: '2m', target: 0 },
],
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<800'],
},
};
export default function () {
const url = `${__ENV.BASE_URL}/api/healthcheck-dependency`;
const res = http.get(url, {
headers: {
'Accept': 'application/json',
'X-Request-Source': 'loadtest',
},
timeout: '10s',
});
check(res, {
'status is 200': (r) => r.status === 200,
});
sleep(1);
}Perché funziona: si generano fasi di carico definite e si ottengono percentili di latenza oltre al tasso di errore come criteri stringenti. Quando fallisce: se l’endpoint non è rappresentativo (p.es. «/health» senza DB) o autenticazione, caching e volume dati non sono realistici. I test di carico devono riprodurre le transazioni, non solo la raggiungibilità.
Localizzare con precisione i colli di bottiglia: modelli tipici e contromisure
In esercizio i pattern si ripetono. È fondamentale riconoscere il pattern prima di «toccare tutte le leve».
1) Blocchi del database e saturazione del connection pool
Sintomi: p95/p99 aumentano, CPU non necessariamente alta, il DB mostra molte sessioni in attesa o lock-wait. Cause frequenti: indici mancanti, transazioni lunghe, job batch concorrenti o pool troppo piccoli. Contromisure tipiche: analisi di query/indici, verificare i confini delle transazioni, spostare le finestre dei batch, adattare con cautela le dimensioni del pool e impostare timeout sensati (troppo corti generano ritentativi, troppo lunghi bloccano risorse).
2) Latenza dello storage/I/O
Sintomi: aumenta l’I/O-wait, latenza DB e app crescono in parallelo, occasionali „picchi“ causati dallo storage-backend (Snapshots, Rebalance, Tiering). Contromisure: verificare lo storage-queueing, identificare i limiti della virtualizzazione (IOPS Caps), valutare le impostazioni di journaling/writeback, isolare il „Noisy Neighbor“, collocare log/file temporanei su volumi separati. Critica è la strategia di fallback: se un storage-tier è instabile, è necessario tornare rapidamente a un percorso noto (p.es. altro datastore, policy QoS modificata).
3) Percorso di rete, DNS e MTU
Sintomi: timeout sporadici, retransmits, latenza solo da determinate località, TLS-handshake lenti. Il DNS è un tipico moltiplicatore: se la risoluzione dei nomi è lenta, ogni transazione rallenta. Problemi di MTU (pacchetti troppo grandi, frammentazione/PMTUD-errori) causano blocchi difficili da spiegare. Contromisure: controllare perdite di pacchetti/errori, verificare la gerarchia della cache DNS, misurare la latenza di forwarder/resolver, validare l’MTU end-to-end. Per l’ottimizzazione specifica del DNS vale la pena un approfondimento dedicato.
4) Code applicative e Backpressure
Sintomi: lunghezze delle code in aumento, worker saturi, latenza che cresce a gradini. Backpressure significa: il sistema rallenta intenzionalmente invece di collassare in modo incontrollato. Questo è di per sé corretto, ma solo se è visibile. Contromisure: metriche delle code come metrica di prima classe, verificare i meccanismi di dead-letter, armonizzare timeout e retry, aumentare la capacità solo insieme alla capacità downstream.
Checklist: da “lento” alla causa in 30–60 minuti
La checklist seguente è pensata come componente di un runbook. È deliberatamente concreta e operativa.
- Scope: Quale transazione, quale gruppo di utenti, quale regione? p95/p99 vs. p50?
- Fehlerbild: Timeout? 5xx? 429? Retry/backoff visibili?
- Change-Korrelation: Deployments, configurazioni, modifiche a rete/firewall, certificati, job DB?
- Edge: latenza upstream al LB/Proxy, accodamento, TLS-handshake, errori di connessione.
- App: pool di worker/thread, code interne, picchi di GC (se rilevante), tasso di hit della cache.
- DB: sessioni attive, attese per lock, query lente, utilizzo dei pool, latenza dello storage.
- Plattform: CPU-steal, memory-pressure, accodamento su disco, drop/errori di rete, limiti FD.
- Mitigation: limitare il traffico (rate limit), fermare/spostare batch, aumentare la scalabilità in modo controllato, disabilitare feature-toggle/export.
- Nacharbeit: aggiornare le baseline, affinare le regole di allarme, integrare metriche mancanti, aggiungere un caso di test di carico.
Strategia di fallback: cosa fare se mancano o sono contraddittori i dati di misura?
In pratica l’observability non è mai perfetta. Serve quindi una strategia di fallback che sia comunque sicura:
- Stabilizzare in modo conservativo: prima ridurre il tasso di errore (es. rate limit, traffic shaping), poi analizzare le cause. I costi degli errori sono di solito superiori ai costi di latenza.
- Modifiche minimamente invasive: niente sessioni di tuning su larga scala durante l’incidente. Ogni modifica deve essere revertibile.
- Aggiungere punti di misura: se non è visibile se l’accodamento o i pool sono il limite, è un problema strutturale. Prioritizzate queste metriche nel lavoro post-incidente.
- Riproducibilità: definire un test di carico minimo che induca il problema senza causare danni. Misurare in modo mirato.
È importante non nascondere il fallback come un „piano B“, ma considerarlo parte integrante della disciplina operativa: ogni ottimizzazione senza rollback è un rischio.
Conclusione: i colli di bottiglia non si trovano con il fiuto, ma con struttura
Chi vuole localizzare i problemi di performance ha bisogno di una catena costituita da tre elementi: metriche end-to-end che rendano visibile l’impatto sugli utenti; baseline che oggettivino stati normali e deviazioni; e test di carico che riproducano i colli di bottiglia in modo controllato. In combinazione si ottiene una pratica diagnostica che funziona sotto stress: si restringe rapidamente il campo, si associano i pattern (pool, code, DB, storage, rete) e si applicano mitigazioni revertibili.
Se volete fare di questo un livello operativo permanente, prendete la checklist come punto di partenza del runbook e integrate per ogni transazione SLOs, annotazioni di change e un piccolo test di carico ripetibile. Così da “lento” si passa a un dato misurabile e risolvibile — e dalle imprese eroiche isolate a una routine operativa affidabile.
Per questo tema sono importanti anche il monitoraggio end-to-end e la pianificazione dei test di carico. Il contributo contestualizza questi aspetti in modo chiaro e mostra cosa conta nella pratica quotidiana.