setTimeout(() => console.log("timeout"));
setImmediate(() => console.log("immediate"));
Promise.resolve().then(() => console.log("promise"));
process.nextTick(() => console.log("nextTick"));
// nextTick, promise, then timeout/immediate in non-deterministic order (main module)
Node.js Internals
draftedV8 + libuv, event loop phases, thread pool, streams/backpressure, worker_threads vs cluster vs child_process, memory leak & heap snapshot, graceful shutdown, profiling, security.
0/63 rated · mastery 0%
Bài học
12 bài · ~155 phút đọc. Học theo thứ tự, rồi làm câu hỏi.- 01~14 phút· 5 câuverify
Kiến trúc runtime: V8, libuv, bindings và thread pool
Một process Node gồm những gì, JS chạy trên một thread nhưng process có nhiều thread, đường đi của một lời gọi fs/crypto qua binding và thread pool, vì sao 5.000 socket không cần 5.000 thread, cách size UV_THREADPOOL_SIZE, và khi nào Node là lựa chọn sai.
- 02~12 phút· 6 câuverify
Event loop của Node: các phase của libuv, nextTick, setImmediate và timer
Một vòng uv_run chạy những gì theo thứ tự nào, vì sao poll phase block và khi nào nó thôi chờ, nextTick queue và microtask được drain ở đâu, thứ tự setTimeout vs setImmediate, thay đổi của Node 11 và libuv 1.45, starvation, và các gotcha của timer.
- 03~12 phút· 4 câuverify
Buffer và stream: dữ liệu nhị phân, bốn loại stream, highWaterMark và backpressure
Buffer nằm ở đâu trong bộ nhớ và vì sao không thấy trong heapUsed, alloc vs allocUnsafe, ký tự multi-byte bị cắt giữa chunk, bốn loại stream, objectMode, buffer nội bộ và highWaterMark, write() trả false và 'drain', chế độ paused/flowing và vì sao for await là cách đọc nên dùng.
- 04~12 phút· 7 câuverify
Stream pipelines trong production: pipeline(), CSV import, export và bulk indexing
Vì sao pipe() rò file descriptor còn pipeline() thì không, nối stage bằng async generator, parser CSV giữ phần dư giữa các chunk, batch cho bulk insert, review endpoint import bị OOMKilled và export 'dùng stream' vẫn tốn vài GB, backpressure tới tận database, và đẩy dữ liệu vào Elasticsearch không làm sập cluster.
- 05~12 phút· 5 câuverify
EventEmitter, lỗi cấp process và cancellation trong Node
EventEmitter gọi listener đồng bộ ra sao, sự kiện 'error' đặc biệt thế nào, MaxListenersExceededWarning nói gì, listener async và captureRejections, uncaughtException vs unhandledRejection và các chế độ --unhandled-rejections, vì sao 'log rồi chạy tiếp' làm hỏng state, fire-and-forget không mất dữ liệu, và huỷ việc bằng AbortSignal.
- 06~14 phút· 6 câuverify
Việc CPU-bound trong Node: chặn event loop, chia nhỏ, worker_threads, cluster và tách service
Chặn event loop trông như thế nào trong production, những cách vô tình chặn phổ biến, xử lý một response JSON 30 MB, worker_threads vs cluster vs child_process, vì sao chuyển sang worker lại chậm hơn, worker pool có hàng đợi giới hạn, cluster trên Kubernetes và Socket.IO, và thiết kế cho việc nặng như PDF hay resize ảnh.
- 07~14 phút· 4 câuverify
Memory trong Node production: GC của V8, đọc memoryUsage và săn leak bằng heap snapshot
Scavenge và Mark-Compact làm gì, heap_size_limit và --max-old-space-size thật sự giới hạn phần nào, đọc rss/heapUsed/external/arrayBuffers để biết leak nằm trong hay ngoài heap, đáy răng cưa sau GC, quy trình chứng minh một leak khiến pod bị OOMKilled mỗi hai ngày, và leak listener SSE đo bằng heap snapshot.
- 08~14 phút· 6 câuverify
Observability và profiling cho Node: AsyncLocalStorage, event loop metrics, CPU profile và điều tra p99
Gắn requestId/tenantId vào mọi log bằng AsyncLocalStorage và những chỗ context bị mất, event loop delay vs event loop utilization, GC metrics, đọc CPU profile, quy trình điều tra p99 spike khi CPU trung bình vẫn thấp, bộ observability mặc định cho mọi service, và kể chuyện load test hay incident theo STAR.
- 09~13 phút· 4 câuverify
HTTP server và client trong Node: timeout của server, keep-alive sau load balancer, connection pool, retry và DNS
Các timeout của http.Server và giá trị mặc định trên Node 24, vì sao keepAliveTimeout lệch với idle timeout của load balancer gây 502 lẻ tẻ, Agent/undici pool và connection reuse, timeout-retry-concurrency cho outbound call, bulkhead theo provider, và DNS lookup như một nút thắt ẩn trên thread pool.
- 10~13 phút· 6 câuverify
Graceful shutdown và kết nối sống lâu: HTTP server, SSE/WebSocket, Kafka consumer và job dài
server.close() thật sự làm gì và vì sao callback của nó có thể chờ thêm 5 giây, closeIdleConnections/closeAllConnections và Connection: close, review đoạn shutdown gây 5xx mỗi lần deploy, đóng SSE/WebSocket không gây reconnect storm, dừng Kafka consumer không mất offset, job 50 triệu bản ghi sống qua restart, và hệ thống push realtime cho 100k client.
- 11~14 phút· 6 câuverify
Module, package và cấu hình: resolution, module cache, dependencies, env/secrets và vòng đời version Node
Node quyết định một file là CommonJS hay ESM thế nào, thuật toán tìm package và field exports, module wrapper và vì sao __dirname không phải global, module cache là singleton theo đường dẫn, circular require, require(esm), dependencies/devDependencies/peerDependencies và npm ci, config và secret theo 12-factor, lịch LTS và cách nâng major version cho 30 service.
- 12~13 phút· 4 câuverify
Bảo mật runtime Node: prototype pollution, ReDoS, command injection, path traversal và supply chain
Vì sao một key __proto__ trong request body biến mọi object thành admin, --disable-proto chặn được gì và không chặn được gì, regex backtracking làm sập cả server một thread, exec qua shell so với execFile, path traversal khi ghép đường dẫn, permission model của Node, và cách giảm rủi ro supply chain của npm.
Tóm tắt & cheat sheet của track
TL;DR
- Node.js = V8 (chạy JS, GC) + libuv (event loop, async I/O, thread pool) + C++ bindings. JS chạy trên một main thread, nhưng process có nhiều thread (libuv pool, V8 GC/compiler threads).
- Network I/O dùng cơ chế non-blocking của OS (epoll/kqueue/IOCP) — không dùng thread pool. File system,
dns.lookup, một sốcryptovàzlibasync dùng thread pool (mặc định 4 thread,UV_THREADPOOL_SIZE). - Throughput của một Node service phụ thuộc vào việc không block event loop: CPU-bound →
worker_threads/service riêng; dữ liệu lớn → streams + backpressure. - Production-ready = biết đo (event loop delay/utilization, heap, GC, CPU profile), tìm leak (heap snapshot comparison), shutdown đúng cách (SIGTERM, drain keep-alive connections) và phòng thủ (prototype pollution, ReDoS, command injection, supply chain).
- Version: Node 24 là LTS đang dùng (local v24); nhiều hành vi thay đổi theo major — các hint có
verifycần check lại docs đúng version.
Khái niệm cốt lõi
| Khái niệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Event loop | Vòng lặp của libuv, chạy callback theo phase | timers → poll → check → close |
process.nextTick queue | Queue của Node, chạy trước promise microtasks, sau mỗi callback | nextTick in trước Promise.then |
| Microtask | Promise reaction, queueMicrotask — drain hết sau mỗi macrotask callback | await tiếp tục trong microtask |
| Thread pool | Pool thread của libuv cho việc không có async API của OS | fs.readFile, crypto.pbkdf2, dns.lookup |
| Stream | Xử lý dữ liệu theo chunk thay vì load hết vào RAM | fs.createReadStream().pipe(res) |
| Backpressure | Cơ chế consumer báo producer chậm lại | write() trả false → chờ 'drain' |
worker_threads | Thread JS riêng (V8 isolate riêng) trong cùng process | Resize ảnh, parse file lớn |
cluster | Fork nhiều process chia chung port | Tận dụng nhiều core trên VM |
| Heap snapshot | Ảnh chụp heap V8 để tìm object bị giữ reference | Comparison 2 snapshot → retainers |
AsyncLocalStorage | Context đi theo async call chain | request-id, tenantId cho log |
Cơ chế hoạt động
Event loop phases
- Từ Node 11, nextTick + microtasks được drain sau mỗi callback (không phải chỉ giữa các phase) — giống browser.
- Từ libuv 1.45 (Node 20), timers chạy sau poll trong mỗi iteration (trước đó chạy trước poll); libuv vẫn chạy timers một lần trước khi vào loop (verify khi dựa vào thứ tự chi tiết).
Ai dùng thread pool, ai không
Streams & backpressure
Graceful shutdown (Kubernetes)
Trade-offs
| Cách chạy song song | Isolation | Chia sẻ memory | Chi phí khởi tạo | Dùng khi |
|---|---|---|---|---|
worker_threads | V8 isolate riêng, cùng process | SharedArrayBuffer, transfer ArrayBuffer | Thấp–vừa (vài ms, MB heap) | CPU-bound trong service (parse, hash, image) |
cluster | Process riêng | Không (IPC) | Cao | Nhiều core trên VM không có orchestrator |
child_process | Process riêng, có thể là binary khác | Không (stdio/IPC) | Cao | Gọi ffmpeg, script Python, tool CLI |
| N container/pod | Hoàn toàn | Không | Cao nhất | Mặc định trên Kubernetes — scale bằng replica |
| Xử lý dữ liệu lớn | RAM | Độ phức tạp | Ghi chú |
|---|---|---|---|
readFile + JSON.parse | O(file) | Thấp | Block event loop khi parse, OOM với file lớn |
Streams + pipeline | O(highWaterMark × stages) | Vừa | Backpressure tự động, xử lý lỗi/cleanup đúng |
| Job queue + worker | Tuỳ | Cao | Tách khỏi request path, retry, quan sát được |
Pitfalls
- Sync API trong request path:
fs.readFileSync,crypto.pbkdf2Sync,zlib.gzipSync,JSON.parse/stringifypayload lớn, regex backtracking. bcrypt/pbkdf2async chiếm hết 4 thread pool →fsvàdns.lookupcủa request khác bị xếp hàng.readable.pipe(writable)không destroy stream khi lỗi → leak file descriptor; dùngstream.pipeline.- Bỏ qua giá trị trả về của
write()→ buffer phình trong RAM. - Listener
on(...)đăng ký mỗi request mà khôngoff→ leak +MaxListenersExceededWarning. - Cache
Mapkhông giới hạn, closure giữ object lớn, timer khôngclearInterval. server.keepAliveTimeoutnhỏ hơn idle timeout của load balancer → 502 lẻ tẻ.- Chạy
npm startlàm PID 1 trong container → signal có thể không tới Node; dùngnodetrực tiếp hoặc init (tini,--init). process.on('uncaughtException')rồi chạy tiếp → process ở trạng thái không xác định.- Deep merge JSON từ client → prototype pollution;
exec("convert " + userInput)→ command injection.
Cheat sheet
- Thứ tự trong một tick: sync code →
process.nextTickqueue → promise microtasks → phase kế tiếp. - Main module:
setTimeout(0)vssetImmediatekhông xác định; trong I/O callback:setImmediateluôn trước. - Thread pool: default 4, max 1024; set
UV_THREADPOOL_SIZEtrước khi pool được dùng lần đầu (tốt nhất là env var khi start). - Stream highWaterMark mặc định: 64 KiB cho byte stream từ Node 22 (trước đó 16 KiB), 16 object cho objectMode (verify).
- Đo:
performance.eventLoopUtilization(),perf_hooks.monitorEventLoopDelay(),--cpu-prof,--heap-prof,--heapsnapshot-signal=SIGUSR2,--heapsnapshot-near-heap-limit=N,--trace-gc. process.memoryUsage():rss(toàn process) ·heapUsed/heapTotal(V8 heap) ·external+arrayBuffers(Buffer ngoài heap).--unhandled-rejectionsmặc địnhthrow(từ Node 15) → rejection không có handler làm crash process.setTimeoutvới delay > 2147483647 ms bị set thành 1 ms (kèmTimeoutOverflowWarning).- Default EventEmitter
maxListeners= 10 (chỉ là warning, không phải giới hạn cứng).
References
- Node.js — The Node.js Event Loop ↗
- Node.js — Don't Block the Event Loop (or the Worker Pool) ↗
- Node.js — Backpressuring in Streams ↗
- Node.js API — Stream ↗
- Node.js API — Worker threads ↗
- Node.js API — Command-line options (UV_THREADPOOL_SIZE, heap flags) ↗
- Node.js API — Process (signals, events) ↗
- Node.js API — HTTP (server timeouts, closeIdleConnections) ↗
- Node.js — Diagnostics / Memory ↗
- Node.js API — Asynchronous context tracking (AsyncLocalStorage) ↗
- Node.js — Security Best Practices ↗
- Node.js Release Schedule ↗
- libuv — Thread pool work scheduling ↗
Personal notes (Notion)
Ghi chú cá nhân — có thể sai hoặc lỗi thời, dùng để tham khảo.
Interview Questions
Showing 63 / 63
const Env = z.object({
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
FEATURE_X: z.enum(["true", "false"]).transform((v) => v === "true"),
});
export const env = Env.parse(process.env); // crash at boot, not at 3am
// A: main module
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
// B: inside an I/O callback
import { readFile } from "node:fs";
readFile(import.meta.filename, () => {
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
});
setTimeout(() => {
console.log("t1");
Promise.resolve().then(() => console.log("p1"));
process.nextTick(() => console.log("n1"));
}, 0);
setTimeout(() => {
console.log("t2");
}, 0);
import { once } from "node:events";
for (const row of rows) {
if (!out.write(format(row))) await once(out, "drain");
}
out.end();
import { pipeline } from "node:stream/promises";
import { createGzip } from "node:zlib";
await pipeline(
fs.createReadStream("report.csv"),
async function* (src) { for await (const chunk of src) yield redact(chunk); },
createGzip(),
res,
);
import { AsyncLocalStorage } from "node:async_hooks";
export const ctx = new AsyncLocalStorage<{ requestId: string; tenantId?: string }>();
app.use((req, _res, next) => {
ctx.run({ requestId: req.get("x-request-id") ?? crypto.randomUUID() }, next);
});
logger.info = (msg) => base.info({ ...ctx.getStore(), msg });
app.get("/search", async (req, res) => {
const ac = new AbortController();
res.on("close", () => { if (!res.writableFinished) ac.abort(); });
const signal = AbortSignal.any([ac.signal, AbortSignal.timeout(2000)]);
const r = await fetch(searchUrl(req.query), { signal });
res.json(await r.json());
});
const server = app.listen(3000);
server.keepAliveTimeout = 65_000; // > ALB idle timeout (60s)
server.headersTimeout = 66_000; // must exceed keepAliveTimeout
import { monitorEventLoopDelay, performance } from "node:perf_hooks";
const h = monitorEventLoopDelay({ resolution: 20 }); h.enable();
let last = performance.eventLoopUtilization();
setInterval(() => {
const now = performance.eventLoopUtilization(last); last = performance.eventLoopUtilization();
metrics.gauge("elu", now.utilization);
metrics.gauge("loop_delay_p99_ms", h.percentile(99) / 1e6); h.reset();
}, 10_000).unref();
import { setImmediate as yieldToLoop } from "node:timers/promises";
for (let i = 0; i < rows.length; i += 1000) {
processBatch(rows.slice(i, i + 1000));
await yieldToLoop(); // let I/O and timers run
}
await pipeline(
fs.createReadStream(path),
parse({ columns: true, bom: true }), // csv-parse: Transform in objectMode
async function* (rows) {
let batch: Row[] = [];
for await (const r of rows) {
batch.push(validate(r));
if (batch.length === 1000) { yield batch; batch = []; }
}
if (batch.length) yield batch;
},
new Writable({ objectMode: true, async write(batch, _e, cb) {
try { await repo.bulkInsert(batch); cb(); } catch (e) { cb(e as Error); }
} }),
);
function merge(t: any, s: any) {
for (const k in s) {
if (typeof s[k] === "object") merge((t[k] ??= {}), s[k]);
else t[k] = s[k];
}
}
merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
({} as any).isAdmin; // true — every object is now "admin"
const EMAIL = /^([a-zA-Z0-9]+)*@example\.com$/;
EMAIL.test("a".repeat(35) + "!"); // exponential backtracking, blocks the loop
import { exec } from "node:child_process";
app.post("/convert", (req, res) => {
const name = req.body.filename; // e.g. "report.docx"
exec(`libreoffice --headless --convert-to pdf /uploads/${name}`, (err, out) => {
if (err) return res.status(500).end();
res.json({ ok: true });
});
});
import { EventEmitter } from "node:events";
export const priceBus = new EventEmitter(); // module-level singleton
app.get("/prices/stream", (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
priceBus.on("price", (p) => res.write(`data: ${JSON.stringify(p)}\n\n`));
});
app.post("/import", upload.single("file"), async (req, res) => {
const text = await fs.promises.readFile(req.file!.path, "utf8");
const rows = text.split("\n").map((l) => l.split(","));
await Promise.all(rows.map((r) => db.insert("products", toProduct(r))));
res.json({ imported: rows.length });
});
const out = fs.createWriteStream("/tmp/export.csv");
const cursor = db.query("SELECT * FROM orders").stream();
cursor.on("data", (row) => {
out.write(toCsvLine(row)); // return value ignored
});
cursor.on("end", () => out.end());
// login
const hash = await new Promise<Buffer>((ok, ko) =>
crypto.pbkdf2(password, salt, 600_000, 64, "sha512", (e, k) => (e ? ko(e) : ok(k))));
// other endpoints
const tpl = await fs.promises.readFile("templates/invoice.html", "utf8");
const r = await fetch("https://inventory.internal/stock"); // dns.lookup under the hood
let shuttingDown = false;
async function shutdown(signal: string) {
if (shuttingDown) return; shuttingDown = true;
logger.info({ signal }, "shutting down");
health.ready = false;
await sleep(5_000); // let the LB stop routing
const force = setTimeout(() => { server.closeAllConnections(); process.exit(1); }, 20_000).unref();
const closed = new Promise<void>((r) => server.close(() => r()));
const sweep = setInterval(() => server.closeIdleConnections(), 500); // sockets that go idle after close()
await closed; clearInterval(sweep);
await Promise.allSettled([consumer.disconnect(), db.end(), redis.quit()]);
clearTimeout(force);
process.exit(0);
}
process.on("SIGTERM", shutdown).on("SIGINT", shutdown);
process.on("SIGTERM", () => {
server.close();
db.end();
process.exit(0);
});
import { EventEmitter } from "node:events";
const orders = new EventEmitter();
orders.on("created", async (order) => {
await inventory.reserve(order); // rejects when out of stock
});
try {
orders.emit("created", order);
} catch (e) {
logger.error(e); // never reached
}
app.get("/files/:id", (req, res) => {
const src = fs.createReadStream(pathFor(req.params.id));
src.pipe(createGzip()).pipe(res);
});