Mạng cho lập trình viên: TCP, HTTP và chẩn đoán thực chiến

Loạt bài chuyên sâu về networking cho lập trình viên: TCP bắt tay, TCP vs UDP, Nagle, socket buffer, keep-alive, connection pool, TLS handshake, DNS, trạng thái socket, băng thông vs độ trễ, HTTP/2. Mỗi bài đo THẬT bằng Go và công cụ Linux trong container, giải thích cơ chế và nêu đánh đổi.

12/12 phần đã đăng Lập trình
1 TCP bắt tay 3 bước nhìn từ code: vì sao mỗi kết nối mới tốn một vòng khứ hồi Mỗi lần net.Dial hay mở một kết nối HTTP, TCP phải bắt tay 3 bước SYN/SYN-ACK/ACK trước khi gửi được byte dữ liệu đầu tiên — tốn đúng một RTT. Bài này chạy thật trong Go: đo thời gian bắt tay, so kết nối mới mỗi request với tái dùng một kết nối (nhanh hơn 13 lần), và đọc trạng thái socket từ /proc/net/tcp để thấy LISTEN, ESTABLISHED và đống TIME_WAIT. 22/09/2026 · 6 phút đọc 2 TCP vs UDP: đo thật cảnh UDP mất 99,9% gói và vì sao đó không phải lỗi TCP đảm bảo gói tới đủ và đúng thứ tự; UDP bắn và quên, không đảm bảo gì. Bài này chạy thật trong Go: gửi 200.000 gói — TCP nhận đủ 100% đúng thứ tự, còn UDP khi bị bắn dồn dập vào buffer nhỏ mất tới 99,9%. Nhưng UDP nhanh hơn và header chỉ 8 byte. Giải thích vì sao mỗi giao thức đúng cho việc của nó, không cái nào tệ hơn. 22/09/2026 · 6 phút đọc 3 Nagle và TCP_NODELAY: bắt tận tay cú trễ 40ms trốn trong TCP Có một cú trễ đúng 40ms ẩn trong TCP khiến nhiều hệ request-response chậm bí ẩn: thuật toán Nagle gặp delayed ACK. Bài này chạy thật trong Go và tái hiện được ngay trên localhost — mẫu write-write-read với Nagle bật dính 40,9ms mỗi vòng, tắt Nagle chỉ 5µs, chênh gần 9000 lần. Giải thích cơ chế, vì sao Go mặc định tắt Nagle, và khi nào Nagle lại có ích. 22/09/2026 · 6 phút đọc 4 Socket buffer và listen backlog: hai hàng đợi ẩn quyết định TCP nghẽn ở đâu Đằng sau mỗi socket TCP là hai hàng đợi trong kernel ít người để ý: buffer nhận/gửi và hàng đợi backlog. Buffer đầy thì TCP chặn người gửi (flow control, khác UDP mất gói); backlog đầy thì kết nối mới bị rớt lúc tải đỉnh. Bài này chạy thật trong Go: client bị chặn sau 44KB với buffer nhỏ, và mở được đúng 4097 kết nối trước khi backlog đầy (somaxconn=4096). 22/09/2026 · 5 phút đọc 5 HTTP keep-alive: vì sao 500 request chỉ nên mở 1 kết nối, không phải 500 Bài 1 cho thấy bắt tay TCP tốn một RTT mỗi kết nối. Keep-alive giải đúng chuyện đó ở tầng HTTP: giữ kết nối mở để nhiều request dùng chung, bắt tay đúng một lần. Bài này chạy thật trong Go và đếm kết nối TCP thực: 500 request với keep-alive mở đúng 1 kết nối và nhanh hơn 3,9 lần so với tắt keep-alive mở 500 — cùng những cái bẫy vô tình làm mất keep-alive. 22/09/2026 · 5 phút đọc 6 Connection pool: vì sao pool mặc định 2 làm bạn mở 546 kết nối cho 2000 request Keep-alive tái dùng một kết nối cho các request tuần tự, nhưng khi nhiều request chạy song song, mỗi luồng cần một kết nối cùng lúc — đó là việc của connection pool. Bài này chạy thật trong Go: 2000 request song song 50 luồng với pool mặc định (2) mở tới 546 kết nối vì churn, còn pool đủ lớn chỉ mở 50 và nhanh hơn. Giải thích cách chọn kích cỡ pool đúng. 22/09/2026 · 5 phút đọc 7 TLS handshake: cái giá thật của HTTPS đo được, và session resumption cắt nó ra sao HTTPS an toàn nhưng không miễn phí: sau bắt tay TCP, TLS thêm một bắt tay riêng tốn CPU (toán khóa công khai) và một RTT. Bài này chạy thật trong Go với self-signed cert: TLS full handshake tốn 391µs so với TCP thường 23µs — thêm ~368µs, phần lớn là CPU. Session resumption (500/500 kết nối resumed) bỏ phần đắt; giải thích vì sao trên localhost nó chỉ nhanh 1,1 lần còn trên mạng thực tiết kiệm cả một RTT. 22/09/2026 · 6 phút đọc 8 DNS resolution: chuyến đi mạng ẩn xảy ra trước cả bắt tay TCP Trước khi net.Dial kịp bắt tay TCP, có một bước vô hình phải xong: phân giải tên miền thành IP. Bài này chạy thật trong Go: DNS cold tốn 6,97ms (có lúc vọt lên 140ms), cached chỉ 794µs (~9 lần nhanh hơn), còn /etc/hosts thì 1µs không đi mạng. Và httptrace tách được DNS chiếm bao nhiêu trong một request — vì sao DNS chậm/timeout là nguyên nhân 'app chậm' hay bị bỏ sót. 22/09/2026 · 6 phút đọc 9 Trạng thái socket TCP: đọc /proc/net/tcp để phân biệt TIME_WAIT bình thường với CLOSE_WAIT là bug Một kết nối TCP đi qua nhiều trạng thái, và hai trong số đó hay gây sự cố: TIME_WAIT và CLOSE_WAIT. Bài này chạy thật trong Go đọc /proc/net/tcp: mở 3000 kết nối ngắn để lại 3091 TIME_WAIT (ăn cổng cục bộ), còn server quên gọi Close để lại đúng 200 CLOSE_WAIT (rò rỉ fd). Giải thích vì sao TIME_WAIT là bình thường còn CLOSE_WAIT tăng không giảm luôn là bug. 22/09/2026 · 6 phút đọc 10 Băng thông vs độ trễ: vì sao đường 'nhanh' vẫn có thể truyền chậm Băng thông và độ trễ là hai đại lượng khác nhau nhưng hay bị gộp làm 'nhanh'. Bài này chạy thật trong Go: đo throughput 10,5 GB/s và RTT 3µs riêng biệt, tính Bandwidth-Delay Product, và chứng minh cửa sổ nhỏ làm throughput sụt từ 12 GB/s xuống gần 0 — dù băng thông vật lý không đổi. Giải thích vì sao đường dài băng thông cao vẫn chậm nếu cửa sổ nhỏ hơn BDP. 22/09/2026 · 6 phút đọc 11 HTTP/2 multiplexing: 300 request song song trên đúng một kết nối HTTP/1.1 chỉ chạy một request tại một thời điểm trên mỗi kết nối (head-of-line blocking), nên phải mở nhiều kết nối để làm việc song song. HTTP/2 đan xen nhiều stream trên một kết nối. Bài này chạy thật trong Go: 300 request song song qua HTTP/2 mở 0 kết nối mới (dùng lại 1 kết nối), còn HTTP/1.1 mở 221 — và HTTP/2 nhanh gấp gần 4 lần. Kèm lý do vì sao vẫn cần HTTP/3. 22/09/2026 · 6 phút đọc 12 Chẩn đoán mạng: khi 'mạng chậm', tách một request ra là biết ngay lỗi ở tầng nào Khép lại loạt Mạng: khi ai đó nói 'mạng chậm', đừng đoán mò — tách một request thành DNS, TCP, TLS, TTFB rồi nhìn phần nào lớn nhất. Bài này chạy thật bằng httptrace: một request tới coffeecode.vn cho thấy DNS+TCP+TLS chỉ ~8ms nhưng TTFB tận 2015ms — nghĩa là mạng hoàn toàn ổn, server mới là thủ phạm. Kèm cây chẩn đoán triệu chứng → tầng → công cụ nối lại cả 12 phần. 22/09/2026 · 6 phút đọc