„JavaScript“ vykdymo laikas naudoja vieną apdorojimo giją. Variklis vienu metu atlieka vieną dalyką ir turi užbaigti vykdymą, kad galėtų padaryti ką nors kita. Dėl to retai kyla problemų naršyklėje, nes vienas vartotojas sąveikauja su programa. Tačiau Node.js programos gali apdoroti šimtus vartotojų užklausų. Kelių gijų naudojimas gali užkirsti kelią kliūtims jūsų programoje.
Apsvarstykite Node.js žiniatinklio programą, kurioje vienas vartotojas galėtų suaktyvinti sudėtingą dešimties sekundžių JavaScript skaičiavimą. Programa negalės apdoroti gaunamų užklausų iš kitų naudotojų, kol nebus baigtas šis skaičiavimas.
Tokios kalbos kaip PHP ir Python taip pat yra vienos gijos, tačiau jos paprastai naudoja kelių gijų žiniatinklio serverį, kuris kiekvieną užklausą paleidžia naują vertėjo egzempliorių. Tai reikalauja daug išteklių, todėl Node.js programos dažnai teikia savo lengvą žiniatinklio serverį.
„Node.js“ žiniatinklio serveris veikia vienoje gijoje, tačiau „JavaScript“ sumažina našumo problemas dėl neblokuojančių įvykių ciklo. Programos gali asinchroniškai vykdyti tokias operacijas kaip failas, duomenų bazė ir HTTP, kurios veikia kitose OS gijose. Įvykio ciklas tęsiasi ir gali atlikti kitas „JavaScript“ užduotis, kol bus baigtos įvesties / išvesties operacijos.
Deja, ilgai veikiantis „JavaScript“ kodas, pvz., vaizdo apdorojimas, gali sugadinti dabartinę įvykio ciklo iteraciją. Šiame straipsnyje paaiškinama, kaip perkelti apdorojimą į kitą giją naudojant:
Node.js darbuotojų gijos
Darbininkų siūlai yra Node.js atitikmenys interneto darbuotojai. Pagrindinė gija perduoda duomenis kitam scenarijui, kuris (asinchroniškai) juos apdoroja atskiroje gijoje. Pagrindinė gija toliau vykdoma ir paleidžia atgalinio skambučio įvykį, kai darbuotojas baigia savo darbą.
Atminkite, kad „JavaScript“ naudoja savo struktūrinio klono algoritmas nuosekliai suskirstyti duomenis į eilutę, kai jie perduodami darbuotojui ir iš jo. Tai gali apimti vietinius tipus, pvz., eilutes, skaičius, loginius, masyvus ir objektus. bet ne funkcijos. Negalėsite perduoti sudėtingų objektų, tokių kaip duomenų bazių jungtys, nes dauguma jų turės metodus, kurių negalima klonuoti. Tačiau galite:
- Asinchroniškai nuskaitykite duomenų bazės duomenis pagrindinėje gijoje ir perduokite gautus duomenis darbuotojui.
- Darbuotoje sukurkite kitą ryšio objektą. Tai kainuos paleidimo išlaidas, bet gali būti praktiška, jei jūsų funkcijai atlikti reikia papildomų duomenų bazės užklausų kaip skaičiavimo dalies.
Node.js darbuotojo gijos API konceptualiai yra panaši į Web Workers API naršyklėje, tačiau yra sintaksinių skirtumų. Deno ir bandelė palaiko ir naršyklę, ir Node.js API.
Darbo siūlų demonstravimas
Šioje demonstracijoje parodytas Node.js procesas, kuris kas sekundę įrašo dabartinį laiką į konsolę: Atidarykite Node.js demonstraciją naujame naršyklės skirtuke.
Tada pradedamas ilgalaikis kauliukų metimo skaičiavimas. Ciklas užbaigia 100 milijonų iteracijų, o tai sustabdo laiko išvedimą:
timer process 12:33:18 PM
timer process 12:33:19 PM
timer process 12:33:20 PM
NO THREAD CALCULATION STARTED...
┌─────────┬──────────┐
│ (index) │ Values │
├─────────┼──────────┤
│ 2 │ 2776134 │
│ 3 │ 5556674 │
│ 4 │ 8335819 │
│ 5 │ 11110893 │
│ 6 │ 13887045 │
│ 7 │ 16669114 │
│ 8 │ 13885068 │
│ 9 │ 11112704 │
│ 10 │ 8332503 │
│ 11 │ 5556106 │
│ 12 │ 2777940 │
└─────────┴──────────┘
processing time: 2961ms
NO THREAD CALCULATION COMPLETE
timer process 12:33:24 PM
Kai baigsite, tas pats skaičiavimas paleidžiamas darbuotojo gijoje. Laikrodis toliau veikia, kol vyksta kauliukų apdorojimas:
WORKER CALCULATION STARTED...
timer process 12:33:27 PM
timer process 12:33:28 PM
timer process 12:33:29 PM
┌─────────┬──────────┐
│ (index) │ Values │
├─────────┼──────────┤
│ 2 │ 2778246 │
│ 3 │ 5556129 │
│ 4 │ 8335780 │
│ 5 │ 11114930 │
│ 6 │ 13889458 │
│ 7 │ 16659456 │
│ 8 │ 13889139 │
│ 9 │ 11111219 │
│ 10 │ 8331738 │
│ 11 │ 5556788 │
│ 12 │ 2777117 │
└─────────┴──────────┘
processing time: 2643ms
WORKER CALCULATION COMPLETE
timer process 12:33:30 PM
Darbuotojo procesas yra šiek tiek greitesnis nei pagrindinė gija, nes jis gali sutelkti dėmesį į vieną užduotį.
Kaip naudoti darbininkų gijas
A dice.js
failas demonstraciniame projekte apibrėžia kauliukų metimo funkciją. Perduotas bėgimų (metimų), kauliukų skaičius ir kiekvieno kauliuko pusių skaičius. Kiekvieno metimo metu funkcija apskaičiuoja kauliukus sum
ir padidina, kiek kartų jis pastebėtas stat
masyvas. Funkcija grąžina masyvą, kai visi metimai baigti:
export function diceRun(runs = 1, dice = 2, sides = 6) {
const stat = [];
while (runs > 0) {
let sum = 0;
for (let d = dice; d > 0; d--) {
sum += Math.floor(Math.random() * sides) + 1;
}
stat[sum] = (stat[sum] || 0) + 1;
runs--;
}
return stat;
}
Pagrindinis index.js
scenarijus pradeda laikmačio procesą, kuris kas sekundę išveda dabartinę datą ir laiką:
const intlTime = new Intl.DateTimeFormat([], { timeStyle: "medium" });
timer = setInterval(() => {
console.log(` timer process ${ intlTime.format(new Date()) }`);
}, 1000);
Kai pagrindinis sriegis vykdo diceRun()
tiesiogiai laikmatis sustoja, nes nieko daugiau negali veikti, kol vyksta skaičiavimas:
import { diceRun } from "./dice.js";
const
throws = 100_000_000,
dice = 2,
sides = 6;
const stat = diceRun(throws, dice, sides);
console.table(stat);
Norėdami atlikti skaičiavimą kitoje gijoje, kodas apibrėžia naują Darbininko objektas su darbuotojo scenarijaus failo pavadinimu. Tai praeina a workerData
kintamasis – objektas su savybėmis throws
, dice
ir sides
:
const worker = new Worker("./src/worker.js", {
workerData: { throws, dice, sides }
});
Tai paleidžia darbuotojo scenarijų, kuris vykdomas diceRun()
su įvestais parametrais workerData
:
import { workerData, parentPort } from "node:worker_threads";
import { diceRun } from "./dice.js";
const stat = diceRun(workerData.throws, workerData.dice, workerData.sides);
parentPort.postMessage(stat);
The parentPort.postMessage(stat);
skambutis perduoda rezultatą atgal į pagrindinę giją. Tai kelia a "message"
įvykis index.js
kuris gauna rezultatą ir parodo jį konsolėje:
worker.on("message", result => {
console.table(result);
});
Galite apibrėžti kitų darbuotojų įvykių tvarkykles:
- Galima naudoti pagrindinį scenarijų
worker.postMessage(data)
bet kuriuo metu siųsti darbuotojui savavališkus duomenis. Tai sukelia a"message"
įvykis darbuotojo scenarijuje:parentPort.on("message", data => { console.log("from main:", data); });
"messageerror"
suaktyvina pagrindinėje gijoje, kai darbuotojas gauna duomenis, kurių jis negali deserializuoti."online"
suveikia pagrindinėje gijoje, kai pradedama vykdyti darbuotojo gija."error"
suaktyvina pagrindinėje gijoje, kai darbuotojo gijoje įvyksta „JavaScript“ klaida. Tai galite naudoti norėdami atleisti darbuotoją. Pavyzdžiui:worker.on("error", e => { console.log(e); worker.terminate(); });
"exit"
suveikia pagrindinėje gijoje, kai darbuotojas nutraukia darbą. Tai gali būti naudojama valymui, registravimui, našumo stebėjimui ir pan.:worker.on("exit", code => { console.log("worker complete"); });
Inline darbuotojo gijos
Viename scenarijaus faile gali būti tiek pagrindinis ir darbuotojo kodas. Jūsų scenarijus turėtų patikrinti, ar jis veikia pagrindinėje gijoje naudojant isMainThread
tada vadinasi darbuotoju, naudojančiu import.meta.url
kaip failo nuoroda ES modulyje (arba __filename
„CommonJS“):
import { Worker, isMainThread, workerData, parentPort } from "node:worker_threads";
if (isMainThread) {
const worker = new Worker(import.meta.url, {
workerData: { throws, dice, sides }
});
worker.on("message", msg => {});
worker.on("exit", code => {});
}
else {
const stat = diceRun(workerData.throws, workerData.dice, workerData.sides);
parentPort.postMessage(stat);
}
Ar tai praktiška, ar ne, yra kitas dalykas. Rekomenduoju padalinti pagrindinius ir darbinius scenarijus, nebent jie naudoja identiškus modulius.
Gijos duomenų bendrinimas
Galite bendrinti duomenis tarp gijų naudodami a SharedArrayBuffer objektas, vaizduojantis fiksuoto ilgio neapdorotus dvejetainius duomenis. Ši pagrindinė gija apibrėžia 100 skaitmeninių elementų nuo 0 iki 99, kuriuos ji siunčia darbuotojui:
import { Worker } from "node:worker_threads";
const
buffer = new SharedArrayBuffer(100 * Int32Array.BYTES_PER_ELEMENT),
value = new Int32Array(buffer);
value.forEach((v,i) => value[i] = i);
const worker = new Worker("./worker.js");
worker.postMessage({ value });
Darbuotojas gali gauti value
objektas:
import { parentPort } from 'node:worker_threads';
parentPort.on("message", value => {
value[0] = 100;
});
Šiuo metu pagrindinės arba darbinės gijos gali pakeisti elementus value
masyvas ir jis pasikeitė abiejuose. Dėl to padidėja efektyvumas, nes nėra duomenų serializavimo, tačiau:
- galite bendrinti tik sveikuosius skaičius
- gali prireikti siųsti pranešimus, nurodančius, kad duomenys pasikeitė
- yra rizika, kad dvi gijos gali pakeisti tą pačią reikšmę tuo pačiu metu ir prarasti sinchronizavimą
Kai kurioms programoms reikės sudėtingo dalijimosi duomenimis, tačiau tai gali būti tinkama parinktis didelio našumo programose, pvz., žaidimuose.
Node.js antriniai procesai
Vaikų procesai paleiskite kitą programą (nebūtinai „JavaScript“), perduokite duomenis ir gaukite rezultatą paprastai per skambutį. Jie veikia panašiai kaip darbuotojai, tačiau paprastai yra mažiau veiksmingi ir reikalauja daug procesų, nes priklauso nuo procesų, esančių už Node.js ribų. Taip pat gali būti OS skirtumų ir nesuderinamumo.
Node.js turi tris bendruosius antrinių procesų tipus su sinchroniniais ir asinchroniniais variantais:
spawn
: sukuria naują procesąexec
: sukuria apvalkalą ir paleidžia jame komandąfork
: sukuria naują Node.js procesą
Naudojama toliau nurodyta funkcija spawn
paleisti komandą asinchroniškai perduodant komandą, argumentų masyvą ir skirtąjį laiką. Pažadas panaikinamas arba atmetamas naudojant objektą, kuriame yra savybių complete
(true
arba false
), a code
(paprastai 0
už sėkmę) ir a result
eilutė:
import { spawn } from 'node:child_process';
function execute(cmd, args = [], timeout = 600000) {
return new Promise((resolve, reject) => {
try {
const
exec = spawn(cmd, args, {
timeout
});
let ret = '';
exec.stdout.on('data', data => {
ret += '\n' + data;
});
exec.stderr.on('data', data => {
ret += '\n' + data;
});
exec.on('close', code => {
resolve({
complete: !code,
code,
result: ret.trim()
});
});
}
catch(err) {
reject({
complete: false,
code: err.code,
result: err.message
});
}
});
}
Ją galite naudoti norėdami paleisti OS komandą, pvz., įtraukti darbinio katalogo turinį kaip eilutę „MacOS“ arba „Linux“:
const ls = await execute('ls', ['-la'], 1000);
console.log(ls);
Node.js grupavimas
Node.js klasteriai leidžia atlikti daugybę identiškų procesų, kad kroviniai būtų tvarkomi efektyviau. Pradinis pirminis procesas gali išsišakoti – galbūt vieną kartą kiekvienam grąžinamam CPU os.cpus()
. Jis taip pat gali tvarkyti paleidimus iš naujo, kai egzempliorius sugenda, ir tarpininkauti komunikacijos pranešimus tarp šakotųjų procesų.
The cluster
biblioteka siūlo ypatybes ir metodus, įskaitant:
.isPrimary
arba.isMaster
: grįžtatrue
pagrindiniam pirminiam procesui.fork()
: sukuria vaiko darbo procesą.isWorker
: grąžina teisingą darbuotojo procesams
Toliau pateiktame pavyzdyje pradedamas žiniatinklio serverio darbuotojo procesas kiekvienam įrenginio CPU / branduoliui. 4 branduolių įrenginys sukurs keturis žiniatinklio serverio egzempliorius, todėl gali atlaikyti iki keturių kartų didesnę apkrovą. Taip pat iš naujo paleidžiamas bet koks nepavykęs procesas, todėl programa turėtų būti patikimesnė:
import cluster from 'node:cluster';
import process from 'node:process';
import { cpus } from 'node:os';
import http from 'node:http';
const cpus = cpus().length;
if (cluster.isPrimary) {
console.log(`Started primary process: ${ process.pid }`);
for (let i = 0; i < cpus; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${ worker.process.pid } failed`);
cluster.fork();
});
}
else {
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello!');
}).listen(8080);
console.log(`Started worker process: ${ process.pid }`);
}
Visi procesai dalijasi prievadu 8080
ir bet kuris gali apdoroti gaunamą HTTP užklausą. Žurnalas, kai vykdomos programos, rodo kažką panašaus į tai:
$ node app.js
Started primary process: 1001
Started worker process: 1002
Started worker process: 1003
Started worker process: 1004
Started worker process: 1005
...etc...
worker 1002 failed
Started worker process: 1006
Nedaug kūrėjų bando kurti grupes. Aukščiau pateiktas pavyzdys yra paprastas ir veikia gerai, tačiau kodas gali tapti vis sudėtingesnis, kai bandote tvarkyti gedimus, paleidimus iš naujo ir pranešimus tarp šakių.
Procesų vadovai
Node.js proceso tvarkyklė gali padėti paleisti kelis vienos Node.js programos egzempliorius neįrašant klasterio kodo. Labiausiai žinomas yra PM2. Ši komanda paleidžia jūsų programos egzempliorių kiekvienam CPU / branduoliui ir paleidžia iš naujo, kai nepavyksta:
pm2 start ./app.js -i max
Programų egzemplioriai prasideda fone, todėl idealiai tinka naudoti tiesioginiame serveryje. Įvesdami galite patikrinti, kurie procesai veikia pm2 status
:
$ pm2 status
┌────┬──────┬───────────┬─────────┬─────────┬──────┬────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │
├────┼──────┼───────────┼─────────┼─────────┼──────┼────────┤
│ 1 │ app │ default │ 1.0.0 │ cluster │ 1001 │ 4D │
│ 2 │ app │ default │ 1.0.0 │ cluster │ 1002 │ 4D │
└────┴──────┴───────────┴─────────┴─────────┴──────┴────────┘
PM2 taip pat gali paleisti ne Node.js programas, parašytas Deno, Bun, Python ir pan.
Konteinerių orkestruotė
Klasteriai ir procesų valdytojai susieja programą su konkrečiu įrenginiu. Jei jūsų serveris arba OS priklausomybė sugenda, jūsų programa sugenda, nepaisant vykdomų egzempliorių skaičiaus.
Konteineriai yra panaši į virtualiųjų mašinų koncepciją, tačiau jie nemėgdžioja aparatinės įrangos, o imituoja operacinę sistemą. Konteineris yra lengvas įvyniojimas aplink vieną programą su visomis būtinomis OS, biblioteka ir vykdomaisiais failais. Viename konteineryje gali būti atskiras Node.js ir jūsų programos egzempliorius, todėl jis veikia viename įrenginyje arba tūkstančiuose įrenginių.
Konteinerių orkestravimas nepatenka į šio straipsnio taikymo sritį, todėl turėtumėte pažvelgti atidžiau Dokeris ir Kubernetes.
Išvada
Node.js darbuotojai ir panašūs kelių gijų metodai pagerina programos našumą ir sumažina kliūtis paleisdami kodą lygiagrečiai. Jie taip pat gali padaryti programas patikimesnes paleisdami pavojingas funkcijas atskirose gijose ir nutraukdamos jas, kai apdorojimo laikas viršija tam tikras ribas.
Darbuotojai turi pridėtines išlaidas, todėl gali prireikti šiek tiek eksperimentuoti, kad jie pagerintų rezultatus. Jums gali nereikėti jų atliekant sunkias asinchronines įvesties / išvesties užduotis, o procesų / sudėtinių rodinių valdymas gali pasiūlyti paprastesnį būdą išplėsti programas.