Prestazioni I/O lato server: Node vs. PHP vs. Java vs. Go

Ott 22, 2021
admin

Comprendere il modello di Input/Output (I/O) della vostra applicazione può significare la differenza tra un’applicazione che affronta il carico a cui è sottoposta, e una che si accartoccia di fronte ai casi d’uso del mondo reale. Forse mentre la vostra applicazione è piccola e non serve carichi elevati, può avere molta meno importanza. Ma quando il carico di traffico della vostra applicazione aumenta, lavorare con il modello di I/O sbagliato può portarvi in un mondo di dolore.

E come la maggior parte delle situazioni in cui sono possibili più approcci, non è solo una questione di quale sia migliore, è una questione di capire i compromessi. Facciamo una passeggiata attraverso il panorama dell’I/O e vediamo cosa possiamo spiare.

In questo articolo, confronteremo Node, Java, Go, e PHP con Apache, discutendo come i diversi linguaggi modellano il loro I/O, i vantaggi e gli svantaggi di ogni modello, e concludiamo con alcuni rudimentali benchmark. Se sei preoccupato per le prestazioni di I/O della tua prossima applicazione web, questo articolo è per te.

Fondamenti di I/O: A Quick Refresher

Per capire i fattori coinvolti nell’I/O, dobbiamo prima rivedere i concetti a livello di sistema operativo. Mentre è improbabile che abbiate a che fare direttamente con molti di questi concetti, li affrontate indirettamente attraverso l’ambiente di runtime della vostra applicazione tutto il tempo. E i dettagli sono importanti.

Chiamate di sistema

In primo luogo, abbiamo le chiamate di sistema, che possono essere descritte come segue:

  • Il vostro programma (nella “terra dell’utente”, come si dice) deve chiedere al kernel del sistema operativo di eseguire un’operazione I/O per suo conto.
  • Una “syscall” è il mezzo con cui il vostro programma chiede al kernel di fare qualcosa. Le specifiche di come questo è implementato variano tra i sistemi operativi, ma il concetto di base è lo stesso. Ci sarà qualche istruzione specifica che trasferisce il controllo dal vostro programma al kernel (come una chiamata di funzione ma con qualche salsa speciale specifica per affrontare questa situazione). In generale, le syscalls sono bloccanti, il che significa che il vostro programma aspetta che il kernel ritorni al vostro codice.
  • Il kernel esegue l’operazione di I/O sottostante sul dispositivo fisico in questione (disco, scheda di rete, ecc.) e risponde alla syscall. Nel mondo reale, il kernel potrebbe dover fare una serie di cose per soddisfare la vostra richiesta, tra cui aspettare che il dispositivo sia pronto, aggiornare il suo stato interno, ecc. Questo è il lavoro del kernel.

Chiamate bloccanti e non bloccanti

Ora, ho appena detto sopra che le syscalls sono bloccanti, e questo è vero in senso generale. Tuttavia, alcune chiamate sono classificate come “non bloccanti”, il che significa che il kernel prende la vostra richiesta, la mette in coda o in un buffer da qualche parte, e poi ritorna immediatamente senza aspettare che avvenga l’I/O effettivo. Quindi si “blocca” solo per un periodo di tempo molto breve, giusto il tempo necessario per mettere in coda la vostra richiesta.

Alcuni esempi (di syscalls Linux) potrebbero aiutare a chiarire:- read() è una chiamata bloccante – gli si passa un handle che dice quale file e un buffer di dove consegnare i dati che legge, e la chiamata ritorna quando i dati sono lì. Notate che questo ha il vantaggio di essere bello e semplice.- epoll_create(), epoll_ctl() e epoll_wait() sono chiamate che, rispettivamente, vi permettono di creare un gruppo di handle da ascoltare, aggiungere/rimuovere gestori da quel gruppo e poi bloccare finché non c’è attività. Questo vi permette di controllare in modo efficiente un gran numero di operazioni di I/O con un singolo thread, ma sto andando avanti. Questo è ottimo se avete bisogno della funzionalità, ma come potete vedere è certamente più complesso da usare.

E’ importante capire l’ordine di grandezza della differenza nei tempi qui. Se il core di una CPU funziona a 3GHz, senza entrare nelle ottimizzazioni che la CPU può fare, sta eseguendo 3 miliardi di cicli al secondo (o 3 cicli per nanosecondo). Una chiamata di sistema non bloccante potrebbe richiedere decine di cicli per essere completata – o “relativamente pochi nanosecondi”. Una chiamata che si blocca per informazioni ricevute in rete potrebbe richiedere un tempo molto più lungo – diciamo per esempio 200 millisecondi (1/5 di secondo). E diciamo, per esempio, che la chiamata non bloccante ha impiegato 20 nanosecondi, e quella bloccante 200.000.000 nanosecondi. Il vostro processo ha appena aspettato 10 milioni di volte di più per la chiamata bloccante.

Il kernel fornisce i mezzi per fare sia I/O bloccante (“leggi da questa connessione di rete e dammi i dati”) che I/O non bloccante (“dimmi quando una di queste connessioni di rete ha nuovi dati”). E quale meccanismo viene usato bloccherà il processo chiamante per lunghezze di tempo drammaticamente diverse.

Schedulazione

La terza cosa fondamentale da seguire è cosa succede quando si hanno molti thread o processi che iniziano a bloccare.

Per i nostri scopi, non c’è una grande differenza tra un thread e un processo. Nella vita reale, la differenza più evidente in termini di prestazioni è che, poiché i thread condividono la stessa memoria e i processi hanno ciascuno il proprio spazio di memoria, creare processi separati tende ad occupare molta più memoria. Ma quando si parla di programmazione, ciò che si riduce veramente ad una lista di cose (sia thread che processi) che hanno bisogno di ottenere una fetta di tempo di esecuzione sui core della CPU disponibili. Se avete 300 thread in esecuzione e 8 core su cui eseguirli, dovete dividere il tempo in modo che ognuno ottenga la sua parte, con ogni core in esecuzione per un breve periodo di tempo e poi passare al thread successivo. Questo viene fatto attraverso un “cambio di contesto”, facendo passare la CPU dall’esecuzione di un thread/processo a quello successivo.

Questi cambi di contesto hanno un costo associato ad essi – richiedono del tempo. In alcuni casi veloci, può essere meno di 100 nanosecondi, ma non è raro che ci vogliano 1000 nanosecondi o più, a seconda dei dettagli di implementazione, della velocità/architettura del processore, della cache della CPU, ecc.

E più thread (o processi), più commutazione di contesto. Quando stiamo parlando di migliaia di thread, e centinaia di nanosecondi per ciascuno, le cose possono diventare molto lente.

Tuttavia, le chiamate non bloccanti in sostanza dicono al kernel “chiamami solo quando hai qualche nuovo dato o evento su uno di questi collegamenti”. Queste chiamate non bloccanti sono progettate per gestire in modo efficiente grandi carichi di I/O e ridurre la commutazione di contesto.

Ci siamo capiti fin qui? Perché ora arriva la parte divertente: Guardiamo cosa fanno alcuni linguaggi popolari con questi strumenti e traiamo alcune conclusioni sui compromessi tra facilità d’uso e prestazioni… e altre chicche interessanti.

Come nota, mentre gli esempi mostrati in questo articolo sono banali (e parziali, con solo le parti rilevanti mostrate); l’accesso al database, i sistemi di caching esterni (memcache, ecc.) e tutto ciò che richiede I/O finirà per eseguire una sorta di chiamata I/O sotto il cappuccio che avrà lo stesso effetto dei semplici esempi mostrati. Inoltre, per gli scenari in cui l’I/O è descritto come “bloccante” (PHP, Java), la richiesta HTTP e la lettura e scrittura della risposta sono esse stesse chiamate bloccanti: Di nuovo, più I/O nascosto nel sistema con i relativi problemi di prestazioni da prendere in considerazione.

Ci sono molti fattori che entrano nella scelta di un linguaggio di programmazione per un progetto. Ci sono anche molti fattori quando si considerano solo le prestazioni. Ma, se siete preoccupati che il vostro programma sarà limitato principalmente dall’I/O, se le prestazioni dell’I/O sono fondamentali per il vostro progetto, queste sono cose che dovete sapere.

L’approccio “Keep It Simple”: PHP

Negli anni 90, molte persone indossavano scarpe Converse e scrivevano script CGI in Perl. Poi è arrivato PHP e, per quanto ad alcune persone piaccia prenderlo in giro, ha reso la creazione di pagine web dinamiche molto più facile.

Il modello usato da PHP è abbastanza semplice. Ci sono alcune variazioni, ma il tuo server PHP medio è così:

Una richiesta HTTP arriva dal browser di un utente e colpisce il tuo server web Apache. Apache crea un processo separato per ogni richiesta, con alcune ottimizzazioni per riutilizzarli al fine di minimizzare il numero che deve fare (creare processi è, relativamente parlando, lento).Apache chiama PHP e gli dice di eseguire il file .php appropriato sul disco.Il codice PHP viene eseguito e fa chiamate I/O bloccanti. Tu chiami file_get_contents() in PHP e sotto il cofano fa read() chiamate al sistema e aspetta i risultati.

E naturalmente il codice effettivo è semplicemente incorporato nella tua pagina, e le operazioni sono bloccanti:

<?php// blocking file I/O$file_data = file_get_contents('/path/to/file.dat');// blocking network I/O$curl = curl_init('http://example.com/example-microservice');$result = curl_exec($curl);// some more blocking network I/O$result = $db->query('SELECT id, data FROM examples ORDER BY id DESC limit 100');?>

In termini di come questo si integra con il sistema, è così:

Praticamente semplice: un processo per richiesta. Le chiamate I/O si bloccano e basta. Vantaggio? È semplice e funziona. Svantaggio? Colpitelo con 20.000 clienti contemporaneamente e il vostro server andrà in fiamme. Questo approccio non scala bene perché gli strumenti forniti dal kernel per gestire l’I/O ad alto volume (epoll, ecc.) non vengono utilizzati. E per aggiungere la beffa al danno, l’esecuzione di un processo separato per ogni richiesta tende ad usare molte risorse di sistema, specialmente la memoria, che è spesso la prima cosa che si esaurisce in uno scenario come questo.

Nota: L’approccio usato per Ruby è molto simile a quello di PHP, e in un modo ampio, generale, che si può considerare lo stesso per i nostri scopi.

L’approccio multithreaded: Java

Così arriva Java, proprio nel periodo in cui hai comprato il tuo primo nome di dominio ed era figo dire a caso “dot com” dopo una frase. E Java ha il multithreading incorporato nel linguaggio, che (specialmente per quando è stato creato) è piuttosto impressionante.

La maggior parte dei server web Java funziona avviando un nuovo thread di esecuzione per ogni richiesta che arriva e poi in questo thread alla fine chiama la funzione che tu, come sviluppatore dell’applicazione, hai scritto.

Fare I/O in un Java Servlet tende ad assomigliare a qualcosa come:

public void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException{// blocking file I/OInputStream fileIs = new FileInputStream("/path/to/file");// blocking network I/OURLConnection urlConnection = (new URL("http://example.com/example-microservice")).openConnection();InputStream netIs = urlConnection.getInputStream();// some more blocking network I/Oout.println("...");}

Siccome il nostro metodo doGet sopra corrisponde a una richiesta ed è eseguito nel suo proprio thread, invece di un processo separato per ogni richiesta che richiede la sua propria memoria, abbiamo un thread separato. Questo ha alcuni vantaggi, come la possibilità di condividere lo stato, i dati nella cache, ecc. tra i thread perché possono accedere alla memoria dell’altro, ma l’impatto su come interagisce con la pianificazione è ancora quasi identico a quello che è stato fatto nell’esempio PHP in precedenza. Ogni richiesta ottiene un nuovo thread e le varie operazioni di I/O si bloccano all’interno di quel thread fino a quando la richiesta è completamente gestita. I thread sono raggruppati per minimizzare il costo della loro creazione e distruzione, ma comunque, migliaia di connessioni significano migliaia di thread, il che è un male per lo scheduler.

Un’importante pietra miliare è che nella versione 1.4 Java (e un aggiornamento significativo ancora nella 1.7) ha ottenuto la capacità di fare chiamate I/O non bloccanti. La maggior parte delle applicazioni, web e non, non la usano, ma almeno è disponibile. Alcuni server web Java cercano di approfittarne in vari modi; tuttavia, la stragrande maggioranza delle applicazioni Java distribuite funzionano ancora come descritto sopra.

Java ci avvicina e certamente ha alcune buone funzionalità out-of-the-box per l’I/O, ma ancora non risolve realmente il problema di cosa succede quando si ha un’applicazione pesantemente legata all’I/O che viene pestata a morte con molte migliaia di thread bloccanti.

L’I/O non bloccante come cittadino di prima classe: Node

Il ragazzo popolare sul blocco quando si tratta di I/O migliore è Node.js. A chiunque abbia avuto anche la più breve introduzione a Node è stato detto che è “non-blocking” e che gestisce l’I/O in modo efficiente. E questo è vero in senso generale. Ma il diavolo è nei dettagli e i mezzi con cui questa stregoneria è stata ottenuta contano quando si tratta di prestazioni.

In sostanza il cambio di paradigma che Node implementa è che invece di dire essenzialmente “scrivi il tuo codice qui per gestire la richiesta”, dicono invece “scrivi il codice qui per iniziare a gestire la richiesta”. Ogni volta che hai bisogno di fare qualcosa che coinvolge l’I/O, fai la richiesta e dai una funzione di callback che Node chiamerà quando ha finito.

Il codice tipico di Node per fare un’operazione di I/O in una richiesta va così:

http.createServer(function(request, response) {fs.readFile('/path/to/file', 'utf8', function(err, data) {response.end(data);});});

Come puoi vedere, ci sono due funzioni di callback qui. La prima viene chiamata quando inizia una richiesta, e la seconda viene chiamata quando i dati del file sono disponibili.

Quello che fa è fondamentalmente dare a Node l’opportunità di gestire in modo efficiente l’I/O tra queste callback. Uno scenario in cui sarebbe ancora più rilevante è quello in cui si sta facendo una chiamata al database in Node, ma non mi preoccuperò dell’esempio perché è esattamente lo stesso principio: si avvia la chiamata al database e si dà a Node una funzione di callback, esso esegue le operazioni di I/O separatamente usando chiamate non bloccanti e poi invoca la funzione di callback quando i dati richiesti sono disponibili. Questo meccanismo di accodare le chiamate di I/O e lasciare che Node le gestisca e poi ottenere una callback è chiamato “Event Loop”. E funziona abbastanza bene.

C’è comunque una fregatura in questo modello. Sotto il cofano, la ragione ha molto più a che fare con il modo in cui il motore JavaScript V8 (il motore JS di Chrome che è usato da Node) è implementato 1 di qualsiasi altra cosa. Il codice JS che si scrive viene eseguito in un singolo thread. Pensate a questo per un momento. Significa che mentre l’I/O viene eseguito utilizzando tecniche efficienti non bloccanti, il tuo JS che sta facendo operazioni legate alla CPU viene eseguito in un singolo thread, ogni pezzo di codice che blocca il successivo. Un esempio comune di dove questo potrebbe accadere è il looping sui record del database per processarli in qualche modo prima di mostrarli al client. Ecco un esempio che mostra come funziona:

var handler = function(request, response) {connection.query('SELECT ...', function (err, rows) {if (err) { throw err };for (var i = 0; i < rows.length; i++) {// do processing on each row}response.end(...); // write out the results})};

Mentre Node gestisce l’I/O in modo efficiente, quel ciclo for nell’esempio sopra sta usando cicli di CPU all’interno del tuo unico thread principale. Questo significa che se hai 10.000 connessioni, quel ciclo potrebbe portare l’intera applicazione ad un arresto, a seconda di quanto tempo impiega. Ogni richiesta deve condividere una fetta di tempo, una alla volta, nel vostro thread principale.

La premessa su cui si basa tutto questo concetto è che le operazioni di I/O sono la parte più lenta, quindi è più importante gestire quelle in modo efficiente, anche se ciò significa fare altre elaborazioni in serie. Questo è vero in alcuni casi, ma non in tutti.

L’altro punto è che, e mentre questa è solo un’opinione, può essere abbastanza noioso scrivere un mucchio di callback annidati e alcuni sostengono che rende il codice significativamente più difficile da seguire. Non è raro vedere callback annidati a quattro, cinque o anche più livelli in profondità nel codice Node.

Siamo tornati di nuovo ai compromessi. Il modello Node funziona bene se il vostro principale problema di prestazioni è l’I/O. Tuttavia, il suo tallone d’achille è che si può andare in una funzione che sta gestendo una richiesta HTTP e mettere del codice ad alta intensità di CPU e portare ogni connessione ad un punto morto se non si sta attenti.

Naturalmente non bloccante: Go

Prima di entrare nella sezione per Go, è opportuno che io riveli che sono un fanboy di Go. L’ho usato per molti progetti e sono apertamente un sostenitore dei suoi vantaggi di produttività, e li vedo nel mio lavoro quando lo uso.

Detto questo, diamo un’occhiata a come tratta l’I/O. Una caratteristica chiave del linguaggio Go è che contiene il proprio scheduler. Invece di ogni thread di esecuzione corrispondente ad un singolo thread del sistema operativo, lavora con il concetto di “goroutine”. E il runtime di Go può assegnare una goroutine ad un thread del sistema operativo e farla eseguire, o sospenderla e non associarla ad un thread del sistema operativo, in base a ciò che quella goroutine sta facendo. Ogni richiesta che arriva dal server HTTP di Go è gestita in una goroutine separata.

Il diagramma di come funziona lo scheduler assomiglia a questo:

Sotto il cappuccio, questo è implementato da vari punti nel runtime di Go che implementano la chiamata I/O facendo la richiesta di scrivere/lettura/collegamento/ecc, mettono a dormire la goroutine corrente, con l’informazione di risvegliare la goroutine quando possono essere intraprese ulteriori azioni.

In effetti, il runtime di Go sta facendo qualcosa di non terribilmente dissimile da ciò che sta facendo Node, tranne che il meccanismo di callback è incorporato nell’implementazione della chiamata di I/O e interagisce con lo scheduler automaticamente. Inoltre non soffre della restrizione di dover far eseguire tutto il tuo codice di gestione nello stesso thread, Go mapperà automaticamente le tue goroutine su quanti thread del sistema operativo ritiene appropriati in base alla logica del suo scheduler. Il risultato è un codice come questo:

func ServeHTTP(w http.ResponseWriter, r *http.Request) {// the underlying network call here is non-blockingrows, err := db.Query("SELECT ...")for _, row := range rows {// do something with the rows,// each request in its own goroutine}w.Write(...) // write the response, also non-blocking}

Come potete vedere sopra, la struttura di base del codice di ciò che stiamo facendo assomiglia a quella degli approcci più semplicistici, e tuttavia raggiunge l’I/O non bloccante sotto il cappuccio.

Nella maggior parte dei casi, questo finisce per essere “il meglio dei due mondi”. L’I/O non bloccante è usato per tutte le cose importanti, ma il vostro codice sembra essere bloccante e quindi tende ad essere più semplice da capire e mantenere. L’interazione tra lo scheduler di Go e lo scheduler del sistema operativo gestisce il resto. Non è una magia completa, e se si costruisce un sistema di grandi dimensioni, vale la pena dedicare del tempo a capire più in dettaglio come funziona; ma allo stesso tempo, l’ambiente che si ottiene “out-of-the-box” funziona e scala abbastanza bene.

Go può avere i suoi difetti, ma in generale, il modo in cui gestisce l’I/O non è tra questi.

Lies, Damned Lies and Benchmarks

È difficile dare tempi esatti sulla commutazione di contesto coinvolta con questi vari modelli. Potrei anche sostenere che è meno utile per voi. Quindi, invece, vi darò alcuni benchmark di base che confrontano le prestazioni generali del server HTTP di questi ambienti server. Tenete a mente che molti fattori sono coinvolti nelle prestazioni dell’intero percorso richiesta/risposta HTTP end-to-end, e i numeri presentati qui sono solo alcuni campioni che ho messo insieme per dare un confronto di base.

Per ognuno di questi ambienti, ho scritto il codice appropriato per leggere un file da 64k con byte casuali, eseguire un hash SHA-256 su di esso N volte (N è specificato nella stringa di query dell’URL, ad esempio, .../test.php?n=100) e stampare l’hash risultante in esadecimale. Ho scelto questo perché è un modo molto semplice per eseguire gli stessi benchmark con qualche I/O consistente e un modo controllato per aumentare l’utilizzo della CPU.

Vedete queste note di benchmark per un po’ più di dettagli sugli ambienti utilizzati.

Prima di tutto, guardiamo alcuni esempi a bassa concorrenza. L’esecuzione di 2000 iterazioni con 300 richieste concorrenti e un solo hash per richiesta (N=1) ci dà questo:

I tempi sono il numero medio di millisecondi per completare una richiesta tra tutte le richieste concorrenti. Più basso è meglio.

È difficile trarre una conclusione da questo solo grafico, ma questo mi sembra che, a questo volume di connessione e calcolo, stiamo vedendo tempi che hanno più a che fare con l’esecuzione generale delle lingue stesse, molto più che con l’I/O. Si noti che i linguaggi considerati “linguaggi di scripting” (digitazione libera, interpretazione dinamica) sono i più lenti.

Ma cosa succede se aumentiamo N a 1000, sempre con 300 richieste concorrenti – lo stesso carico ma 100 volte più iterazioni di hash (significativamente più carico di CPU):

I tempi sono il numero medio di millisecondi per completare una richiesta tra tutte le richieste concorrenti. Più basso è meglio.

Tutto d’un tratto, le prestazioni di Node calano significativamente, perché le operazioni ad alta intensità di CPU in ogni richiesta si bloccano a vicenda. Ed è interessante notare che le prestazioni di PHP migliorano molto (rispetto agli altri) e battono Java in questo test. (Vale la pena notare che in PHP l’implementazione SHA-256 è scritta in C e il percorso di esecuzione passa molto più tempo in quel ciclo, dato che ora stiamo facendo 1000 iterazioni dell’hash).

Ora proviamo 5000 connessioni concorrenti (con N=1) – o il più vicino possibile. Sfortunatamente, per la maggior parte di questi ambienti, il tasso di fallimento non era insignificante. Per questo grafico, guarderemo il numero totale di richieste al secondo. Più alto è meglio è:

Numero totale di richieste al secondo. Più alto è meglio.

E l’immagine sembra molto diversa. È una supposizione, ma sembra che ad un alto volume di connessioni l’overhead per connessione coinvolto nello spawning di nuovi processi e la memoria aggiuntiva associata ad esso in PHP+Apache sembra diventare un fattore dominante e affossa le prestazioni di PHP. Chiaramente, Go è il vincitore qui, seguito da Java, Node e infine PHP.

Mentre i fattori coinvolti nel throughput complessivo sono molti e variano ampiamente da un’applicazione all’altra, più si capisce cosa sta succedendo sotto il cofano e i compromessi coinvolti, meglio sarà.

In sintesi

Con tutto quanto sopra, è abbastanza chiaro che come i linguaggi si sono evoluti, le soluzioni per trattare con applicazioni su larga scala che fanno molto I/O si sono evolute con essi.

Per essere giusti, sia PHP che Java, nonostante le descrizioni in questo articolo, hanno implementazioni di I/O non bloccanti disponibili per l’uso in applicazioni web. Ma queste non sono così comuni come gli approcci descritti sopra, e bisognerebbe prendere in considerazione l’overhead operativo che comporta il mantenimento di server che utilizzano tali approcci. Per non parlare del fatto che il vostro codice deve essere strutturato in modo da funzionare con tali ambienti; la vostra “normale” applicazione web PHP o Java di solito non funzionerà senza modifiche significative in un tale ambiente.

Come confronto, se consideriamo alcuni fattori significativi che influenzano le prestazioni e la facilità d’uso, otteniamo questo:

Lingua Threads vs. Processi Non-I/O non bloccante Facilità d’uso
PHP Processi No
Java Threads Available Requires Callbacks
Node.js Threads Richiede Callbacks
Go Threads (Goroutines) No Callbacks Needed

I thread sono generalmente molto più efficienti in termini di memoria dei processi, poiché condividono lo stesso spazio di memoria, mentre i processi no. Combinando questo con i fattori relativi all’I/O non bloccante, possiamo vedere che almeno con i fattori considerati sopra, man mano che scendiamo nella lista la configurazione generale relativa all’I/O migliora. Quindi, se dovessi scegliere un vincitore nel concorso di cui sopra, sarebbe certamente Go.

Anche così, in pratica, la scelta di un ambiente in cui costruire la vostra applicazione è strettamente collegata alla familiarità che il vostro team ha con tale ambiente, e la produttività complessiva che potete raggiungere con esso. Quindi potrebbe non avere senso per ogni team tuffarsi e iniziare a sviluppare applicazioni e servizi web in Node o Go. Infatti, trovare sviluppatori o la familiarità del proprio team interno è spesso citata come la ragione principale per non usare un linguaggio e/o un ambiente diverso. Detto questo, i tempi sono cambiati negli ultimi quindici anni o giù di lì, molto.

Spero che quanto sopra aiuti a dipingere un quadro più chiaro di ciò che accade sotto il cofano e vi dia alcune idee su come affrontare la scalabilità nel mondo reale per la vostra applicazione. Buon inserimento ed estrazione!

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.