Các bài trước cho thấy mỗi kết nối TCP+TLS tốn bắt tay, và connection pool giúp tái dùng. Nhưng HTTP/1.1 có một giới hạn cứng: trên một kết nối, chỉ một request-response tại một thời điểm. Request sau phải chờ request trước xong — gọi là head-of-line blocking. Đó là lý do trình duyệt phải mở nhiều kết nối song song (thường 6/host) chỉ để tải song song. HTTP/2 giải quyết tận gốc bằng multiplexing: nhiều stream chạy đan xen trên một kết nối. Bài này (phần 11 loạt Mạng) chạy thật để đếm số kết nối HTTP/2 thực sự cần so với HTTP/1.1.
Cơ chế: từ nối tiếp sang đan xen
HTTP/1.1 trên một kết nối xử lý nối tiếp:
kết nối: [--- req A ---][--- req B ---][--- req C ---]
// req B phải CHỜ req A xong -> head-of-line blocking
Muốn song song, phải mở nhiều kết nối. HTTP/2 cắt mỗi request thành các frame mang stream id, rồi đan xen frame của nhiều stream trên cùng một kết nối:
kết nối: [A1][B1][C1][A2][B2][C2]... // frame của nhiều stream xen kẽ
// mỗi request = 1 stream; frame gắn stream id -> bên nhận ghép lại đúng
// nhiều request CHẠY CÙNG LÚC trên 1 kết nối, không chờ nhau
Kết quả: một kết nối phục vụ hàng trăm request song song, bớt hẳn số bắt tay TCP+TLS. Trong Go, HTTP/2 tự bật qua TLS (đàm phán bằng ALPN):
srv := httptest.NewUnstartedServer(h); srv.EnableHTTP2 = true; srv.StartTLS()
tr.ForceAttemptHTTP2 = true // client dùng h2
// ép HTTP/1.1 để so: TLSNextProto rỗng + NextProtos = ["http/1.1"]

Hình 1: HTTP/1.1 xử lý nối tiếp trên mỗi kết nối (head-of-line blocking) nên cần nhiều kết nối; HTTP/2 đan xen nhiều stream (có stream id) trên một kết nối — nhiều request song song không chờ nhau.
Đo thật: đếm kết nối cho 300 request song song
Mình dựng server TLS h2, gửi 300 request song song (100 luồng), đếm số kết nối TCP mới sau khi kết nối đã "ấm":

Hình 2: Chạy thật — 300 request song song sau khi kết nối đã ấm: HTTP/2 (proto HTTP/2.0) mở 0 kết nối mới (dùng lại 1 kết nối), 21ms; HTTP/1.1 mở 221 kết nối, 82ms.
Đọc kết quả đo được:
- HTTP/2: 0 kết nối mới cho 300 request song song: sau một request khởi động (làm ấm kết nối), toàn bộ 300 request song song chạy qua đúng kết nối đó — server không thấy thêm một
StateNewnào. Mỗi request là một stream đan xen; một kết nối TCP phục vụ tất cả. Đây là multiplexing đúng nghĩa: song song mà không cần thêm kết nối. - HTTP/1.1: 221 kết nối cho cùng tải: vì mỗi kết nối chỉ chạy một request tại một thời điểm, 100 luồng song song buộc phải có nhiều kết nối cùng lúc, cộng với churn khi tái dùng không đủ — tổng 221 kết nối mới được mở. Chênh lệch 0 vs 221 là minh chứng rõ nhất cho khác biệt kiến trúc.
- HTTP/2 nhanh hơn ~4 lần: 21ms vs 82ms. Nhanh hơn vì hai lý do: bớt hẳn bắt tay (1 kết nối thay vì hàng trăm) và không có head-of-line blocking ở tầng ứng dụng (request không xếp hàng chờ nhau).
Đánh đổi cần cân nhắc
HTTP/2 giải head-of-line ở tầng ứng dụng, nhưng TCP head-of-line vẫn còn. Đây là điểm quan trọng và hay bị bỏ qua. HTTP/2 để mọi stream chung một kết nối TCP — nên khi một gói TCP bị mất, TCP phải chờ truyền lại gói đó trước khi giao bất kỳ dữ liệu nào sau nó, chặn tất cả các stream cùng lúc. Ở HTTP/1.1 với nhiều kết nối, một gói mất chỉ ảnh hưởng một kết nối. Nghịch lý: trên mạng mất gói cao, HTTP/2 một-kết-nối có thể tệ hơn. Đây chính là lý do HTTP/3 (QUIC trên UDP) ra đời — nó cho các stream độc lập ở tầng vận chuyển, một gói mất chỉ chặn stream của nó.
Ép nhiều thứ vào một kết nối cũng dồn rủi ro. Một kết nối HTTP/2 mang toàn bộ traffic tới một host là hiệu quả, nhưng nếu kết nối đó gặp vấn đề (nghẽn, reset), mọi request đang bay đều bị ảnh hưởng cùng lúc. Với HTTP/1.1 nhiều kết nối, rủi ro phân tán hơn. Trong thực tế lợi ích của HTTP/2 vẫn vượt trội cho web thông thường, nhưng cần biết đánh đổi này khi thiết kế hệ nhạy độ tin cậy.
Multiplexing không phải luôn nhanh hơn cho mọi tải. Với một request đơn lẻ (không song song), HTTP/2 không nhanh hơn HTTP/1.1 đáng kể — lợi ích của nó là ở song song. Và server phải xử lý các stream đồng thời, tốn CPU/bộ nhớ theo số stream. HTTP/2 thắng rõ khi có nhiều request nhỏ song song tới cùng host (đúng mô hình web hiện đại), không phải mọi kịch bản.
Ba ý mang về
- HTTP/2 multiplexing: nhiều stream song song trên một kết nối: đo thật 300 request song song qua HTTP/2 mở 0 kết nối mới (dùng lại 1 kết nối) so với HTTP/1.1 mở 221 — giải head-of-line blocking ở tầng ứng dụng, request không xếp hàng chờ nhau.
- Ít kết nối = ít bắt tay = nhanh hơn: đo thật HTTP/2 nhanh gần 4 lần (21 vs 82ms) nhờ bớt bắt tay TCP+TLS và không chờ nối tiếp; Go bật h2 tự động qua TLS nên đa số ứng dụng đã hưởng lợi sẵn.
- Nhưng TCP head-of-line vẫn còn — đó là lý do có HTTP/3: mọi stream chung một kết nối TCP nên một gói TCP mất chặn tất cả stream; HTTP/3 (QUIC trên UDP) cho stream độc lập ở tầng vận chuyển để giải nốt vấn đề này.
Nguồn
- RFC 9113 — HTTP/2: https://www.rfc-editor.org/rfc/rfc9113.html
- Cloudflare — HTTP/2 vs HTTP/1.1 và head-of-line blocking: https://blog.cloudflare.com/http-3-vs-http-2/
- Go docs — net/http (HTTP/2 support) và golang.org/x/net/http2: https://pkg.go.dev/net/http
Phần sau là bài tổng kết loạt: một cây công cụ và quy trình chẩn đoán mạng — khi "mạng chậm" hay "kết nối lỗi", bắt đầu từ đâu, dùng công cụ nào ở tầng nào, và cách khoanh vùng lỗi qua các bài đã học.