"Non bloccare il ciclo degli eventi..." - probabilmente avrete sentito questa frase molte volte... Non mi sorprende, perché è uno dei presupposti più importanti quando si lavora con Node. Ma c'è anche una seconda "cosa" che si dovrebbe evitare di bloccare: il pool di lavoratori. Se trascurato, può avere un impatto significativo sulle prestazioni dell'applicazione e persino sulla sua sicurezza.
Fili
La cosa principale da ricordare è che ci sono due tipi di thread in Node.js: Thread principale - che viene gestito da Ciclo di eventi, e Pool di lavoratori (pool di thread) - che è il pool di thread
grazie a libuv. Ognuno di essi ha un compito diverso. L'obiettivo del primo è gestire le operazioni di I/O non bloccanti, mentre il secondo è responsabile del lavoro intensivo della CPU e anche dell'I/O bloccante.
Ma cos'è un thread e in che modo è diverso da un processo? Ci sono diverse differenze, ma la più importante per noi è il modo in cui la memoria viene allocata. Si può pensare a un processo come a un'applicazione. All'interno di ogni processo, c'è una porzione di memoria dedicata solo a questo processo. Quindi, un processo non ha accesso alla memoria del secondo e questa proprietà garantisce un'elevata sicurezza. Per stabilire una comunicazione tra i due processi, dobbiamo fare un po' di lavoro. I thread sono diversi. I thread vengono eseguiti all'interno di un processo e condividono la stessa memoria, quindi non c'è alcun problema con la condivisione dei dati da parte dei thread.
Tuttavia, una questione causa un problema. Si tratta della cosiddetta condizione di gara. I thread possono essere eseguiti contemporaneamente, quindi come facciamo a sapere quale termina per primo? Può accadere che la prima volta che si esegue, la prima operazione termini per prima, mentre la volta successiva può accadere il contrario e che la seconda operazione termini prima della prima. Immaginate di lavorare con operazioni di scrittura/lettura in queste condizioni! Un incubo! A volte è molto difficile scrivere operazioni corrette codice in un ambiente multi-thread.
Inoltre, i linguaggi multi-thread hanno un grande overhead di memoria perché creano un thread separato per ogni richiesta; quindi, se si vogliono chiamare 1000 richieste, creano 1000 thread.
Come affrontare un simile problema? Utilizzando invece un singolo thread! E questo è ciò che Nodo vi offre.
Come JavaScript sviluppatore Vi invito a guardare il film
in cui Bart Belder spiega chiaramente il concetto di ciclo di eventi. Il diagramma qui sopra è tratto dalla sua presentazione. E se non conoscete affatto questi termini, sia Nodo e Libuv hanno una documentazione eccellente 🙂
Informazioni sul blocco
In Sviluppo JavaScript dicono che perché Nodo è a thread singolo e non bloccante, si può ottenere una maggiore concurrency con le stesse risorse rispetto alle soluzioni multi-thread. È vero, ma non è così bello e facile come può sembrare.
Da quando Node.js è a thread singolo (parte JS), i task ad alta intensità di CPU bloccheranno tutte le richieste in corso fino al completamento di quel particolare task. Quindi, è vero che in Node.js è possibile bloccare ogni richiesta solo perché una di esse contiene un'istruzione bloccante. Il codice bloccante significa che richiede più di qualche millisecondo per essere completato. Ma non bisogna confondere i tempi di risposta lunghi con il blocco. La risposta del database può richiedere molto tempo, ma non blocca il processo (l'applicazione).
I metodi bloccanti vengono eseguiti in modo sincrono e quelli non bloccanti in modo asincrono.
Come si può rallentare (o bloccare) il ciclo degli eventi?
- regex vulnerabili - un'espressione regolare vulnerabile è quella per la quale il motore di espressione regolare potrebbe impiegare un tempo esponenziale; è possibile leggere maggiori informazioni al riguardo qui,
- Operazioni JSON su oggetti di grandi dimensioni,
- utilizzando le API sincrone di Nodo invece delle versioni asincrone; tutti i metodi di I/O della libreria standard Node.js forniscono anche le loro versioni asincrone,
- altri errori di programmazione, come i loop infiniti sincroni.
In questo caso, dato che il Worker Pool utilizza un pool di thread, è possibile bloccare anche questi? Sfortunatamente, sì 🙁 Nodo si basa su una filosofia un filo per molti clienti.
Supponiamo che un determinato compito eseguito da uno specifico Worker sia molto complesso e richieda più tempo per essere completato. Di conseguenza, il Worker viene bloccato e non può essere utilizzato per eseguire altre attività in sospeso finché le sue istruzioni non vengono eseguite. Come si sarà capito, questo può influire sulle prestazioni. È possibile evitare questi problemi riducendo al minimo la variazione dei tempi dei task utilizzando il partizionamento dei task.
Conclusione
Evitare il blocco, questo è certo. Se proprio potete, scegliete sempre le versioni asincrone delle API di libreria standard. In caso contrario, dopo l'esecuzione dell'applicazione, il client può riscontrare diversi problemi, a partire dalla riduzione del throughput fino al ritiro completo, che è fatale dal punto di vista dell'utente.
Per saperne di più:
Perché si dovrebbe (probabilmente) usare Typescript
Come non uccidere un progetto con cattive pratiche di codifica?
Strategie di recupero dei dati in NextJS