Benchmark infrastruttura Ocmis (10/01/25)¶
Panoramica¶
L'analisi ha l'obiettivo di determinare quale configurazione infrastrutturale consenta di ottenere le migliori performance su AWS e di stimare in maniera precisa il massimo carico che l'architettura puo' sostenere prima che le prestazioni degradino a livelli non accettabili. Lo studio e' essenziale per identificare i limiti operativi dell'architettura, garantire una gestione ottimale delle risorse e pianificare un utilizzo efficiente.
I test sono stati condotti confrontando tre stack applicativi (PHP, Node.js, Golang) contro database PostgreSQL su istanze RDS, con e senza l'utilizzo di PgBouncer come connection pooler esterno.
Importante: quando si effettua questa tipologia di test non devono esserci problemi di sorta sulla connessione Internet, in modo da evitare risultati fittizi.
Considerazioni post test¶
Limite di sicurezza identificato per ciascuna tecnologia:
| Tecnologia | Limite di sicurezza (macchine concorrenti) |
|---|---|
| PHP | < 1.800 |
| Node.js | < 10.000 |
| Golang | < 14.000 |
Osservazioni generali sulla scelta della tecnologia:
- PHP: il principale collo di bottiglia e' rappresentato dal Bouncer, che consuma eccessive risorse. Anche adottando macchine piu' potenti, non si riuscirebbe a colmare il divario prestazionale rispetto a Node.js o Golang. PHP non risulta una scelta ottimale in questo contesto.
- Node.js: il collo di bottiglia e' duplice. Da un lato tende a generare un numero elevato di sessioni sul database RDS, causando un sovraccarico su di esso; dall'altro comporta un maggiore consumo di risorse sulle macchine bilanciate rispetto a Golang. Con un numero molto elevato di connessioni, il carico sulle macchine bilanciate potrebbe diventare un ulteriore punto critico. Per i carichi attuali, tuttavia, il collo di bottiglia e' solo sulle sessioni RDS. Node.js risulta una scelta ottima in questo contesto.
- Golang: il principale collo di bottiglia e' rappresentato dal database RDS. All'aumentare delle richieste, i bilanciatori consumano piu' risorse, ma l'incremento e' marginale perche' estremamente piccolo. Le sessioni sul database hanno un limite che, se superato, potrebbe portare a un sovraccarico critico. In generale, i tempi di risposta rispetto a Node.js sono migliori. Golang risulta la scelta migliore in questo contesto.
Variabili e parametri analizzati¶
Tipi di istanza RDS¶
Durante i test sono stati esaminati due tipi di istanze RDS per il database:
| Istanza | Caratteristiche | Uso tipico |
|---|---|---|
| db.t3.small | 1 core, 2,5 GHz / 2 vCPU, 2 GB RAM, 24 crediti CPU/ora | Configurazione entry-level, carichi leggeri e sviluppo |
| db.t3.medium | Maggiore memoria e capacita' computazionale | Workload con maggiore intensita' computazionale o di memoria |
Le istanze T3 accumulano crediti CPU quando un carico di lavoro funziona al di sotto della soglia di base. Ogni credito CPU guadagnato fornisce alle istanze T3 l'opportunita', quando necessario, di espandere le prestazioni fino a raggiungere quelle di un intero core di CPU per un minuto. Le istanze T3 di Amazon RDS sono configurate con modalita' Illimitata: a costo aggiuntivo, e' possibile espanderle oltre la soglia di base su una finestra temporale di 24 ore.
PgBouncer¶
Sono state condotte prove con e senza l'uso di PgBouncer. Lo strumento, installato sulle istanze client (che fungono da web server e stabiliscono connessioni verso il database), e' stato utilizzato per ottimizzare la gestione delle connessioni verso il database riducendo l'overhead derivante da un elevato numero di connessioni simultanee. Sono state testate sia la modalita' transaction sia la modalita' session.
Diversita' delle macchine client¶
Sono state utilizzate diverse tipologie di istanze client per verificare come i vari livelli di prestazioni hardware influiscano sulla capacita' complessiva di gestire le connessioni.
PHP — db.t3.small¶
Istanza client: alpha.es2000.it (2 CPU) per i test standalone, successivamente web.irrigation-dataserver.com (bilanciamento su web1 e web2) per i test in PRODUCTION.
Host di test: https://web-alpha.irrigation-dataserver.com (standalone) e https://web.irrigation-dataserver.com (production).
Sintesi risultati PHP¶
Ogni test ha durata prevista di 5 minuti. Per ciascuno scenario vengono riportati tempo trascorso effettivo, richieste totali, tasso di successo, percentili dei tempi di risposta e note sul carico infrastrutturale.
Test 100 macchine¶
| Parametro | NO BOUNCER | BOUNCER |
|---|---|---|
| Numero macchine | 100 | 100 |
| Tempo trascorso | 6m 40,146s | 5m 20,491s |
| Richieste effettuate | 1.220 | 1.096 |
| Richieste successo | 1.220 (100,00%) | 1.093 (99,73%) |
| Min | 347,561 ms | 198,492 ms |
| Avg | 398,850 ms | 225,673 ms |
| p50 | 393,838 ms | 220,982 ms |
| p75 | 407,783 ms | 229,330 ms |
| p90 | 428,249 ms | 243,692 ms |
| p99 | 510,457 ms | 297,453 ms |
| Max | 779,216 ms | 695,267 ms |
Test 500 macchine¶
| Parametro | NO BOUNCER | BOUNCER |
|---|---|---|
| Numero macchine | 500 | 500 |
| Tempo trascorso | 7m 8,524s | 6m 41,166s |
| Richieste effettuate | 6.090 | 6.096 |
| Richieste successo | 6.082 (99,87%) | 6.056 (99,34%) |
| Min | 345,286 ms | 200,885 ms |
| Avg | 420,264 ms | 387,555 ms |
| p50 | 406,553 ms | 256,720 ms |
| p75 | 426,303 ms | 280,552 ms |
| p90 | 459,091 ms | 315,872 ms |
| p99 | 681,509 ms | 6,330 s |
| Max | 7,196 s | 7,502 s |
Test 1.000 macchine¶
| Parametro | NO BOUNCER | BOUNCER (transaction mode) |
|---|---|---|
| Numero macchine | 1.000 | 1.000 |
| Tempo trascorso | 8m 20,641s | 8m 21,451s |
| Richieste effettuate | 13.724 | 13.859 |
| Richieste successo | 13.711 (99,91%) | 13.835 (99,83%) |
| Min | 355,118 ms | 207,063 ms |
| Avg | 493,693 ms | 328,275 ms |
| p50 | 448,830 ms | 321,977 ms |
| p75 | 501,838 ms | 358,637 ms |
| p90 | 620,092 ms | 400,081 ms |
| p99 | 1,203 s | 531,683 ms |
| Max | 2,007 s | 3,709 s |
Fino a questo punto si osserva che il Bouncer garantisce prestazioni migliori. Non si evidenziano altre anomalie.
Test 1.500 macchine¶
| Parametro | NO BOUNCER | BOUNCER (transaction mode) |
|---|---|---|
| Numero macchine | 1.500 | 1.500 |
| Tempo trascorso | 10m 13,767s | 10m 32,476s |
| Richieste effettuate | 23.236 | 23.315 |
| Richieste successo | 23.182 (99,77%) | 20.134 (86,36%) |
| CPU Bouncer | — | > 70% |
| Stato DB | Sovraccarico | Non sovraccarico |
| Min | 341,034 ms | 204,222 ms |
| Avg | 912,319 ms | 1,530 s |
| p50 | 580,140 ms | 1,593 s |
| p75 | 863,720 ms | 2,308 s |
| p90 | 1,731 s | 2,713 s |
| p99 | 4,624 s | 3,753 s |
| Max | 14,578 s | 6,641 s |
Con 1.500 macchine senza Bouncer il database entra in sovraccarico. Con il Bouncer i risultati NON sono accettabili: il consumo CPU del Bouncer supera il 70% mentre il database non risulta sovraccaricato.
Test 2.000 macchine¶
| Parametro | NO BOUNCER | BOUNCER (transaction mode) | BOUNCER (session mode) |
|---|---|---|---|
| Numero macchine | 2.000 | 2.000 | 2.000 |
| Tempo trascorso | 11m 57,106s | 11m 56,604s | 11m 54,645s |
| Richieste effettuate | 34.276 | 34.229 | 34.323 |
| Richieste successo | 28.080 (81,92%) | 23.501 (68,66%) | 16.559 (48,24%) |
| CPU Bouncer | — | > 85% | > 70% |
| Stato DB | Molto sovraccarico | Non sovraccarico | Non sovraccarico |
| Min | 345,861 ms | 203,252 ms | 195,598 ms |
| Avg | 5,428 s | 1,571 s | 2,690 s |
| p50 | 4,347 s | 1,387 s | 3,086 s |
| p75 | 10,654 s | 2,644 s | 3,649 s |
| p90 | 14,156 s | 3,255 s | 4,118 s |
| p99 | 15,632 s | 4,445 s | 5,246 s |
| Max | 1m 15,970s | 16,583 s | 18,267 s |
A prescindere dai risultati poco incoraggianti, il Bouncer rimane la scelta migliore da adottare. Dato che il database resta a regimi medio/minimi, il collo di bottiglia si presenta a causa di una delle seguenti condizioni:
- Configurazione non adatta per il Bouncer
- Configurazioni PHP non corrette
- Hardware insufficiente per gestire la mole di carico
Il traffico di 2.000 macchine e' gestito su una singola macchina: in realta' viene diviso su 2 macchine distinte, quindi 2k potrebbe essere ancora un numero gestibile. In ogni caso l'obiettivo e' arrivare a 3.000; almeno con 1.500 host le richieste devono andare a buon fine.
Test in PRODUCTION (web1 + web2 bilanciate)¶
Per verificare se le limitazioni dipendono dall'hardware, i test successivi sono stati svolti direttamente sull'ambiente ufficiale dove il traffico su RDS viene bilanciato dalle due macchine web1 e web2.
1.500 BOUNCER (transaction mode — PRODUCTION)¶
| Parametro | Valore |
|---|---|
| Host | https://web.irrigation-dataserver.com |
| Numero macchine | 1.500 |
| Tempo trascorso | 10m 13,007s |
| Richieste effettuate | 23.222 |
| Richieste successo | 22.991 (99,01%) |
| Min | 93,044 ms |
| Avg | 393,136 ms |
| p50 | 211,220 ms |
| p75 | 365,566 ms |
| p90 | 790,581 ms |
| p99 | 3,232 s |
| Max | 5,513 s |
1.800 BOUNCER (transaction mode — PRODUCTION)¶
| Parametro | Valore |
|---|---|
| Numero macchine | 1.800 |
| Tempo trascorso | 11m 26,386s |
| Richieste effettuate | 29.753 |
| Richieste successo | 29.532 (99,26%) |
| Min | 93,369 ms |
| Avg | 1,102 s |
| p50 | 898,123 ms |
| p75 | 1,139 s |
| p90 | 1,618 s |
| p99 | 3,784 s |
| Max | 6,988 s |
1.900 BOUNCER (transaction mode — PRODUCTION)¶
| Parametro | Valore |
|---|---|
| Numero macchine | 1.900 |
| Tempo trascorso | 11m 34,753s |
| Richieste effettuate | 31.968 |
| Richieste successo | 31.512 (98,57%) |
| Min | 92,251 ms |
| Avg | 1,260 s |
| p50 | 956,966 ms |
| p75 | 1,343 s |
| p90 | 2,323 s |
| p99 | 4,081 s |
| Max | 5,622 s |
2.000 BOUNCER (transaction mode — PRODUCTION)¶
| Parametro | Valore |
|---|---|
| Numero macchine | 2.000 |
| Tempo trascorso | 11m 57,717s |
| Richieste effettuate | 34.480 |
| Richieste successo | 31.652 (91,80%) |
| Min | 91,036 ms |
| Avg | 1,027 s |
| p50 | 372,290 ms |
| p75 | 1,532 s |
| p90 | 2,975 s |
| p99 | 6,231 s |
| Max | 14,290 s |
Risultati ottenuti (PHP)¶
Durante i test effettuati nell'ambiente di produzione e' emerso che il consumo della CPU aumentava progressivamente fino a saturare entrambi i core. Con un carico di 2.000 macchine il consumo raggiungeva il 100% su entrambi i core. Il database RDS, invece, non mostrava segni di sovraccarico.
Per sicurezza e' consigliabile mantenere il numero di macchine sotto le 1.800.
Come incrementare questa soglia? Lato database non sembra necessario effettuare modifiche, come ad esempio il passaggio da un'istanza small a una medium. Anche nei test che prevedevano l'uso del Bouncer il database non e' mai stato in sovraccarico, raggiungendo al massimo un utilizzo del 30%: la configurazione attuale e' adeguata. Il collo di bottiglia si verifica lato CPU delle istanze applicative, dove il Bouncer consuma risorse significative. Sarebbe utile passare da due a tre core, ma questa opzione non e' praticabile con le istanze disponibili su AWS: per le configurazioni small, medium e large i core restano invariati mentre aumenta solo la memoria RAM (rispettivamente 2, 4 e 8 GB).
Soluzione proposta: aggiunta di una terza macchina. Con questa configurazione si stima di poter gestire un carico fino a circa 2.800 macchine. Il costo aggiuntivo riguarda non solo la nuova istanza, ma anche il bilanciamento su Cloudflare, stimato intorno a 5 euro in piu' al mese.
Node.js — db.t3.small¶
Istanza client: web.irrigation-dataserver.com (bilanciamento su web1 e web2). I test sono stati svolti direttamente in produzione.
In un'architettura basata su Node.js che si connette a un database PostgreSQL non ha molto senso utilizzare PgBouncer. Node.js, grazie a librerie come postgres.js, ha gia' un connection pool integrato che mantiene un numero definito di connessioni aperte verso il database, evitando di aprire e chiudere continuamente nuove connessioni. Aggiungere PgBouncer introduce un livello di pooling ridondante e un livello intermedio tra Node.js e PostgreSQL potrebbe introdurre latenza inutile nelle query.
Nella maggior parte dei casi, per applicazioni Node.js che si connettono direttamente a PostgreSQL, basta il connection pooling a livello applicativo. L'aggiunta di PgBouncer puo' essere controproducente, a meno che non ci siano vincoli di connessioni lato database.
Sintesi risultati Node.js¶
Tutti i test sono stati eseguiti senza PgBouncer.
Test 2.000 — 6.000 macchine¶
| Parametro | 2.000 | 3.000 | 4.000 | 5.000 | 6.000 |
|---|---|---|---|---|---|
| Tempo trascorso | 11m 48,291s | 15m 22,510s | 18m 39,080s | 22m 6,530s | 25m 14,546s |
| Richieste effettuate | 34.607 | 61.497 | 95.306 | 136.825 | 183.434 |
| Richieste successo | 34.561 (99,87%) | 61.405 (99,85%) | 95.209 (99,90%) | 136.745 (99,94%) | 183.339 (99,95%) |
| CPU RDS | 15% | 25% | 35% | 35% | 40% |
| Sessioni RDS (medie) | 1-2 | 2-3 | 4-5 (picchi 6) | 5-6 (picchi 7) | 6-7 (picchi 8) |
| CPU macchine web | 15-20% | 25-30% | 35-40% | 35-40% | 40-45% |
| Min | 75,580 ms | 77,538 ms | 75,865 ms | 76,620 ms | 75,257 ms |
| Avg | 114,720 ms | 123,793 ms | 192,393 ms | 167,712 ms | 285,646 ms |
| p50 | 104,905 ms | 108,769 ms | 112,903 ms | 117,360 ms | 122,300 ms |
| p75 | 118,039 ms | 125,262 ms | 136,488 ms | 153,190 ms | 168,138 ms |
| p90 | 140,604 ms | 161,528 ms | 204,157 ms | 279,822 ms | 479,205 ms |
| p99 | 256,232 ms | 355,197 ms | 3,228 s | 906,787 ms | 3,193 s |
| Max | 7,050 s | 14,340 s | 14,435 s | 14,490 s | 14,412 s |
Note:
- Gia' con 2.000 macchine si ottiene un risultato eccellente, confermando la netta superiorita' di Node.js rispetto a PHP. Il limite di sicurezza fissato per PHP era di 1.800 macchine, superato con facilita'.
- Con 4.000 macchine inizia a emergere un collo di bottiglia legato al limite di sessioni che il database RDS puo' gestire, determinato dalla tipologia di istanza (db.t3.small).
- Con 5.000 macchine si puo' considerare il limite di connessioni su RDS come dato certo. Durante i test con PHP il collo di bottiglia era PgBouncer sulle macchine web; ora il limite emerge direttamente sull'istanza RDS ed e' determinato esclusivamente dal numero di sessioni apribili (non dalla CPU). Con db.t3.small si possono sostenere fino a un numero X di connessioni simultanee in funzione del numero di sessioni determinato dall'istanza.
I test fino a 6.000 macchine hanno dato ottimi risultati, percio' ha senso fermarsi con i test standard e aumentare la "potenza di fuoco". Per i test successivi sono state effettuate modifiche allo script che lo hanno reso piu' prestante (rapido nella creazione) ma piu' intensivo in termini di consumo di banda. I risultati ottimi ottenuti nei test successivi sono unicamente riconducibili alle migliorie sul programma.
Test 8.000 — 10.000 macchine (script ottimizzato)¶
| Parametro | 8.000 | 9.000 | 10.000 |
|---|---|---|---|
| Tempo trascorso | 13m 51,087s | 13m 33,104s | 14m 44,670s |
| Richieste effettuate | 142.358 | 167.858 | 193.443 |
| Richieste successo | 142.356 (100,00%) | 167.856 (100,00%) | 193.440 (100,00%) |
| CPU RDS | 40-45% | 40-45% | 40-45% |
| Sessioni RDS (medie) | 9-10 (picchi 12) | 9-10 (fisse su 10) | 11-12 (fisse su 12, picchi 13) |
| CPU macchine web | 50-55% | 50-55% | 55-60% |
| Min | 32,138 ms | 32,459 ms | 32,705 ms |
| Avg | 787,817 ms | 838,125 ms | 1,761 s |
| p50 | 233,735 ms | 291,928 ms | 846,599 ms |
| p75 | 878,244 ms | 1,022 s | 2,772 s |
| p90 | 2,509 s | 2,424 s | 4,935 s |
| p99 | 6,382 s | 5,053 s | 8,251 s |
| Max | 10,557 s | 5,782 s | 10,847 s |
Visto il numero di sessioni raggiunte, si puo' presupporre di riuscire a tollerare ancora 1.000-1.500 macchine ma difficilmente di piu'. Limite di sicurezza fissato a meno di 10.000.
Golang — db.t3.small¶
Istanza client: web.irrigation-dataserver.com (bilanciamento su web1 e web2). I test sono stati svolti direttamente in produzione.
Come nel caso di Node.js, la libreria di connessione pgx/v5 a PostgreSQL utilizzata in Golang ha gia' un connection pool integrato che mantiene un numero definito di connessioni aperte verso il database, evitando di aprire e chiudere continuamente nuove connessioni. Non si necessita quindi del PgBouncer.
Sintesi risultati Golang¶
Tutti i test sono stati eseguiti senza PgBouncer.
Test 2.000 — 5.000 macchine¶
| Parametro | 2.000 | 3.000 | 4.000 | 5.000 |
|---|---|---|---|---|
| Tempo trascorso | 12m 50,247s | 15m 29,067s | 18m 35,013s | 22m 33,469s |
| Richieste effettuate | 34.228 | 61.588 | 95.466 | 135.861 |
| Richieste successo | 34.176 (99,85%) | 61.527 (99,90%) | 95.393 (99,92%) | 135.748 (99,92%) |
| CPU RDS | 10-15% | 15-20% | < 20% | < 25% |
| Sessioni RDS (medie) | 1-2 | 2-3 | 3-4 | 3-4 |
| CPU macchine web | 10-15% | 15-20% | 15-25% | 20-25% |
| Min | 69,795 ms | 71,755 ms | 69,137 ms | 72,170 ms |
| Avg | 192,816 ms | 107,991 ms | 206,163 ms | 635,488 ms |
| p50 | 98,475 ms | 94,603 ms | 101,889 ms | 109,729 ms |
| p75 | 127,783 ms | 104,337 ms | 134,157 ms | 285,816 ms |
| p90 | 247,435 ms | 127,940 ms | 398,746 ms | 1,920 s |
| p99 | 1,745 s | 346,512 ms | 1,601 s | 7,160 s |
| Max | 1m 15,191s | 14,386 s | 14,487 s | 40,217 s |
Come per Node.js, dopo i test standard e' stato adottato uno script piu' performante (piu' rapido nella creazione ma piu' intensivo in banda). I risultati ottimi ottenuti nei test successivi sono unicamente riconducibili alle migliorie del programma.
Test 8.000 — 10.000 macchine (script ottimizzato)¶
| Parametro | 8.000 | 9.000 | 10.000 |
|---|---|---|---|
| Tempo trascorso | 12m 32,103s | 13m 28,102s | 14m 29,890s |
| Richieste effettuate | 141.864 | 170.118 | 198.763 |
| Richieste successo | 141.864 (100,00%) | 170.117 (99,9994%) | 198.763 (100,00%) |
| CPU RDS | < 25% | < 30% | < 30% |
| Sessioni RDS (medie) | 5-6 (picco 7,77) | 6-7 (principale 7, picchi 9) | 7-8 (picchi 9) |
| CPU macchine web | 25-30% | 25-35% | 25-35% |
| Min | 26,607 ms | 25,852 ms | 26,791 ms |
| Avg | 615,850 ms | 200,869 ms | 321,915 ms |
| p50 | 95,569 ms | 115,620 ms | 212,248 ms |
| p75 | 318,882 ms | 278,536 ms | 468,853 ms |
| p90 | 1,976 s | 495,005 ms | 762,397 ms |
| p99 | 7,241 s | 896,817 ms | 1,448 s |
| Max | 11,848 s | 1,524 s | 2,488 s |
Con 8.000 macchine il numero massimo di sessioni del DB RDS non e' ancora stato saturato: si prevede di riuscire a raggiungere le 10.000 macchine gestite.
Stress test 14.000 macchine¶
| Parametro | Valore |
|---|---|
| Numero macchine | 14.000 |
| Tempo trascorso | 21m 7,703s |
| Richieste effettuate | 333.894 |
| Richieste successo | 333.848 (99,99%) |
| CPU RDS | < 35% |
| Sessioni RDS (medie) | 8-9 (picchi 11) |
| CPU macchine web | 25-35% |
| Min | 26,703 ms |
| Avg | 5,875 s |
| p50 | 5,625 s |
| p75 | 10,305 s |
| p90 | 12,529 s |
| p99 | 18,430 s |
| Max | 26,992 s |
Visto il numero di sessioni raggiunte, si puo' presupporre di riuscire a tollerare ancora 1.000-2.000 macchine ma difficilmente di piu'. Limite di sicurezza fissato a meno di 14.000.
Conclusioni sulla scelta della tecnologia¶
La comparazione fra i tre stack, a parita' di istanza RDS (db.t3.small), evidenzia progressioni nette del limite di sicurezza: 1.800 (PHP), 10.000 (Node.js), 14.000 (Golang).
- PHP e' limitato dal consumo CPU del PgBouncer sulle macchine applicative; la soluzione piu' sensata per alzare la soglia e' aggiungere una terza istanza web.
- Node.js elimina il PgBouncer grazie al pool integrato: il bottleneck si sposta sul numero di sessioni aperte verso RDS.
- Golang e' il piu' efficiente sia in termini di CPU sia in termini di sessioni RDS consumate, con tempi di risposta medi migliori rispetto a Node.js. A parita' di istanza sostiene quasi il 40% di carico in piu' rispetto a Node.js.
Il database RDS non si e' mai rivelato il limite ultimo del sistema: i veri colli di bottiglia sono il pooler esterno (PHP) o il tetto di sessioni dell'istanza (Node.js, Golang), modulabile salendo di classe (t3.medium o superiore) senza dover intervenire sull'architettura.