Skip to content

บทที่ 10 — Performance + Scaling: เมื่อ thread เดียวไม่พอ

← บทที่ 9: Database + REST API | สารบัญ | บทที่ 11: Production + Modern Node →

บทระดับสูง — มือใหม่ข้ามไปบทที่ 11 ก่อนได้ กลับมาเมื่อเจอปัญหา performance จริง · ต้องเข้าใจ event loop (บทที่ 0, 3) มาก่อน

บทที่ 0 บอกว่า Node เก่ง I/O แต่ตันกับงาน CPU หนัก บทนี้ลงรายละเอียด: จะรู้ได้ยังไงว่ากำลังตัน, แก้ด้วยอะไร (worker/cluster/child_process), และหา bottleneck (จุดคอขวด — จุดที่ช้าที่สุดที่ฉุดทั้งระบบ) ด้วย profiler (เครื่องมือวัดว่าโปรแกรมใช้เวลา/memory ไปกับอะไรบ้าง)

  1. ทบทวนความจริง single-thread + วิธีตรวจจับ "event loop lag"
  2. worker_threads — โยนงาน CPU หนักออกจาก main thread
  3. cluster — ใช้ CPU ครบทุก core (1 process ใช้ได้ core เดียว)
  4. child_process — รันโปรแกรมอื่น
  5. Profiling หา bottleneck + หา memory leak

1. ทบทวน: single-thread ตันเมื่อไหร่ + วิธีตรวจจับ

Node รันโค้ด JS ของเราบน thread เดียว (main thread/event loop) งาน I/O (DB, network, ไฟล์) ถูกโยนให้ libuv/OS ทำเบื้องหลัง — main thread ว่างไปรับงานอื่น (บทที่ 0) แต่งานคำนวณ (CPU) รันบน main thread ถ้าหนักมันจะบล็อกทุก request:

javascript
// งาน CPU หนัก — บล็อก event loop ทั้งตัว
function fibonacci(n) {
  if (n < 2) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);   // ช้าแบบ exponential
}

// สมมติ `app` คือ Express app จากบทที่ 5 (`const app = express();`)
app.get("/fib/:n", (req, res) => {
  const result = fibonacci(Number(req.params.n));   // ⚠️ fib(45) ใช้เวลาหลายวิ
  res.json({ result });
  // ระหว่างคำนวณ — request อื่นทุกตัว "ค้าง" รอ เพราะ main thread ติดอยู่ตรงนี้
});

ตรวจจับ event loop lag

ถ้าระบบ "ช้าเป็นช่วง ๆ" สงสัยว่ามีอะไรบล็อก event loop วัดได้ด้วย perf_hooks:

javascript
import { monitorEventLoopDelay } from "node:perf_hooks";

const h = monitorEventLoopDelay({ resolution: 20 });
h.enable();
setInterval(() => {
  // ค่าปกติควรต่ำ (< ไม่กี่ ms) — ถ้าพุ่งสูง = มีอะไรบล็อก event loop
  console.log(`event loop lag (p99): ${(h.percentile(99) / 1e6).toFixed(1)}ms`);
  h.reset();
}, 5000).unref();

💡 หลักคิด: ถ้างานคำนวณใช้เวลา > ~10ms ต่อ request และเกิดบ่อย → ควรย้ายออกจาก main thread (ข้อ 2) หรือทำ async/แบ่งชิ้น · งาน I/O ปกติ ไม่ บล็อก ไม่ต้องกังวล


2. worker_threads — โยนงาน CPU หนักออกไป

worker_threads สร้าง thread ใหม่ที่รัน JS แยกขนานกับ main thread — เหมาะกับ งาน CPU หนัก (ประมวลผลภาพ, เข้ารหัส, parse ไฟล์ใหญ่, คำนวณหนัก) main thread จะได้ว่างรับ request ต่อ:

javascript
// fib-worker.mjs — โค้ดที่รันใน worker (thread แยก)
import { parentPort, workerData } from "node:worker_threads";

function fibonacci(n) {
  if (n < 2) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const result = fibonacci(workerData);   // workerData = ค่าที่ main ส่งมา
parentPort.postMessage(result);         // ส่งผลกลับ main thread
javascript
// app.mjs — main thread เรียกใช้ worker
import { Worker } from "node:worker_threads";

function runFib(n) {
  return new Promise((resolve, reject) => {
    // สร้าง worker ใหม่ รันไฟล์ fib-worker บน thread แยก
    const worker = new Worker(new URL("./fib-worker.mjs", import.meta.url), {
      workerData: n,
    });
    worker.on("message", resolve);   // ได้ผลลัพธ์
    worker.on("error", reject);
    worker.on("exit", (code) => {
      if (code !== 0) reject(new Error(`worker exit code ${code}`));
    });
  });
}

app.get("/fib/:n", async (req, res) => {
  const result = await runFib(Number(req.params.n));   // ไม่บล็อก main thread แล้ว!
  res.json({ result });
  // ระหว่าง worker คำนวณ main thread ยังรับ request อื่นได้ปกติ
});

💡 สร้าง worker ใหม่ทุก request แพง (มี overhead) — production ใช้ worker pool ที่ reuse worker เหมือน connection pool (บทที่ 9) · worker thread ไม่แชร์ตัวแปร กับ main โดยตรง — สื่อสารผ่าน message (ปกติ worker แต่ละตัวแยกหน่วยความจำกันเด็ดขาด ต่างจาก thread ในภาษาอื่นที่แชร์ memory กันได้โดยตรง) หรือ SharedArrayBuffer สำหรับขั้นสูง — buffer ที่ share memory ข้าม thread ได้จริง ๆ

ตัวอย่าง worker pool ด้วย piscina (library ยอดนิยม) — reuse worker ข้าม request:

javascript
// pool.mjs
import Piscina from "piscina";
export const fibPool = new Piscina({
  filename: new URL("./fib-worker.mjs", import.meta.url).href,
  maxThreads: 4,   // worker สูงสุดในบ่อ (ปกติ ~ จำนวน core)
});

// app.mjs
import { fibPool } from "./pool.mjs";
app.get("/fib/:n", async (req, res) => {
  const result = await fibPool.run(Number(req.params.n));   // ยืม worker จากบ่อ, คืนอัตโนมัติ
  res.json({ result });
});
javascript
// fib-worker.mjs (รูปแบบ piscina — export default function)
export default function fibonacci(n) {
  if (n < 2) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

3. cluster — ใช้ CPU ครบทุก core

ปัญหาอีกข้อ: เครื่อง production มี 8 core แต่ 1 process ของ Node ใช้ได้ core เดียว → เปลือง 7 core cluster แก้ด้วยการ fork process ลูกหลายตัว (เท่าจำนวน core) ที่แชร์ port เดียวกัน — OS กระจาย request ไปแต่ละตัว:

javascript
// cluster.mjs
import cluster from "node:cluster";
import { availableParallelism } from "node:os";   // ⓘ availableParallelism() เพิ่มใน Node 19.4+ (บทนี้ใช้ Node 22 LTS — มีแน่นอน) · Node เก่าใช้ os.cpus().length แทน
import process from "node:process";

if (cluster.isPrimary) {
  const cpus = availableParallelism();   // จำนวน core (เช่น 8)
  console.log(`Primary ${process.pid} — fork ${cpus} workers`);

  // ⚠️ ใน container ที่ตั้ง CPU limit (cgroup เช่น K8s resource limit) availableParallelism() อาจรายงานจำนวน core ของ host ทั้งเครื่อง ไม่ใช่โควตาจริงที่ container ได้ (พฤติกรรมต่างกันตาม container runtime/Node version) — fork เกินโควตาจะเสีย memory เปล่า ๆ โดยไม่ได้ throughput เพิ่ม ควรตรวจสอบก่อนใช้ค่านี้ตรง ๆ ใน production
  for (let i = 0; i < cpus; i++) cluster.fork();   // สร้าง process ลูก 1 ตัวต่อ core

  // ถ้าลูกตาย → fork ใหม่ (self-healing)
  cluster.on("exit", (worker) => {
    console.log(`worker ${worker.process.pid} ตาย — fork ใหม่`);
    cluster.fork();
  });
} else {
  // โค้ดนี้รันในแต่ละ worker — server จริง (แชร์ port 3000 กันได้)
  // ⚠️ top-level await ใช้ได้แค่ระดับบนสุดของไฟล์ ไม่ใช่ข้างใน if/else block แบบนี้ — ต้องห่อด้วย async function ก่อน
  (async () => {
    const { app } = await import("./app.mjs");
    app.listen(3000, () => console.log(`worker ${process.pid} พร้อม`));
  })();
}

ผล: 8 process รับ request พร้อมกัน — throughput (ปริมาณงานที่ทำได้ต่อวินาที เช่น req/s) ขยายได้ใกล้เคียงจำนวน core บนงานที่ CPU-bound ล้วน ๆ (ตรงข้ามกับ I/O-bound ที่พูดถึงตอนต้นบท — งานที่ CPU ต้องคิดหนักเอง ไม่ใช่แค่รอ DB/network ตอบกลับ)

💡 โลก production ปี 2026 แนะนำให้เลี่ยงเขียน cluster เอง — ทำ 1 container = 1 Node process แล้วให้ Kubernetes (หรือ ECS, Docker Swarm) scale จำนวน replica แทน · ข้อดี: log/metric/resource limit/health check แยกต่อ container, restart ง่าย, rolling update ฟรี · ถ้าไม่ได้รัน container ใช้ PM2 (pm2 start app.js -i max) ซึ่งห่อ cluster ให้แล้ว · เขียน cluster ดิบ ๆ ในโค้ดเองเหมาะกับ learning เท่านั้น

⚠️ scaling ไม่ linear ในชีวิตจริง — ตัวเลข "เพิ่ม N เท่าตาม N core" เป็น ideal case เท่านั้น:

  • I/O-bound (รอ DB/network): bottleneck อยู่ที่ DB ไม่ใช่ CPU → เพิ่ม process มักไม่ช่วย (และอาจทำ DB connection ระเบิด)
  • memory คูณตาม process: 8 worker = หน่วยความจำคูณ 8 (cache/buffer ไม่ share)
  • accept-lock contention (การแย่งล็อกกันตอนหลาย process รับ connection ใหม่พร้อมกัน ทำให้เสียเวลา): Linux kernel มี cost ตอน process แย่งกัน accept connection
  • shared state พัง: in-memory cache/session/rate-limit ของ process หนึ่ง ไม่เห็นจาก process อื่น → ต้องย้ายไป Redis

วัดก่อนเสมอด้วย autocannon หรือ wrk เทียบจริง อย่าเชื่อตัวเลข theoretical

💡 เลือกเครื่องมือ — ตารางสรุปสั้น · cluster = หลาย process แชร์ port (รับ request เยอะขึ้น), worker_threads = หลาย thread ใน process เดียว (งาน CPU หนักชิ้นเดียว), child_process = รันโปรแกรมภายนอก (ภาษาอื่น, CLI tool)

📘 คำศัพท์เล็ก ๆ ก่อนอ่านต่อ (advanced — ข้ามได้ ค่อยกลับมา)

  • WebSocket = protocol ที่เปิด connection สองทางค้างไว้ ใช้กับ chat/notification realtime
  • SSE (Server-Sent Events) = server push event ทิศทางเดียวผ่าน HTTP connection ค้าง (เบากว่า WebSocket)
  • long-poll = client ยิง HTTP ค้างไว้นาน รอ server มี data ใหม่ค่อยตอบ (วิธี realtime แบบเก่า)
  • sticky session = บังคับให้ client คนเดิมไปเข้า process/server ตัวเดิมทุกครั้ง
  • load balancer = ตัวกระจาย request ไปยัง process/server หลายตัว (nginx, HAProxy, k8s service)
  • in-flight request = request ที่ server รับแล้วและกำลังประมวลผลอยู่ (ยังไม่ได้ตอบ client)
  • zero-downtime deploy = deploy โดยที่ user ไม่เห็น downtime — request ที่กำลังทำอยู่ต้องเสร็จก่อน worker ตาย

⚠️ WebSocket / SSE + cluster ต้อง sticky session — connection แบบ persistent (ค้างไว้นาน เช่น WebSocket/SSE/long-poll) ผูกกับ process ตัวเดียวตลอด life ของ connection ถ้า load balancer กระจายแบบ round-robin ระหว่าง reconnect → client คุยกับ process ใหม่ที่ไม่มี state เดิม จะพังเงียบ ๆ

วิธี config sticky (ขั้นสูง — ค่อยกลับมาเมื่อใช้ realtime):

  • PM2: บาง setup ใช้ flag --sticky ได้ แต่ sticky session ของ PM2 cluster mode มักต้องพึ่ง module เสริมหรือ config เพิ่ม — ตรวจสอบเอกสาร PM2 เวอร์ชันที่ใช้ก่อนพึ่งวิธีนี้ (ถ้าไม่ชัวร์ ให้ทำ sticky ที่ layer nginx/K8s แทนจะเสถียรกว่า)
  • nginx: ใช้ ip_hash (route ตาม IP ของ client)
  • Kubernetes: ตั้ง sessionAffinity: ClientIP ที่ Service

⚠️ Cluster + graceful reload pitfall (บทที่ 11) — คำสั่งอย่าง pm2 reload หรือ signal SIGUSR2 (signal ที่ pm2 ใช้สั่ง reload โดยไม่ฆ่า process) ไม่ใช่ zero-downtime อัตโนมัติ: ถ้า worker เก่าถูก SIGTERM (signal สั่งให้ปิดอย่างสุภาพ) ระหว่างมี in-flight request ที่ใช้เวลานาน, request นั้นจะถูกตัด · ต้อง handle process.on('SIGTERM', ...) → หยุดรับ connection ใหม่ (server.close()) → รอ request ที่ค้างเสร็จ → exit · WebSocket ต้องส่ง close frame ให้ client reconnect · รายละเอียดในบทที่ 11 (graceful shutdown)

สรุปเลือกเครื่องมือ

ปัญหาเครื่องมือ
งาน CPU หนัก ชิ้นเดียว บล็อก (เข้ารหัส, ประมวลผลภาพ)worker_threads
อยากใช้ CPU ครบทุก core รับ request เยอะขึ้นcluster (หรือ PM2 / หลาย container)
ต้องรัน โปรแกรมภายนอก (ffmpeg, git, python script)child_process (ข้อ 4)

4. child_process — รันโปรแกรมอื่น

บางครั้งต้องเรียกโปรแกรมภายนอก (แปลงวิดีโอด้วย ffmpeg, รัน git, เรียก script ภาษาอื่น):

javascript
import { execFile } from "node:child_process";
import { promisify } from "node:util";   // บทที่ 3

const execFileP = promisify(execFile);

// execFile — รันโปรแกรม + รอผลลัพธ์ (เหมาะกับ output สั้น ๆ)
const { stdout } = await execFileP("git", ["rev-parse", "HEAD"]);
console.log("commit ปัจจุบัน:", stdout.trim());
javascript
import { spawn } from "node:child_process";

// spawn — รันโปรแกรมแล้วรับ output แบบ stream (เหมาะกับงานนาน / output เยอะ)
const ffmpeg = spawn("ffmpeg", ["-i", "in.mp4", "out.webm"]);
ffmpeg.stdout.on("data", (chunk) => process.stdout.write(chunk));   // stream (บทที่ 4)
ffmpeg.on("close", (code) => console.log(`ffmpeg จบด้วย code ${code}`));

⚠️ Pitfall ความปลอดภัย: อย่าใช้ exec ที่รับ string คำสั่งทั้งบรรทัด กับ input จาก user (exec(\convert ${userInput}`)) — เป็น **command injection** (เหมือน SQL injection แต่ระดับ shell) ใช้ **execFile/spawn** ที่แยก argument เป็น array แทน (input เป็นแค่ argument ไม่ใช่คำสั่ง — shell: falseคือ default ของexecFile/spawnอยู่แล้ว ปลอดภัย) เทียบกับ parameterized query บทที่ 9 · เพิ่มเติม:execFileรับ optiontimeout(ตัดเมื่อรันนานเกิน) และรับAbortSignal` เพื่อยกเลิกได้ — ใช้กับ user-triggered job ที่ต้อง cancel ได้


5. Profiling + หา memory leak

อย่าเดาว่า "ตรงไหนช้า" — วัด เครื่องมือ profile ของ Node:

CPU profiling

bash
# สร้าง CPU profile (.cpuprofile) — เปิดดูใน Chrome DevTools (tab Performance)
node --cpu-prof app.js

# ปรับ sample interval (us) — ละเอียดขึ้นเห็นฟังก์ชันสั้น ๆ ชัด (default 1000us = 1ms)
node --cpu-prof --cpu-prof-interval=100 app.js

# heap profile — ดูว่าจุดไหน allocate memory เยอะ (.heapprofile)
node --heap-prof app.js

# แบบ live: เปิด inspector port แล้วต่อ Chrome DevTools (chrome://inspect)
node --inspect app.js   # ใช้ tab Memory ถ่าย heap snapshot, ใช้ tab Performance บันทึก profile แบบสด

เปิดไฟล์ .cpuprofile / .heapprofile ใน Chrome DevTools จะเห็น flame graph — กราฟที่บอกว่าฟังก์ชันไหนกินเวลา/หน่วยความจำมากสุด (ฟังก์ชันที่ "กว้าง" = ใช้เวลาเยอะ) ตรงนั้นแหละ bottleneck ที่ควร optimize

💡 เครื่องมือยอดนิยม: clinic.js (npx clinic doctor -- node app.js) วินิจฉัยให้อัตโนมัติว่าปัญหาคือ CPU, I/O, event loop หรือ GC · autocannon ยิง load test วัด req/s · 0x ทำ flame graph สวย ๆ · PerformanceObserver (node:perf_hooks) บันทึก metric เฉพาะจุดในโค้ดเอง (เช่นวัดเวลา query แต่ละชนิด)

Memory leak — RAM ค่อย ๆ โตจนพัง

📘 คำศัพท์ memory ที่จะเจอในส่วนนี้

  • garbage collector (GC) = ตัวเก็บขยะ memory อัตโนมัติของ V8 — หา object ที่ไม่มีใครอ้างถึงแล้วเอาคืน
  • GC cycle = รอบที่ GC ทำงาน (มี minor/major) — heap จะ "ลด" ทุกรอบเหมือนฟันเลื่อย ปกติ
  • V8 heap = พื้นที่ memory ที่ JS object ใช้อยู่ (ส่วนที่ GC ดูแล)
  • memory leak = object ที่ไม่ใช้แล้วแต่ยังถูกอ้างถึง → GC เก็บไม่ได้ → heap โตเรื่อย ๆ
  • OOM (Out Of Memory) = RAM หมดจนระบบ (kernel หรือ container limit) ฆ่า process ทิ้ง
  • native addon = module ที่เขียนด้วย C++ และ load เข้า Node — memory อยู่นอก V8 heap
  • SharedArrayBuffer = buffer ที่ share memory จริง ๆ ข้าม worker thread (ขั้นสูง)

วิธีตรวจที่ถูกต้องprocess.memoryUsage() มีหลายค่า อย่าดูแค่ค่าเดียว:

  • heapUsed = V8 heap ที่ใช้อยู่ — ขึ้น/ลงเป็นฟันเลื่อยตาม GC cycle เป็นเรื่องปกติ ไม่ใช่ leak ดูค่าดิบหลอกตา ต้องดู baseline หลัง major GC (รันด้วย node --expose-gc แล้วเรียก global.gc() ก่อนวัด) — baseline ที่ค่อย ๆ สูงขึ้นทุกครั้ง = leak จริง
  • rss (Resident Set Size) = หน่วยความจำทั้ง process รวม native — leak บางอย่าง (Buffer, native addon, worker_threads) ไม่อยู่ใน V8 heap แต่อยู่ใน rss ต้องดูคู่กันเสมอ
  • external = memory ของ C++ object ที่ V8 จัดการ (เช่น Buffer) — โตผิดปกติ = leak Buffer
javascript
// run: node --expose-gc app.js
setInterval(() => {
  global.gc();   // บังคับ major GC เพื่อตัดสัญญาณรบกวน (ห้ามใช้ใน production!)
  const m = process.memoryUsage();
  console.log(`heap=${(m.heapUsed/1e6).toFixed(1)}MB rss=${(m.rss/1e6).toFixed(1)}MB ext=${(m.external/1e6).toFixed(1)}MB`);
}, 10_000);

ถ้า baseline หลัง gc ค่อย ๆ สูงขึ้นทุก sample = leak ของจริง

แหล่ง leak ที่เจอบ่อยใน Node:

javascript
// ❌ 1. global array/Map ที่ push เข้าเรื่อย ๆ ไม่เคยลบ
const cache = [];
app.get("/x", (req, res) => { cache.push(req.body); res.end(); });   // โตไม่หยุด

// ❌ 2. ลงทะเบียน event listener ซ้ำ ๆ ไม่เคย removeListener (บทที่ 3)
//     → Node เตือน "MaxListenersExceededWarning"
someEmitter.on("data", handler);   // ถ้าเรียกในฟังก์ชันที่ถูกเรียกบ่อย → listener สะสม

// ❌ 3. closure ที่ค้างอ้างถึง object ใหญ่ / timer ที่ไม่เคย clear
bash
# ถ่าย heap snapshot มาดูว่าอะไรกิน RAM (เปิดใน Chrome DevTools tab Memory)
node --heapsnapshot-signal=SIGUSR2 app.js   # ส่ง signal เพื่อถ่าย snapshot ระหว่างรัน

⚠️ ถ้าแอปนี้ดัก SIGUSR2 ไว้ทำ graceful shutdown อยู่แล้ว (บทที่ 7 — nodemon ใช้ signal นี้สั่ง restart) การใช้ --heapsnapshot-signal=SIGUSR2 จะชนกัน: signal เดียวกันแต่มีสอง listener แย่งกันทำงาน (ถ่าย snapshot vs ปิดเซิร์ฟเวอร์) เลือก signal อื่นแทน เช่น --heapsnapshot-signal=SIGUSR1

ถ่าย snapshot 2 ครั้งห่างกัน แล้วเทียบว่า object ชนิดไหนเพิ่มขึ้นเรื่อย ๆ = ตัวต้นเหตุ

💡 การ optimize ที่คุ้มที่สุดมักไม่ใช่ worker/cluster — แต่คือ:

  • caching (อย่า query ซ้ำ) — ใช้ Redis (in-memory key-value store ที่รันแยก process/server, แชร์ได้ข้าม Node instance) หรือ in-memory cache (Map/LRU — Least Recently Used, วิธีจัดการ cache ที่ทิ้งข้อมูลเก่าสุดที่ไม่ถูกใช้ก่อนเมื่อ cache เต็ม — ในตัวแอป, ใช้แค่ใน 1 process)
  • แก้ N+1 query (= query loop ที่ยิง DB ซ้ำ 1+N ครั้ง ทั้ง ๆ ที่ JOIN ครั้งเดียวได้ เช่น query users → loop ยิง query orders ต่อ user → 1+N round-trip) — รายละเอียดบทที่ 9 + Database บทที่ 4 (ถ้ามีในเล่ม)
  • ไม่บล็อก event loop (ย้ายงาน CPU หนักไป worker, ระวัง JSON.parse/regex/crypto sync ของใหญ่)

วัดก่อน optimize เสมอ อย่าเดา

🧵 เกร็ด libuv thread pool: Node มี thread pool (default 4) ที่ libuv ใช้ทำ fs, dns.lookup, crypto (pbkdf2/scrypt/randomBytes), zlib แอปที่ทำ crypto/hash หนัก (เช่น bcrypt) หรืออ่านไฟล์เยอะพร้อมกัน อาจตันที่นี่ — เพิ่มได้ด้วย UV_THREADPOOL_SIZE=8 node app.js (max 1024) · ตั้งก่อน Node เริ่ม run เท่านั้น (ตั้ง runtime ไม่มีผล)


🛠️ Checkpoint 10 — ลงมือก่อนไปบทถัดไป

  1. ทำ route /fib/:n แบบบล็อก แล้วเปิด 2 แท็บพร้อมกัน (แท็บ 1 ขอ /fib/45, แท็บ 2 ขอ /health) — ยืนยันว่าแท็บ 2 ค้างรอ เพราะ event loop ถูกบล็อก
  2. ย้าย fibonacci ไป worker_threads แล้วทดสอบซ้ำ — ยืนยันว่า /health ตอบทันทีระหว่าง /fib/45 คำนวณ
  3. วัด event loop lag ด้วย monitorEventLoopDelay ขณะรันงานบล็อก vs ไม่บล็อก — ดูตัวเลขต่างกัน
  4. รัน app ด้วย cluster ตามจำนวน core แล้วยิง load ด้วย npx autocannon http://localhost:3000 เทียบ req/s กับแบบ process เดียว
  5. ใช้ execFile รัน node --version หรือ git rev-parse HEAD จากในโค้ด แล้วพิมพ์ผล
  6. สร้าง memory leak จงใจ (global array ที่ push ทุก request) แล้วดู process.memoryUsage().heapUsed โตขึ้นเรื่อย ๆ → ถ่าย heap snapshot มาดู

เฉลยข้อ 3: ค่า h.percentile(99) ตอนงานบล็อกจะพุ่งเป็นพัน ๆ ms (เท่ากับเวลาที่ fib ใช้) ส่วนตอนปกติจะอยู่หลัก ms เดียว — เพราะ lag = "นานแค่ไหนกว่า event loop จะวนกลับมาได้"


สรุปบทที่ 10

  • Node รันโค้ด JS thread เดียว — งาน CPU หนักบล็อกทุก request · ตรวจด้วย event loop lag (monitorEventLoopDelay)
  • worker_threads = โยนงาน CPU หนักไป thread แยก (สื่อสารผ่าน message) — production ใช้ worker pool (piscina)
  • cluster = fork หลาย process ใช้ CPU ครบทุก core (1 process = 1 core) — จริง ๆ ใช้ PM2 หรือหลาย container + K8s
  • child_process (execFile/spawn) = รันโปรแกรมภายนอก — เลี่ยง exec กับ user input (command injection)
  • Profile ก่อน optimize: --cpu-prof + flame graph, clinic.js, autocannon · memory leak จาก global ที่โตไม่หยุด / listener สะสม → heap snapshot
  • ROI สูงสุดมักคือ caching + แก้ N+1 + ไม่บล็อก event loop ไม่ใช่ worker/cluster

บทสุดท้าย: เอาขึ้น production — Docker, process manager, health check, structured logging, security + ฟีเจอร์ Node สมัยใหม่ และ roadmap สู่ NestJS

→ บทที่ 11: Production + Modern Node


🔤 Glossary · 📋 Style guide · 📅 last_verified: 2026-06-03