Bài trước nhắc redirect tốn "một vòng khứ hồi". Bài này đào vào chính cái giá đó: mỗi khi client mở một kết nối mới tới server, có một chuỗi bắt tay xảy ra trước khi một byte dữ liệu nào được gửi. Với HTTP/1.0 nguyên thủy, mỗi request mở một kết nối rồi đóng — trả giá handshake mỗi lần. HTTP/1.1 sửa điều đó bằng keep-alive: giữ kết nối mở để tái dùng. Nghe nhỏ, nhưng đo thật cho thấy khác biệt tới gần 20 lần. Ta đo bằng curl 7.88 và một socket đếm kết nối.

Cái giá của một kết nối mới

Trước khi gửi request, một kết nối HTTPS mới phải qua:

  1. DNS: tra tên miền → IP (nếu chưa cache).
  2. Bắt tay TCP ba bước (SYN → SYN-ACK → ACK): tốn một vòng khứ hồi (RTT).
  3. Bắt tay TLS (nếu HTTPS): thêm 1–2 RTT nữa để thỏa thuận khóa (chi tiết ở bài TLS sau).

Chỉ sau toàn bộ chuỗi đó, request mới được gửi và byte dữ liệu đầu tiên mới về. Trên mạng có độ trễ, mỗi RTT là hàng chục tới hàng trăm mili-giây — nhân lên cho mỗi kết nối mới.

curl -o /dev/null -w \
  "DNS=%{time_namelookup} TCP=%{time_connect} TLS=%{time_appconnect} first_byte=%{time_starttransfer}" URL
# time_connect - time_namelookup = chi phí TCP
# time_appconnect - time_connect  = chi phí TLS

Ảnh chụp đoạn mã nền tối giải thích kết nối bền vì sao mở lại TCP cho mỗi request là lãng phí, chi phí mở một kết nối trước khi gửi được byte dữ liệu nào mỗi kết nối mới phải qua nhiều vòng khứ hồi RTT gồm DNS tra tên miền ra IP, TCP bắt tay ba bước SYN SYN-ACK ACK bằng một RTT, TLS bắt tay nếu HTTPS bằng một tới hai RTT chỉ sau đó mới gửi request và nhận byte đầu tiên, keep-alive tái dùng một kết nối cho nhiều request HTTP/1.1 mặc định Connection keep-alive giữ socket mở request sau bỏ qua DNS TCP TLS gửi thẳng nhanh hơn nhiều, curl URL1 URL2 URL3 một lệnh nhiều URL curl tái dùng socket, curl -H Connection close ép đóng mỗi request một kết nối mới, đo bằng curl -w thấy từng mốc thời gian time_namelookup time_connect time_appconnect time_starttransfer

Hình 1: Chuỗi bắt tay của một kết nối mới (DNS → TCP → TLS) diễn ra trước mọi byte dữ liệu. keep-alive tái dùng kết nối để các request sau bỏ qua chuỗi này; curl -w cho đo từng mốc.

Đo thật: bắt tay tốn bao nhiêu

Gọi curl -w tới một host thật trên Internet để thấy từng mốc:

Ảnh chụp bảng kết quả đo thật nền tối chạy curl 7.88 và socket, phần một chi phí bắt tay thật curl -w tới một host DNS bằng 0.011s TCP bằng 0.038s TLS bằng 0.069s first_byte bằng 0.128s TCP tốn khoảng 27ms bằng 0.038 trừ 0.011 TLS thêm khoảng 31ms bằng 0.069 trừ 0.038 khoảng 69ms trôi qua trước khi gửi request mỗi kết nối mới trả lại từ đầu, phần hai số kết nối TCP 6 request keep-alive vs close keep-alive một curl 6 URL server thấy 1 kết nối TCP Connection close 6 lần server thấy 6 kết nối TCP cùng 6 request nhưng số lần bắt tay chênh 6 lần, phần ba wall-clock 300 request lên localhost keep-alive tái dùng 0.038s một kết nối mỗi request một kết nối 0.737s 300 kết nối chậm gấp khoảng 19 lần ngay cả trên localhost gần như không độ trễ trên mạng thật mỗi handshake bằng một cộng RTT khoảng cách còn lớn hơn nhiều

Hình 2: Handshake thật — TCP tốn ~27ms, TLS thêm ~31ms, tổng ~69ms trước byte đầu. 6 request keep-alive chỉ mở 1 kết nối TCP (so với 6 khi Connection: close). Và 300 request tái dùng nhanh gấp ~19 lần.

Ba lớp bằng chứng, đều đo thật:

  • Handshake tốn thật: tới một host Internet, time_connect (TCP) ≈ 38ms, time_appconnect (TLS xong) ≈ 69ms — nghĩa là ~69ms trôi qua trước khi request được gửi. Với mỗi kết nối mới, khoản này trả lại từ đầu.
  • keep-alive giảm số kết nối: một server đếm kết nối TCP cho thấy 6 request qua một lệnh curl (tái dùng) chỉ mở 1 kết nối; còn gửi 6 lần với Connection: close mở 6 kết nối — tức 6 lần bắt tay.
  • Khác biệt thời gian rõ rệt: 300 request lên localhost, keep-alive mất 0.038s (1 kết nối), còn mở mới mỗi lần mất 0.737s (300 kết nối) — chậm gấp ~19 lần, ngay cả khi localhost gần như không có độ trễ mạng. Trên mạng thật, mỗi handshake là 1+ RTT nên khoảng cách còn lớn hơn nhiều.

Con số 19 lần trên localhost là chặn dưới — nó chỉ phản ánh chi phí tạo/hủy socket và syscall, chưa tính RTT. Đây là lý do mọi HTTP client nghiêm túc (thư viện, trình duyệt, reverse proxy) đều dùng connection pool: giữ sẵn vài kết nối mở để tái dùng thay vì mở mới liên tục.

keep-alive hoạt động thế nào

Trong HTTP/1.1, keep-alive là mặc định — kết nối được giữ mở sau mỗi response trừ khi có Connection: close. Client gửi request tiếp trên cùng socket, bỏ qua toàn bộ DNS+TCP+TLS. Server đóng kết nối khi rảnh quá lâu (keep-alive timeout, thường vài giây tới vài chục giây) hoặc khi đã phục vụ đủ số request cấu hình. Với curl, đưa nhiều URL cùng một host vào một lệnh là nó tự tái dùng socket; các thư viện thì dùng connection pool bên dưới.

Lưu ý: keep-alive của HTTP/1.1 vẫn tuần tự trên một kết nối — request sau phải chờ response trước xong (head-of-line blocking). Muốn nhiều request song song trên một kết nối phải tới HTTP/2 (bài sau).

Đánh đổi và lưu ý

Kết nối mở tốn tài nguyên server. Mỗi kết nối keep-alive giữ một socket và bộ nhớ ở server. Timeout quá dài + nhiều client nhàn rỗi = cạn socket. Đó là lý do server đặt keep-alive timeout hợp lý (nginx mặc định 75s cho client) và giới hạn số request mỗi kết nối. Cân bằng giữa tái dùng (nhanh) và giải phóng tài nguyên.

Không phải lúc nào cũng thắng. Một request đơn lẻ tới một host rồi thôi thì keep-alive không giúp gì (không có request sau để tái dùng). Lợi ích đến khi có nhiều request tới cùng host — đúng trường hợp phổ biến của trình duyệt tải một trang (HTML + CSS + JS + ảnh) hay client gọi API liên tục.

Đo bằng curl -w, đừng đoán. Các biến %{time_namelookup}, %{time_connect}, %{time_appconnect}, %{time_starttransfer}, %{time_total} tách bạch từng giai đoạn — cực hữu ích khi một request "chậm" mà không rõ chậm ở DNS, bắt tay, hay chờ server xử lý.

Ba ý mang về

  1. Mỗi kết nối mới trả giá handshake trước khi gửi dữ liệu: đo thật tới một host Internet, ~69ms trôi qua cho DNS+TCP+TLS trước byte đầu tiên — mở mới mỗi request là trả khoản này lặp lại.
  2. keep-alive tái dùng kết nối: đo thật, 6 request qua một socket keep-alive chỉ mở 1 kết nối TCP (so với 6 khi Connection: close); 300 request tái dùng nhanh gấp ~19 lần ngay cả trên localhost — trên mạng thật còn lớn hơn.
  3. Biết giới hạn: HTTP/1.1 keep-alive vẫn tuần tự (head-of-line blocking → cần HTTP/2 để song song); kết nối mở tốn tài nguyên server nên có timeout; và lợi ích chỉ đến khi có nhiều request tới cùng host — đo bằng curl -w thay vì đoán.

Nguồn

Phần sau ta tiến thêm một bước: HTTP/2 giải quyết đúng giới hạn tuần tự vừa nhắc — multiplexing nhiều request song song trên một kết nối, và head-of-line blocking dịch chuyển đi đâu.