*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.
Lascia un commento