GĐ01 — Systems Foundation: OS, Linux, Database Internals & Networking
Backend chạy trên operating system, nói chuyện qua network và lưu dữ liệu bằng database. Giai đoạn này xây mental model đủ dùng để debug production; không nhằm đào tạo kernel, network hay database engineer.
Học song song với GĐ02–05. Mỗi khái niệm phải gắn với một quan sát hoặc thí nghiệm trên máy.
Kiểm chứng ngày 2026-10-05: các thí nghiệm 1 đến 11 ở phần Thực hành đã chạy trên macOS với Node 24.21,
curl8.7.1, driverpg8.23.1 và PostgreSQL 17.9 (một instance riêng khởi tạo trong thư mục tạm, cổng 54410, đã dừng sau khi chạy).server.keepAliveTimeoutBuffercó từ Node 24.6.0 (tài liệuhttp). Chưa xác minh: cột Linux của bảng công cụ debug (không có máy Linux để kiểm),tcpdumpvàdtruss(cần quyền root).
Phần A — Operating System#
1. Process và Thread#
Process có không gian địa chỉ và tài nguyên riêng. Thread chia sẻ bộ nhớ của process nhưng có stack và trạng thái thực thi riêng.
Trong Node.js:
- JavaScript thường chạy trên một main thread.
- libuv dùng thread pool cho một số tác vụ như filesystem, DNS và crypto.
worker_threadsphù hợp CPU-bound JavaScript.child_processtạo process riêng, isolation mạnh hơn nhưng giao tiếp đắt hơn.
Cần trả lời được:
- Vì sao một vòng lặp CPU-bound chặn mọi request trên Node process?
- Khi nào tăng số worker giúp, khi nào chỉ làm database quá tải?
- Process crash ảnh hưởng gì tới memory và connection đang giữ?
2. Scheduling và Context Switch#
OS scheduler chia CPU time cho thread/process có thể chạy. Context switch lưu trạng thái tác vụ hiện tại và khôi phục tác vụ khác.
Nhiều concurrency không đồng nghĩa nhiều throughput:
- Quá nhiều runnable thread làm tăng context switch.
- Quá nhiều request đồng thời làm queue dài và tăng latency.
- CPU utilization cao kéo dài thường làm tail latency tăng mạnh.
Thực hành: chạy một workload CPU-bound bằng một process, nhiều process và worker thread; đo throughput, p95 và CPU thay vì chỉ cảm nhận.
3. Virtual Memory, Stack và Heap#
Virtual memory cho mỗi process một không gian địa chỉ riêng, được OS ánh xạ sang physical memory.
- Stack: call frame, biến cục bộ, lifetime theo lời gọi hàm.
- Heap: object có lifetime linh hoạt, do runtime/garbage collector quản lý.
- RSS: lượng memory process đang giữ trong RAM, không đồng nhất với V8 heap.
- Swap: có thể tránh crash tức thì nhưng latency rất xấu cho server.
Trong Node, cần quan sát:
process.memoryUsage().- Heap growth qua thời gian.
- Object bị giữ tham chiếu ngoài ý muốn.
- Buffer/native memory không xuất hiện đầy đủ trong
heapUsed.
4. System Call và File Descriptor#
Application vào kernel qua system call để đọc file, mở socket, tạo process và thực hiện I/O.
File descriptor là handle số đại diện cho file, socket, pipe và nhiều tài nguyên I/O khác.
Backend có thể cạn file descriptor vì:
- Không đóng file/socket.
- Connection pool đặt quá lớn.
- Client không reuse connection.
- Server giữ quá nhiều connection nhàn rỗi.
Khi gặp EMFILE hoặc too many open files, tăng limit chỉ là chữa triệu chứng nếu code đang leak descriptor.
Phần B — Linux Fundamentals#
5. Filesystem, Permission và User#
Cần nắm:
- Absolute vs relative path.
- File, directory, symbolic link.
- Owner/group/other và quyền read/write/execute.
chmod,chown,umaskở mức dùng được.- Vì sao container/app không nên chạy bằng root.
- Log và uploaded file cần owner/permission phù hợp.
Không commit secret hoặc .env; permission không thay thế secret manager.
6. Process, Signal và Exit Code#
Các signal quan trọng:
SIGTERM: yêu cầu shutdown có kiểm soát.SIGINT: thường từ Ctrl+C.SIGKILL: dừng ngay, process không cleanup được.SIGHUP: truyền thống dùng cho reload terminal/config tuỳ application.
Exit code 0 nghĩa là thành công; khác 0 báo lỗi cho shell, CI và orchestrator.
Backend nhận SIGTERM phải:
- Ngừng nhận request mới.
- Chờ request/job đang chạy trong giới hạn thời gian.
- Đóng DB, queue và telemetry connection sau khi HTTP đã drain xong, không phải trước.
- Thoát với code phù hợp.
server.close() chỉ ngừng nhận kết nối mới và callback chỉ chạy khi mọi kết nối đã đóng, nên phải await nó (kèm hẹn giờ thoát cưỡng bức; từ Node 19 close() đã tự đóng kết nối keep-alive đang rảnh); gọi rồi process.exit(0) ngay sẽ cắt request đang chạy. Mẫu đầy đủ, đã chạy thử, nằm ở GĐ09 mục 18; không chép lại ở đây.
7. Pipe, Redirect và Shell#
Shell là công cụ quan sát production, không chỉ nơi chạy npm start.
Cần dùng được:
- Pipe output giữa các command.
- Redirect stdout/stderr.
- Environment variable theo process.
- Tìm process và port đang lắng nghe.
- Đọc log lớn bằng filter thay vì mở cả file.
- Hiểu quoting để tránh command injection trong script.
Nguyên tắc: production automation cần script có set -euo pipefail, input được quote, exit code rõ và không in secret.
Phần C — Database Internals#
8. Page, B-tree và Index#
Database đọc/ghi theo page, không theo từng field độc lập. B-tree giảm số page cần đọc bằng một cây thấp, nhiều nhánh.
Cần hiểu:
- Index lookup thường gần
O(log n)nhưng cost thật phụ thuộc I/O và cache. - Composite index tuân theo thứ tự cột.
- Index-only scan cần dữ liệu phù hợp và visibility information.
- Nhiều index làm write chậm hơn và tốn storage.
- Low-cardinality column đứng một mình thường không chọn lọc tốt.
Luôn kiểm bằng EXPLAIN (ANALYZE, BUFFERS), không phỏng đoán từ tên index.
9. WAL và Durability#
Write-Ahead Log ghi mô tả thay đổi bền vững trước khi data page được flush.
WAL giúp:
- Recovery sau crash.
- Replication.
- Point-in-time recovery khi kết hợp base backup và archive phù hợp.
COMMIT thành công không nhất thiết nghĩa mọi data page đã được ghi xuống vị trí cuối; nó nghĩa durability contract của database/WAL đã được đáp ứng theo cấu hình.
Sơ đồ: thứ tự ghi của một COMMIT
Sơ đồ khái niệm (suy ra từ tài liệu PostgreSQL về WAL), không phải đo đạc.
Điểm cần nhớ: ghi tuần tự vào WAL rẻ hơn ghi ngẫu nhiên nhiều data page, nên đó là chỗ database chọn để đảm bảo durability. synchronous_commit = off đổi một khoảng mất dữ liệu ngắn lấy độ trễ thấp hơn, không làm hỏng dữ liệu.
10. MVCC và Vacuum#
MVCC cho transaction đọc snapshot mà không chặn mọi writer. PostgreSQL tạo version mới của row khi update thay vì sửa tại chỗ theo cách đơn giản.
Hệ quả:
- Transaction giữ quá lâu giữ lại old row versions.
- Dead tuples làm table/index phình.
- Autovacuum là thành phần correctness và operations quan trọng, không phải housekeeping tuỳ chọn.
- “Không có lock ở application” không nghĩa database không quản lý concurrency.
Sơ đồ: vì sao transaction dài giữ lại dead tuple
Sơ đồ khái niệm; lệnh để tự quan sát nằm ở lời giải thí nghiệm 8 trong "Thực hành bắt buộc".
11. Locking, Isolation và Anomaly#
Cần phân biệt:
- Row lock, table lock, advisory lock.
- Read Committed, Repeatable Read, Serializable.
- Dirty read, non-repeatable read, phantom, lost update, write skew.
- Blocking vs deadlock.
Khi deadlock xảy ra, database huỷ một transaction để phá vòng chờ. Application phải rollback và retry có giới hạn nếu operation an toàn để retry.
Thực hành: mở hai session PostgreSQL, cố ý tạo lost update và deadlock; quan sát lock và sửa bằng transaction/locking phù hợp.
Phần D — Networking sâu hơn#
12. Socket Lifecycle#
Một TCP server đi qua các bước chính:
- Tạo socket.
- Bind địa chỉ/port.
- Listen.
- Accept connection.
- Read/write.
- Close.
TCP connection được nhận diện bởi source IP/port và destination IP/port. Sau khi đóng, trạng thái như TIME_WAIT tồn tại để xử lý packet trễ và tránh nhầm connection cũ.
Sơ đồ: vòng đời socket phía server và client
Trong Node, server.listen() gồm socket + bind + listen, còn accept do libuv làm và đưa ra sự kiện connection. Lỗi EADDRINUSE là bước bind thất bại vì process khác đang giữ port; EMFILE là hết file descriptor khi mở socket mới. Quan sát trạng thái: lsof -nP -iTCP:<port> (macOS) hoặc ss -tan (Linux).
13. Connection Pool và Connection Exhaustion#
Mỗi connection có chi phí ở client, server, proxy và database.
Nếu mỗi application instance mở pool 20 connection và scale lên 20 instance, database có thể nhận tới 400 connection chưa tính worker và migration.
Nguyên tắc:
- Budget connection trên toàn hệ thống, không riêng từng process.
- Queue có giới hạn tốt hơn mở connection vô hạn.
- Đặt acquire timeout.
- Theo dõi active, idle, waiting và saturation.
- Scale app phải xem lại DB pool.
Lời giải: tính connection budget
Công thức: tổng kết nối = số process × pool mỗi process, cộng thêm phần dành cho thứ không phải app. Ví dụ với max_connections = 100 (giá trị mẫu, đọc giá trị thật bằng SHOW max_connections):
Khi cạn: giảm pool, đặt PgBouncer ở chế độ transaction pooling, hoặc tách đọc/ghi. Trong lúc rolling deploy, instance cũ và mới tồn tại cùng lúc nên phải tính cả hai. Số 100 và ví dụ trên chỉ để minh hoạ phép tính.
14. Keep-Alive và Timeout Budget#
Keep-alive tái sử dụng connection, giảm chi phí TCP/TLS handshake. Nhưng connection nhàn rỗi cũng giữ tài nguyên.
Các timeout phải phối hợp:
- Client request timeout.
- Load balancer/proxy timeout.
- Server timeout.
- Database acquire/query timeout.
- Upstream API timeout.
Timeout ngoài nên lớn hơn timeout trong vừa đủ để tầng trong có cơ hội fail rõ ràng; không đặt mọi tầng cùng một con số.
Sơ đồ: timeout lồng nhau
Các con số là ví dụ minh hoạ, không phải khuyến nghị cố định.
Giá trị mặc định và mốc phiên bản của Node (keepAliveTimeout 5 s) xem GĐ03 mục 12a và GĐ15. Thí nghiệm 9 ở phần Thực hành chứng minh cuộc đua này bằng số liệu.
15. HTTP/1.1, HTTP/2 và HTTP/3#
HTTP/1.1#
- Nhiều request có thể reuse connection với keep-alive.
- Browser thường mở nhiều connection để tăng song song.
- Pipelining ít được dùng rộng rãi vì head-of-line blocking và complexity.
HTTP/2#
- Multiplex nhiều stream trên một TCP connection.
- Header compression.
- TCP packet loss vẫn có thể ảnh hưởng mọi stream trên connection.
HTTP/3#
- Chạy trên QUIC/UDP.
- Stream độc lập hơn khi packet loss.
- Connection migration hữu ích khi client đổi mạng.
- Deployment, proxy và observability phức tạp hơn.
Đối với application engineer, hiểu trade-off và kiểm tra support của CDN/load balancer là đủ; không cần tự triển khai protocol.
Thực hành bắt buộc#
-
Viết Node server xử lý
SIGTERMvà chứng minh request đang chạy hoàn tất: tạo route/slowmất vài giây,curlnó, gửikill -TERM <pid>giữa chừng, và kiểm tracurlvẫn nhận đủ body (exit code 0) rồi process mới thoát. Dùng mẫu ở GĐ09 mục 18.Lời giải và cách kiểm tra
Hướng làm: route
/slowmất 3 giây; trình xử lý signal đặt cờ, hẹn giờ thoát cưỡng bức,awaitserver.close(), rồi mới đóng dependency vàexit(0)(mẫu đầy đủ ở GĐ09 mục 18).typescriptReadybashReadyKết quả mong đợi:
curlindone,curl exit=0; server logSIGTERM: drainingrồiclosed,server exit=0, và process thoát sau khi request xong (khoảng 2 giây saukill, không phải ngay). Đối chứng (bỏawait, gọiserver.close(); process.exit(0)): request bị cắt,curlin lỗi "Empty reply from server" vàcurl exit=52. Đã chạy (Node 24.21, macOS):curlindone,curl exit=0; server logSIGTERM: draining,closed,server exit=0sau 2 giây. Bản đối chứngserver.close()rồi thoát ngay:curl: (52) Empty reply from server,curl exit=52, server thoát ngay (0 giây). Lỗi hay gặp:SIGINT+SIGTERMcùng đến gọishutdownhai lần (đó là lý do có cờshuttingDown); chạy quash -choặc shell script làm PID 1 trong container thì signal đến shell chứ không đếnnode(dùng dạng exec củaCMDhoặcexec node ...; chạynpm startcũng thêm một tầng trung gian nên nên chạynodetrực tiếp). -
Quan sát PID, port, open file/socket và memory của server.
Lời giải và cách kiểm tra
Hướng làm: chạy server (thí nghiệm 1) rồi đọc từ bên ngoài, sau đó so sánh với số liệu bên trong process.
bashReadytypescriptReadyKết quả mong đợi: sau mỗi lần gọi
/mem,arrayBufferstăng khoảng 200 MB cònheapUsedgần như không đổi (Buffer nằm ngoài V8 heap); đó là lý do Done khi đòi phân biệt heap, native memory và RSS. Đểrsstăng đúng bằng đó, ghi vào buffer (ví dụBuffer.alloc(n).fill(1)), vì OS thường chỉ cấp page vật lý khi page được chạm tới. Để thấy leak descriptor, mở 200 kết nối rồi giữ:for i in $(seq 200); do nc 127.0.0.1 4410 & done, đếm lạilsof -p "$PID" | wc -lsẽ tăng khoảng 200; đóngncthì số giảm. Chạmulimit -nthì mở thêm descriptor lỗiEMFILE; thử trên một process con (shđặt cả soft lẫn hard limit):bashReadyĐã chạy: in
EMFILE 245(256 trừ khoảng chục descriptor có sẵn của chính Node). Với server chịu limit 256 và 400 kết nối cùng lúc, server không ném lỗi hay phát sự kiệnerror: kernel trảEMFILEchoaccept, libuv bỏ kết nối thừa, và phía client thấy khoảng 150 trên 400 kết nối bị đóng ngay. Hai điểm cần biết: trong zsh,ulimit -n 64chỉ hạ soft limit và Node nâng soft limit lên bằng hard limit khi khởi động (đã thấy mở được hơn 184 000 descriptor, xấp xỉsysctl kern.maxfilesperproc= 184320), nên dùngsh -choặc đặt cảulimit -Hn; limit quá thấp (ví dụ 64) thì Node không khởi động nổi (kqueue(): Too many open files). Đã đo thêm với server có route/mem: hai lần gọi choarrayBuffers200 rồi 400 MB,heapUsedquanh 6 đến 7 MB,rss262 rồi 463 MB (cófill(1)); 200 kết nốincđưalsof -p "$PID" | wc -ltừ 22 lên 222, đóngncthì về 22. Lỗi hay gặp: nângulimit -nmà không tìm leak. -
Cố ý tạo CPU-bound route, đo ảnh hưởng lên request khác, sau đó chuyển sang worker thread.
Lời giải và cách kiểm tra
typescriptReadybashReadyKết quả mong đợi:
/pingkhi/cpuchạy phải chờ gần hết thời gian củafib(40)(cỡ giây, tuỳ máy); khi dùng/cpu-workerthì/pingtrả trong vài ms. Để so sánh một process, nhiều process, worker thread, ghi mỗi cấu hình ba số (throughput, p95, CPU) bằng cùng một công cụ tải; throughput chỉ tăng đến khoảng số core rồi phẳng, p95 xấu đi khi số tác vụ chạy được vượt số core. Đã chạy:ping while /cpu0,45 đến 0,51 giây (máy 12 core;fib(40)chạy khoảng 0,5 giây nên chờ gần trọn thời gian đó),ping while /cpu-worker0,0007 giây. Lỗi hay gặp:new Workermỗi request không nên dùng ngoài thí nghiệm (dùng worker pool, xem GĐ03 mục 10). -
Benchmark HTTP request với keep-alive bật và tắt.
Lời giải và cách kiểm tra
typescriptReadyKết quả mong đợi:
keepAlive=truenhanh hơnfalse, nhưng chênh lệch trên loopback nhỏ vì không có latency mạng hay TLS; chênh lệch lớn hơn khi gọi qua mạng thật hoặc HTTPS (mỗi kết nối mới tốn thêm round-trip TCP và TLS). Kiểm chứng cơ chế bằng đếm kết nối: sau lầnfalse,netstat -an | grep 4410 | grep -c TIME_WAITphải ra cỡ hàng nghìn dòng (mỗi request một kết nối đã đóng, macOS và Linux), còn lầntruechỉ dùng một socket. Đã chạy:keepAlive=true110 ms,keepAlive=false208 ms cho 2 000 request (loopback, chênh khoảng 2 lần, đúng nhận định chênh nhỏ ở đây); sau lầnfalse,netstat -an | grep 4410 | grep -c TIME_WAITra 2002 dòng (2000 kết nối của lượtfalsecộng 2 socket keep-alive của hai lượttruemàagent.destroy()đóng;TIME_WAITchỉ nằm ở phía chủ động đóng, ở đây là client: cả 2002 dòng có cột foreign là cổng 4410, không dòng nào ở đầu server; mỗi kết nối hiện một lần, không phải hai; chạy lại nhiều lần liên tiếp trong cửa sổTIME_WAITthì số dòng cộng dồn). Lỗi hay gặp: benchmark mà không khởi động nóng (code trên chạy trước một lượt 200 request và bỏ dòng log đầu), bản chạy đầu chịu chi phí JIT. -
Làm cạn một connection pool có giới hạn, quan sát waiting và timeout.
Lời giải và cách kiểm tra
typescriptReadyKết quả mong đợi: tại 500 ms thấy
{ total: 3, idle: 0, waiting: 7 }; 3 query đầu hoàn tất sau khoảng 2000 ms; 7 query còn lại bị từ chối sau khoảng 1000 ms vớitimeout exceeded when trying to connect(vì acquire timeout 1 s ngắn hơn thời gian chờ 2 s để có slot). ĐặtconnectionTimeoutMillisđủ lớn (từ 7000 trở lên, ví dụ 10000) thì cả 10 query đều thành công theo bốn đợt (3, 3, 3, 1): query thứ 10 phải chờ khoảng 6 s, nên 5000 vẫn timeout. Bài học:waitingtăng là tín hiệu bão hoà, và pool không có timeout thì request treo vô hạn (tham sốconnectionTimeoutMillismặc định là0tức không giới hạn, theo tài liệunode-postgres). Đã chạy (pg8.23.1, PostgreSQL 17.9, cổng 54410): tại 500 ms in{ total: 3, idle: 0, waiting: 7 }; với 1000 ms bảy query lỗitimeout exceeded when trying to connectsau 1002 đến 1003 ms, ba query còn lại xong ở 2007 ms; với 5000 ms query thứ 10 lỗi ở 5002 ms trong khi các query khác xong ở 2, 4 và 6 giây; với 10000 ms cả 10 query xong, query cuối ở 8012 ms. Lỗi hay gặp: quênpool.end()nên script không thoát. -
Tạo index PostgreSQL, so sánh
EXPLAIN (ANALYZE, BUFFERS)trước/sau.Lời giải và cách kiểm tra
textReadyKết quả mong đợi: trước đây là
Seq Scan on orders(với bảng này planner chọnParallel Seq ScandướiGather) kèmFiltervàRows Removed by Filterxấp xỉ cả bảng,Buffersđọc gần toàn bộ page của bảng; sau làIndex ScanhoặcBitmap Heap Scantrênorders_customer_id_idxvớiBuffersnhỏ hơn rất nhiều vàExecution Timegiảm tương ứng (khoảng 10 row mỗicustomer_id). Cách đọc: soactual rowsvớirows(ước lượng), đọcBuffers: shared hit(trong cache) vàread(từ đĩa). Thử thêmWHERE customer_id < 90000(không chọn lọc) để thấy planner vẫn chọnSeq Scan: index không luôn thắng. Đã chạy (PostgreSQL 17.9, 1 triệu dòng): trước làParallel Seq Scan11,96 ms,Buffers: shared hit=2304 read=3136; sau làBitmap Index ScankèmBitmap Heap Scan0,053 ms,Buffers: shared hit=6 read=7;customer_id < 90000vẫnSeq Scan48,8 ms. Lỗi hay gặp: quênANALYZEnên thống kê cũ; dùngEXPLAINkhôngANALYZE(chỉ là ước lượng, không chạy thật); chạyEXPLAIN ANALYZEvớiUPDATE/DELETEthật (nó thực thi, bọc trongBEGIN ... ROLLBACK). -
Tạo deadlock bằng hai transaction và ghi lại thứ tự lock đúng để tránh nó.
Lời giải và cách kiểm tra
textReadyDeadlock: mở hai terminal
psql, chạy theo thứ tự thời gian.textReadyBên bị huỷ phải
ROLLBACK; bên còn lại hoàn tất. Cách sửa: mọi transaction khoá theo cùng một thứ tự, ví dụ luôn khoá id nhỏ trước:SELECT id FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;rồi mớiUPDATE. Chạy lại: một bên chờ bên kia commit, không còn deadlock. Application bắt40P01và retry có giới hạn (chỉ khi thao tác an toàn để lặp).Lost update (mức mặc định Read Committed): A và B cùng đọc
balance = 100ở app, A ghi90, B ghi80; kết quả80nhưng đúng phải là70.textReadyBa cách sửa, chọn theo ngữ cảnh: (1) cập nhật nguyên tử
SET balance = balance - 10(không đọc rồi ghi ở app); (2)SELECT ... FOR UPDATEđể B chờ A rồi đọc giá trị mới; (3)REPEATABLE READthì B bị lỗicould not serialize access due to concurrent update(SQLSTATE40001) và app phải retry. Đã chạy (PostgreSQL 17.9, hai tiến trìnhpsqlđiều phối bằngsleep): deadlock raERROR: deadlock detectedkèmDETAIL: Process X waits for ShareLock on transaction ...; blocked by process Y, bên bị huỷROLLBACK, bên còn lại commit (số dư 90 và 90). Với khoá có thứ tự (ORDER BY id FOR UPDATE) cả hai commit sau khoảng 2 giây chờ, số dư 80 và 80, không có deadlock. Lost update: đọc rồi ghi giá trị tuyệt đối ra80, cập nhật nguyên tử ra70;FOR UPDATEkhiến B đọc90sau khi A commit;REPEATABLE READchoERROR: could not serialize access due to concurrent update. Lỗi hay gặp: nghĩ "có transaction là đủ"; transaction ở Read Committed không chặn kiểu đọc rồi ghi này. -
Quan sát một long-running transaction ảnh hưởng vacuum/dead tuples trong môi trường local.
Lời giải và cách kiểm tra
textReadyKết quả mong đợi: lần
VACUUM VERBOSEđầu (A còn mở) báo gần 0 tuple bị loại bỏ và một số lớn "dead but not yet removable";pg_stat_activitycho thấy session A cóxact_startcũ vàbackend_xminđược đặt; sau khi ACOMMIT, lầnVACUUMthứ hai loại bỏ khoảng 200000 tuple. Câu chữ chính xác của log thay đổi theo phiên bản, hãy tìm cụm "dead but not yet removable". Với session ở trạng tháiidle in transactionkéo dài, đặtidle_in_transaction_session_timeoutở server local để thấy nó bị ngắt. Đã chạy (PostgreSQL 17.9): lầnVACUUMđầu intuples: 0 removed, 400000 remain, 200000 are dead but not yet removable,n_dead_tupbằng 200000, session A hiệnidle in transactionvớixact_startvàbackend_xminđã đặt; sauCOMMITcủa A, lần hai intuples: 200000 removed, 200000 remain, 0 are dead but not yet removablevàn_dead_tupvề 0. Lỗi hay gặp: dùngSELECTtrong transaction rồi quênCOMMITtrong ứng dụng (đặc biệt khiawaitgọi API ngoài giữa transaction); mở transaction chỉ để gọi dịch vụ ngoài. -
Chứng minh cuộc đua giữa keep-alive của server và idle timeout của load balancer (nguồn của
502rải rác).Lời giải và cách kiểm tra
Hướng làm: server Node có
keepAliveTimeout200 ms; một client TCP thô đóng vai load balancer (không đọc gợi ýKeep-Alivecủa server), dùng lại kết nối sau một khoảng rảnh, lặp 100 lần và đếm kết quả.keepAliveTimeoutBuffer(Node 24.6+, mặc định 1000 ms) được cộng vàokeepAliveTimeout; đặt về 0 để thấy cơ chế trần trụi. Server thật đóng kết nối rảnh saukeepAliveTimeout + buffer.typescriptReadybashReadyĐã chạy (Node 24.21, 100 lần mỗi dòng; số lần thay đổi mỗi lượt, nhưng hình dạng giữ nguyên):
textReadyCách đọc: lỗi (
ECONNRESET, hoặc kết nối bị đóng không có phản hồi) chỉ xuất hiện khi LB dùng lại kết nối đúng quanh lúc server đóng, vì FIN của server còn trên đường đi trong khi LB đã ghi request. Dòng thứ hai ra 100 trên 100okchỉ vì LB (socket thô, không đọc gợi ýKeep-Alive) dùng lại ở 200 ms, còn xa mốc đóng thật 1200 ms. Theo tài liệu Node, buffer mặc định 1000 ms tồn tại để client có đọc gợi ýKeep-Alive: timeoutrời đi trước khi server đóng; thí nghiệm này không đo điều đó. Buffer không xoá cuộc đua với LB dùng lại đúng mốc đóng (dòng thứ ba). Cách xử lý thật: idle timeout của server phải lớn hơn của LB phía trước (LB 60 giây thì server 65 giây), xem mục 14 và GĐ03 mục 12a. Client Node (http.AgentvớikeepAlive) đọc gợi ý của server nên không tự gặp cuộc đua này: bản thử vớihttp.getcho 100 trên 100ok, đó là lý do thí nghiệm dùng socket thô. Lỗi hay gặp: đo trên localhost bằng một client có tôn trọng gợi ý rồi kết luận "không có vấn đề"; đặtkeepAliveTimeoutcủa Node nhỏ hơn idle timeout của LB. -
Tách thời gian của một request bằng
curl -w: DNS, connect, TLS, time-to-first-byte, tổng.Lời giải và cách kiểm tra
Hướng làm: chạy server của thí nghiệm 1 (route
/slowmất 3 giây) rồi in từng mốc thời gian màcurlđo.bashReadyĐã chạy (curl 8.7.1):
dns 0.000016s | connect 0.000489s | tls 0.000000s | ttfb 3.007122s | total 3.007177s. Lần gọi hai trong lệnh thứ hai inconnect=0.000000svànum_connects=0: kết nối được dùng lại. Cách đọc:connect - dnslà bắt tay TCP;tls - connectlà bắt tay TLS (bằng 0 ở đây vì HTTP thường, chưa chạy với HTTPS);ttfb - tls(hoặcttfb - connect) là thời gian server xử lý cộng một chiều mạng;total - ttfblà tải body. Ở ví dụ gần như toàn bộ 3 giây nằm ởttfb, nên lỗi ở server chứ không phải mạng. Lỗi hay gặp: lấytime_totalrồi kết luận mạng chậm; quên rằng mốctime_*là thời gian cộng dồn từ đầu chứ không phải từng pha. -
Tìm ai đang chặn ai trong PostgreSQL:
pg_stat_activity,pg_blocking_pidsvàpg_locks.Lời giải và cách kiểm tra
Hướng làm: session A mở transaction, cập nhật một dòng và giữ; session B cập nhật cùng dòng nên bị chặn; session thứ ba chạy truy vấn bên dưới (dùng bảng
accountscủa thí nghiệm 7).textReadyĐã chạy (PostgreSQL 17.9): truy vấn đầu trả đúng một dòng, session B với
blocked_by = {pidA},state = active,wait_event_type = Lock,wait_event = transactionid;pg_lockscho dòngtransactionid | ShareLock | fcủa B; truy vấn thứ ba cho thấy A ở trạng tháiidle in transactionvớixact_agehai giây và câu lệnh cuối làUPDATE. Cách đọc: người gây chặn thường không "đang chạy" gì cả, chỉ là một transaction mở quênCOMMIT; xử lý bằng cách sửa ứng dụng, đặtidle_in_transaction_session_timeout, và chỉ khi khẩn cấp mớiSELECT pg_terminate_backend(pidA). Lỗi hay gặp: chỉ nhìnpg_stat_activitycủa session bị chặn rồi không tìm ra thủ phạm; dùngpg_locksmà không nối được vềpid(hãy bắt đầu từpg_blocking_pids).
Khung và mã dùng chung
Tự làm trước, rồi mới mở. Các thí nghiệm 1 đến 11 đã được chạy thật (xem dòng "Đã chạy" trong từng lời giải); riêng những chỗ ghi "chưa chạy" (TLS của curl, tcpdump, dtruss, cột Linux) và mọi mục "mong đợi" không có dòng "Đã chạy" là suy ra từ code và tài liệu Node/PostgreSQL. Số đo của bạn sẽ khác; hãy ghi số đo của máy bạn vào sổ thí nghiệm. Ghi chú môi trường: lệnh lsof/ps viết cho macOS, kèm tương đương Linux; PostgreSQL nên chạy trên một instance local riêng cho học tập (không dùng database chung), Node 24, driver pg. Cổng ví dụ 4410 cho HTTP.
Sổ thí nghiệm (cho dòng cuối của Done khi): mỗi thí nghiệm lưu lệnh, output và một câu kết luận trong thư mục của bạn.
Bảng công cụ debug: macOS và Linux#
Cột macOS là lệnh đã chạy được trên máy kiểm chứng; cột Linux viết theo tài liệu thông dụng và chưa kiểm trên máy này.
| Triệu chứng | macOS | Linux |
|---|---|---|
Ai đang giữ port (EADDRINUSE) | lsof -nP -iTCP:PORT -sTCP:LISTEN | ss -ltnp |
Trạng thái kết nối (TIME_WAIT, ESTABLISHED) | netstat -an -p tcp | grep PORT | ss -tan |
| Số file descriptor và giới hạn | lsof -p PID | wc -l; ulimit -n; launchctl limit maxfiles | ls /proc/PID/fd | wc -l; cat /proc/PID/limits |
| Bộ nhớ của process | ps -o pid,rss,vsz,command -p PID; vm_stat | grep VmRSS /proc/PID/status; free -m |
| CPU toàn máy, process nào nóng | top -l 1 -n 0 | top -H -p PID |
| Lấy mẫu call stack đang chạy | sample PID 5 | perf top -p PID |
| Xem system call | sudo dtruss -p PID (cần root, chưa chạy) | strace -f -p PID |
| DNS có phân giải không | dig +short HOST; scutil --dns | dig +short HOST; resolvectl status |
| Cổng có mở không | nc -vz HOST PORT | nc -vz HOST PORT |
| Băng thông theo process | nettop -P | ss -tin |
| Bắt gói tin | sudo tcpdump -i lo0 port PORT (chưa chạy) | sudo tcpdump -i lo port PORT |
| Thời gian từng pha của request | curl -w (thí nghiệm 10) | curl -w |
vmstat, strace và ss không có sẵn trên macOS; thay bằng vm_stat, dtruss/sample và netstat/lsof.
Done khi#
-
Giải thích được process, thread, event loop và worker thread khác nhau thế nào.
Đáp án
Process có không gian địa chỉ riêng; thread chia sẻ bộ nhớ trong một process; event loop là vòng lặp trên MỘT thread JS, chạy callback khi I/O sẵn sàng;
worker_threadsthêm thread JS có event loop riêng, chia sẻ bộ nhớ có kiểm soát. Hệ quả: vòng lặp CPU-bound trên main thread chặn mọi request; worker thread giúp CPU-bound, không giúp thêm cho việc chờ I/O. Xem GĐ01 mục 1 và thí nghiệm 3. -
Phân biệt stack, V8 heap, native memory và RSS.
Đáp án
Stack: call frame, sống theo lời gọi. V8 heap: object do GC quản lý (
heapUsed). Native memory: Buffer, thư viện C++ (external,arrayBuffers) nằm ngoài heap. RSS: phần RAM thật process đang giữ, gồm tất cả cộng code và overhead. Cách tự kiểm: thí nghiệm 2 phải cho thấy cấp phát Buffer làmarrayBufferstăng màheapUsedkhông đổi. Sai thường gặp: đọcheapUsedrồi kết luận không rò rỉ bộ nhớ. Xem mục 3. -
Chẩn đoán được process giữ port hoặc cạn file descriptor.
Đáp án
Dùng
lsof -nP -iTCP:<port> -sTCP:LISTEN(hoặcss -ltnp) để biết PID giữ port (lỗiEADDRINUSE);lsof -p <pid> | wc -lhoặcls /proc/<pid>/fd | wc -lđể đếm descriptor và theo dõi xu hướng.EMFILEmà số descriptor tăng đều thì là leak; nângulimit -nchỉ hoãn. Xem mục 4. -
Dùng được signal để graceful shutdown.
Đáp án
Bắt
SIGTERM/SIGINT, đặt cờ chống gọi lặp, ngừng nhận kết nối mới,await server.close(), đóng dependency sau cùng, hẹn giờ thoát cưỡng bức nhỏ hơn grace period. Cách tự kiểm: thí nghiệm 1,curlgiữa chừng nhận đủ body,exit=0và server thoát sau request. Xem mục 6 và GĐ09 mục 18. -
Đọc được permission Linux và không chạy application bằng root khi không cần.
Đáp án
Đọc
-rw-r-----thành owner đọc ghi, group đọc, other không có;ls -l,id,chmod,chown. App không cần root vì lỗi trong app (hoặc thư viện) sẽ có quyền root trên container/host; chỉ cần quyền đọc code và ghi đúng thư mục log/upload. Cách tự kiểm:ps -o user= -p <pid>không raroot; trong container,USER nodetrong Dockerfile. Xem mục 5. -
Giải thích B-tree, WAL và MVCC bằng ngôn ngữ của mình.
Đáp án
B-tree: cây thấp nhiều nhánh theo page để mỗi lookup chỉ đọc vài page. WAL: ghi mô tả thay đổi xuống đĩa trước khi data page được ghi xuống đĩa, nên crash thì phát lại được và COMMIT trả OK khi WAL đã bền. MVCC: update tạo phiên bản row mới, reader dùng snapshot không chặn writer; phiên bản cũ do VACUUM dọn khi không còn transaction nào cần. Tự kiểm: giải thích được với ví dụ ở thí nghiệm 6 và 8. Xem mục 8 đến 10 (có sơ đồ).
-
Tạo và sửa được deadlock/lost update trong PostgreSQL local.
Đáp án
Deadlock: hai transaction khoá chéo nhau; PostgreSQL huỷ một bên với
40P01; sửa bằng khoá theo thứ tự nhất quán và retry có giới hạn. Lost update: đọc rồi ghi ở app dưới Read Committed; sửa bằngSET x = x - n,FOR UPDATE, hoặcREPEATABLE READkèm retry40001. Cách tự kiểm: thí nghiệm 7 tái hiện được rồi hết lỗi sau khi sửa. Xem mục 11. -
Tính được connection budget khi application scale nhiều instance.
Đáp án
process × poolcho mọi loại process (API, worker, migration, cron), cộng phần dành riêng, so vớimax_connections; tính cả rolling deploy khi hai thế hệ instance cùng chạy. Ví dụ: 20 instance × pool 20 = 400, vượt hẳn cấu hìnhmax_connectionsthông thường. Xem tính toán trong mục 13. -
Giải thích keep-alive và timeout budget.
Đáp án
Keep-alive tái dùng kết nối, bỏ chi phí bắt tay TCP/TLS nhưng giữ tài nguyên khi nhàn rỗi. Timeout ngoài lớn hơn timeout trong vừa đủ để tầng trong fail rõ ràng; keep-alive timeout của server lớn hơn idle timeout của LB phía trước. Sai thường gặp: mọi tầng cùng 30 giây. Xem mục 14 (có sơ đồ).
-
So sánh HTTP/1.1, HTTP/2 và HTTP/3 theo vấn đề chúng giải quyết.
Đáp án
HTTP/1.1: một request tại một thời điểm trên một kết nối, nên browser mở nhiều kết nối. HTTP/2: nhiều stream trên một kết nối TCP, nhưng mất một packet làm đứng cả kết nối (head-of-line ở tầng TCP). HTTP/3: chạy trên QUIC/UDP, stream độc lập khi mất packet, kết nối sống sót khi đổi mạng. Xem mục 15.
-
Chứng minh được cuộc đua keep-alive với load balancer và tìm ra transaction đang chặn người khác.
Đáp án
Cuộc đua: server đóng kết nối rảnh đúng lúc LB dùng lại nó, nên LB ghi vào kết nối chết và trả
502; phòng bằng idle timeout của server lớn hơn của LB. Chứng minh bằng thí nghiệm 9 (tỉ lệ lỗi khác 0 khikeepAliveTimeoutBufferbằng 0 và LB dùng lại gần mốc đóng, bằng 0 khi LB dùng lại sớm hơn mốc đó). Tìm người chặn:pg_blocking_pids(pid)trongpg_stat_activity, rồi xem transactionidle in transactioncủa người gây chặn (thí nghiệm 11). Xem mục 14 và mục 11. -
Hoàn thành tám thí nghiệm và lưu code/kết quả đo.
Đáp án
Mỗi thí nghiệm có thư mục riêng chứa code, lệnh chạy, output thật và một câu kết luận; số của bạn sẽ khác số mong đợi ở khối lời giải, và điều đó bình thường. Đạt khi người khác chạy lại theo ghi chú của bạn và ra kết quả cùng dạng.
Câu hỏi mở / chưa giải quyết#
-
Production target chính là PaaS, container trên VPS hay Kubernetes?
Hướng trả lời hiện tại
Chưa chốt: nên học nền ở đây theo hướng độc lập với nền tảng, rồi quyết định target ở GĐ15 và GĐ18; với một sản phẩm AI SaaS giai đoạn đầu, PaaS hoặc container đơn giản thường đủ, còn Kubernetes chỉ cần khi có nhiều service.
-
Database managed đang dùng giới hạn connection và backup/WAL như thế nào?
Hướng trả lời hiện tại
Chưa chốt: giới hạn connection và chính sách backup/WAL của database managed phụ thuộc nhà cung cấp và gói; đọc trang tài liệu của nhà cung cấp đang cân nhắc và ghi số đó vào bảng connection budget ở mục 13.