IlmHamroh
JavaScript Full-stack/24-qism. Node.js5/55-dars16 daqiqa
Mundarija (25)

libuv thread pool va OS asinxron I/O: Node ishni kimga topshiradi

Qisqacha: JavaScript kodingiz bitta thread'da ishlaydi, lekin Node'ning o'zi bitta thread emas. Tarmoq ishlarini (TCP, HTTP) operatsion tizim o'zi asinxron bajaradi — Linux'da epoll, macOS'da kqueue, Windows'da IOCP orqali. Fayl tizimi (fs), dns.lookup, og'ir crypto va zlib esa libuv'ning thread pool'ida bajariladi — sukut bo'yicha 4 ta ishchi thread. Pool to'lib qolsa, hatto kichik readFile va localhost ga fetch ham navbatda kutadi (o'lchandi).

Bu darsda

  • Node'da qaysi ish OS'ga, qaysi biri thread pool'ga, qaysi biri JS thread'ga tushishini ajrata olasiz.
  • UV_THREADPOOL_SIZE ni qachon va qanday o'zgartirishni bilasiz — va nega uni ko'r-ko'rona oshirmaslik kerakligini.
  • Pool to'lib qolishini o'z ko'zingiz bilan ko'rasiz: 4 ta pbkdf2 bitta kichik fayl o'qishni 700 ms ushlab turadi.
  • fetch("http://localhost") bilan fetch("http://127.0.0.1") nega boshqacha kutishini tushuntira olasiz.
  • epoll, kqueue va IOCP nima ekanini bir gap bilan ayta olasiz.

Oldin bilishingiz kerak: Node.js nima: runtime, V8 va libuv, Blocking va non-blocking I/O, Event loop fazalari, Microtask, nextTick va setImmediate.

1. Nega bu kerak?

Node.js nima darsida "Node — bir threadli" degan gapni eshitdingiz. Blocking va non-blocking I/O darsida esa readFile kutish vaqtida loop boshqa ishni qilishini ko'rdingiz. Shu yerda tabiiy savol tug'iladi: agar thread bitta bo'lsa, faylni kim o'qiyapti?

Javob: JS kodingiz bitta thread'da ishlaydi, lekin Node ichida yana bir nechta "yordamchi" bor. Ularni bilmasangiz, bir kun g'alati muammoga duch kelasiz. Masalan, server parollarni hash qila boshlaganda (crypto darsida ko'rasiz) birdan fayl o'qish ham, tashqi API'ga so'rov ham sekinlashadi. CPU bo'sh turibdi, loop bloklanmagan — lekin hamma narsa kutyapti.

O'xshatish: katta oshxona. Oshpaz (JS thread) bitta — u buyurtmalarni qabul qiladi va tarqatadi. Go'shtni qiyma qilish uchun 4 ta yordamchi (thread pool) bor. Non esa tashqi novvoyxonadan keladi (operatsion tizim) — oshpaz faqat "olib keling" deydi va kutmaydi. Agar to'rt yordamchining to'rtalasi ham katta buyurtma bilan band bo'lsa, kichik "piyoz to'g'rang" topshirig'i ham navbatda turadi.

2. Node ichidagi uch "ishchi"

2.1 Kim nima qiladi

libuv — Node ichidagi C kutubxona. U event loop'ni yuritadi va I/O ishlarini bajaradi (Node.js nima darsida tanishgansiz). Uning ishi uch joyga taqsimlanadi:

Ish Qayerda bajariladi Misollar
Sizning JS kodingiz asosiy thread (V8) sikl, JSON.parse, callback'lar
Tarmoq I/O operatsion tizim TCP, HTTP, fetch, dns.resolve
Fayl, DNS lookup, og'ir hisob libuv thread pool fs, dns.lookup, crypto.pbkdf2, zlib.gzip

Diagrammada yo'nalishga qarang: JS faqat topshiradi va callback'ni oladi, og'ir ish boshqa joyda bo'ladi.

flowchart LR
  JS["JS kodingiz<br/>(asosiy thread)"] --> L["libuv<br/>event loop"]
  L -->|"tarmoq"| OS["OS: epoll / kqueue / IOCP"]
  L -->|"fs, dns.lookup,<br/>crypto, zlib"| P["thread pool<br/>4 ishchi"]
  OS -->|"tayyor"| L
  P -->|"tayyor"| L
  L -->|"callback"| JS

2.2 Nega tarmoq pool'ga tushmaydi

Tarmoq bilan ishlashda dastur aslida hech narsa qilmaydi — u faqat ma'lumot kelishini kutadi. Zamonaviy operatsion tizimlarda buning uchun maxsus mexanizm bor: "mana shu 10 000 ta ulanishni kuzatib tur, qaysi biriga ma'lumot kelsa, menga ayt". Bitta tizim chaqiruvi minglab ulanishni kuzatadi.

Har OS'da bu mexanizmning nomi boshqacha:

OS Mexanizm Qisqacha
Linux epoll "qaysi ulanish tayyor?" deb so'raladi
macOS, FreeBSD kqueue xuddi shu g'oya, BSD oilasida
Windows IOCP (I/O Completion Ports) "ish tugaganda xabar ber" — tugash haqida bildirish

libuv bularning ustidan bitta umumiy qatlam yasaydi, shuning uchun sizning fetch kodingiz uchala tizimda bir xil ishlaydi. Bu qatlam tufayli bitta Node jarayoni minglab ochiq ulanishni ushlab tura oladi — Blocking va non-blocking I/O darsidagi C10k muammosining yechimi shu.

2.3 Nega fayl pool'ga tushadi

Fayllar bilan bunday qulay mexanizm hamma OS'da bir xil emas. Linux'ning epoll'i oddiy fayllar uchun ishlamaydi: disk "doim tayyor" deb javob beradi, o'qish esa baribir thread'ni to'xtatadi. Shuning uchun libuv oddiy yo'lni tanlagan: fayl amalini alohida thread'da sinxron bajaradi, tugagach natijani loop'ga qaytaradi. Sizning JS thread'ingiz uchun bu asinxron ko'rinadi.

dns.lookup ham shu sababdan pool'da. U OS'ning getaddrinfo funksiyasini chaqiradi, bu funksiya esa bloklovchi. crypto.pbkdf2, scrypt, randomBytes (callback'li), zlib.gzip kabilar umuman I/O emas — ular CPU ishi. Node ularni asosiy thread'ni to'xtatmaslik uchun pool'ga chiqaradi.

Maslahat: Linux'da libuv ba'zi fayl amallari uchun yangi io_uring mexanizmini ham sinab ko'rgan. libuv hujjatiga ko'ra hozirgi versiyalarda u sukut bo'yicha o'chirilgan. Biz Windows'da o'lchadik — Linux'da sinalmagan, raqamlar farq qilishi mumkin.

Tekshirib ko'ring: Quyidagilardan qaysi biri thread pool'ga tushadi: (a) fetch("https://cbu.uz/...") dagi ma'lumot kutish, (b) readFile("kurslar.json"), (c) JSON.parse(matn)?

Javob

Faqat (b). (a) — tarmoq, uni OS kuzatadi (Windows'da IOCP). (c) — oddiy JS kodi, u asosiy thread'da bajariladi va pool'ga hech qachon chiqmaydi. Katta JSON.parse loop'ni bloklaydi — buni Event loop'ni bloklash darsida o'lchaymiz.

3. Pool hajmi: 4 ta ishchi

3.1 O'lchov: 8 ta ish, har xil pool

Kanon tajribasi: 8 ta pbkdf2 (parolni hash qilish, har biri 300 000 marta takrorlanadi) bir vaqtda boshlandi. Har safar UV_THREADPOOL_SIZE muhit o'zgaruvchisi bilan pool hajmi o'zgartirildi. Har hajm uch marta, alohida jarayonda o'lchandi:

text
UV_THREADPOOL_SIZE=1: 8 × pbkdf2(300k) — mediana 2155 ms
UV_THREADPOOL_SIZE=2: 8 × pbkdf2(300k) — mediana 965 ms
UV_THREADPOOL_SIZE=4: 8 × pbkdf2(300k) — mediana 560 ms
UV_THREADPOOL_SIZE=8: 8 × pbkdf2(300k) — mediana 381 ms

Grafikda ko'ring — har ikki baravar ishchi vaqtni deyarli ikki baravar kamaytiradi, lekin 8 da tejash kamayadi:

8 ta pbkdf2 parallel: pool hajmi va umumiy vaqt
  • 1 threadhammasi navbat bilan2 155 ms
  • 2 thread965 ms
  • 4 thread (sukut)ikki to'lqin560 ms
  • 8 threadbitta to'lqin381 ms

Manba: O'lchandi: IlmHamroh kanoni, Node 24.21.0, Windows 11, i5-12500H (16 mantiqiy yadro), har biri 3 jarayon mediana, 2026-10-23

Sukut bo'yicha pool'da 4 ta thread. 8 ta ish ikki "to'lqin" bo'lib bajariladi: avval 4 tasi, keyin qolgan 4 tasi. Pool 8 ta bo'lganda hammasi bir to'lqinda tugaydi.

3.2 Jonli model: to'lqinlarni hisoblash

Pool qanday ishlashini oddiy model bilan ko'ramiz. Har ish eng birinchi bo'shagan ishchiga beriladi. Bitta pbkdf2 270 ms deylik:

js
function simulatePool(size, durations) {
  const freeAt = Array(size).fill(0); // har ishchi qachon bo'shaydi
  return durations.map((ms) => {
    const worker = freeAt.indexOf(Math.min(...freeAt));
    freeAt[worker] += ms;
    return freeAt[worker]; // shu ish tugagan vaqt
  });
}

const jobs = Array(8).fill(270);
for (const size of [1, 2, 4, 8]) {
  const done = simulatePool(size, jobs);
  console.log(`${size} thread: ${Math.max(...done)} ms`);
}

Konsolda:

text
1 thread: 2160 ms
2 thread: 1080 ms
4 thread: 540 ms
8 thread: 270 ms

Model 1, 2 va 4 da o'lchovga juda yaqin. 8 da esa model 270 deydi, haqiqat 381. Nega? Protsessor yadrolari ham cheksiz emas: 8 ta og'ir hisob bir vaqtda ishlaganda protsessor chastotasini pasaytiradi, "giper-thread" yadrolar to'liq yadrodek tez emas. Thread qo'shish — yadro qo'shish emas.

3.3 UV_THREADPOOL_SIZE ni o'zgartirish

Pool hajmi jarayon boshlanishida o'rnatiladi:

bash
UV_THREADPOOL_SIZE=8 node server.js

Windows PowerShell'da: $env:UV_THREADPOOL_SIZE=8; node server.js. Qoidalar (libuv va Node hujjatlari):

  • Sukut — 4, eng ko'pi — 1024.
  • Pool birinchi marta kerak bo'lganda yaratiladi. Shundan keyin o'zgartirish ta'sir qilmaydi.
  • Kod ichida process.env.UV_THREADPOOL_SIZE = "8" ni eng birinchi qatorga yozish ba'zan ishlaydi. Biz Windows'da sinadik: pool hajmi haqiqatan 1 ga tushdi (4 ta pbkdf2 — 1 131 ms, sukutda 301 ms). Lekin bu "pool hali yaratilmagan" degan taxminga tayanadi. Ishonchli yo'l — muhit o'zgaruvchisi.

Pool kattalashsa nima yo'qotasiz? Har thread xotira oladi va yadrolar uchun raqobat kuchayadi. 16 yadroli mashinada 128 ta pbkdf2 thread'i ishni tezlatmaydi, faqat almashinish ko'payadi. Odatiy maslahat: CPU ishi uchun yadrolar sonidan oshirmang; ko'p fayl kutadigan dastur uchun biroz kattaroq bo'lishi mumkin. Har holda — o'lchang.

Tekshirib ko'ring: Kod server.listen() dan keyin, 10 daqiqa ishlagach, process.env.UV_THREADPOOL_SIZE = "16" qiladi. Pool kattalashadimi?

Javob

Yo'q. Pool o'sha paytgacha allaqachon yaratilgan (server ishga tushganda ham fs yoki dns.lookup ishlatilgan bo'lishi deyarli aniq). Hajm faqat jarayon boshida o'qiladi — qayta ishga tushirish kerak.

4. Pool to'lib qolishi

4.1 Tajriba: kichik ish katta navbatda

Endi eng qiziq qismi. 4 ta og'ir pbkdf2 (har biri 1 000 000 takror) pool'ning to'rtala ishchisini band qiladi. Shu paytda biz:

  • 127.0.0.1 ga fetch yuboramiz;
  • localhost ga fetch yuboramiz;
  • dns.lookup("localhost") qilamiz;
  • kichik faylni o'qiymiz (readFile).

Biz ishga tushirdik (Node 24.21.0, Windows 11, i5-12500H). Vaqtlar har safar biroz farq qiladi, shuning uchun bu blok tekshiruvchida emas, haqiqiy chiqish pastda:

js
import { pbkdf2 } from "node:crypto";
import { readFile } from "node:fs/promises";
import { lookup } from "node:dns/promises";
import { createServer } from "node:http";

const server = createServer((req, res) => res.end("ok"));
await new Promise((r) => server.listen(0, "127.0.0.1", r));
const port = server.address().port;

const start = performance.now();
const t = () => `${Math.round(performance.now() - start)} ms`;

for (let i = 1; i <= 4; i++) {
  pbkdf2("parol", "tuz", 1_000_000, 64, "sha512", () =>
    console.log(`pbkdf2 #${i} tugadi: ${t()}`));
}
fetch(`http://127.0.0.1:${port}/`).then(() =>
  console.log(`fetch 127.0.0.1: ${t()}`));
fetch(`http://localhost:${port}/`).then(() =>
  console.log(`fetch localhost: ${t()}`));
lookup("localhost").then(() => console.log(`dns.lookup: ${t()}`));
readFile(import.meta.filename).then(() =>
  console.log(`readFile: ${t()}`));
setTimeout(() => server.close(), 3000);

Sukut (4 thread):

text
fetch 127.0.0.1: 33 ms
pbkdf2 #4 tugadi: 775 ms
dns.lookup: 780 ms
pbkdf2 #1 tugadi: 782 ms
readFile: 784 ms
fetch localhost: 785 ms
pbkdf2 #2 tugadi: 820 ms
pbkdf2 #3 tugadi: 855 ms

UV_THREADPOOL_SIZE=8 bilan:

text
dns.lookup: 26 ms
fetch 127.0.0.1: 44 ms
readFile: 45 ms
fetch localhost: 48 ms
pbkdf2 #3 tugadi: 860 ms
pbkdf2 #2 tugadi: 876 ms
pbkdf2 #4 tugadi: 917 ms
pbkdf2 #1 tugadi: 957 ms

4.2 Natijani o'qiymiz

Birinchi jadvalda uchta narsaga qarang:

  1. fetch 127.0.0.1 — 33 ms. IP manzil tayyor, DNS kerak emas. Ulanish va javob — tarmoq, ya'ni OS (IOCP). Pool band ekani unga ta'sir qilmadi.
  2. dns.lookup va readFile — ~780 ms. Ikkalasi ham pool'da. Ular birinchi pbkdf2 tugashini — ishchi bo'shashini — kutdi. Bir necha baytli fayl 780 ms o'qildi!
  3. fetch localhost — 785 ms. Eng kutilmagani shu. localhost — nom, uni IP'ga aylantirish kerak. fetch buning uchun dns.lookup ni ishlatadi — demak u ham pool navbatiga tushdi. Tarmoq so'rovi bo'lsa ham, DNS qismi tufayli kutdi.

8 thread bilan hammasi ~45 ms da tugadi: pool'da bo'sh ishchi qoldi. pbkdf2 lar esa biroz sekinlashdi (775–855 → 860–957 ms) — endi ular 4 ta "kichik" ish bilan yadro bo'lishdi.

Bu holatni pool to'lib qolishi (thread pool saturation) deyishadi. Belgisi: CPU bo'sh emas, lekin event loop ham bloklanmagan. Shunga qaramay fayl, DNS va nom bilan so'rovlar sekin.

4.3 Tekshiriladigan variant: faqat tartib

Vaqtni emas, faqat "kim kimni kutdi" ni chiqarsak, natija har safar bir xil. Bu blok tekshiruvchida haqiqiy Node 24 da ishga tushiriladi. Saytda «Ishga tushir» tugmasi yo'q — uni o'z terminalingizda node pool.mjs bilan yurgizing:

js
import { pbkdf2 } from "node:crypto";
import { readFile } from "node:fs/promises";
import { createServer } from "node:http";

const server = createServer((req, res) => res.end("ok"));
await new Promise((r) => server.listen(0, "127.0.0.1", r));
const { port } = server.address();

let hashed = 0; // nechta pbkdf2 tugadi
for (let i = 0; i < 4; i++) {
  pbkdf2("parol", "tuz", 1_000_000, 64, "sha512", () => {
    hashed++;
    if (hashed === 4) server.close();
  });
}

await fetch(`http://127.0.0.1:${port}/`);
console.log(`fetch 127.0.0.1 — tugagan pbkdf2: ${hashed}`);

await readFile(import.meta.filename);
console.log(`readFile — tugagan pbkdf2: ${hashed > 0 ? "1+" : 0}`);

Konsolda:

text
fetch 127.0.0.1 — tugagan pbkdf2: 0
readFile — tugagan pbkdf2: 1+

fetch hech bir pbkdf2 ni kutmadi. readFile esa kamida bittasi tugamaguncha boshlana olmadi.

Diqqat: readFile bitta amal emas: u faylni ochadi, hajmini so'raydi, o'qiydi va yopadi. Har biri pool'ga alohida navbatga turadi. Shuning uchun to'lgan pool'da katta fayl bo'lak-bo'lak o'qilganda kechikish bir necha marta qo'shiladi.

Tekshirib ko'ring: Server tashqi API'ga fetch("https://cbu.uz/...") yuboradi va bir vaqtda foydalanuvchi parollarini scrypt bilan hash qiladi. Nega API so'rovlari sekinlashishi mumkin?

Javob

cbu.uz — nom. fetch uni dns.lookup bilan IP'ga aylantiradi, dns.lookup esa thread pool'da. scrypt lar pool'ni band qilsa, DNS so'rovi navbatda turadi. Ulanishning o'zi va javob kutish esa OS'da — ular sekinlashmaydi.

5. Pool'ni nima band qiladi — ro'yxat

Node hujjatining "Don't Block the Event Loop" qo'llanmasi va libuv hujjati bo'yicha:

Modul Pool'da (callback/Promise variant) Pool'da EMAS
fs deyarli hamma amal fs.watch (OS kuzatadi), *Sync (asosiy thread)
dns lookup, lookupService resolve* — c-ares orqali, tarmoqda
crypto pbkdf2, scrypt, randomBytes, randomFill, generateKeyPair createHash — sinxron, asosiy thread
zlib gzip, deflate, brotliCompress (async) gzipSync — asosiy thread

dns.resolve4 va dns.lookup farqiga e'tibor bering. lookup OS sozlamalarini (hosts fayli, localhost) hisobga oladi va brauzer kabi ishlaydi. resolve esa to'g'ridan DNS serverga so'rov yuboradi va pool'ni band qilmaydi. fetch va http.request sukut bo'yicha lookup ni ishlatadi. Ikkalasining farqini net, dns va tls darsida chuqur ko'rasiz.

Sinxron (Sync) variantlar pool'ga umuman tushmaydi — ular to'g'ridan asosiy thread'ni to'xtatadi. createHash ham sinxron: u kichik matnni mikrosekundlarda hash qiladi, shuning uchun pool'ga chiqarishning ma'nosi yo'q. Bizning valyuta CLI'da kesh kaliti aynan createHash bilan yasaladi (crypto 1) — pool'ga tegmaydi.

6. Pool to'lganda nima qilish kerak

Muammoni ko'rdik. Yechimlar arzonidan boshlab:

  1. O'lchang. Belgilar: fayl va DNS sekin, loop kechikishi past (Event loop'ni bloklash darsidagi monitorEventLoopDelay). Bir xil ishni UV_THREADPOOL_SIZE bilan va usiz solishtiring.
  2. Og'ir CPU ishini pool'dan ajrating. Parol hash qilish kabi ishni alohida worker_threads yoki alohida xizmatga chiqaring. Shunda fayl va DNS uchun pool bo'sh qoladi.
  3. Bir vaqtdagi og'ir ishlar sonini cheklang. 1 000 foydalanuvchi bir vaqtda kirsa, 1 000 ta scrypt ni birdaniga pool'ga tashlamang — navbat qiling (keyingi darslarda "concurrency limit" naqshini ko'rasiz).
  4. DNS'ni keshlang yoki IP bilan ulaning — ichki xizmatlar uchun.
  5. Oxirida — pool hajmini oshiring, o'lchov bilan. Yadrolar sonidan ancha oshirish odatda foyda bermaydi.

7. CLI qadami: holat yo'q

Bu darsda valyuta kodi o'zgarmaydi. Sabab o'lchovda: CLI bitta buyruqda bitta fetch yoki bitta readFile qiladi. Pool'ning 4 ishchisidan bittasi ham to'liq band bo'lmaydi. Kesh kaliti createHash bilan sinxron (pool'ga tegmaydi), tarmoq so'rovi esa OS'da.

Lekin valyuta da bitta foydali saboq bor. Uning manba moduli CBU'ga nom bilan (cbu.uz) murojaat qiladi — demak birinchi so'rovda dns.lookup pool'dan o'tadi. Agar kelajakda CLI'ni serverga aylantirsangiz va unda og'ir hisob bo'lsa, bu darsdagi "localhost ham kutdi" holati sizda ham takrorlanadi.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Fayl o'qish yoki fetch birdan sekin, CPU band pool crypto/zlib bilan to'lgan og'ir ishni worker'ga, sonini cheklang
UV_THREADPOOL_SIZE=64 qo'yildi, tezlashmadi ish CPU'ga bog'liq, yadrolar soni cheklangan yadrolar sonidan oshirmang, o'lchang
Kodda process.env.UV_THREADPOOL_SIZE o'zgartirildi, ta'sir yo'q pool allaqachon yaratilgan jarayonni muhit o'zgaruvchisi bilan ishga tushiring
"Node bir threadli — demak readFile loop'ni to'xtatadi" JS thread va libuv thread'lari aralashtirildi readFile pool'da; readFileSync esa JS thread'da
localhost sekin, 127.0.0.1 tez nom → dns.lookup → pool navbati ichki ulanishda IP; pool'ni bo'shating

9. Mashqlar

Mashqlar kurs/mashqlar/24/05-thread-pool/ da.

1-mashq (oson): qayerda bajariladi?

Har amal uchun yozing: JS (asosiy thread), pool yoki OS.

  • fs.promises.readFile("a.txt") —
  • crypto.createHash("sha256").update("x").digest("hex") —
  • fetch("http://127.0.0.1:3000") dagi javob kutish —
  • zlib.gzip(buf, cb) —
  • dns.lookup("example.com", cb) —
Yechim
  • pool — fayl amallari libuv pool'ida sinxron bajariladi.
  • JS — createHash sinxron, kichik matn uchun pool keraksiz.
  • OS — IP manzil bilan tarmoq ishi epoll/kqueue/IOCP orqali.
  • pool — callback'li zlib pool'da; gzipSync esa JS thread'da.
  • pool — getaddrinfo bloklovchi, shuning uchun pool'da. dns.resolve4 bo'lsa — pool emas.

2-mashq (o'rta): navbatni hisoblang

«Jonli model» blokidagi simulatePool dan foydalaning. Pool 4 ta. Avval 4 ta og'ir ish (har biri 700 ms), keyin 3 ta yengil ish (har biri 5 ms) keladi. Har yengil ish qachon tugashini chiqaring. Keyin pool 8 ta bo'lganda ham hisoblang.

Yechim
js
function simulatePool(size, durations) {
  const freeAt = Array(size).fill(0);
  return durations.map((ms) => {
    const worker = freeAt.indexOf(Math.min(...freeAt));
    freeAt[worker] += ms;
    return freeAt[worker];
  });
}

const jobs = [700, 700, 700, 700, 5, 5, 5];
for (const size of [4, 8]) {
  const light = simulatePool(size, jobs).slice(4);
  console.log(`${size} thread: yengillar ${light.join(", ")} ms`);
}

Konsolda:

text
4 thread: yengillar 705, 705, 705 ms
8 thread: yengillar 5, 5, 5 ms

4 thread'da yengil ishlar 700 ms gacha kutadi: to'rtala ishchi band. 700 ms da hammasi bo'shaydi va uchala yengil ish uch xil ishchiga tushib, 705 ms da tugaydi. 8 thread'da 4 ta ishchi bo'sh — yengillar darhol bajariladi. Bu «Pool to'lib qolishi» tajribasining kichik nusxasi: readFile ~780 ms kutdi.

3-mashq (qiyin): o'z o'lchovingiz

Node 24 da pool-olch.mjs yozing: 8 ta pbkdf2("p", "s", 300_000, 64, "sha512", cb) ni bir vaqtda boshlang va hammasi tugagan vaqtni millisekundda chiqaring. Uni UV_THREADPOOL_SIZE=1, 2, 4, 8 bilan to'rt marta ishga tushiring. Natijangizni «Pool hajmi: 4 ta ishchi» bo'limidagi grafik bilan solishtiring. Kompyuteringizda nechta yadro borligini os.availableParallelism() bilan chiqaring.

Yechim
js
import { pbkdf2 } from "node:crypto";
import { availableParallelism } from "node:os";

const size = process.env.UV_THREADPOOL_SIZE ?? "4 (sukut)";
const start = performance.now();
let left = 8;
for (let i = 0; i < 8; i++) {
  pbkdf2("p", "s", 300_000, 64, "sha512", () => {
    left--;
    if (left > 0) return;
    const ms = Math.round(performance.now() - start);
    console.log(`pool ${size}: ${ms} ms`);
    console.log(`yadrolar: ${availableParallelism()}`);
  });
}

Biz ishga tushirdik (UV_THREADPOOL_SIZE=4, Node 24.21.0, i5-12500H):

text
pool 4: 756 ms
yadrolar: 16

O'lchov paytida kompyuterda boshqa og'ir jarayonlar ham ishlagan — kanonning toza o'lchovi 560 ms edi. Sizda raqamlar boshqacha bo'ladi — muhimi nisbat: 1 → 2 → 4 da vaqt deyarli ikki baravardan kamayadi, 8 da tejash kichrayadi. Yadrolaringiz 4 ta bo'lsa, 8 thread deyarli foyda bermasligi kerak. PowerShell'da: $env:UV_THREADPOOL_SIZE=1; node pool-olch.mjs.

10. Real ishda

  • Serverlarda parol hash qilish — crypto 2 darsida scrypt va argon2 ni ko'rasiz. Ular ataylab sekin. Login sahifasiga yuklama tushganda pool to'ladi va fayl o'qish, DNS ham sekinlashadi — production'dagi "sirli sekinlik" sabablaridan biri.
  • Rasm va siqish — sharp kabi kutubxonalar ham libuv pool'ida ishlaydi; ko'p rasm bir vaqtda qayta ishlansa, xuddi shu muammo.
  • Intervyu savoli: "Node bir threadli bo'lsa, fayl qanday asinxron o'qiladi?" Javob: JS bitta thread'da, libuv fayl amallarini 4 thread'li pool'da bajaradi, tarmoqni esa OS (epoll/kqueue/IOCP) kuzatadi.
  • Monitoring — pool'ning o'z o'lchagichi yo'q. Amalda fayl va DNS kechikishi, loop kechikishi va CPU birga kuzatiladi (Diagnostika vositalari).

Xulosa

  • JS kodingiz — bitta thread; libuv esa ishni OS'ga va thread pool'ga taqsimlaydi.
  • Tarmoq — OS mexanizmlari: Linux'da epoll, macOS'da kqueue, Windows'da IOCP.
  • Pool'da: fs, dns.lookup, og'ir crypto (pbkdf2, scrypt), async zlib. Sukut — 4 thread, UV_THREADPOOL_SIZE bilan 1024 gacha.
  • Pool to'lsa, kichik readFile va nom bilan fetch ham kutadi (o'lchandi: ~780 ms).
  • Yechim tartibi: o'lchash → og'ir ishni ajratish → sonini cheklash → oxirida pool hajmi.

Keyingi dars: Event loop'ni bloklash: aniqlash va oldini olish — bu safar muammo pool'da emas, asosiy thread'ning o'zida: og'ir sikl, katta JSON.parse va ReDoS'ni o'lchaymiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
libuv thread pool va OS asinxron I/O: Node ishni kimga topshiradi — IlmHamroh