Si può estrarre più valore dai Microservizi se si utilizzano linguaggi nati nell’era del Cloud

Forse oggi ci sono strumenti migliori di Java per sviluppare applicazioni su architetture moderne

Enrico Piccinin
9 min readOct 24, 2020

Da qualche anno stiamo assistendo ad una sempre più estesa adozione delle architetture a Microservizi che si appoggiano su containers. In contesti IT di tipo tradizionale, spesso le troviamo utilizzate con un linguaggio di programmazione quale Java, nato nei primi anni ’90 e progettato per un mondo di applicazioni monolitiche (vi ricordate i cari vecchi Application Servers?).

A volte questa scelta è dettata da solide ragioni: necessità di integrare sistemi legacy tramite librerie presenti solo nell’ecosistema Java, convenienza ad utilizzare competenze già esistenti in azienda. A volte, tuttavia, questa scelta può derivare da una non attenta valutazione delle alternative oggi disponibili e dei vantaggi che queste possono portare.

Nell’ultima decade infatti, si è assistito all’affermazione di una serie di piattaforme di programmazione specificamente disegnate per ottimizzare il moderno “networked computing”, paradigma che sta alla base delle architetture a Microservizi. Ignorare queste evoluzioni può portare a costruire soluzioni sub-ottimali sia in termini di costi di gestione che di qualità. In altre parole: buttare via soldi e fornire un servizio non all’altezza.

Ignorare le piattaforme di programmazione sviluppate negli ultimi 10 anni può portare a risultati ed a costi di gestione sub-ottimali, in particolare quando si realizzano architetture basate su Microservizi

Si consideri inoltre che la diffusione spinta di containers quali Docker ha reso irrilevante il concetto “write once, everywhere”, che un tempo rappresentava uno dei principali valori di questa piattaforma.

In questo post vogliamo occuparci specificamente di due di queste “relativamente recenti” tecnologie, Node e Go.

Perché proprio Node e Go? Perché sono fra le prime nate ed hanno dimostrato di essere piattaforme sulle quali si possono costruire servizi complessi, solidi e scalabili(per esempio: Uber utilizza Go in una parte core della sua offerta, Paypal e Netflix si appoggiano molto su Node; stando in Italia, la nuova app io.it sviluppata da PagoPa e che si propone di essere la porta digitale per la Pubblica Amministrazione si basa su Node per la parte server).

Attorno a loro si sono sviluppati ecosistemi significativi. Vantano comunità di sviluppatori numerose, attive ed appassionate (si veda per esempio l’ultimo StackOverflow 2020 Developer Survey). Possono pertanto essere considerate ormai tecnologie mature anche in ambito Enterprise.

Novembre 2009, 15 anni dopo la prima release di Java

8 Novembre 2009, Berlino. Ryan Dahl presenta per la prima volta Node. Si tratta di una piattaforma che permette di eseguire Javascript (ed ora anche Typescript) lato server.

Due giorni dopo, 10 Novembre 2009, Mountain View. Google annuncia Go, un nuovo linguaggio di programmazione open source.

In soli due giorni due annunci, due tecnologie destinate ad influenzare significativamente il modo in cui si sviluppano le applicazioni. Si sentiva, è evidente, il bisogno di risposte nuove ai nuovi problemi che stavano emergendo.

Possiamo scalare a costi ottimali con i Microservizi, ma solo se li progettiamo per gestire molti “piccoli task” in maniera concorrente

Nel 2009 il trend di crescita esponenziale dei servizi digitali era ormai un fatto incontrovertibile. Questa crescita di richieste fatte ai sistemi da parte di utenti sempre più esigenti non poteva però essere affrontata con una crescita altrettanto esponenziale in termini di infrastrutture. Sarebbe stato antieconomico, e oggi lo sarebbe ancora di più.

Come risposta a questo problema emerse una nuova tendenza architetturale: costruire soluzioni scalabili orizzontalmente basate su processori ormai diventati commodity (e.g. x86). Queste architetture, che sono alla base dei nostri Microservizi, hanno dimostrato sul campo di poter scalare a costi ottimali, sempre che i loro Microservizi siano in grado di gestire molti “piccoli task” contemporaneamente (in altre parole con un throughput alto). Questo significa capacità di servire molte richieste “concorrenti” da parte degli utenti e fornire a questi utenti tempi di risposta ottimizzati, sfruttando al meglio le risorse (CPU e memoria) a disposizione.

Linguaggi tradizionali come Java, nati in un’era di Application Server monolitici e di processi monolitici, non sono stati progettati per gestire molti “piccoli tasks” contemporaneamente (si pensi per esempio alla gestione del multithreading, notoriamente non il pezzo più forte di Java) e possono quindi mostrare dei limiti quando utilizzati con questi modelli architetturali.

I Microservizi, per essere efficaci, devono ottimizzare l’esecuzione concorrente di tanti piccoli semplici task, e questo non è il tipo di lavoro per cui linguaggi come Java sono stati originariamente concepiti

Node e Go fornisco una risposta a questi limiti.

Cosa hanno in comune Node e Go? Supportano in modo naturale modelli concorrenti di elaborazione.

Al di là delle tante differenze, Node e Go hanno una cosa in comune: sono stati progettati per essere in grado di gestire in maniera naturale, oggi diremmo nativa, molte richieste di elaborazione “contemporaneamente”. E questa capacità di supportare modelli di elaborazione concorrenti potrebbe essere la causa profonda che spiega perché sono nati quasi nello stesso giorno. Cerchiamo di capire perché.

Supportare bene la concorrenza è importante

Guardiamo cosa fa un tipico programma “transazionale”, un programma che fa tante operazioni di Input/Output (I/O): magari usa un DB, magari chiama un servizio esterno, magari legge o scrive su dello storage.

Questo programma per lo più “aspetta”, “aspetta” che qualche risorsa esterna completi una operazione di I/O da lui iniziata. Aspetta perché le operazioni di I/O sono, per ragioni fisiche, ordini di grandezza più lente rispetto alle operazione svolte dalla CPU. E, nell’”aspettare”, spreca cicli di CPU (questa tipologia di processo viene detta “I/O bound”, ovvero limitata dall’I/O, in quanto il numero di cose che si riescono a fare nell’unità di tempo è sostanzialmente determinato dalla velocità con cui riusciamo a completare le operazioni di I/O, non dalla velocità del processore su cui gira la nostra logica).

Esempio di un programma I/O bound

Ma se sprechiamo cicli di CPU vuol dire che non utilizziamo in maniera efficiente la nostra infrastruttura. Aumentiamo i costi, aumentiamo i tempi di risposta e non serviamo al meglio i nostri clienti. E’ un poco come affittare un appartamento con dieci stanze ma poi usarne solo una. Vuol dire buttare via soldi e rinunciare alla qualità che stiamo comprando.

Con programmi transazionali, non sfruttare bene la concorrenza è come affittare un appartamento con dieci stanze per poi finire a vivere solo in una

Tutto ciò a meno che non facciamo qualche cosa per gestire più richieste “contemporaneamente”, che significa gestire la “concorrenza”.

Questo è tutt’altro che un problema nuovo. Tradizionalmente nel mondo Java la gestione della concorrenza era demandata agli Application Server. Ma non c’è più molto spazio per questi ingombranti oggetti nel mondo delle architetture a Microservizi. Ed è invece qui che piattaforme come Node e Go possono fornire interessanti risposte.

Supporto alla concorrenza in Node e Go

Node e Go sono in grado di gestire in maniera nativa molte richieste “contemporaneamente”, sfruttando così al meglio le risorse computazionali a disposizione. Finchè ci sono “task” in coda, la CPU non sta mai ferma. Non ci sono cicli buttati via “aspettando”.

Le tecniche che usano sono molto diverse ma il risultato ha molti lati in comune.

Un core che lavora in modo concorrente su processi I/O bound può essere utilizzato in maniera ottimale

Alla base c’è il fatto che sia Node che Go gestiscono le operazioni di I/O senza bloccare l’esecuzione dell’intero programma in attesa del risultato, ma alternando l’esecuzione dei vari “task” sulle risorse computazionali disponibili. Ciò, in particolare nel caso di processi I/O bound, permette di ottimizzare l’utilizzo di CPU e memoria. I cicli di CPU vengono utilizzati per servire le richieste dei clienti e non sono sprecati aspettando.

L’utilizzo ottimale delle risorse di CPU e memoria può comportare notevoli benefici economici soprattutto nel caso di Microservizi che si appoggiano su Public Cloud

Il potenziale beneficio economico può essere molto interessante, in particolare se si devono gestire tipici processi transazionali (I/O bound), in particolare se si è in Cloud, dove ogni ciclo di CPU si riflette sui costi di fine mese.

Ma non c’è solo la concorrenza

Se è vero che un naturale supporto alla concorrenza è ciò che accumuna Node e Go, in tutti gli altri aspetti queste piattaforme sono molto diverse fra loro. Offrono comunque interessanti caratteristiche rispetto ai linguaggi più tradizionali come Java.

Node, ovvero il sacro Graal di un linguaggio unico per Front End e Back End

Node è una piattaforma di esecuzione server per codice Javascript o Typescript, ovvero i linguaggi oggi centrali nello sviluppo di Front End (è quasi impossibile oggi pensare a sviluppi di Front End che non richiedano l’utilizzo di Javascript o Typescript).

Con Node quindi possiamo utilizzare un solo linguaggio sia lato Front End che lato Back End (anche se non tutte le tipologie di processo di back end possono essere servite al meglio da Node; per esempio i processi che richiedono computazioni intense mal si sposano con la natura single thread di Node). Ciò rappresenta una leva aggiuntiva di flessibilità ed efficienza nell’organizzare i team di sviluppo.

In questi ultimi anni Node ha ottenuto un successo straordinario (si pensi che è stata la prima piattaforma disponibile per scrivere serveless functions su tutte le maggiori piattaforme Cloud pubbliche). Un enorme ecosistema si è costruito attorno ad esso. Questo successo è anche uno dei suoi punti deboli. Se è vero che si trovano package Node per tutto, è anche vero che spesso la loro qualità è discutibile, tanto da portare alcuni a definire Node il “Far West della programmazione”. Se si decide di esplorare la strada di Node è necessario porre attenzione agli aspetti di controllo continuo di qualità e sicurezza.

Tuttavia, se siamo di fronte ad uno scenario che vede tante richieste di elaborazione I/O bound concorrenti e se abbiamo competenze Javascript/Typescript in casa, Node è certamente una piattaforma da valutare.

Go, ovvero della riscoperta della semplicità e delle prestazioni

Go è semplice. Il linguaggio è costituito da 25 parole riservate e le specifiche stanno in 86 pagine, per la maggior parte dedicate ad esempi. Giusto per confronto, le specifiche di Java nell’edizione dell’anno 2000 contavano già 350 pagine.

Questa semplicità facilita l’apprendimento e facilita la comprensione del codice. Il codice Go è di solito facile da leggere, facile da capire e di conseguenza da manutenere. La chiarezza del codice è un valore molto importante, anche dal punto di vista economico, visto che secondo importanti studi il tempo speso a leggere codice è dieci volte superiore a quello speso a scriverlo.

Go è stato anche progettato per essere molto performante, sia in termini di velocità di compilazione, che di esecuzione, che di memoria richiesta. Il compilatore Go genera codice nativo che normalmente non necessita di altre librerie per essere eseguito. La dimensione di tali eseguibili è relativamente piccola. Immagini Docker che contengano programmi Go possono occupare 10–20 MB, a fronte delle centinaia di MB richieste da immagini Docker con applicativi Java. E la dimensione delle immagini è importante sia per questioni di prestazioni che di sicurezza.

Non ci sono solo Node e Go

Oltre a Node e Go, in questi ultimi anni sono emerse diverse altre interessanti novità nell’ambito dello sviluppo applicativo quali Kotlin, Clojure e GraalVM, tecnologie legate alla JVM, oppure Rust, un moderno e più sicuro C++.

Node e Go rimangono tuttavia speciali proprio nella loro capacità di gestire la concorrenza in maniera molto efficiente, mantenendo una natura “developer friendly” con una curva di apprendimento relativamente bassa. Forse anche per queste ragioni possono vantare una diffusione superiore ed in continua ascesa.

Conclusioni

Il mondo del software evolve velocemente. Le novità sono continue ed è difficile capire quale sia il giusto equilibrio fra lo sfruttare i benefici che tali novità promettono, il livello di maturità richiesto a livello enterprise ed il giusto livello di standardizzazione che comunque è necessario per qualunque organizzazione.

Node e Go hanno negli anni dimostrato di essere tecnologie mature ad “Enterprise level” ed hanno la potenzialità di portare importanti benefici sia in termini di costi infrastrutturali che di prestazioni rispetto a linguaggi tradizionali come Java, in particolare su architetture orizzontali e per ambiti applicativi transazionali, quali sono molte delle applicazioni Enterprise. Sono supportate da community numerose ed hanno ecosistemi molto ampi.

Se è vero che le aziende, in particolare quelle con una più lunga storia IT, dovranno continuare ad utilizzare in maniera predominante piattaforme tradizionali quali Java dato l’enorme livello di investimento che su di esse si è accumulato, è altrettanto opportuno che inizino ad adottare anche strumenti relativamente nuovi, quali Node e Go, se vogliono massimizzare i benefici che le moderne architetture a Microservizi promettono.

Questo articolo è una sintesi in italiano del post originale che potete leggere su Hackernoon qui.

--

--

Enrico Piccinin

A man with passion for code and for some strange things that sometimes happen in IT organizations. Views and thoughts here are my own.