Phần trước giảm số request tới origin. Phần này giảm kích thước của những request còn lại. Nén HTTP là tính năng bật bằng một dòng cấu hình nginx, và vì thế ít ai đo nó — bật lên, thấy byte giảm, xong. Nhưng nén là một phép đổi CPU lấy băng thông, và tỉ giá của phép đổi đó thay đổi theo kích thước thân, loại dữ liệu, và băng thông thật giữa hai đầu. Có những trường hợp nén làm chậm hơn, và bài này đo để tìm ra chúng.
Thương lượng bằng header

Hình 1: Server chọn thuật toán theo Accept-Encoding, ghi Content-Encoding và Vary. Phần đo tách riêng CPU nén (server) và CPU giải (client).
Giao thức đơn giản: client khai Accept-Encoding: br, gzip, server chọn một trong số đó, nén thân và ghi Content-Encoding. Hai header đi kèm mà thiếu là hỏng ở chỗ khác:
Vary: Accept-Encoding— đúng bài học của phần 4: cùng URL giờ có hai thân (nén và không nén). Cache dùng chung không cóVarysẽ trả thân gzip cho client không hỗ trợ gzip, và client đó nhận một đống byte nhị phân thay vì JSON.- Không
Content-Lengthcủa bản thô. Nén theo luồng không biết trước kích thước; server phải dùng chunked encoding hoặc nén xong rồi mới ghiContent-Length. GhiContent-Lengthcủa bản thô lên thân nén là client chờ byte không bao giờ tới.
Đo thật: kích thước và CPU
JSON sản phẩm lấy từ PostgreSQL, năm kích thước từ 1 dòng (308 byte) tới 7.000 dòng (2,09 MB). Mỗi ô là trung bình của nhiều lần nén rồi giải nén trong bộ nhớ:

Hình 2: Kết quả thật. Bốn mục: theo kích thước, theo mức nén, hai trường hợp nén làm hại, và thương lượng qua HTTP thật.
JSON nén được khoảng 10 lần, và tỉ lệ đó gần như không đổi từ 2 KB tới 2 MB. 21,2 KB → 2,2 KB; 213 KB → 21,4 KB; 2,09 MB → 212 KB. JSON là định dạng cực kỳ lặp — mỗi dòng đều có "id":, "ten":, "gia": — và gzip bắt được đúng thứ đó. Với API trả danh sách, con số 10× là thứ bạn có thể trông đợi.
CPU nén tỉ lệ tuyến tính với kích thước: khoảng 5 µs mỗi KB với gzip-6. 21 KB mất 125 µs, 213 KB mất 1,07 ms, 2 MB mất 11,3 ms. Giải nén rẻ hơn 4–5 lần (26 µs cho 21 KB). Ở thân 2 MB, 11,3 ms nén là con số đáng để ý — nó lớn hơn thời gian xử lý của nhiều endpoint — và gzip-1 làm cùng việc trong 3,98 ms với kết quả chỉ to hơn 7%.
Mức nén gần như vô nghĩa với JSON. Mục 2: gzip-1 cho 10,9%, gzip-9 cho 10,6% — chênh 0,3 điểm phần trăm, tức 70 byte trên 21 KB. Brotli từ mức 1 tới 9 nằm trong 9,5–10,8%. Chỉ brotli-11 nhảy xuống 8,5%, và nó tốn 16,85 ms — gấp 135 lần brotli-4 — để mua thêm 1 điểm. Mức 11 là để nén tài nguyên tĩnh một lần lúc build; dùng nó cho phản hồi động là biến một endpoint 1 ms thành endpoint 18 ms.
Brotli thắng gzip khoảng 10% kích thước ở cùng tốc độ (br-4: 2,0 KB/159 µs so với gzip-6: 2,2 KB/125 µs). Thắng, nhưng không phải cách biệt lớn — lợi thế thật của brotli nằm ở từ điển dựng sẵn cho HTML/CSS/JS, thứ JSON không hưởng nhiều.
Khi nén làm hại
Mục 3 là phần mình thấy ít tài liệu nói:
{"ok":true}11 byte nén thành 35 byte. Gzip có header 10 byte, trailer 8 byte, và một bảng Huffman — thân dưới khoảng 100 byte luôn phình. Một API trả nhiều phản hồi nhỏ ({"ok":true},{"id":7}) mà bật nén toàn cục là tốn CPU để làm thân to hơn. nginx cógzip_min_length(mặc định 20 byte, nên đặt 256–1024) đúng vì lý do này.- JSON chứa ảnh base64 150 KB chỉ xuống 75%, tốn 2,12 ms — gấp 17 lần một JSON cùng cỡ. Ảnh JPEG/PNG đã nén sẵn; base64 chỉ thêm 33% mà gzip gỡ lại được phần nào, còn ruột ảnh thì không nén thêm được. Thân như vậy phải gửi
Content-Encoding: identityhoặc không đi qua bộ nén, và tốt hơn là đừng nhúng ảnh vào JSON — trả URL.
Điểm hoà vốn, tính từ số đo
Mọi số đo ở trên là loopback, nên không thấy được lợi ích của băng thông. Nhưng từ chúng tính được tổng thời gian cho thân 21,2 KB → 2,2 KB (tiết kiệm 19 KB) với chi phí CPU 125 + 26 = 151 µs:
| băng thông | thời gian truyền 19 KB tiết kiệm được | CPU nén + giải | lời/lỗ |
|---|---|---|---|
| 10 Mbps (4G yếu) | 15,2 ms | 0,15 ms | lời 100 lần |
| 100 Mbps | 1,5 ms | 0,15 ms | lời 10 lần |
| 1 Gbps (nội bộ) | 0,15 ms | 0,15 ms | hoà |
| 10 Gbps (cùng rack) | 0,015 ms | 0,15 ms | lỗ 10 lần |
Bảng này là phép tính, không phải phép đo, nhưng kết luận của nó khớp với thực hành: nén cho client ở xa (trình duyệt, app di động) gần như luôn lời; nén giữa hai service trong cùng datacenter thường lỗ. Với gRPC nội bộ, hoặc REST giữa các microservice, tắt nén thường nhanh hơn — và CPU đó dùng vào việc khác.
Đánh đổi cần cân nhắc
Nén ở đâu: ứng dụng hay nginx. Để nginx làm (gzip on; gzip_types application/json; gzip_min_length 512;) là cách ít code nhất và tách CPU nén khỏi tiến trình ứng dụng. Nén trong ứng dụng cho phép quyết định theo endpoint — tắt cho thân nhỏ, tắt cho thân chứa nhị phân — và quan trọng hơn: nén trước khi cache. Lưu bản nén trong Redis hoặc cache HTTP rồi phục vụ thẳng là cách duy nhất để nén không tốn CPU mỗi request; nginx có gzip_static cho tĩnh, còn API động thì phải tự làm.
ETag và nén. Bài phần 5 dùng strong ETag; cùng thân nén bằng gzip và br cho byte khác nhau, nên theo RFC thì ETag phải khác — nginx tự gỡ ETag hoặc đổi sang weak khi nén động. Nếu ETag sinh trong ứng dụng và nén ở nginx, kiểm tra xem 304 còn hoạt động không; nhiều hệ mất 304 im lặng sau khi bật gzip.
BREACH. Nén thân chứa cả bí mật (token CSRF, session) lẫn dữ liệu do người dùng điều khiển, qua TLS, là mở cửa cho tấn công đoán bí mật qua kích thước. API JSON trả token trong thân kèm trường do client gửi lên là đúng hình mẫu đó. Cách chữa: không đặt bí mật trong thân có nén, hoặc tắt nén cho những endpoint đó.
Số đo của một luồng, một CPU. 125 µs mỗi request ở 1.000 request/giây là 12,5% một lõi — không nhỏ, không lớn. Ở 10.000 request/giây, nén chiếm hơn một lõi và là thứ đầu tiên hiện trong profile. Lúc đó câu trả lời không phải "tắt nén" mà là "nén trước khi cache", như trên.
Ba ý mang về
- JSON nén được 10 lần và mức nén gần như không quan trọng: gzip-1 và gzip-9 chênh 0,3 điểm phần trăm, còn brotli-11 tốn 16,85 ms (gấp 135 lần br-4) để mua thêm 1 điểm — mức 11 chỉ dành cho tài nguyên tĩnh nén một lần.
- Hai nơi nén làm hại: thân dưới ~100 byte phình (11 → 35 byte), và dữ liệu đã nén sẵn (ảnh base64) tốn CPU gấp 17 lần cho 25% tiết kiệm. Đặt
gzip_min_lengthvà đừng nhúng nhị phân vào JSON. - Nén là đổi CPU lấy băng thông, tỉ giá phụ thuộc đường truyền: 151 µs CPU đổi 19 KB là lời 100 lần ở 10 Mbps, hoà ở 1 Gbps, lỗ ở 10 Gbps — nén cho client ở xa, cân nhắc tắt giữa service nội bộ, và nén trước khi cache nếu lưu lượng lớn.
Nguồn
- RFC 9110 — HTTP Semantics, mục 8.4 Content-Encoding và 12.5.3 Accept-Encoding: https://www.rfc-editor.org/rfc/rfc9110#section-8.4
- RFC 7932 — Brotli Compressed Data Format: https://www.rfc-editor.org/rfc/rfc7932
- nginx docs — ngx_http_gzip_module (
gzip_min_length,gzip_types): https://nginx.org/en/docs/http/ngx_http_gzip_module.html - Go — compress/gzip: https://pkg.go.dev/compress/gzip ; andybalholm/brotli: https://github.com/andybalholm/brotli
- BREACH attack: https://www.breachattack.com/
Phần sau chuyển từ "làm phản hồi nhỏ hơn" sang "từ chối bớt request": giới hạn tần suất bằng token bucket, các header chuẩn RateLimit-* và Retry-After, và đo xem client xử lý 429 đúng cách khác client xử lý sai bao nhiêu.