gRPC nhanh không chỉ nhờ protobuf — nó dựa trên HTTP/2, và tính năng cốt lõi của HTTP/2 là multiplexing: nhiều request chạy song song trên một kết nối TCP duy nhất. Đây là bước nhảy lớn so với HTTP/1.1, nơi một kết nối chỉ xử lý một request tại một thời điểm. Hiểu multiplexing giải thích vì sao HTTP/2 (và gRPC trên nó) vượt trội cho giao tiếp nhiều request. Bài này dựng server + client HTTP/2 trong Go, đo thật chênh lệch so với HTTP/1.1, và mổ xẻ cơ chế stream/frame bên dưới.

Vấn đề HTTP/1.1: head-of-line blocking

Một kết nối HTTP/1.1 xử lý đúng một request tại một thời điểm — request sau phải chờ request trước hoàn tất. Đây gọi là head-of-line (HOL) blocking: một request chậm chặn tất cả request phía sau trên cùng kết nối.

[conn] req1 --50ms--> req2 --50ms--> req3 ...   (tuần tự)

Trình duyệt bù bằng cách mở nhiều kết nối song song (thường ~6 mỗi host), nhưng mỗi kết nối tốn một lần bắt tay TCP (và TLS), và số lượng bị giới hạn.

HTTP/2: một kết nối, nhiều stream

HTTP/2 chia mỗi request thành một stream có id riêng, và cắt dữ liệu thành các frame (HEADERS, DATA...) đan xen nhau trên cùng một kết nối TCP:

[conn] [s1-frame][s3-frame][s1-frame][s5-frame]...  (đan xen)

Các stream chạy đồng thời — không stream nào chặn stream khác. Trong Go, dựng server HTTP/2 chỉ cần bật (thực tế: dùng TLS là net/http tự bật HTTP/2; ở đây dùng h2c — HTTP/2 không TLS — cho demo):

h2s := &http2.Server{}
srv := &http.Server{Addr: ":8099", Handler: h2c.NewHandler(mux, h2s)}

Ảnh chụp đoạn mã Go nền tối minh hoạ HTTP 2 multiplexing nhiều request song song trên một kết nối, vấn đề HTTP 1.1 head-of-line blocking một kết nối HTTP 1.1 xử lý một request tại một thời điểm request sau phải chờ request trước xong nối tiếp conn req1 50ms req2 50ms req3 tuần tự trình duyệt bù bằng cách mở nhiều kết nối thường 6 nhưng mỗi kết nối là một cái bắt tay TCP TLS tốn kém, HTTP 2 một kết nối nhiều stream song song HTTP 2 chia mỗi request thành một stream có id riêng cắt nhỏ thành các frame đan xen nhau trên cùng một kết nối TCP conn s1-frame s3-frame s1-frame s5-frame đan xen các stream chạy đồng thời không request nào chặn request khác, server HTTP 2 trong Go h2c không TLS cho demo h2s bằng http2 Server srv bằng http Server Addr 8099 Handler h2c NewHandler mux h2s bọc để nói h2c thực tế chỉ cần dùng TLS net http tự bật HTTP 2, client 20 request đồng thời trên 1 kết nối h2 bằng http Client Transport http2 Transport for i 0 i nhỏ hơn 20 i cộng go func h2 Get http 20 stream song song HTTP 2 Transport tự dồn chúng vào 1 kết nối thành 20 stream

Hình 1: HTTP/1.1 một kết nối xử lý tuần tự (HOL blocking). HTTP/2 chia mỗi request thành stream độc lập, cắt thành frame đan xen trên một kết nối TCP, chạy đồng thời. Go bật HTTP/2 tự động khi dùng TLS.

Đo thật: 20 request trên một kết nối

Gửi 20 request đồng thời (mỗi handler ngủ 50ms), ép cả hai dùng một kết nối để cô lập tác dụng của multiplexing:

Ảnh chụp bảng kết quả đo thật nền tối 20 request trên 1 kết nối HTTP 2 xong 54ms HTTP 1.1 mất 1,136s 21x, go run x net http2 h2c handler 50ms Go 1.23 arm64 10 core, 20 request đồng thời mỗi handler ngủ 50ms giao thức 1 kết nối HTTP 2 HTTP 2.0 20 stream song song 54 ms HTTP 1.1 20 request nối tiếp 1.136 ms HTTP 2 nhanh hơn 21,2x, vì sao chênh lệch lớn vậy HTTP 2 20 stream chạy đồng thời trên 1 kết nối tổng xấp xỉ thời gian một request 50ms cộng chút overhead bằng 54ms HTTP 1.1 1 kết nối chỉ 1 request lần 20 nhân 50ms nối tiếp bằng 1000ms cộng overhead bằng 1.136ms đây là head-of-line blocking, cơ chế bên trong mỗi request bằng 1 stream có id độc lập trên cùng kết nối dữ liệu cắt thành frame HEADERS DATA đan xen nhau HPACK nén header bỏ lặp header giống nhau giữa các request flow control per-stream tránh một stream chiếm hết băng thông, cốt lõi multiplexing nhiều stream song song trên 1 kết nối TCP xóa HOL request không chặn nhau khác HTTP 1.1 nối tiếp đo thật 20 req 1 conn HTTP 2 54ms vs HTTP 1.1 1,136s 21x Go dùng TLS là net http tự bật HTTP 2 không cần cấu hình đánh đổi 1 kết nối lỗi ảnh hưởng mọi stream TCP HOL vẫn còn

Hình 2: 20 request đồng thời trên một kết nối. HTTP/2 xong trong 54 ms (20 stream song song ≈ thời gian một request), HTTP/1.1 mất 1.136 ms (20 × 50ms nối tiếp) — HTTP/2 nhanh hơn 21,2 lần.

  • HTTP/2 (một kết nối): 54 ms.
  • HTTP/1.1 (một kết nối): 1.136 ms.

Chênh lệch 21,2 lần. Với HTTP/2, 20 stream chạy đồng thời trên một kết nối nên tổng thời gian ≈ thời gian một request (~50ms) cộng chút overhead. Với HTTP/1.1, một kết nối chỉ xử lý một request tại một lần, nên 20 request nối tiếp = 20 × 50ms ≈ 1 giây. Đây là head-of-line blocking đo được bằng con số thật.

Ngoài multiplexing, HTTP/2 còn có: HPACK (nén header, bỏ lặp các header giống nhau giữa các request), flow control per-stream (một stream không chiếm hết băng thông), và server push (ít dùng, đang bị loại bỏ dần).

Đánh đổi cần cân nhắc

Một kết nối lỗi ảnh hưởng mọi stream trên nó. Multiplexing dồn mọi request vào một kết nối TCP — nếu kết nối đó đứt, tất cả stream đang chạy đều hỏng cùng lúc. Với HTTP/1.1 nhiều kết nối, một kết nối đứt chỉ ảnh hưởng request của nó. Trong thực tế HTTP/2 xử lý reconnect tốt, nhưng đây là đánh đổi kiến trúc cần biết.

TCP head-of-line blocking vẫn còn ở tầng dưới. HTTP/2 xóa HOL blocking ở tầng ứng dụng (giữa các stream), nhưng một gói TCP mất vẫn chặn toàn bộ kết nối ở tầng vận chuyển — vì TCP đảm bảo thứ tự byte. Đây là lý do HTTP/3 chuyển sang QUIC (trên UDP): mỗi stream độc lập cả ở tầng vận chuyển, mất gói của stream này không chặn stream khác. HTTP/2 chỉ giải nửa bài toán.

Trong Go, HTTP/2 gần như tự động — nhưng chú ý h2c. net/http tự bật HTTP/2 khi dùng TLS (ALPN thương lượng), nên server HTTPS của bạn đã dùng HTTP/2 mà không cần code thêm. Nhưng HTTP/2 không TLS (h2c) cần cấu hình rõ ràng (h2c.NewHandler), và nhiều client không hỗ trợ h2c — chỉ dùng cho môi trường nội bộ tin cậy. Với gRPC, thư viện lo phần này.

Ba ý mang về

  1. HTTP/2 multiplexing cho nhiều request chạy song song trên một kết nối TCP: mỗi request là một stream độc lập có id, dữ liệu cắt thành frame đan xen nhau — xóa head-of-line blocking của HTTP/1.1 (một kết nối chỉ một request/lần).
  2. Chênh lệch đo được rất lớn: đo thật 20 request đồng thời trên một kết nối, HTTP/2 xong 54 ms (song song) so với HTTP/1.1 1.136 ms (nối tiếp) — nhanh hơn 21 lần; cộng thêm HPACK nén header và flow control per-stream.
  3. Đánh đổi và giới hạn: một kết nối lỗi ảnh hưởng mọi stream, và TCP head-of-line blocking vẫn còn ở tầng vận chuyển (HTTP/3/QUIC mới giải triệt để) — trong Go, HTTP/2 tự bật khi dùng TLS, còn h2c (không TLS) cần cấu hình rõ ràng.

Phần sau ta xét một giao thức thời gian thực khác, giữ kết nối hai chiều mở liên tục: Phần sau dựng một WebSocket server thật trong Go — nâng cấp từ HTTP, đọc/ghi message hai chiều, và so với HTTP/2 stream cùng gRPC bidi.