Node.js
Node.js

Follow

24 febbraio 2017 – 14 min read

Questo articolo viene da Tomislav Capan, consulente tecnico e appassionato di Node.js. Tomislav ha originariamente pubblicato questo nell’agosto 2013 nel blog di Toptal – potete trovare il post originale qui; il blog è stato leggermente aggiornato. Il seguente argomento è basato sull’opinione e le esperienze di questo autore.

La crescente popolarità di JavaScript ha portato con sé molti cambiamenti, e il volto dello sviluppo web oggi è drammaticamente diverso. Le cose che possiamo fare sul web oggi con JavaScript in esecuzione sul server, così come nel browser, erano difficili da immaginare solo alcuni anni fa, o erano incapsulate all’interno di ambienti sandboxed come Flash o Java Applets.

Prima di addentrarsi in Node.js, potreste voler leggere i benefici dell’uso di JavaScript in tutto lo stack, che unifica il linguaggio e il formato dei dati (JSON), permettendovi di riutilizzare in modo ottimale le risorse dello sviluppatore. Poiché questo è più un vantaggio di JavaScript che di Node.js in particolare, non ne parleremo molto qui. Ma è un vantaggio chiave per incorporare Node.js nel tuo stack.

Node.js è un ambiente di esecuzione JavaScript costruito sul motore JavaScript V8 di Chrome. Vale la pena notare che Ryan Dahl, il creatore di Node.js, mirava a creare siti web in tempo reale con capacità push, “ispirato da applicazioni come Gmail”. In Node.js, ha dato agli sviluppatori uno strumento per lavorare nel paradigma I/O non bloccante e guidato dagli eventi.

In una frase: Node.js brilla nelle applicazioni web in tempo reale che impiegano la tecnologia push su websockets. Cosa c’è di così rivoluzionario in questo? Beh, dopo oltre 20 anni di web senza stato basato sul paradigma richiesta-risposta senza stato, abbiamo finalmente applicazioni web con connessioni bidirezionali in tempo reale, dove sia il client che il server possono avviare la comunicazione, permettendo loro di scambiarsi dati liberamente.

Questo è in netto contrasto con il tipico paradigma di risposta web, dove il client avvia sempre la comunicazione. Inoltre, è tutto basato sullo stack web aperto (HTML, CSS e JS) che gira sulla porta standard 80.

Si potrebbe sostenere che abbiamo avuto questo per anni sotto forma di Flash e Java Applets – ma in realtà, questi erano solo ambienti sandboxed che usavano il web come un protocollo di trasporto da consegnare al client. Inoltre, erano eseguiti in isolamento e spesso operavano su porte non standard, che possono aver richiesto permessi extra e simili.

Con tutti i suoi vantaggi, Node.js ora gioca un ruolo critico nello stack tecnologico di molte aziende di alto profilo che dipendono dai suoi benefici unici. La Node.js Foundation ha consolidato tutte le migliori riflessioni sul perché le imprese dovrebbero considerare Node.js in una breve presentazione che può essere trovata sulla pagina Case Studies della Node.js Foundation.

In questo post, discuterò non solo come si realizzano questi vantaggi, ma anche perché si potrebbe voler usare Node.js – e perché no – usando alcuni dei classici modelli di applicazioni web come esempi.

Come funziona?

L’idea principale di Node.js: usare l’I/O non bloccante ed event-driven per rimanere leggeri ed efficienti di fronte ad applicazioni ad alta intensità di dati in tempo reale che girano su dispositivi distribuiti.

E’ un po’ lungo.

Quello che significa veramente è che Node.js non è una nuova piattaforma che dominerà il mondo dello sviluppo web. Invece, è una piattaforma che riempie un bisogno particolare. E capire questo è assolutamente essenziale. Sicuramente non volete usare Node.js per operazioni ad alta intensità di CPU; infatti, usarlo per calcoli pesanti annullerà quasi tutti i suoi vantaggi. Dove Node.js brilla davvero è nella costruzione di applicazioni di rete veloci e scalabili, in quanto è in grado di gestire un enorme numero di connessioni simultanee con un elevato throughput, che equivale ad un’alta scalabilità.

Come funziona sotto il cofano è piuttosto interessante. Rispetto alle tradizionali tecniche di web-serving in cui ogni connessione (richiesta) genera un nuovo thread, occupando la RAM del sistema e finendo per esaurire la quantità di RAM disponibile, Node.js opera su un singolo thread, utilizzando chiamate I/O non bloccanti, permettendo di supportare decine di migliaia di connessioni concorrenti (tenute nel ciclo dell’evento).

*Immagine presa dal post originale del blog.

Un rapido calcolo: supponendo che ogni thread abbia potenzialmente 2 MB di memoria con sé, l’esecuzione su un sistema con 8 GB di RAM ci mette ad un massimo teorico di 4000 connessioni concorrenti (calcoli presi dall’articolo di Michael Abernethy “Just what is Node.js?”, pubblicato su IBM developerWorks nel 2011; purtroppo, l’articolo non è più disponibile), più il costo del context-switching tra threads. Questo è lo scenario con cui si ha a che fare tipicamente nelle tecniche tradizionali di web-serving. Evitando tutto questo, Node.js raggiunge livelli di scalabilità di oltre 1M di connessioni simultanee, e oltre 600k connessioni websockets simultanee.

C’è, naturalmente, la questione della condivisione di un singolo thread tra tutte le richieste dei clienti, ed è una potenziale insidia della scrittura di applicazioni Node.js. In primo luogo, un calcolo pesante potrebbe soffocare il singolo thread di Node e causare problemi a tutti i client (più avanti su questo) poiché le richieste in arrivo verrebbero bloccate fino al completamento di tale calcolo. In secondo luogo, gli sviluppatori devono stare molto attenti a non permettere che un’eccezione salga fino al nucleo (più in alto) del ciclo degli eventi di Node.js, che causerà la terminazione dell’istanza di Node.js (mandando effettivamente in crash il programma).

La tecnica usata per evitare che le eccezioni salgano in superficie è passare gli errori indietro al chiamante come parametri di callback (invece di lanciarli, come in altri ambienti). Anche se qualche eccezione non gestita riesce ad emergere, sono stati sviluppati strumenti per monitorare il processo Node.js ed eseguire il necessario recupero di un’istanza in crash (anche se probabilmente non sarete in grado di recuperare lo stato attuale della sessione utente), il più comune è il modulo Forever, o utilizzando un approccio diverso con strumenti di sistema esterni upstart e monit, o anche solo upstart.

npm: Il Node Package Manager

Quando si parla di Node.js, una cosa che sicuramente non dovrebbe essere omessa è il supporto integrato per la gestione dei pacchetti utilizzando lo strumento npm che viene fornito di default con ogni installazione di Node.js. L’idea dei moduli npm è abbastanza simile a quella di Ruby Gems: un insieme di componenti pubblicamente disponibili e riutilizzabili, disponibili attraverso una facile installazione tramite un repository online, con gestione delle versioni e delle dipendenze.

Un elenco completo dei moduli pacchettizzati può essere trovato sul sito web di npm, o accessibile utilizzando lo strumento npm CLI che viene automaticamente installato con Node.js. L’ecosistema dei moduli è aperto a tutti, e chiunque può pubblicare il proprio modulo che sarà elencato nel repository di npm. Una breve introduzione a npm può essere trovata nella Beginner’s Guide, e i dettagli sulla pubblicazione dei moduli nel npm Publishing Tutorial.

Alcuni dei moduli npm più utili oggi sono:

  • express – Express.js, un framework di sviluppo web ispirato a Sinatra per Node.js, e lo standard de-facto per la maggior parte delle applicazioni Node.js là fuori oggi.
  • hapi – un framework molto modulare e semplice da usare incentrato sulla configurazione per la costruzione di applicazioni web e di servizi
  • connect – Connect è un framework server HTTP estensibile per Node.js, che fornisce una collezione di “plugin” ad alte prestazioni noti come middleware; serve come base per Express.
  • socket.io e sockjs – Componente lato server dei due componenti websockets più comuni oggi in circolazione.
  • pug (ex Jade) – Uno dei popolari motori di template, ispirato a HAML, di default in Express.js.
  • mongodb e mongojs – wrapper di MongoDB per fornire l’API per i database a oggetti MongoDB in Node.js.
  • redis – Libreria client Redis.
  • lodash (underscore, lazy.js) – La cinghia di utilità JavaScript. Underscore ha iniziato il gioco, ma è stato rovesciato da una delle sue due controparti, principalmente a causa delle migliori prestazioni e dell’implementazione modulare.
  • per sempre – Probabilmente l’utilità più comune per garantire che un dato script node venga eseguito continuamente. Mantiene il tuo processo Node.js in produzione di fronte a qualsiasi fallimento inaspettato.
  • bluebird – Un’implementazione completa di Promises/A+ con prestazioni eccezionalmente buone
  • moment – Una leggera libreria JavaScript di date per analizzare, validare, manipolare e formattare le date.

La lista continua. Ci sono tonnellate di pacchetti veramente utili là fuori, disponibili per tutti (senza offesa per quelli che ho omesso qui).

Dove dovrebbe essere usato Node.js

Chat è la più tipica applicazione multiutente in tempo reale. Da IRC (ai tempi), attraverso molti protocolli proprietari e aperti che girano su porte non standard, alla capacità di implementare tutto oggi in Node.js con websockets che girano sulla porta standard 80.

L’applicazione di chat è davvero l’esempio di sweet-spot per Node.js: è un’applicazione leggera, ad alto traffico, data-intensive (ma bassa elaborazione/computazione) che gira su dispositivi distribuiti. È anche un grande caso d’uso per l’apprendimento, poiché è semplice, ma copre la maggior parte dei paradigmi che userete mai in una tipica applicazione Node.js.

Provo a descrivere come funziona.

Nello scenario più semplice, abbiamo una singola chatroom sul nostro sito web dove le persone vengono e possono scambiarsi messaggi in modo uno-a-molti (in realtà tutti). Per esempio, diciamo che abbiamo tre persone sul sito web tutte collegate alla nostra bacheca.

Sul lato server, abbiamo una semplice applicazione Express.js che implementa due cose: 1) un gestore di richiesta GET ‘/’ che serve la pagina web contenente sia una bacheca che un pulsante ‘Invia’ per inizializzare l’input di nuovi messaggi, e 2) un server websockets che ascolta i nuovi messaggi emessi dai client websockets.

Sul lato client, abbiamo una pagina HTML con un paio di gestori impostati, uno per l’evento click del pulsante ‘Invia’, che raccoglie il messaggio di input e lo invia lungo il websocket, e un altro che ascolta i nuovi messaggi in arrivo sul client websockets (cioè, messaggi inviati da altri utenti, che il server ora vuole che il client visualizzi).

Quando uno dei client invia un messaggio, ecco cosa succede:

  • Browser cattura il clic sul pulsante ‘Invia’ attraverso un gestore JavaScript, preleva il valore dal campo di input (cioè il testo del messaggio), ed emette un messaggio websocket usando il client websocket connesso al nostro server (inizializzato all’inizializzazione della pagina web).
  • Il componente lato server della connessione websocket riceve il messaggio e lo inoltra a tutti gli altri client connessi usando il metodo broadcast.
  • Tutti i client ricevono il nuovo messaggio come un messaggio push tramite un componente lato client websockets in esecuzione all’interno della pagina web. Quindi raccolgono il contenuto del messaggio e aggiornano la pagina web sul posto aggiungendo il nuovo messaggio alla bacheca.

Immagine presa dal blog originale.

Questo è l’esempio più semplice. Per una soluzione più robusta, si potrebbe usare una semplice cache basata sul negozio Redis. O in una soluzione ancora più avanzata, una coda di messaggi per gestire l’instradamento dei messaggi ai clienti e un meccanismo di consegna più robusto che può coprire le perdite temporanee di connessione o la memorizzazione dei messaggi per i clienti registrati mentre sono offline. Ma indipendentemente dai miglioramenti apportati, Node.js continuerà ad operare secondo gli stessi principi di base: reagire agli eventi, gestire molte connessioni concorrenti e mantenere la fluidità dell’esperienza utente.

API SU UN OBJECT DB

Anche se Node.js brilla davvero con le applicazioni in tempo reale, è abbastanza naturale per esporre i dati da object DB (ad esempio MongoDB). I dati memorizzati in JSON permettono a Node.js di funzionare senza il mismatch dell’impedenza e la conversione dei dati.

Per esempio, se stai usando Rails, dovresti convertire da JSON a modelli binari, poi esporli di nuovo come JSON su HTTP quando i dati vengono consumati da React.js, Angular.js, ecc, o anche semplici chiamate jQuery AJAX. Con Node.js, potete semplicemente esporre i vostri oggetti JSON con un’API REST per il client da consumare. Inoltre, non devi preoccuparti di convertire tra JSON e qualsiasi altra cosa quando leggi o scrivi dal tuo database (se stai usando MongoDB). In sintesi, puoi evitare la necessità di conversioni multiple utilizzando un formato di serializzazione dei dati uniforme tra client, server e database.

INPUTQUEUED

Se stai ricevendo un’elevata quantità di dati concorrenti, il tuo database può diventare un collo di bottiglia. Come illustrato sopra, Node.js può facilmente gestire da solo le connessioni concorrenti. Ma poiché l’accesso al database è un’operazione bloccante (in questo caso), ci troviamo nei guai. La soluzione è riconoscere il comportamento del client prima che i dati siano veramente scritti nel database.

Con questo approccio, il sistema mantiene la sua reattività sotto un carico pesante, il che è particolarmente utile quando il client non ha bisogno di una conferma sicura della riuscita della scrittura dei dati. Esempi tipici includono: la registrazione o la scrittura di dati di tracciamento degli utenti, elaborati in batch e non utilizzati fino a un momento successivo; così come le operazioni che non hanno bisogno di essere riflesse istantaneamente (come l’aggiornamento del conteggio dei “Mi piace” su Facebook) dove la consistenza finale (così spesso utilizzata nel mondo NoSQL) è accettabile.

I dati vengono accodati attraverso un qualche tipo di caching o infrastruttura di accodamento dei messaggi (MQ) (ad es, RabbitMQ, ZeroMQ) e digeriti da un processo separato di scrittura batch del database, o da servizi di backend di elaborazione intensiva del calcolo, scritti in una piattaforma più performante per tali compiti. Un comportamento simile può essere implementato con altri linguaggi/frameworks, ma non sullo stesso hardware, con lo stesso throughput elevato e mantenuto.

Immagine presa dall’articolo originale.

In breve: con Node, è possibile spingere le scritture del database a lato e occuparsene più tardi, procedendo come se fossero riuscite.

DATA STREAMING

Nelle piattaforme web più tradizionali, le richieste e le risposte HTTP sono trattate come eventi isolati; in realtà, sono flussi. Questa osservazione può essere utilizzata in Node.js per costruire alcune caratteristiche interessanti. Per esempio, è possibile elaborare i file mentre vengono ancora caricati, dato che i dati arrivano attraverso un flusso e possiamo elaborarli in modo online. Questo potrebbe essere fatto per la codifica audio o video in tempo reale, e per il proxy tra diverse fonti di dati (vedi la prossima sezione).

PROXY

Node.js è facilmente impiegato come un proxy lato server dove può gestire una grande quantità di connessioni simultanee in modo non bloccante. È particolarmente utile per il proxy di diversi servizi con diversi tempi di risposta, o per raccogliere dati da più punti sorgente.

Un esempio: si consideri un’applicazione lato server che comunica con risorse di terze parti, che preleva dati da diverse fonti, o che memorizza risorse come immagini e video su servizi cloud di terze parti.

Anche se esistono server proxy dedicati, usare invece Node potrebbe essere utile se la tua infrastruttura di proxy non è esistente o se hai bisogno di una soluzione per lo sviluppo locale. Con questo, voglio dire che si potrebbe costruire un’applicazione lato client con un server di sviluppo Node.js per le risorse e il proxy/stubbing delle richieste API, mentre in produzione si potrebbero gestire tali interazioni con un servizio proxy dedicato (nginx, HAProxy, ecc.).

BROKERAGE – STOCK TRADER’S DASHBOARD

Torniamo al livello delle applicazioni. Un altro esempio in cui il software desktop domina, ma potrebbe essere facilmente sostituito da una soluzione web in tempo reale, è il software di trading dei broker, usato per seguire i prezzi delle azioni, eseguire calcoli/analisi tecniche e creare grafici/carte.

Il passaggio a una soluzione basata sul web in tempo reale permetterebbe ai broker di cambiare facilmente stazione di lavoro o luogo di lavoro. Presto, potremmo iniziare a vederli sulla spiaggia in Florida… o Ibiza… o Bali.

APPLICATION MONITORING DASHBOARD

Un altro caso d’uso comune in cui Node-with-web-sockets si adatta perfettamente: monitoraggio dei visitatori del sito web e visualizzazione delle loro interazioni in tempo reale. Potresti raccogliere statistiche in tempo reale dal tuo utente, o anche passare al livello successivo introducendo interazioni mirate con i tuoi visitatori aprendo un canale di comunicazione quando raggiungono un punto specifico nel tuo imbuto – un esempio di questo può essere trovato con CANDDi.

Immagina come potresti migliorare il tuo business se sapessi cosa stanno facendo i tuoi visitatori in tempo reale – se tu potessi visualizzare le loro interazioni. Con i socket bidirezionali in tempo reale di Node.js, ora è possibile.

SYSTEM MONITORING DASHBOARD

Ora visitiamo il lato infrastrutturale delle cose. Immaginate, per esempio, un fornitore SaaS che vuole offrire ai suoi utenti una pagina di monitoraggio dei servizi (per esempio, la pagina di stato di GitHub). Con il ciclo di eventi di Node.js, possiamo creare una potente dashboard basata sul web che controlla gli stati dei servizi in modo asincrono e spinge i dati ai client usando i websockets.

Gli stati dei servizi sia interni (intra-aziendali) che pubblici possono essere riportati dal vivo e in tempo reale usando questa tecnologia. Spingete questa idea un po’ più in là e provate a immaginare un Centro Operativo di Rete (NOC) che monitorizza le applicazioni di un operatore di telecomunicazioni, di un provider di cloud/network/hosting, o di qualche istituzione finanziaria, tutti eseguiti sullo stack web aperto supportato da Node.js e websockets invece di Java e/o Java Applets.

Nota: Non provate a costruire sistemi hard real-time in Node.js (cioè, sistemi che richiedono tempi di risposta costanti). Erlang è probabilmente una scelta migliore per quella classe di applicazioni.

APPLICAZIONI WEB SERVER-SIDE

Node.js con Express.js può anche essere usato per creare applicazioni web classiche sul lato server. Tuttavia, anche se possibile, questo paradigma richiesta-risposta in cui Node.js si porta dietro l’HTML renderizzato non è il caso d’uso più tipico. Ci sono argomenti a favore e contro questo approccio. Ecco alcuni fatti da considerare:

Pros:

  • Se la vostra applicazione non ha alcun calcolo intensivo della CPU, potete costruirla in Javascript da cima a fondo, anche fino al livello del database se usate JSON storage Object DB come MongoDB. Questo facilita lo sviluppo (comprese le assunzioni) in modo significativo.
  • I cercatori ricevono una risposta HTML completamente renderizzata, che è molto più SEO-friendly di, diciamo, una Single Page Application o un’applicazione websockets eseguita sopra Node.js.

Cons:

  • Ogni calcolo intensivo della CPU bloccherà la reattività di Node.js, quindi una piattaforma threaded è un approccio migliore. In alternativa, si potrebbe provare a scalare il calcolo(*).
  • Utilizzare Node.js con un database relazionale è ancora abbastanza doloroso (vedi sotto per maggiori dettagli). Fatevi un favore e prendete qualsiasi altro ambiente come Rails, Django o ASP.Net MVC se state cercando di eseguire operazioni relazionali.

(*) Un’alternativa ai calcoli intensivi della CPU è quella di creare un ambiente altamente scalabile supportato da MQ con elaborazione back-end per mantenere Node come “impiegato” frontale per gestire le richieste dei client in modo asincrono.

SERVER-SIDE WEB APPLICATION WITH A RELATIONAL DATABASE BEHIND

Confrontando Node.js con Express.js contro Ruby on Rails, per esempio, c’è una decisione netta a favore di quest’ultimo quando si tratta di accesso ai dati relazionali.

Gli strumenti DB relazionali per Node.js sono ancora piuttosto poco sviluppati, rispetto alla concorrenza. D’altra parte, Rails fornisce automaticamente la configurazione dell’accesso ai dati fin dall’inizio, insieme agli strumenti di supporto per le migrazioni di schemi DB e altre gemme (gioco di parole). Rails e i suoi framework simili hanno implementazioni mature e provate di Active Record o Data Mapper per l’accesso ai dati, che vi mancheranno molto se provate a replicarle in puro JavaScript.(*)

Ancora, se siete davvero inclini a rimanere JS fino in fondo, date un’occhiata a Sequelize e Node ORM2.

(*) È possibile e non raro usare Node.js solo come facciata rivolta al pubblico, mantenendo il back-end Rails e il suo facile accesso ad un DB relazionale.

Computing/Processo SERVER-SIDE pesante

Quando si tratta di calcolo pesante, Node.js non è la migliore piattaforma in circolazione. No, sicuramente non volete costruire un server di calcolo di Fibonacci in Node.js. In generale, qualsiasi operazione intensiva della CPU annulla tutti i benefici di throughput che Node offre con il suo modello di I/O guidato dagli eventi e non bloccante, perché qualsiasi richiesta in arrivo sarà bloccata mentre il thread è occupato con il vostro calcolo numerico.

Come detto in precedenza, Node.js è a thread singolo e utilizza un solo core della CPU. Quando si tratta di aggiungere concorrenza su un server multi-core, c’è del lavoro fatto dal core team di Node sotto forma di un modulo cluster. È anche possibile eseguire diverse istanze del server Node.js abbastanza facilmente dietro un reverse proxy tramite nginx.

Con il clustering, si dovrebbe ancora scaricare tutti i calcoli pesanti su processi in background scritti in un ambiente più appropriato per questo, e farli comunicare tramite un server di code di messaggi come RabbitMQ.

Anche se l’elaborazione in background potrebbe essere eseguita sullo stesso server inizialmente, un tale approccio ha il potenziale per una scalabilità molto elevata. Quei servizi di elaborazione in background potrebbero essere facilmente distribuiti su server worker separati senza la necessità di configurare i carichi dei server web front-facing.

Ovviamente, si potrebbe usare lo stesso approccio anche su altre piattaforme, ma con Node.js si ottiene quell’alto throughput reqs/sec di cui abbiamo parlato, poiché ogni richiesta è un piccolo compito gestito in modo molto rapido ed efficiente.

Conclusione

Abbiamo discusso di Node.js dalla teoria alla pratica, iniziando con i suoi obiettivi e ambizioni, e finendo con i suoi punti dolci e le sue insidie. Quando le persone hanno problemi con Node, quasi sempre si riduce al fatto che le operazioni di blocco sono la radice di tutti i mali – il 99% degli abusi di Node sono una diretta conseguenza.

Ricorda: Node.js non è mai stato creato per risolvere il problema dello scaling di calcolo. È stato creato per risolvere il problema dello scaling dell’I/O, cosa che fa molto bene.

Ricordate: Node.js non è mai stato creato per risolvere il problema dello scaling di calcolo.