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"]

Ảnh chụp đoạn mã Go nền tối minh hoạ HTTP2 multiplexing, vấn đề HTTP1.1 head-of-line blocking trên 1 kết nối HTTP1.1 chỉ 1 request-response tại 1 thời điểm kết nối req A req B req C req B phải chờ req A xong head-of-line blocking trình duyệt lách bằng cách mở nhiều kết nối song song 6 host, HTTP2 nhiều stream đan xen trên 1 kết nối multiplexing kết nối A1 B1 C1 A2 B2 C2 frame của nhiều stream xen kẽ mỗi request 1 stream có id frame gắn stream id 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 bớt bắt tay TCP cộng TLS chỉ 1 kết nối nhanh ít tài nguyên, demo bằng Go h2 tự bật qua TLS ALPN srv httptest NewUnstartedServer h srv EnableHTTP2 true srv StartTLS client HTTP2 mặc định Go đàm phán h2 qua ALPN khi TLS tr ForceAttemptHTTP2 true ép HTTP1.1 để so TLSNextProto rỗng NextProtos http1.1 gửi nhiều request song song đếm kết nối TCP qua ConnState

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":

Ảnh chụp bảng kết quả chạy thật HTTP2 multiplexing output thật, 300 request song song tới cùng server sau khi kết nối đã ấm HTTP2 multiplexing proto HTTP2.0 số kết nối TCP mới 0 tổng 21 ms HTTP1.1 một request kết nối proto HTTP1.1 số kết nối TCP mới 221 tổng 82 ms, ý nghĩa HTTP2 300 request song song dùng lại 1 kết nối đã có 0 kết nối mới mọi request là stream đan xen trên 1 kết nối HTTP1.1 mỗi request song song cần kết nối riêng mở 221 HTTP2 nhanh hơn 21 vs 82ms bớt bắt tay cộng không head-of-line ứng dụng, kết luận HTTP2 nhiều stream 1 kết nối ít kết nối ít bắt tay TLS giải head-of-line blocking ở tầng ứng dụng request không chờ nhau nhưng vẫn còn TCP head-of-line 1 gói TCP mất chặn mọi stream vì tất cả chung 1 kết nối TCP HTTP3 QUIC trên UDP giải nốt Go bật h2 tự động qua TLS đa số client server hiện đại đã dùng h2

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 StateNew nà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ề

  1. 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.
  2. Í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.
  3. 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

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.