Node.js cluster moduli: clustering nima va nimaga kerak

Assalamu Alaykum bugun Nodejsdacluster modul va u bizga nima uchun kerakligini to'liq ko'rib chiqamiz va benchmark qilamiz.

Maqolani boshlashdan oldin bu maqolalarni o'qing.

  1. Nodejs single threadmi yoki multi thread ?

  2. Child process

Clustering nima va nimaga kerak ?

Oldingi maqolalarimizdan ko'rgandek Nodejs CPUning to'liq ishlatmaydi bu degani qolgan CPUlar doim ishlatilmaydi va bundan to'liq foydalanish uchun bizga cluster module yordam beradi. U bir Nodejs process bir necha marta qayta ishga tushurish orqali har bir CPU corega bir nodejs process ishlatish bilan CPUni to'liq ishlatishga yordam beradi. CPU haqidagi ma'lumotlarni os moduli orqali olishimiz mumkin masalan os.cpus() cpular haqida to'liq malumot beradi, os.availableParallelism bu bizga nechta marta Nodejs processni yaratishimiz mumkinligini bildiradi (ya'ni nechta core borligini bildiradi) . Yangi process yaratish Cluster moduldagifork metodi orqali bo'ladi va u chaqirilgandan son'g o'zidan yangi xuddi shu nodejs processni yaratadi , bu oldingi maqolamizga child processga o'xshaydi, yaratgan process coordinator (parent) qolganlar esa child process bo'ladi.

Parent process child processlarni boshqarib turadi agar biror process tugatilsa qayta ishga tushuradi, u ko'p CPU talab qilmaydi. Oddiy qilib aytganda dastur xuddi oldingiday bo'ladi ammo ko'p resurs (process) bilan (child processlar evaziga).

Clustring real hayotda

Pastda cluster modul ishlatilinishi va ular nima vazifa bajarishi haqida bir necha misollar ko'rib chiqamiz.

const cluster = require("cluster")
if (cluster.isPrimary) {
  cluster.fork()
}

Tepadagi misolda cluster.fork() orqali yangi nodejs process yaratdik va unda parent process mavjud . cluster.isPrimary orqali biz hozirgi process child yoki main processligini bilib olamiz.

const cluster = require("cluster")
if (cluster.isPrimary) {
  cluster.fork()
  cluster.fork()
  console.log("Parent process")
} else {
  console.log("Child process")
}

Tepadagi misolda bir necha process yaratdik buni rasmdan ham bilishingiz mumkin. 15232 Idlik process bu parent bo'lib qolgan 2 process uning child lari

const cluster = require("cluster")
const numberOfCPU = require("os").availableParallelism()
if (cluster.isPrimary) {
  for (let i = 0; i < numberOfCPU; i++) {
    cluster.fork();
  }
  console.log("Parent process")
} else {
  console.log("Child process")
}

Bu misolda dastur samarali tarzda nechta process bilan ishlay olsa shuncha process (main processdan tashqari) yaratdik hardware haqidagi ma'lumotni os modul orqali oldik.

Bu yerda mendagi kompyuter 8 corelik bo'lganligi uchun 8ta child hamda bir dona parent process yaratdi.

Xo'sh bu bizga qanday yordam beradi ?

Bizda endi 8 ta oldingi processdan bor agarda birorta processda event loop blockga tushib qolsa yoki crash bo'lsa ham bizdagi boshqa processlar ularni o'rnini to'ldiradi. Pastda o'zingiz tekshirib ko'rishingiz uchun main threadni (event loopni) blocklovchi funksiya.

function blockMainThread(timeInSeconds = 2) {
  const startTime = Date.now()
  while (Date.now() - startTime < timeInSeconds * 1000) {}
}
blockMainThread(10)

Bu kod processni 10 sekundga bloklaydi ya'ni u boshqa so'rovlar bilan amal bajarolmaydi va ular kutib qoladi. Cluster modulda boshqa bo'sh process uni o'rnini egallab so'rovlarga javob berishda davom etadi.

Bir nechta serverni ishga tushurish

Biz cluster orqali bir nechta serverni ishga tushuramiz.

const http = require("http");
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}
const server = http.createServer((req, res) => {
  const parsedUrl = url.parse(req.url, true);
  if (parsedUrl.pathname === "/heavy") {
    const n = 35;
    const result = fibonacci(n);
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ result, message: "Work done" }));
  } else {
    res.writeHead(200, { "Content-Type": "text/plain" });
    res.end("Hello from worker\n");
  }
});
server.listen(3000, () => {
  console.log(`Worker ${process.pid} started`);
  console.log("Server listening on port 8000");
});
const cluster = require("cluster");
const os = require("os");
if (cluster.isPrimary) {
  cluster.schedulingPolicy = cluster.SCHED_RR;
  const numCPUs = os.cpus().length;
  console.log(`Primary process ${process.pid} is running`);
  console.log(`Forking for ${numCPUs} CPUs...\n`);
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  cluster.on("exit", (worker) => {
    console.log(`Worker ${worker.process.pid} died`);
    cluster.fork();
  });
} else {
  require("./single-server.js");
}

Tepada ko'rganimizdek portni eshitish logi har bir process uchun alohida ishga tushdi bu hamma process bir portni eshitadi degani emas. Haqiqatda child processlar portga ulanmaydi parent process portga ulangan holda hamma so'rovlarni yo'naltirib turadi. Bizda endi oldingi serverdan 8 ta bor deb hisoblasak ham bo'laveradi. Endi ularni benchmark qilamiz.

BenchmarkBu uchun autocannon dan foydalanamiz.

autocannon -c 100 -d 20 http://localhost:3000/heavy

Birinchi bo'lib single-server.jsni oddiy holatda tekshiramiz.

server benchmark
server benchmark

Endi Cluster holda ishlayotgan server.jsni benchmark qilamiz.

cluster-server benchmark
cluster-server benchmark

Tepadagi misolda event loopni bloklaydigan API bilan benchmark qilib ko'rdik , ko'rib turganingizdek o'rtacha sekundiga so'rovlar 5 martadan ko'proq ko'paygan va kutish vaqti ham2,5 barobar qisqargan.

Parent process va Child process o'rtasida ma'lumot almashish

Bundan tashqari siz worker processlarga ma'lumot jo'natishingiz mumkin yoki worker objectini saqlagan holda keyinchalik ishlatishingiz mumkin.

const cluster = require("cluster");
const nmbCPU = require("os").availableParallelism();
const workers = [];
if (cluster.isPrimary) {
  for (let i = 0; i < nmbCPU; i++) {
    const worker = cluster.fork();
    workers.push(worker);
    worker.send("something send from parent");
  }
  cluster.on("exit", (worker, code, signal) => {
    cluster.fork();
  });
  console.log("Parent process");
} else {
  require("./worker");
}
process.on("message", (data) => {
  console.log("Message from parent:", data);
});
communication
communication

Child process ham process.send("something")orqali parent processga ma'lumot uzatishi mumkin. Buni parent processda ushbu yo'l orqali o'qiy olamiz.

cluster.on("message", (worker, msg) => {
  console.log(`Message from worker ${worker.pid} :`, msg);
});
cluster.on("listening", (worker, address) => {
  console.log(
    `A worker with ${worker.pid} id is now connected to ${address.address}:${address.port}`
  );
});

teapadagi kod orqali biz processda server ishga tushganini bilishimiz mumkin. process.listen metodi eventni ishga tushuradi. fork event esa yangi process yaratilganda ishga tushadi.

Barcha kodlarni ushbu repodan topshingiz mumkin. Repo

Xulosa

Xulosa qilib aytadigan bo'lsak Nodejs bitta event loop bilan ishlaydi va faqat bitta CPU yadrodan foydalanadi. Bu oddiy vaziyatlarda muammo tug‘dirmaydi, ammo katta trafik(high throughput)yoki og‘ir hisoblash jarayonlarida bu bottleneckbo‘lishi mumkin. Bu paytda bizga clustering yordam beradi , u bizga hamma CPU yadrolarini ishlatishga yordam beradi. Benchmarkdan ham ko'rdikki CPU-intensive endpointda bir necha barobar ustunlik berdi ,masalan So‘rovlar soni (throughput) 5 martadan ko‘proq oshdi Kutish vaqti (latency) 2,5 barobar kamaydi (oddiy endpointda buncha foyda bermasligi mumkin) .