โหมดมืด
บทที่ 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 ไปกับอะไรบ้าง)
- ทบทวนความจริง single-thread + วิธีตรวจจับ "event loop lag"
worker_threads— โยนงาน CPU หนักออกจาก main threadcluster— ใช้ CPU ครบทุก core (1 process ใช้ได้ core เดียว)child_process— รันโปรแกรมอื่น- 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 threadjavascript
// 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หรือ signalSIGUSR2(signal ที่ pm2 ใช้สั่ง reload โดยไม่ฆ่า process) ไม่ใช่ zero-downtime อัตโนมัติ: ถ้า worker เก่าถูก SIGTERM (signal สั่งให้ปิดอย่างสุภาพ) ระหว่างมี in-flight request ที่ใช้เวลานาน, request นั้นจะถูกตัด · ต้อง handleprocess.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 ที่ไม่เคย clearbash
# ถ่าย 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 — ลงมือก่อนไปบทถัดไป
- ทำ route
/fib/:nแบบบล็อก แล้วเปิด 2 แท็บพร้อมกัน (แท็บ 1 ขอ/fib/45, แท็บ 2 ขอ/health) — ยืนยันว่าแท็บ 2 ค้างรอ เพราะ event loop ถูกบล็อก - ย้าย
fibonacciไปworker_threadsแล้วทดสอบซ้ำ — ยืนยันว่า/healthตอบทันทีระหว่าง/fib/45คำนวณ - วัด event loop lag ด้วย
monitorEventLoopDelayขณะรันงานบล็อก vs ไม่บล็อก — ดูตัวเลขต่างกัน - รัน app ด้วย
clusterตามจำนวน core แล้วยิง load ด้วยnpx autocannon http://localhost:3000เทียบ req/s กับแบบ process เดียว - ใช้
execFileรันnode --versionหรือgit rev-parse HEADจากในโค้ด แล้วพิมพ์ผล - สร้าง 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 + K8schild_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
🔤 Glossary · 📋 Style guide · 📅 last_verified: 2026-06-03