Vai al contenuto

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:

  1. Configurazione non adatta per il Bouncer
  2. Configurazioni PHP non corrette
  3. 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.