Come, in generale, Node.js gestisce 10.000 richieste simultanee?


395

Comprendo che Node.js utilizza un singolo thread e un loop di eventi per elaborare le richieste elaborandone solo una alla volta (che non è bloccante). Tuttavia, come funziona, diciamo 10.000 richieste simultanee. Il ciclo di eventi elaborerà tutte le richieste? Non ci vorrebbe troppo tempo?

Non riesco a capire (ancora) come può essere più veloce di un web server multi-thread. Capisco che il web server multi-thread sarà più costoso in termini di risorse (memoria, CPU), ma non sarebbe ancora più veloce? Probabilmente ho torto; per favore spiega come questo thread singolo è più veloce in molte richieste e cosa fa di solito (ad alto livello) quando serve molte richieste come 10.000.

Inoltre, quel singolo thread si ridimensionerà bene con quella grande quantità? Si prega di tenere presente che sto appena iniziando a imparare Node.js.


5
Perché la maggior parte del lavoro (spostamento dei dati) non coinvolge la CPU.
OrangeDog,

5
Nota anche che solo perché c'è un solo thread che esegue Javascript, non significa che non ci siano molti altri thread che funzionano.
OrangeDog,

Questa domanda è o troppo ampia, o un duplicato di varie altre domande.
OrangeDog,


Insieme al thread singolo, Node.js esegue qualcosa chiamato "I / O non bloccante". Qui è dove viene fatta tutta la magia
Anand N,

Risposte:


764

Se devi porre questa domanda, probabilmente non hai familiarità con ciò che fanno la maggior parte delle applicazioni / servizi web. Probabilmente stai pensando che tutto il software faccia questo:

user do an action
       
       v
 application start processing action
   └──> loop ...
          └──> busy processing
 end loop
   └──> send result to user

Tuttavia, non è così che funzionano le applicazioni Web o qualsiasi applicazione con un database come back-end. Le app Web fanno questo:

user do an action
       
       v
 application start processing action
   └──> make database request
          └──> do nothing until request completes
 request complete
   └──> send result to user

In questo scenario, il software impiega la maggior parte del tempo di esecuzione utilizzando lo 0% del tempo di CPU in attesa del ritorno del database.

App di rete multithread:

Le app di rete multithread gestiscono il carico di lavoro sopra indicato in questo modo:

request ──> spawn thread
              └──> wait for database request
                     └──> answer request
request ──> spawn thread
              └──> wait for database request
                     └──> answer request
request ──> spawn thread
              └──> wait for database request
                     └──> answer request

Quindi il thread trascorre la maggior parte del tempo usando CPU allo 0% in attesa che il database restituisca i dati. Nel fare ciò, hanno dovuto allocare la memoria richiesta per un thread che include uno stack di programma completamente separato per ogni thread, ecc. Inoltre, avrebbero dovuto avviare un thread che, sebbene non sia costoso come l'avvio di un processo completo, non è ancora esattamente economico.

Ciclo di eventi con filetto singolo

Dato che impieghiamo la maggior parte del nostro tempo utilizzando la CPU allo 0%, perché non eseguire un po 'di codice quando non utilizziamo la CPU? In questo modo, ogni richiesta otterrà comunque la stessa quantità di tempo CPU delle applicazioni multithread ma non è necessario avviare un thread. Quindi facciamo questo:

request ──> make database request
request ──> make database request
request ──> make database request
database request complete ──> send response
database request complete ──> send response
database request complete ──> send response

In pratica entrambi gli approcci restituiscono dati con approssimativamente la stessa latenza poiché è il tempo di risposta del database a dominare l'elaborazione.

Il vantaggio principale qui è che non abbiamo bisogno di generare un nuovo thread, quindi non abbiamo bisogno di fare un sacco di malloc che ci rallenterebbero.

Threading magico e invisibile

La cosa apparentemente misteriosa è come entrambi gli approcci sopra riescono a eseguire il carico di lavoro in "parallelo"? La risposta è che il database è thread. Quindi la nostra app a thread singolo sta effettivamente sfruttando il comportamento multi-thread di un altro processo: il database.

Laddove l'approccio a singlethreaded fallisce

Un'app con singlethreading ha esito negativo se è necessario eseguire molti calcoli della CPU prima di restituire i dati. Ora, non intendo un ciclo for che elabora il risultato del database. Questo è ancora principalmente O (n). Voglio dire cose come fare trasformata di Fourier (ad esempio codifica mp3), ray tracing (rendering 3D) ecc.

Un altro inconveniente delle app con filetto singolo è che utilizzerà solo un singolo core della CPU. Quindi, se si dispone di un server quad-core (non raro oggi) non si utilizzano gli altri 3 core.

Dove l'approccio multithreading fallisce

Un'app multithreading fallisce se devi allocare molta RAM per thread. Innanzitutto, l'utilizzo della RAM in sé significa che non è possibile gestire tutte le richieste di un'app basata su singlet. Peggio ancora, il malloc è lento. Allocare un sacco di oggetti (che è comune per i moderni framework Web) significa che possiamo potenzialmente finire per essere più lenti delle app a singolo canale. Qui di solito vince node.js.

Un caso d'uso che alla fine peggiora il multithreading è quando è necessario eseguire un altro linguaggio di scripting nel thread. Prima di solito devi mallocare l'intero runtime per quella lingua, quindi devi mallocare le variabili usate dal tuo script.

Quindi, se stai scrivendo app di rete in C o go o java, l'overhead del threading di solito non sarà troppo male. Se stai scrivendo un server web C per servire PHP o Ruby, è molto facile scrivere un server più veloce in javascript o Ruby o Python.

Approccio ibrido

Alcuni server Web utilizzano un approccio ibrido. Nginx e Apache2, ad esempio, implementano il loro codice di elaborazione di rete come un pool di thread di loop di eventi. Ogni thread esegue un loop di eventi contemporaneamente elaborando le richieste a thread singolo ma le richieste vengono bilanciate in base al carico tra più thread.

Alcune architetture a thread singolo utilizzano anche un approccio ibrido. Invece di avviare più thread da un singolo processo è possibile avviare più applicazioni, ad esempio 4 server node.js su una macchina quad-core. Quindi si utilizza un bilanciamento del carico per distribuire il carico di lavoro tra i processi.

In effetti i due approcci sono immagini speculari tecnicamente identiche l'una dell'altra.


106
Questa è di gran lunga la migliore spiegazione per il nodo che ho letto finora. Quell'app "single-threaded sta effettivamente sfruttando il comportamento multi-thread di un altro processo: il database".
Ha

che dire se il client sta effettuando richieste multiple nel nodo, come ad esempio ottenere un nome e modificarlo, e dire che queste operazioni spingono sul server per gestire molto velocemente da molti client. come potrei gestire un simile scenario?
Remario

3
@CaspainCaldion Dipende da cosa intendi per cliente molto veloce e molti. Allo stesso modo, node.js può elaborare fino a 1000 richieste al secondo e la velocità è limitata solo alla velocità della scheda di rete. Si noti che sono 1000 richieste al secondo e non client connessi contemporaneamente. Può gestire senza problemi i 10000 client simultanei. Il vero collo di bottiglia è la scheda di rete.
slebetman

1
@slebetman, la migliore spiegazione di sempre. una cosa però, se ho un algoritmo di Machine Learning che elabora alcune informazioni e fornisce risultati di conseguenza, dovrei usare l'approccio multi-thread o single-threaded
Ganesh Karewad,

5
Gli algoritmi di @GaneshKarewad utilizzano CPU, i servizi (database, API REST ecc.) Utilizzano l'I / O. Se l'IA è un algoritmo scritto in js, è necessario eseguirlo in un altro thread o processo. Se l'intelligenza artificiale è un servizio in esecuzione su un altro computer (come Amazon o Google o servizi di intelligenza artificiale IBM), utilizzare una singola architettura thread.
Slebetman,

46

Quello che sembra pensare è che la maggior parte dell'elaborazione sia gestita nel ciclo degli eventi del nodo. Il nodo in realtà esegue il pull delle operazioni di I / O sui thread. Le operazioni di I / O richiedono in genere ordini di grandezza più lunghi delle operazioni della CPU, quindi perché la CPU deve attendere? Inoltre, il sistema operativo può già gestire molto bene le attività di I / O. In effetti, poiché Node non attende, consente un utilizzo della CPU molto più elevato.

Per analogia, pensa a NodeJS come a un cameriere che prende gli ordini dei clienti mentre gli chef I / O li preparano in cucina. Altri sistemi hanno più chef, che prendono un ordine dei clienti, preparano il pasto, svuotano il tavolo e solo allora si occupano del cliente successivo.


5
Grazie per l'analogia del ristorante! Trovo le analogie e gli esempi del mondo reale molto più facili da imparare.
LaVache il

13

Comprendo che Node.js utilizza un singolo thread e un loop di eventi per elaborare le richieste elaborandone solo una alla volta (che non è bloccante).

Potrei fraintendere quello che hai detto qui, ma "uno alla volta" sembra che potresti non comprendere appieno l'architettura basata sugli eventi.

In un'architettura di applicazione "convenzionale" (non guidata da eventi), il processo trascorre molto tempo in attesa che qualcosa accada. In un'architettura basata su eventi come Node.js il processo non si limita ad aspettare, ma può continuare con altri lavori.

Ad esempio: ottieni una connessione da un client, la accetti, leggi le intestazioni della richiesta (nel caso di http), quindi inizi ad agire sulla richiesta. Potresti leggere il corpo della richiesta, in genere finirai per inviare alcuni dati al cliente (questa è una semplificazione deliberata della procedura, solo per dimostrare il punto).

In ognuna di queste fasi, la maggior parte del tempo viene speso in attesa dell'arrivo di alcuni dati dall'altra estremità: il tempo effettivo impiegato nell'elaborazione nel thread JS principale è in genere abbastanza minimo.

Quando lo stato di un oggetto I / O (come una connessione di rete) cambia in modo tale da richiedere l'elaborazione (ad es. I dati vengono ricevuti su un socket, un socket diventa scrivibile, ecc.) Il thread JS Node.js principale viene svegliato con un elenco degli articoli che devono essere elaborati.

Trova la struttura dati pertinente ed emette alcuni eventi su quella struttura che provocano l'esecuzione di callback, che elaborano i dati in entrata o scrivono più dati su un socket, ecc. Una volta che tutti gli oggetti I / O che hanno bisogno di essere elaborati sono stati elaborato, il thread JS Node.js principale attenderà nuovamente fino a quando non viene comunicato che sono disponibili più dati (o che qualche altra operazione è stata completata o scaduta).

La prossima volta che viene svegliato, potrebbe essere dovuto a un diverso oggetto I / O che deve essere elaborato, ad esempio una diversa connessione di rete. Ogni volta, vengono eseguiti i callback rilevanti e poi torna a dormire in attesa che accada qualcos'altro.

Il punto importante è che l'elaborazione delle diverse richieste è intercalata, non elabora una richiesta dall'inizio alla fine e passa alla successiva.

A mio avviso, il vantaggio principale di questo è che una richiesta lenta (ad esempio, stai provando a inviare 1 MB di dati di risposta a un dispositivo di telefonia mobile tramite una connessione dati 2G o stai eseguendo una query di database molto lenta) ha vinto " bloccare quelli più veloci.

In un server Web multi-thread convenzionale, in genere è disponibile un thread per ogni richiesta gestita e verrà elaborata SOLO quella richiesta fino al termine. Cosa succede se hai molte richieste lente? Si finisce con un sacco di thread in attesa di elaborare queste richieste e altre richieste (che potrebbero essere richieste molto semplici che potrebbero essere gestite molto rapidamente) vengono messe in coda dietro di loro.

Esistono molti altri sistemi basati su eventi oltre a Node.js e tendono ad avere vantaggi e svantaggi simili rispetto al modello convenzionale.

Non direi che i sistemi basati su eventi siano più veloci in ogni situazione o con ogni carico di lavoro: tendono a funzionare bene per i carichi di lavoro associati a I / O, non così bene per quelli legati alla CPU.


12

Passaggi di elaborazione del modello di loop di eventi a thread singolo:

  • Client Invia richiesta al Web Server.

  • Node JS Web Server gestisce internamente un pool di thread limitato per fornire servizi alle richieste client.

  • Il nodo JS Web Server riceve tali richieste e le inserisce in una coda. È noto come "Coda eventi".

  • Il nodo JS Web Server ha internamente un componente, noto come "Event Loop". Il motivo per cui ha ottenuto questo nome è che utilizza un ciclo indefinito per ricevere richieste ed elaborarle.

  • Event Loop utilizza solo Single Thread. È il cuore principale del Node JS Platform Processing Model.

  • Event Loop verifica che qualsiasi richiesta del client sia inserita nella coda degli eventi. In caso contrario, attendere le richieste in arrivo per un tempo indefinito.

  • In caso affermativo, preleva una richiesta client dalla coda degli eventi

    1. Inizia l'elaborazione della richiesta client
    2. Se la richiesta client non richiede operazioni IO di blocco, quindi elaborare tutto, preparare la risposta e rispedirla al client.
    3. Se quella richiesta client richiede alcune operazioni IO di blocco come l'interazione con database, file system, servizi esterni, seguirà un approccio diverso
  • Verifica la disponibilità dei thread dal pool di thread interno
  • Prende un thread e assegna questa richiesta client a quel thread.
  • Quel thread è responsabile di accettare quella richiesta, elaborarla, eseguire operazioni di IO bloccanti, preparare la risposta e rispedirla al loop degli eventi

    molto ben spiegato da @Rambabu Posa per ulteriori spiegazioni vai a questo link


il diagramma fornito in quel post sul blog sembra essere sbagliato, ciò che hanno menzionato in quell'articolo non è completamente corretto.
rranj,

11

Aggiunta alla risposta di slebetman: quando si dice che Node.JSpossono gestire 10.000 richieste simultanee, si tratta essenzialmente di richieste non bloccanti, vale a dire che queste richieste riguardano principalmente la query del database.

Internamente, event loopdi Node.JSse controlla un thread pool, in cui ogni thread gestisce un non-blocking requeste loop degli eventi continua ad ascoltare più richiesta dopo delegando il lavoro ad uno dei filo della thread pool. Quando uno dei thread completa il lavoro, invia un segnale al fatto event loopche è finito aka callback. Event loopquindi elaborare questo callback e rispedire la risposta.

Dato che sei nuovo su NodeJS, leggi di più nextTickper capire come funziona internamente il ciclo degli eventi. Leggi i blog su http://javascriptissexy.com , sono stati davvero utili per me quando ho iniziato con JavaScript / NodeJS.


2

Aggiungendo alla risposta di slebetman per maggiore chiarezza su ciò che accade durante l'esecuzione del codice.

Il pool di thread interno in nodeJs ha solo 4 thread per impostazione predefinita. e non è come se l'intera richiesta fosse allegata a un nuovo thread dal pool di thread l'intera esecuzione della richiesta avviene esattamente come una normale richiesta (senza alcuna attività di blocco), solo che ogni volta che una richiesta ha una lunga esecuzione o un'operazione pesante come db call, un'operazione di file o una richiesta http l'attività viene messa in coda al pool di thread interno fornito da libuv. E poiché nodeJs fornisce 4 thread nel pool di thread interno per impostazione predefinita, ogni 5 o successiva richiesta simultanea attende fino a quando un thread è libero e, una volta superate queste operazioni, il callback viene trasferito nella coda di callback. e viene raccolto dal loop degli eventi e restituisce la risposta.

Ora ecco un'altra informazione che non è una sola coda di richiamata, ci sono molte code.

  1. NextTick queue
  2. Coda di attività micro
  3. Coda timer
  4. Coda di callback IO (Richieste, File op, db op)
  5. Coda di polling IO
  6. Controlla la coda delle fasi o SetImmediate
  7. chiudi la coda dei gestori

Ogni volta che arriva una richiesta, il codice viene eseguito in questo ordine di callback in coda.

Non è come quando c'è una richiesta di blocco è allegata a un nuovo thread. Ci sono solo 4 discussioni per impostazione predefinita. Quindi c'è un'altra coda in corso lì.

Ogni volta che in un codice si verifica un processo di blocco come la lettura del file, quindi chiama una funzione che utilizza il thread dal pool di thread e quindi, una volta eseguita l'operazione, il callback viene passato alla rispettiva coda e quindi eseguito nell'ordine.

Tutto viene messo in coda in base al tipo di callback ed elaborato nell'ordine sopra menzionato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.