TCP, HTTP & Web cho Backend/HTTP/2 — multiplexing giải quyết head-of-line blocking
15/29
Bài 15 / 29~18 phútHTTP & WebMiễn phí lượt xem

HTTP/2 — multiplexing giải quyết head-of-line blocking

HTTP/1.1 chỉ xử lý 1 request tại một thời điểm trên mỗi kết nối, dẫn đến head-of-line blocking ứng dụng. HTTP/2 dùng binary framing và stream để gửi nhiều request song song trên 1 kết nối TCP — cắt HOL ứng dụng hoàn toàn. Nhưng HOL ở tầng TCP vẫn còn, và đó là lý do HTTP/3 ra đời.

TL;DR: HTTP/1.1 chỉ gửi được 1 request tại một thời điểm trên mỗi kết nối TCP — request sau phải đợi response trước xong (head-of-line blocking ứng dụng). Workaround cũ là mở 6 kết nối song song, nhưng vừa tốn tài nguyên vừa không triệt để. HTTP/2 giải quyết bằng binary framing: chia nhỏ mọi message thành frame nhị phân, ghép nhiều stream (luồng logic) trên 1 kết nối TCP — request/response đan xen mà không cần chờ nhau. HPACK nén header loại bỏ dữ liệu lặp. Kết quả: page load nhanh hơn rõ rệt mà không cần 6 kết nối. Tuy nhiên, HOL blocking tầng TCP vẫn còn — mất 1 gói TCP chặn mọi stream — đó là bài toán HTTP/3 giải quyết.

Bạn đã biết ở bài 03 — Cookie, session & CORS rằng mỗi request HTTP mang theo header. Bài này phóng to cách transport — thứ quyết định bao nhiêu request có thể bay song song trước khi server trả lời.

1. Vấn đề: HTTP/1.1 và head-of-line blocking ứng dụng

Mở tab Chrome DevTools → Network của bất kỳ trang web nào và để ý cột Waterfall. Nếu trang dùng HTTP/1.1, bạn sẽ thấy các request xếp hàng: nhóm 6 cái chạy song song, nhóm kế đợi nhóm trước xong. Đây là dấu ấn trực tiếp của giới hạn HTTP/1.1.

Vấn đề gốc: HTTP/1.1 dùng text protocol — mỗi request là một dòng văn bản GET /path HTTP/1.1\r\n..., và sau khi gửi request, kết nối TCP bị chiếm cho tới khi nhận đủ response. Request tiếp theo không thể gửi trên cùng kết nối đó.

Ngay cả với keep-alive (HTTP/1.1 mặc định bật), kết nối được tái dùng nhưng vẫn tuần tự: gửi R1 → đợi đáp R1 → gửi R2 → đợi đáp R2. Nếu R1 chậm (query DB nặng), R2 kẹt — dù R2 là ảnh tĩnh xử lý tức thì. Đây là head-of-line blocking ở tầng ứng dụng (application-layer HOL blocking).

📌 HTTP pipelining — thử nghiệm thất bại

HTTP/1.1 có tính năng pipelining: gửi nhiều request liên tiếp mà không đợi từng response. Nhưng server phải trả response theo đúng thứ tự request — nên một response chậm vẫn chặn mọi thứ sau. Thêm vào đó nhiều server/proxy không implement đúng. Trình duyệt hiện đại đã tắt pipelining. Đây không phải giải pháp thật.

Workaround cũ — và vì sao chúng chỉ vá víu

Trước HTTP/2, dev và trình duyệt dùng 3 thủ thuật:

  1. 6 kết nối TCP song song: Chrome mở tối đa 6 kết nối tới cùng một origin. Workaround này tốn bộ nhớ (mỗi kết nối tốn ~40 KB buffer kernel), tốn 1 RTT handshake mỗi kết nối, và vẫn bị chặn nếu có hơn 6 request.
  2. Domain sharding: đặt static assets trên nhiều subdomain (cdn1.example.com, cdn2.example.com) để khai thác thêm 6×N kết nối. Mỗi subdomain cần DNS lookup + TCP handshake riêng — tổng overhead tăng.
  3. Bundling (gộp file): gộp 50 file CSS thành 1, gộp 30 icon thành 1 sprite — giảm số request. Nhưng làm hỏng caching (1 icon thay đổi → invalidate cả sprite), làm tăng kích thước file tải lần đầu.

Cả 3 workaround đều là hack quanh giới hạn giao thức — không giải quyết gốc. HTTP/2 giải quyết gốc.

2. HTTP/2 — binary framing và stream

HTTP/2 (RFC 9113, tiền thân là SPDY của Google) thay đổi cách truyền dữ liệu ở tầng transport, không thay đổi ngữ nghĩa HTTP: method, URL, header, status code — tất cả giữ nguyên từ góc nhìn app.

Sự thay đổi cốt lõi: không còn plain text — thay bằng binary framing layer.

Binary framing layer

Mọi thứ trong HTTP/2 được chia thành frame — đơn vị truyền tải nhỏ nhất, mỗi frame có header 9 byte cố định gồm:

  • Length (3 byte): kích thước phần payload của frame.
  • Type (1 byte): loại frame — HEADERS (header HTTP), DATA (body), SETTINGS, WINDOW_UPDATE, PING, RST_STREAM, GOAWAY
  • Flags (1 byte): cờ điều khiển, ví dụ END_STREAM đánh dấu frame cuối của một message.
  • Stream Identifier (4 byte): frame này thuộc stream nào.

Stream — luồng logic độc lập

Stream trong HTTP/2 là một luồng request/response logic được đánh số nguyên. Client dùng stream ID lẻ (1, 3, 5…), server dùng chẵn (2, 4, 6…). Trên 1 kết nối TCP có thể tồn tại hàng trăm stream đồng thời.

Một request/response HTTP đầy đủ = chuỗi frame thuộc cùng 1 stream ID: frame HEADERS (gửi header request) → frame DATA (gửi body nếu có) → frame HEADERS (response header) → frame DATA (response body, kèm cờ END_STREAM).

Multiplexing — đan xen frame, không chờ nhau

Vì mỗi frame gắn stream ID, nhiều stream có thể đan xen frame trên cùng 1 kết nối TCP mà không cần tách ra:

sequenceDiagram
    participant C as Client
    participant S as Server
    Note over C,S: 1 ket noi TCP duy nhat
    C->>S: HEADERS stream=1 (GET /api/users)
    C->>S: HEADERS stream=3 (GET /api/posts)
    C->>S: HEADERS stream=5 (GET /logo.png)
    S-->>C: HEADERS stream=1 (200 OK)
    S-->>C: DATA stream=3 (posts data)
    S-->>C: DATA stream=1 (users data, END_STREAM)
    S-->>C: DATA stream=5 (image bytes)
    S-->>C: DATA stream=3 (END_STREAM)
    S-->>C: DATA stream=5 (END_STREAM)

Frame của stream 3 và stream 5 có thể đi xen giữa frame của stream 1. Không stream nào cần đợi stream khác. Đây là multiplexing — gửi nhiều luồng dữ liệu đồng thời trên 1 kết nối đơn.

So sánh với HTTP/1.1 tuần tự:

sequenceDiagram
    participant C as Client1 as Client (HTTP/1.1)
    participant S as Server1 as Server

    C->>S: GET /api/users
    S-->>C: 200 OK + users data
    C->>S: GET /api/posts
    S-->>C: 200 OK + posts data
    C->>S: GET /logo.png
    S-->>C: 200 OK + image
    Note over C,S: 3 request: phai doi tung cai - 3 RTT
HTTP/1.1
R1 ──────────── done
R2 ──────── done
R3 ───── done
Tuần tự — tổng = R1+R2+R3
HTTP/2
R1 ──────────── done
R2 ──────── done
R3 ───── done
Song song — tổng = max(R1,R2,R3)

3. HPACK — nén header loại bỏ lặp

HTTP header chiếm một phần không nhỏ trong mỗi request. Trên một trang web, 90% header lặp lại giữa các request: User-Agent, Accept-Encoding, Cookie, Authorization — tất cả gửi nguyên văn mỗi lần trong HTTP/1.1.

HTTP/2 dùng HPACK (RFC 7541) — thuật toán nén header chuyên dụng cho HTTP:

  • Static table: 61 cặp header/value hay gặp được đánh số sẵn. Ví dụ, entry 2 là :method: GET — thay vì gửi 15 byte, client gửi 1 byte (số 2).
  • Dynamic table: cả client lẫn server duy trì một bảng các header đã gửi trong session. Header chưa có trong static table → thêm vào dynamic table → lần sau chỉ gửi index (1-2 byte).
  • Huffman coding: với header giá trị mới hoàn toàn, nén bằng Huffman code tĩnh.

Kết quả thực tế: header size giảm 85-88% trên request điển hình. Cookie dài 500 byte gửi lần đầu; lần sau chỉ tốn vài byte index.

💡 Server push — tính năng đã deprecated

HTTP/2 có server push: server chủ động gửi resource (CSS, JS) kèm response HTML mà không cần client request. Ý tưởng hay, nhưng thực tế browser thường đã có resource trong cache → push lãng phí. Chrome đã xoá hỗ trợ server push năm 2022. Đừng build logic dựa vào nó.

4. Pitfall quan trọng: HOL blocking TCP vẫn còn

Đây là điểm hay bị hiểu nhầm nhất về HTTP/2.

HTTP/2 giải quyết HOL blocking ở tầng ứng dụng — nhiều HTTP request không còn chặn nhau. Nhưng bên dưới, tất cả các stream vẫn đi trên 1 kết nối TCP. TCP đảm bảo thứ tự byte toàn cục — nếu 1 gói TCP bị mất, tất cả dữ liệu sau gói đó (thuộc mọi stream) phải đợi retransmit xong mới được xử lý tiếp.

Ví dụ: 3 stream S1, S2, S3 đang chạy song song. Gói TCP thứ 7 bị mất (thuộc S1). TCP phát hiện mất gói, yêu cầu retransmit. Trong thời gian đó, frame của S2 và S3 dù đã tới đầy đủ vẫn phải nằm trong buffer chờ — vì TCP phải deliver byte theo đúng thứ tự, không thể "nhảy qua" byte bị mất.

flowchart LR
    A["3 streams<br/>S1 S2 S3<br/>HTTP/2"] --> B["1 TCP connection"]
    B --> C["Goi TCP bi mat"]
    C --> D["Moi stream bi chan<br/>cho retransmit"]
    style C fill:#fee2e2,stroke:#ef4444
    style D fill:#fee2e2,stroke:#ef4444

Trên mạng mất gói cao (wifi yếu, 3G kém), HTTP/2 thậm chí có thể chậm hơn HTTP/1.1 với 6 kết nối song song — vì 1 kết nối HTTP/2 bị chặn toàn bộ, trong khi HTTP/1.1 chỉ bị chặn 1 trong 6 kết nối. Đây là TCP-layer HOL blocking, và đây chính xác là lý do HTTP/3 được thiết kế trên QUIC/UDP — xem bài 05 — HTTP/3 & QUIC.

⚠️ HTTP/2 không phải magic bullet

HTTP/2 mang lại cải thiện rõ rệt trên mạng tốt, ít mất gói (băng thông rộng, latency thấp). Trên mạng kém, mất gói nhiều, HTTP/2 có thể kém hơn HTTP/1.1. Hiểu giới hạn này trước khi kết luận "HTTP/2 luôn nhanh hơn".

5. Tóm tắt so sánh HTTP/1.1 vs HTTP/2

Đặc điểmHTTP/1.1HTTP/2
ProtocolTextBinary (frame)
Request/kết nối1 tại một thời điểmNhiều stream song song
HOL blocking ứng dụngKhông
HOL blocking TCPKhông áp dụngVẫn có
HeaderPlain text, lặp lạiHPACK nén, ~85% nhỏ hơn
Số kết nối cần thiết6 (trình duyệt mở thêm)1
Server pushKhôngCó (đã deprecated thực tế)
TLSTuỳ chọnBắt buộc trên thực tế (trình duyệt)

6. Liên hệ các bài khác

7. 📚 Deep Dive — tài liệu gốc

📚 RFC & spec chính thức

Đọc khi muốn đi tới tận gốc:

  • RFC 9113 — HTTP/2 — bản cập nhật 2022 của HTTP/2 (trước là RFC 7540). Mục 4 (HTTP Frames), 5 (Streams and Multiplexing) là core.
  • RFC 7541 — HPACK Header Compression — toàn bộ cơ chế nén header: static table (Appendix A), dynamic table, Huffman coding.
  • Google SPDY White Paper — nguồn gốc HTTP/2, benchmark so sánh với HTTP/1.1 trên các điều kiện mạng khác nhau.

Ghi chú thực tế: Dùng curl --http2 -v https://example.com 2>&1 | grep "< HTTP" để kiểm tra server có hỗ trợ HTTP/2 không. Wireshark có thể decrypt TLS và hiển thị HTTP/2 frame nếu bạn cung cấp TLS session key.

8. Tóm tắt

  • HTTP/1.1 bị head-of-line blocking ứng dụng: chỉ 1 request chạy tại một thời điểm trên 1 kết nối TCP — request sau đợi response trước.
  • Workaround cũ (6 kết nối, domain sharding, bundling) là hack, không giải quyết gốc.
  • HTTP/2 dùng binary framing: chia message thành frame nhỏ, mỗi frame gắn stream ID, nhiều stream đan xen trên 1 kết nối TCP — đây là multiplexing.
  • HPACK nén header qua static table + dynamic table + Huffman: giảm ~85% kích thước header.
  • Server push đã deprecated trên thực tế — Chrome xoá năm 2022.
  • HOL blocking TCP vẫn còn: mất 1 gói TCP chặn mọi stream vì TCP đảm bảo thứ tự byte toàn cục — cơ sở cho HTTP/3.
  • HTTP/2 nhanh hơn rõ trên mạng tốt; có thể kém hơn HTTP/1.1 trên mạng mất gói nhiều.

9. Tự kiểm tra

Tự kiểm tra
Q1
HTTP/1.1 bị head-of-line blocking ứng dụng có nghĩa là gì? Tại sao keep-alive không giải quyết được?
Head-of-line blocking ứng dụng nghĩa là trên 1 kết nối TCP, request sau phải đợi response của request trước mới được gửi — kể cả khi request sau đơn giản hơn nhiều. Keep-alive giúp tái dùng kết nối (tránh tốn RTT handshake mỗi lần), nhưng không bỏ được thứ tự tuần tự: vẫn chỉ có 1 request "in-flight" tại một thời điểm trên 1 kết nối. Cần nhiều kết nối (Chrome mở 6) để parallelism, nhưng vẫn là workaround, không giải quyết gốc.
Q2
HTTP/2 dùng stream và multiplexing để tránh HOL blocking ứng dụng như thế nào?
HTTP/2 chia mọi message thành frame nhị phân, mỗi frame mang stream ID — số hiệu nhận dạng luồng request/response logic. Nhiều stream có thể tồn tại đồng thời trên 1 kết nối TCP; frame của stream khác nhau đan xen nhau trong cùng 1 luồng byte. Đầu nhận dùng stream ID để ráp lại từng response. Vì vậy response chậm của stream 1 không chặn response nhanh của stream 3 hay 5 — đây là multiplexing.
Q3
HPACK nén header HTTP/2 bằng cơ chế gì? Vì sao cần nén riêng thay vì dùng gzip thông thường?
HPACK dùng 3 cơ chế: static table (61 cặp header/value phổ biến được đánh số — ví dụ :method: GET = entry 2, chỉ cần gửi 1 byte), dynamic table (client và server cùng duy trì bảng header đã gửi trong session — lần sau chỉ cần index), và Huffman coding cho giá trị mới. Gzip không phù hợp vì gzip cần context lớn và xử lý độc lập từng request — không tận dụng được thực tế rằng các request trong 1 session chia sẻ nhiều header giống nhau (như Authorization, Cookie). HPACK thiết kế chuyên cho HTTP, khai thác đặc điểm lặp header giữa request.
Q4
HTTP/2 giải quyết HOL blocking ứng dụng, nhưng HOL blocking TCP vẫn còn — nghĩa là gì?
Dù nhiều HTTP stream chạy song song, chúng vẫn đi qua 1 kết nối TCP. TCP đảm bảo byte đến đúng thứ tự — nếu 1 gói bị mất, tất cả byte sau (thuộc mọi stream) phải nằm chờ trong buffer cho đến khi gói mất được retransmit và nhận thành công. Vì vậy, mất 1 gói TCP khiến tất cả stream bị block, dù gói đó chỉ thuộc 1 stream. Đây là TCP-layer HOL blocking — khác với HOL ứng dụng (đã giải quyết), nó nằm sâu hơn ở tầng transport.
Q5
Trong điều kiện nào HTTP/2 có thể chậm hơn HTTP/1.1?
Trên mạng mất gói nhiều (wifi yếu, 3G kém, mạng di động kém ổn định), HTTP/2 có thể chậm hơn. HTTP/1.1 mở 6 kết nối TCP song song: nếu 1 kết nối bị block vì mất gói, 5 kết nối còn lại vẫn tiến được. HTTP/2 chỉ có 1 kết nối: 1 gói mất block tất cả stream. HTTP/2 mang lại lợi thế rõ nhất khi mạng ổn định (băng thông cao, latency thấp, mất gói thấp).
Q6
Vì sao trình duyệt hiện đại thực tế đòi hỏi TLS khi dùng HTTP/2, dù spec không bắt buộc?
RFC 9113 kỹ thuật cho phép HTTP/2 trên plain TCP (gọi là h2c — HTTP/2 cleartext). Nhưng các trình duyệt lớn (Chrome, Firefox, Safari) chỉ implement HTTP/2 qua TLS (gọi là h2). Lý do: không qua TLS, các proxy/middlebox cũ trên đường truyền có thể không hiểu binary HTTP/2 (chúng expect text HTTP/1.1) và có thể corrupt hoặc drop data. TLS tạo một tunnel mã hoá mà middlebox không thể can thiệp — đảm bảo binary framing đến nguyên vẹn. Trên thực tế, h2c chỉ dùng trong mạng nội bộ kiểm soát được (vd gRPC giữa microservice).

Bài tiếp theo: HTTP/3 & QUIC — vì sao chuyển khỏi TCP

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

HTTP/3 & QUIC — vì sao chuyển khỏi TCP