rps = 2000, W_request = 50 ms → in-flight requests = 100
W_db_hold = 5 ms → busy connections = 2000 × 0.005 = 10
pods = 5, pool = 4/pod → 20 connections (2× headroom) ✓
Scenarios: Performance, Scale & Memory
draftedMock interview tình huống production — 10k rps, peak ×1000, slow API, 3MB JSON, JSON.stringify block event loop, memory leak, CPU 100%, pool exhaustion, logs 100GB/ngày và log dữ liệu nhạy cảm.
0/60 rated · mastery 0%
Bài học
5 bài · ~81 phút đọc. Học theo thứ tự, rồi làm câu hỏi.- 01~17 phút· 12 câuverify
Triage một bài scale: hỏi lại, ước lượng và đo đúng chỗ
Khung trả lời cho mọi câu 10k rps / API chậm: câu hỏi làm rõ, Little's Law, percentile và fan-out, đo từ client tới DB, load test không tự lừa mình.
- 02~15 phút· 12 câuverify
Peak, quá tải và tự bảo vệ: queue, autoscale, load shedding, retry storm
Peak ×1000 và flash sale: tách nhận khỏi xử lý (202 + queue), toán drain backlog, autoscale theo lag và lịch, 429 vs 503, load shedding, retry storm và cầm máu endpoint quá tải.
- 03~12 phút· 9 câuverify
Database dưới tải: pool exhaustion, N+1, max_connections và ghi theo batch
Các kịch bản DB là tài nguyên cạn trước: pool leak và transaction bọc HTTP call, too many clients khi scale pod, N+1, concurrency không giới hạn, và ghi 5k event/s bằng batch/COPY an toàn.
- 04~19 phút· 13 câuverify
Event loop và payload lớn: JSON 3MB, stream, worker, nén và CPU 100%
Vì sao một JSON.stringify chặn mọi request, đo nó thế nào, và thứ tự fix: làm ít hơn, pre-serialize, ETag đúng, nén ở proxy, stream/NDJSON, worker có giới hạn, và săn CPU 100%.
- 05~18 phút· 9 câuverify
Memory leak trong production: chứng minh, cầm máu và tìm retainer
RSS cao chưa phải leak; đọc memoryUsage theo thời gian, 3-snapshot technique an toàn trên production, các leak kinh điển (Map, listener, timer, fetch body, AsyncLocalStorage) và sizing heap theo container.
Tóm tắt & cheat sheet của track
TL;DR
- Đây là track mock interview: câu hỏi là một tình huống production ("API phải chịu 10k rps", "RAM tăng 100% sau 2 ngày", "JSON 3MB"). Điểm không nằm ở việc kể tên công nghệ mà ở thứ tự suy nghĩ: hỏi lại → ước lượng → đo → khoanh vùng → fix theo ưu tiên → nói trade-off.
- Mọi câu "scale" đều quy về vài con số: throughput (rps), latency (p50/p99), concurrency = rps × latency (Little's Law), và tài nguyên hữu hạn đầu tiên cạn (CPU event loop, DB connection, memory, quota bên thứ ba).
- Node.js có một điểm yếu cố định: một event loop cho mọi request. Việc sync nặng (
JSON.stringifyvài MB, regex backtracking, bcrypt sync, vòng lặp O(n²)) làm chậm tất cả request chứ không chỉ request đó. - Memory leak = heap tăng đơn điệu sau GC, không phải "RSS cao". Chứng minh bằng heap snapshot (3-snapshot technique), mitigation trước (restart có kiểm soát, giới hạn cache), root cause sau.
- Quá tải là chuyện khi nào, không phải nếu: thiết kế sẵn rate limit (429), load shedding (503 +
Retry-After), queue làm buffer (202), backpressure, và timeout/retry có budget.
Khái niệm cốt lõi
| Khái niệm | Một câu | Ví dụ ngắn |
|---|---|---|
| Throughput vs latency | Số request/giây vs thời gian một request | 10k rps, p99 = 120 ms |
| Little's Law | L = λ × W: số request đang xử lý = arrival rate × thời gian ở trong hệ thống | 2000 rps × 0.05 s = 100 request in-flight |
| Percentile (p50/p95/p99) | Giá trị mà X% request nhanh hơn | p99 = 800 ms: 1/100 request chậm hơn 800 ms |
| Tail amplification | Request gọi N dependency → xác suất dính 1 cái chậm tăng nhanh | 100 call, mỗi call 1% chậm → 63% request chậm |
| Event loop lag / ELU | Độ trễ timer so với lịch / tỷ lệ thời gian event loop bận | monitorEventLoopDelay() p99 = 250 ms |
| Pool wait | Thời gian chờ lấy connection, trước khi query chạy | pool.waitingCount = 180 |
| Backpressure | Tín hiệu "chậm lại" từ phía nhận truyền ngược về phía gửi | res.write() trả false → chờ 'drain' |
| Load shedding | Chủ động từ chối một phần request để phần còn lại sống | Lag > 200 ms → 503 + Retry-After: 2 |
| Rate limiting | Giới hạn theo client/key, lỗi phía client | 429 Too Many Requests |
| 202 Accepted + queue | Nhận việc, xử lý sau, trả link theo dõi | POST /exports → 202, Location: /jobs/42 |
| Heap vs RSS vs external | V8 heap (JS object) / toàn bộ memory process / Buffer ngoài heap | heapUsed 300MB, rss 1.1GB, external 600MB |
| Retained size | Memory được giải phóng nếu object này bị GC | Map 1 entry nhưng retain 400 MB |
| Structured clone | Thuật toán copy object giữa thread (workerData, postMessage) | Gửi object 3MB sang worker = copy trên main thread |
| Redaction | Xoá/che field nhạy cảm trước khi log được ghi | pino redact: ['req.headers.authorization'] |
Cơ chế hoạt động
Một request đi qua đâu và thời gian nằm ở đâu
Mỗi mũi tên là một nơi latency có thể nằm. "API chậm" chỉ có ý nghĩa khi biết đoạn nào chậm: đo bằng tracing (span cho DB, HTTP), Server-Timing, metric pool wait, event loop lag.
Quyết định khi quá tải
Peak ×1000: API nhận nhanh, worker xử lý đều
Memory leak: 3-snapshot technique
Khung trả lời
Dùng lại cho mọi câu "hệ thống chậm / quá tải / phải chịu X rps":
- Hỏi lại (30 giây): loại request (đọc/ghi, CPU/I-O), payload size, SLO (p99 bao nhiêu), peak vs trung bình, data có cache được không, consistency yêu cầu gì.
- Ước lượng: rps peak, concurrency (Little's Law), số instance, số DB connection, bandwidth (rps × payload).
- Cầm máu trước (nếu đang cháy): rate limit, tắt feature nặng bằng flag, trả stale cache, scale ngang, rollback deploy.
- Đo, không đoán: tracing, RED metrics theo endpoint, event loop lag, pool wait, DB slow query, heap/RSS.
- Fix theo ưu tiên: làm ít việc hơn (cache, projection, pagination) → làm việc rẻ hơn (index, batch, bỏ N+1) → làm song song có giới hạn → scale ngang → đổi kiến trúc (queue, CQRS, sharding).
- Trade-off + chứng minh: nói cái giá (stale data, eventual consistency, chi phí, độ phức tạp) và cách verify (load test, dashboard, alert).
Trade-offs
| Lựa chọn | Được | Mất | Khi nào |
|---|---|---|---|
| Scale ngang thêm pod | Nhanh, không đổi code | Dồn tải xuống DB, tốn tiền, DB connection tăng theo số pod | Bottleneck là CPU app, app stateless |
| Cache (CDN/Redis/in-process) | Giảm DB load mạnh nhất cho read-heavy | Stale data, invalidation, stampede | Đọc nhiều, chấp nhận stale vài giây–phút |
| Queue + 202 | Hấp thụ spike, worker chạy đều | Không có kết quả ngay, cần idempotency, UX polling/webhook | Việc không cần kết quả sync (order processing, export, email) |
| Rate limit (429) | Bảo vệ khỏi 1 client/tenant ồn ào | Có thể chặn nhầm client hợp lệ | Multi-tenant, public API |
| Load shedding (503) | Giữ latency cho phần request được nhận | Một phần user bị lỗi | Toàn hệ thống vượt capacity |
| Compression ở proxy/CDN | Giảm bandwidth 5–10× với JSON (verify theo data) | CPU (nhất là brotli quality cao) | Payload > vài KB, client mạng chậm |
| Pre-serialize + cache string | Bỏ hẳn chi phí stringify mỗi request | Memory, invalidation | Response giống nhau cho nhiều user |
| worker_threads | Không block event loop chính | Chi phí clone dữ liệu vào/ra, pool phức tạp | CPU nặng với input nhỏ, output nhỏ |
| Provision cho peak | Đơn giản, không lo scale lag | Tốn tiền 23/24 giờ | Peak không đoán trước, cost không quan trọng |
| Scheduled + reactive autoscale | Rẻ, chịu được peak đã biết | Phải dự đoán; reactive scale mất vài phút | Peak có giờ cố định |
Pitfalls
- Trả lời "scale thêm server" trước khi hỏi bottleneck là gì — thêm pod khi DB đang 95% CPU chỉ làm DB chết nhanh hơn.
- Nói về average latency; interviewer muốn nghe p99 và tail.
Promise.alltrên 10k phần tử — tự DDoS DB/bên thứ ba, cạn pool.- Retry ở mọi tầng không có budget → retry storm nhân tải lên 10–30×.
- Autoscale theo CPU cho worker I/O-bound, hoặc tin rằng autoscale phản ứng kịp spike 30 giây.
- Tin rằng
worker_threadslàmJSON.stringify"miễn phí" — dữ liệu phải clone qua thread. - Stream JSON mà quên dấu phẩy, quên backpressure, gọi
res.end()trước khi data tới. - Nhầm RSS cao là leak; hoặc set
--max-old-space-sizelớn hơn memory limit của container. - Không consume body của
fetchresponse → giữ socket/memory. - Log
req.headers/req.bodynguyên con → token, password, số thẻ nằm trong log store 30 ngày và ai cũng đọc được.
Cheat sheet
- Little's Law:
in-flight = rps × latency(s). 2000 rps × 50 ms = 100 concurrent → pool 20/pod × 5 pod là vừa khít, không có headroom. - Postgres
max_connectionsmặc định 100;pods × pool sizephải nhỏ hơn con số đó (trừ connection cho admin/replication) → PgBouncer khi nhiều pod. - Fan-out tail:
P(slow) = 1 - (1 - p)^N; p = 1%, N = 100 → 63%. - Bandwidth: 3 MB × 1000 rps = 3 GB/s = 24 Gbit/s — một endpoint có thể làm đầy NIC trước khi CPU cạn.
- Log: 100 MB/ngày ≈ 1.2 KB/s (không phải vấn đề); 100 GB/ngày ≈ 1.2 MB/s ≈ ~1,200 dòng 1 KB mỗi giây (vấn đề là chi phí lưu trữ + index).
- 429 = client vượt quota của nó; 503 = server không còn capacity. Cả hai nên có
Retry-After. - Đo event loop:
perf_hooks.monitorEventLoopDelay(),performance.eventLoopUtilization(); CPU:node --cpu-prof; memory:--heapsnapshot-signal=SIGUSR2,--heapsnapshot-near-heap-limit=3. --max-old-space-size≈ 70–75% memory limit container (phần còn lại cho Buffer, native, stack) — con số kinh nghiệm, verify với workload.- Log an toàn: redact ở logger, không bao giờ log password/token/PAN/CVV, retention có hạn, quyền đọc log theo vai trò.
References
- Node.js — Diagnostics guide (memory, CPU profiling, heap snapshots) ↗
- Node.js — Don't block the event loop ↗
- Node.js — perf_hooks (monitorEventLoopDelay, eventLoopUtilization) ↗
- Node.js — worker_threads (workerData, structured clone) ↗
- Node.js — Command-line options (--cpu-prof, --heapsnapshot-signal, --max-old-space-size) ↗
- Node.js — process I/O (stdout synchronous/asynchronous) ↗
- Node.js — Backpressuring in streams ↗
- PostgreSQL — Connections and authentication (max_connections) ↗
- PostgreSQL — COPY ↗
- Kubernetes — Horizontal Pod Autoscaling ↗
- Kubernetes — Resource management for pods and containers (CPU limits, memory limits) ↗
- AWS — Scheduled scaling for Amazon EC2 Auto Scaling ↗
- RFC 9110 — HTTP Semantics (503, Retry-After, conditional requests) ↗
- RFC 6585 — Additional HTTP Status Codes (429) ↗
- MDN — Content-Encoding ↗
- MDN — Server-Timing ↗
- MDN — ETag ↗
- OWASP — Logging Cheat Sheet ↗
- pino — Redaction ↗
- Fastify — Validation and serialization (fast-json-stringify) ↗
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 60 / 60
P(page slow) = 1 - (1 - 0.01)^N
N = 1 → 1%
N = 40 → 33%
N = 100 → 63%
# closed model: 100 connections, mỗi connection chờ response rồi mới gửi tiếp
npx autocannon -c 100 -d 60 https://staging.example.com/products/42
# k6 open model: giữ 10k req/s bất kể server chậm
# scenarios: { load: { executor: 'constant-arrival-rate', rate: 10000, timeUnit: '1s',
# duration: '5m', preAllocatedVUs: 2000, maxVUs: 5000 } }
import { Pool } from 'pg';
// 20 pods × max 10 = 200 connections > max_connections = 100
export const pool = new Pool({
max: 10,
// default connectionTimeoutMillis = 0 → wait forever for a free client
});
app.post('/orders/:id/confirm', async (req, res) => {
const client = await pool.connect();
await client.query('BEGIN');
const { rows } = await client.query('SELECT * FROM orders WHERE id = $1 FOR UPDATE', [req.params.id]);
if (rows.length === 0) return res.status(404).end(); // (a)
const payment = await paymentApi.capture(rows[0].payment_id); // (b) 300 ms – 30 s
await client.query('UPDATE orders SET status = $1 WHERE id = $2', ['confirmed', req.params.id]);
await client.query('COMMIT');
client.release();
res.json({ ok: true, payment });
});
app.get('/orders', async (req, res) => {
const orders = await db.query('SELECT * FROM orders WHERE customer_id = $1 ORDER BY created_at DESC LIMIT 50', [req.user.id]);
for (const o of orders.rows) {
o.items = (await db.query('SELECT * FROM order_items WHERE order_id = $1', [o.id])).rows;
o.shipping = (await db.query('SELECT * FROM shipments WHERE order_id = $1', [o.id])).rows[0];
}
res.json(orders.rows);
});
SELECT pid, state, wait_event_type, wait_event, now() - xact_start AS tx_age, left(query, 80)
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY tx_age DESC NULLS LAST;
POST /orders HTTP/1.1
Idempotency-Key: 7f3c...
HTTP/1.1 202 Accepted
Location: /orders/jobs/8812
Retry-After: 2
import { monitorEventLoopDelay } from 'node:perf_hooks';
const h = monitorEventLoopDelay({ resolution: 20 }); h.enable();
let inFlight = 0;
app.use((req, res, next) => {
const lagMs = h.percentile(99) / 1e6;
if ((lagMs > 200 || inFlight > 500) && !req.path.startsWith('/checkout')) {
res.set('Retry-After', String(1 + Math.floor(Math.random() * 3)));
return res.status(503).json({ error: 'overloaded' });
}
inFlight++; res.on('close', () => inFlight--); next();
});
setInterval(() => h.reset(), 5000).unref();
inflow = 100k/min, capacity = 20k/min, duration = 60 min
backlog = (100k - 20k) × 60 = 4.8M
drain = 4.8M / 20k = 240 min after peak (if inflow drops to 0)
const [user, cart, recs] = await Promise.allSettled([
getUser(id),
getCart(id),
getRecs(id, { signal: AbortSignal.timeout(80) }),
]);
const products = await repo.findAllToEnrich(); // 20,000 rows
await Promise.all(
products.map(async (p) => {
const data = await supplier.get(`/items/${p.sku}`);
await repo.update(p.id, data);
}),
);
const ac = new AbortController();
try {
const [a, b] = await Promise.all([
fetch(urlA, { signal: ac.signal }),
fetch(urlB, { signal: ac.signal }),
]);
} catch (e) {
ac.abort(); // stop the calls that are still running
throw e;
}
gateway (3 retries) → api (3 retries) → service (3 retries) → db
1 user request can become up to 4 × 4 × 4 = 64 DB calls
HTTP/1.1 200 OK
Server-Timing: db;dur=412, cache;desc="miss";dur=2, render;dur=38, ext-pricing;dur=910
app.use((req, res, next) => {
const start = Date.now();
next();
const ms = Date.now() - start;
metrics.histogram('http_duration_ms', ms, { route: req.path });
});
consumer.on('message', async (e) => {
await db.query(
'INSERT INTO events (id, tenant_id, type, payload, created_at) VALUES ($1, $2, $3, $4, $5)',
[e.id, e.tenantId, e.type, e.payload, e.createdAt],
);
});
const buffer: Event[] = [];
app.post('/events', (req, res) => {
buffer.push(req.body);
res.status(202).end();
});
setInterval(async () => {
const batch = buffer.splice(0, buffer.length);
if (batch.length) await repo.insertMany(batch);
}, 1000);
app.use(compression({
threshold: 1024,
filter: (req, res) =>
res.getHeader('Content-Type')?.toString().startsWith('text/event-stream')
? false
: compression.filter(req, res),
}));
app.get('/catalog', async (req, res) => {
const version = await redis.get('catalog:version'); // cheap
const etag = `W/"catalog-${version}"`;
if (req.headers['if-none-match'] === etag) return res.status(304).end();
const body = await getSerializedCatalog(version); // cached string
res.set({ ETag: etag, 'Cache-Control': 'private, max-age=30' }).type('json').send(body);
});
import { performance } from 'node:perf_hooks';
const t0 = performance.now();
const s = JSON.stringify(bigObject);
console.log({ ms: performance.now() - t0, mb: Buffer.byteLength(s) / 1e6 });
app.get('/export', (req, res) => {
const stream = db.queryStream('SELECT * FROM orders'); // object mode, one row per 'data'
res.write('[');
stream.on('data', (row) => {
res.write(JSON.stringify(row));
});
res.end(']');
});
// main thread still pays: structured clone of bigObject (sync)
new Worker('./stringify.js', { workerData: bigObject });
// better: pass a small query and let the worker load + serialize
pool.run({ reportId: 42 }); // piscina, worker returns a transferable Buffer
app.get('/products', {
schema: { response: { 200: { type: 'array', items: {
type: 'object',
properties: { id: { type: 'string' }, name: { type: 'string' }, price: { type: 'number' } },
} } } },
}, async () => repo.list());
import { gzipSync } from 'node:zlib';
let snapshot: { version: string; gz: Buffer } | undefined;
async function rebuild(version: string) { // off the request path
const json = JSON.stringify(await repo.loadCatalog());
snapshot = { version, gz: gzipSync(json, { level: 6 }) }; // once per change
}
app.get('/catalog', (req, res) => {
res.set({ 'Content-Type': 'application/json', 'Content-Encoding': 'gzip', ETag: `W/"${snapshot!.version}"` });
res.end(snapshot!.gz);
});
const priceCache = new Map<string, { value: Price; at: number }>();
export async function getPrice(tenantId: string, sku: string, currency: string, userId: string) {
const key = `${tenantId}:${sku}:${currency}:${userId}`;
const hit = priceCache.get(key);
if (hit && Date.now() - hit.at < 60_000) return hit.value;
const value = await pricing.compute(tenantId, sku, currency, userId);
priceCache.set(key, { value, at: Date.now() });
return value;
}
io.on('connection', (socket) => {
const session = loadSessionSnapshot(socket.data.userId); // ~200 KB
priceBus.on('tick', (t) => {
if (session.watchlist.includes(t.symbol)) socket.emit('tick', t);
});
setInterval(() => {
socket.emit('heartbeat', { at: Date.now(), watched: session.watchlist.length });
}, 10_000);
});
async function check(url: string) {
const res = await fetch(url, { signal: AbortSignal.timeout(2000) });
if (!res.ok) return { url, ok: false, status: res.status }; // body never read
return { url, ok: true, data: await res.json() };
}
node --max-old-space-size=1536 --heapsnapshot-near-heap-limit=2 dist/main.js
node -e "console.log(require('v8').getHeapStatistics().heap_size_limit / 2**20, 'MB')"
function applyPromotions(products: Product[], promos: Promotion[]) {
return products.map((p) => {
const promo = promos.find((x) => x.skus.includes(p.sku)); // nested linear scans
return { ...p, finalPrice: promo ? p.price * (1 - promo.rate) : p.price };
});
}
import pino from 'pino';
import pinoHttp from 'pino-http';
const logger = pino({ level: 'info' });
app.use(pinoHttp({ logger })); // logs req with headers (Authorization, Cookie)
app.post('/login', (req, res) => {
logger.info({ body: req.body }, 'login attempt'); // { email, password }
// ...
});
app.use((err, req, res, next) => {
logger.error({ err, config: err.config }, 'upstream failed'); // axios error: config.headers
res.status(500).end();
});