async function redeem(code: string, userId: string) {
const c = await db.coupon.find(code);
if (c.usedBy) throw new Error('used');
await db.coupon.update(code, { usedBy: userId }); // two requests can both pass the check
}
OS & Concurrency Basics
draftedProcess/thread, concurrency vs parallelism, blocking vs non-blocking I/O, race condition, deadlock, signals, container limits — góc nhìn của một backend Node.js.
0/43 rated · mastery 0%
Bài học
9 bài · ~136 phút đọc. Học theo thứ tự, rồi làm câu hỏi.- 01~17 phút· 5 câuverify
Process, thread & context switch
Process sở hữu gì, thread chia sẻ gì, context switch tốn ở đâu, fork/exec và copy-on-write, zombie/orphan, và cách Node.js tạo process con an toàn.
- 02~14 phút· 5 câuverify
Concurrency, scheduling & event loop
Concurrency khác parallelism thế nào, preemptive vs cooperative scheduling, event loop của Node chạy ra sao, vì sao một đoạn code CPU-bound làm p99 của cả API tăng vọt, và Go/Java giải cùng bài toán khác Node ở đâu.
- 03~14 phút· 5 câuverify
I/O models, epoll & libuv threadpool
Blocking, non-blocking, multiplexing và async I/O; epoll/kqueue cho một thread phục vụ hàng nghìn socket ra sao; vì sao file I/O của Node chạy trên threadpool 4 thread; file descriptor và cách điều tra EMFILE.
- 04~14 phút· 5 câuverify
Tận dụng nhiều core: cluster, worker_threads & shared memory
cluster vs worker_threads vs nhiều container; chi phí thật của worker (khởi tạo, structured clone, transfer); SharedArrayBuffer, Atomics và memory model; khi nào tách việc CPU nặng thành service riêng.
- 05~15 phút· 6 câuverify
Race condition trong code async: mutex, semaphore & backpressure
Vì sao Node single-thread vẫn có race condition, check-then-act qua await, async mutex, semaphore và single-flight, giới hạn concurrency đúng cách, và backpressure trong stream.
- 06~17 phút· 6 câuverify
Deadlock, livelock & locking ở database
Bốn điều kiện Coffman và cách phá từng cái, deadlock/livelock/starvation trong backend thật, pessimistic vs optimistic locking, sửa oversell bằng atomic update, và điều tra deadlock giữa hai job trên PostgreSQL.
- 07~16 phút· 5 câuverify
Distributed lock, fencing token & idempotency
Redis lock với SET NX PX và các failure mode của nó, vì sao lock dựa trên TTL không đảm bảo loại trừ lẫn nhau, fencing token, idempotency, cron chạy trùng khi scale nhiều pod, và limiter theo tenant chống starvation.
- 08~16 phút· 5 câuverify
Signals, PID 1 & graceful shutdown
SIGTERM/SIGINT/SIGKILL/SIGHUP và exit code 128+n, cách Node xử lý signal, vì sao app làm PID 1 trong container bỏ qua SIGTERM, trình tự graceful shutdown đúng, và vòng đời pod Kubernetes khi deploy.
- 09~14 phút· 5 câuverify
Container resources: cgroup CPU, memory & OOM killer
cgroup giới hạn CPU và memory ra sao, CFS throttling, vì sao os.cpus() nói dối trong container, OOM killer và exit 137, giải phẫu memory của một process Node (RSS, heap, external) và cách đặt --max-old-space-size.
Tóm tắt & cheat sheet của track
TL;DR
- Process = không gian địa chỉ + tài nguyên riêng; thread = luồng thực thi chia sẻ memory trong process. Node.js chạy JS trên một thread nhưng process có thêm libuv threadpool và V8 helper threads.
- Concurrency là cấu trúc (nhiều việc tiến triển xen kẽ); parallelism là thực thi đồng thời trên nhiều core. Event loop cho concurrency, không cho parallelism CPU.
- Race condition vẫn xảy ra trong JS single-threaded — mọi
awaitlà một điểm xen kẽ; và luôn xảy ra giữa nhiều instance/pod dùng chung DB/Redis. - Deadlock cần đủ 4 điều kiện Coffman; phá một điều kiện (thường là thứ tự lock cố định hoặc timeout) là hết.
- Production: hiểu signals (SIGTERM → graceful shutdown), PID 1 trong container, file descriptor limit, cgroup memory/CPU limit và OOM killer.
Khái niệm cốt lõi
| Khái niệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Process | Chương trình đang chạy với virtual memory, FD table, PID riêng | Mỗi node server.js là một process |
| Thread | Luồng thực thi trong process, chia sẻ heap, có stack riêng | worker_threads tạo thread với V8 isolate riêng |
| Context switch | OS lưu/khôi phục register để chuyển CPU giữa thread/process | 1000 thread blocking → CPU tốn cho switching |
| Blocking I/O | Thread đứng chờ tới khi I/O xong | fs.readFileSync chặn event loop |
| Non-blocking I/O + multiplexing | Đăng ký nhiều FD, kernel báo khi sẵn sàng (epoll/kqueue/IOCP) | libuv poll phase |
| Race condition | Kết quả phụ thuộc thứ tự xen kẽ không kiểm soát | Hai request cùng trừ tồn kho |
| Critical section | Đoạn code truy cập shared state cần loại trừ lẫn nhau | SELECT ... FOR UPDATE bao đoạn trừ stock |
| Mutex / Semaphore | Mutex: 1 holder; semaphore: tối đa N holder | Semaphore 10 = giới hạn 10 call đồng thời |
| Deadlock | Các bên chờ nhau mãi mãi | Tx A lock row 1 chờ row 2, Tx B ngược lại |
| Atomic / CAS | Thao tác không thể bị chen ngang; compare-and-swap | UPDATE ... SET v = v + 1 WHERE version = 3 |
| Signal | Thông báo bất đồng bộ từ OS tới process | SIGTERM khi Kubernetes stop pod |
Cơ chế hoạt động
Blocking vs non-blocking I/O trong một server
Check-then-act race qua await
Fix: một câu lệnh atomic UPDATE products SET stock = stock - 1 WHERE id = $1 AND stock > 0 rồi kiểm tra rowCount, hoặc SELECT ... FOR UPDATE trong transaction, hoặc optimistic locking theo version.
Graceful shutdown khi nhận SIGTERM
Trade-offs
| Mô hình | Ưu | Nhược | Hợp với |
|---|---|---|---|
| Thread-per-request | Code tuần tự dễ đọc, tận dụng nhiều core | Memory/thread, context switch, cần lock shared state | CPU-bound vừa, stack Java/.NET truyền thống |
| Event loop (Node) | Hàng chục nghìn connection với ít memory, không lock cho JS state | Một tác vụ CPU chặn tất cả; chỉ 1 core cho JS | I/O-bound API, gateway, realtime |
| Multi-process (cluster, nhiều pod) | Tận dụng core, cô lập crash | Không share memory → state phải ra Redis/DB | Scale Node trên máy nhiều core |
| worker_threads | Parallel CPU trong cùng process, share SharedArrayBuffer | Chi phí khởi tạo isolate, message copy | Parse, nén, hash, image |
| Pessimistic lock | Đơn giản, đúng ngay | Giảm throughput, deadlock | Contention cao, xung đột đắt |
| Optimistic (version/CAS) | Không giữ lock, throughput cao | Retry khi conflict | Contention thấp |
Pitfalls
- Tin rằng "Node single-thread nên không có race condition".
- Dùng API
*Sync(readFileSync,bcrypt.hashSync,JSON.parse50 MB) trong request path. CMD npm starttrong Dockerfile: npm là PID 1, có thể không forward SIGTERM → app bị SIGKILL sau grace period, mất request.os.cpus().lengthtrong container trả số core của host, không phải CPU quota → spawn quá nhiều worker; dùngos.availableParallelism()và vẫn kiểm tra cgroup quota.- Không đặt
--max-old-space-sizephù hợp memory limit của container → OOMKilled thay vì GC sớm. - Lock phân tán (Redis) không có TTL/fencing token → lock "vĩnh viễn" hoặc hai holder cùng lúc.
- Unbounded
Promise.alltrên 10k item → cạn connection pool / FD.
Cheat sheet
- Coffman: mutual exclusion, hold-and-wait, no preemption, circular wait → phá circular wait bằng lock ordering.
- Deadlock (không ai tiến) · Livelock (bận rộn nhưng không tiến) · Starvation (một bên không bao giờ được chạy).
SIGTERMbắt được (graceful) ·SIGKILL/SIGSTOPkhông bắt được ·SIGINT= Ctrl+C · exit code 137 = 128 + 9 (SIGKILL, thường OOM) · 143 = 128 + 15 (SIGTERM).- libuv threadpool:
fs,dns.lookup,crypto(pbkdf2, scrypt, randomBytes async),zlib; network socket không dùng threadpool.UV_THREADPOOL_SIZEmặc định 4. - Zombie = process con đã exit nhưng cha chưa
wait(); orphan = cha chết trước, được init nhận nuôi. - "EMFILE: too many open files" → FD limit (
ulimit -n) hoặc leak socket/file.
References
- Node.js — The Node.js Event Loop ↗
- Node.js — Don't Block the Event Loop (or the Worker Pool) ↗
- Node.js — worker_threads ↗
- Node.js — process signal events ↗
- Node.js — os.availableParallelism() ↗
- libuv — Design overview ↗
- MDN — Atomics ↗
- Linux man-pages — signal(7) ↗
- Linux man-pages — epoll(7) ↗
- Kubernetes — Pod termination lifecycle ↗
- PostgreSQL — Explicit locking ↗
Interview Questions
Showing 43 / 43
FROM node:24-alpine
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
CMD ["npm", "start"]
process.on('SIGTERM', () => {
console.log('bye');
process.exit(0);
});
UPDATE orders
SET status = 'paid', version = version + 1
WHERE id = $1 AND version = $2;
-- rowCount = 0 → someone else changed it: reload and decide
let token: string | null = null;
let expiresAt = 0;
export async function getToken() {
if (!token || Date.now() > expiresAt) {
const r = await fetch('https://auth.internal/token', { method: 'POST' });
const body = await r.json();
token = body.access_token;
expiresAt = Date.now() + body.expires_in * 1000;
}
return token;
}
import { spawn } from 'node:child_process';
const p = spawn('ffmpeg', ['-i', inputPath, '-vf', 'scale=640:-1', outPath]);
p.on('exit', (code, signal) => log.info({ code, signal }, 'ffmpeg done'));
import { pipeline } from 'node:stream/promises';
await pipeline(fs.createReadStream('big.csv'), parseCsv(), transformRow(), uploadStream);
-- release.lua
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
return 0
async function reserve(productId: string, qty: number) {
const { stock } = await db.one('SELECT stock FROM products WHERE id = $1', [productId]);
if (stock < qty) throw new OutOfStock();
await db.none('UPDATE products SET stock = $1 WHERE id = $2', [stock - qty, productId]);
}
BEGIN;
SELECT id FROM orders WHERE id = ANY($1) ORDER BY id FOR UPDATE;
UPDATE orders SET status = 'shipped' WHERE id = ANY($1);
COMMIT;
const server = app.listen(3000);
process.on('SIGTERM', async () => {
server.close();
await db.end();
await redis.quit();
process.exit(0);
});
async function runLimited<T>(tasks: (() => Promise<T>)[], limit: number) {
const results: T[] = [];
let i = 0;
const worker = async () => {
while (i < tasks.length) {
results.push(await tasks[i++]());
}
};
Array.from({ length: limit }, worker);
return results;
}