worker_threads va cluster — CPU intensive vazifalar

~20 daq · Node.js · Process

Muammodan boshlaylik

Node.js bitta thread da ishlaydi. Bu ko'p hollarda yaxshi — asinxron I/O tufayli bitta thread minglab so'rovni boshqara oladi. Lekin ikki holat bor:

1. CPU intensive vazifa — katta massivni saralash, kriptografiya, rasm qayta ishlash, video konvertatsiya. Bu hisoblash tugamaguncha thread bloklanadi, server boshqa so'rovlarga javob bera olmaydi.

2. Ko'p yadroli protsessor — zamonaviy serverlar 8, 16, 32 yadroli. Node.js standart holatda faqat bitta yadroni ishlatadi. Qolgan 7 ta yoki 15 ta yoki 31 ta — bo'sh turadi.

Bu ikki muammoni hal qilish uchun worker_threads va cluster bor.


worker_threads — bir jarayon, ko'p thread

worker_threads — bitta Node.js jarayoni ichida bir nechta thread yaratadi. Threadlar xotirani ulashadi — bu child_process.fork dan farqi.

Hayotiy o'xshatma: siz ofisda ishlayapsiz. Og'ir hisob-kitob kelib qoldi. Siz uni hamkasbingizga topshirasiz — ikkalingiz bir xona (bir jarayon) da ishlaysiz, bir xil hujjatlarga (xotira) kirishingiz mumkin. Hamkasbingiz hisoblayotganda siz boshqa ishni qilasiz.

// main.js
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

if (isMainThread) {
  // Asosiy thread
  const worker = new Worker(__filename, {
    workerData: { n: 1_000_000_000 }
  });

  worker.on('message', (result) => {
    console.log('Natija:', result);
  });

  worker.on('error', (err) => {
    console.error('Worker xatosi:', err.message);
  });

  worker.on('exit', (code) => {
    if (code !== 0) console.error(`Worker ${code} kodi bilan tugadi`);
  });

  console.log('Worker ishlamoqda, men boshqa ishni qilaman...');

} else {
  // Worker thread
  const { n } = workerData;
  let sum = 0;
  for (let i = 0; i < n; i++) sum += i;
  parentPort.postMessage(sum);
}

isMainThread — hozir asosiy thread da ishlayapmizmi yoki worker da? Bir xil faylni ikki maqsadda ishlatish mumkin.


Worker Pool — bir nechta worker

Har so'rov uchun yangi worker yaratish — sekin. Yaxshisi, oldindan bir nechta worker yaratib, ularni qayta ishlatish. Bu — Worker Pool pattern.

const { Worker } = require('worker_threads');
const os = require('os');

class WorkerPool {
  constructor(workerFile, size = os.cpus().length) {
    this.workers  = [];
    this.queue    = [];

    for (let i = 0; i < size; i++) {
      this._addWorker(workerFile);
    }
  }

  _addWorker(file) {
    const worker = new Worker(file);
    worker.busy  = false;

    worker.on('message', (result) => {
      worker.busy = false;
      worker.currentResolve(result);
      this._processQueue();
    });

    this.workers.push(worker);
  }

  run(data) {
    return new Promise((resolve) => {
      const freeWorker = this.workers.find(w => !w.busy);

      if (freeWorker) {
        freeWorker.busy = true;
        freeWorker.currentResolve = resolve;
        freeWorker.postMessage(data);
      } else {
        this.queue.push({ data, resolve });
      }
    });
  }

  _processQueue() {
    if (this.queue.length === 0) return;
    const { data, resolve } = this.queue.shift();
    this.run(data).then(resolve);
  }
}

// Ishlatish:
const pool = new WorkerPool('./heavy-worker.js');

// Bir vaqtda bir nechta og'ir hisoblash:
const results = await Promise.all([
  pool.run({ n: 1e9 }),
  pool.run({ n: 2e9 }),
  pool.run({ n: 3e9 }),
]);

cluster — bir nechta jarayon, bir port

cluster — bitta portda bir nechta Node.js jarayonini ishlatish imkonini beradi. Har bir jarayon — alohida, mustaqil. Xotirani ulashmaydi.

Hayotiy o'xshatma: katta restoran. Bitta oshpaz (bitta jarayon) bilan 1000 ta mijozga xizmat qilish qiyin. Yechim — 8 ta oshpaz (8 ta jarayon), hammasi bir xil menyudan (bir xil kod) ishlaydi. Mijozlar esa navbat bilan taqsimlanadi.

const cluster = require('cluster');
const http    = require('http');
const os      = require('os');

if (cluster.isPrimary) {
  // Asosiy jarayon — workerlarni boshqaradi
  const cpuCount = os.cpus().length;
  console.log(`${cpuCount} ta worker yaratilmoqda`);

  for (let i = 0; i < cpuCount; i++) {
    cluster.fork();
  }

  // Worker o'chib qolsa — yangisini yaratish
  cluster.on('exit', (worker, code) => {
    console.log(`Worker ${worker.process.pid} to'xtadi (kod: ${code})`);
    console.log('Yangi worker yaratilmoqda...');
    cluster.fork();
  });

} else {
  // Worker jarayon — haqiqiy server
  http.createServer((req, res) => {
    res.end(`Jarayon ${process.pid} javob berdi`);
  }).listen(3000);

  console.log(`Worker ${process.pid} ishga tushdi`);
}

Ishga tushirganda:

8 ta worker yaratilmoqda
Worker 1234 ishga tushdi
Worker 1235 ishga tushdi
...
Worker 1241 ishga tushdi

Endi http://localhost:3000 ga so'rov yuborsangiz — 8 ta jarayondan biri javob beradi. Agar biri o'chib qolsa — asosiy jarayon yangisini yaratadi.


worker_threads vs cluster — qachon qaysi?

Bu ikki texnologiya turli muammolarni hal qiladi:

worker_threads — bitta so'rov ichidagi og'ir hisoblash uchun. Masalan, foydalanuvchi rasm yukladi — siz uni qayta ishlashingiz kerak. Bu bitta so'rov, lekin og'ir ish. Worker thread bu ishni asosiy threadni bloklamasdan bajaradi.

cluster — ko'p so'rovlarni parallel qabul qilish uchun. Masalan, serveringizga sekundiga 10,000 so'rov keladi. Bitta jarayon yetmaydi — 8 ta jarayon bilan 8 baravar ko'p so'rov qabul qilinadi.

worker_threads:
  Bitta so'rov → og'ir hisoblash → worker thread ga topshir
  Asosiy thread boshqa so'rovlarga javob beradi

cluster:
  Ko'p so'rov → 8 ta jarayon → har biri bir qismini oladi
  Jami throughput 8 baravar oshadi

Amaliy holat — Express + cluster

const cluster = require('cluster');
const os      = require('os');

if (cluster.isPrimary) {
  os.cpus().forEach(() => cluster.fork());

  cluster.on('exit', (worker) => {
    console.error(`Worker ${worker.process.pid} o'chdi, qayta ishga tushirilmoqda`);
    cluster.fork();
  });

} else {
  // Har bir worker o'z Express serverini ishga tushiradi
  const express = require('express');
  const app     = express();

  app.get('/', (req, res) => {
    res.json({ pid: process.pid, message: 'Salom!' });
  });

  app.listen(3000, () => {
    console.log(`Worker ${process.pid} tayyor`);
  });
}

Tez-tez uchraydigan xatolar

worker_threads da xotirani ulashish xavfi:

// SharedArrayBuffer — ikki thread bir xil xotiraga yozsa:
const shared = new SharedArrayBuffer(4);
const arr    = new Int32Array(shared);

// Ikki thread bir vaqtda yozsa — race condition
// Atomics ishlatiladi:
Atomics.add(arr, 0, 1); // xavfsiz

cluster da holat saqlash:

// Xato — har worker alohida xotira:
const sessions = {}; // bitta worker da saqlangan session
                     // boshqa worker ko'rmaydi!

// To'g'ri — Redis yoki boshqa tashqi saqlash:
const redis = require('redis');
// session ni Redis da saqlash

Cluster da workerlar xotirani ulashmaydi — session, cache kabi narsalar tashqi saqlashda (Redis) bo'lishi kerak.


Xulosa

worker_threads — bitta jarayon ichida ko'p thread. Og'ir hisoblashni asosiy threadni bloklamasdan bajarish uchun. Xotirani ulashadi.

cluster — bir portda ko'p jarayon. Ko'p yadroli protsessordan to'liq foydalanish, ko'p so'rovlarni parallel qabul qilish uchun. Xotirani ulashmaydi.

Production da ko'pincha ikkalasi birga ishlatiladi: cluster bilan ko'p jarayon, har jarayon ichida worker_threads bilan og'ir hisoblash.


Keyingi dars

23-dars: Memory Leak — nima, qanday topiladi