Nginx nổi tiếng phục vụ file tĩnh cực nhanh — nhưng nhanh cỡ nào, và nhờ đâu? Bài cuối sê-ri HTTP đo thật ba đòn bẩy hiệu năng của nginx cho nội dung tĩnh: sendfile (gửi file zero-copy), Cache-Control (để client khỏi hỏi lại), và gzip (nén trên đường truyền). Ta dùng ApacheBench (ab) để đo thông lượng và curl để kiểm nén — kèm một cái bẫy đo lường mà nhiều người vấp. Chạy thật với nginx và ab trong container.
Ba đòn bẩy hiệu năng file tĩnh
sendfile on; # gửi file thẳng từ kernel -> socket, bỏ qua bộ đệm user
tcp_nopush on; # gom header + đầu file vào ít gói TCP hơn (đi cùng sendfile)
gzip on; # nén văn bản trên đường truyền
location / {
add_header Cache-Control "public, max-age=3600";
}
sendfile là điểm mấu chốt. Cách thường (không sendfile): dữ liệu đi đĩa → bộ đệm kernel → bộ đệm ứng dụng → socket — hai lần copy qua lại user space, tốn CPU. Với sendfile, kernel gửi thẳng từ bộ đệm của nó ra socket (zero-copy), bỏ qua user space. Kết quả là phục vụ file tĩnh với thông lượng rất cao và ít CPU. tcp_nopush đi kèm để gom dữ liệu vào ít gói TCP đầy hơn.
ab -k -n 10000 -c 50 http://host/asset.txt # -k: dùng keep-alive
curl --compressed -w "%{size_download}" URL # byte gzip thực nhận

Hình 1: Ba đòn bẩy — sendfile (zero-copy), Cache-Control, gzip. sendfile bỏ qua hai lần copy qua user space. Đo throughput bằng ab -k, đo nén bằng curl --compressed.
Đo thật: thông lượng và nén
Dựng nginx với sendfile on, phục vụ file 68KB, rồi bắn ab:

Hình 2: ab -k 10000 request/50 đồng thời: 4301 req/s, 11.6ms/req, transfer 287923 KB/s (~281 MB/s), 0 lỗi. Cache-Control: public, max-age=3600 được gửi kèm. gzip (đo bằng curl HTTP/1.1): file page.html 156.941 byte → 1.258 byte (~124 lần, vì nội dung rất lặp).
Kết quả:
- Thông lượng cao nhờ sendfile: 10.000 request (50 đồng thời, keep-alive) chạy sạch — 4301 req/s, transfer 287923 KB/s (~281 MB/s), 0 request lỗi. Với một container giới hạn 256MB RAM phục vụ file tĩnh, đây là con số rất cao — đúng thế mạnh của nginx.
- Cache-Control giảm tải gốc: mỗi response mang
Cache-Control: public, max-age=3600— trình duyệt cache 1 giờ, khỏi hỏi lại server (như bài cache đã đo). Với asset không đổi, đây là cách giảm request tới server hiệu quả nhất. - gzip nén mạnh văn bản lặp: file
page.html156.941 byte nén còn 1.258 byte (~124 lần) — vì nội dung cố tình rất lặp lại; văn bản thật sẽ khiêm tốn hơn (như bài nén: ~10 lần).
Cái bẫy đo lường: ab dùng HTTP/1.0
Đây là bài học thực chiến quan trọng. Khi tôi thử đo gzip bằng ab -H "Accept-Encoding: gzip", số byte truyền không hề giảm — trông như nginx không nén. Nguyên nhân: ab mặc định gửi request HTTP/1.0, mà chỉ thị gzip của nginx mặc định gzip_http_version 1.1 — không nén cho HTTP/1.0. Vì vậy ab không bao giờ nhận bản gzip, dù curl (HTTP/1.1) thì nhận.
Bài học: công cụ đo có thể làm sai lệch kết quả nếu bạn không biết nó nói giao thức gì. Đo throughput thô thì ab tốt; nhưng đo tỉ lệ nén phải dùng client HTTP/1.1 (curl --compressed). Luôn kiểm chéo bằng curl -v xem Content-Encoding thực sự có xuất hiện không, đừng tin mỗi con số tổng hợp.
Đánh đổi và lưu ý
ab là công cụ cơ bản, không mô phỏng tải thật. Nó bắn cùng một URL, không cookie, không nghĩ. Cho throughput thô của một endpoint thì ổn; nhưng để mô phỏng người dùng thật (nhiều URL, có nghĩ, có phiên) cần công cụ mạnh hơn (wrk, k6, vegeta). Và đo qua Docker NAT (như bài này) có giới hạn cổng — nên dùng -k để tái dùng kết nối, tránh cạn cổng.
Đừng nén thứ đã nén, và nén động tốn CPU. Như bài nén đã nói: ảnh/video bỏ qua gzip. Với văn bản, pre-compress asset tĩnh (nginx gzip_static on phục vụ file .gz dựng sẵn) để khỏi nén lại mỗi request — nén động ở mức cao cho mỗi request là gánh CPU không cần thiết.
Con số đo phụ thuộc rất nhiều vào môi trường. 281 MB/s ở đây là trên localhost/loopback trong container — không phải thông lượng mạng thật ra Internet. Đừng chép số benchmark của người khác (hay của chính mình trên máy khác) làm mục tiêu; luôn đo trên chính hệ thống và tải của bạn.
Ba ý mang về
sendfile(zero-copy) là lý do nginx phục vụ tĩnh nhanh: đo thật 4301 req/s, ~281 MB/s, 0 lỗi trên 10.000 request keep-alive — bỏ qua hai lần copy qua user space;tcp_nopushđi kèm để gom gói.- Kết hợp Cache-Control + gzip:
max-agecho client khỏi hỏi lại (giảm request gốc), gzip nén văn bản trên dây (đo bằng curl: 156 KB → 1.2 KB với nội dung lặp) — hai đòn bẩy băng thông bổ sung cho sendfile. - Biết công cụ đo:
abmặc định HTTP/1.0 nên không nhận gzip (đo nén phải bằng curl HTTP/1.1); ab chỉ cho throughput thô, dùng-kkhi qua NAT; và mọi con số benchmark phụ thuộc môi trường — luôn tự đo, kiểm chéo bằngcurl -v.
Nguồn
- nginx docs — ngx_http_core_module (sendfile, tcp_nopush) và Serving Static Content: https://nginx.org/en/docs/http/ngx_http_core_module.html#sendfile
- ApacheBench (ab) man page: https://httpd.apache.org/docs/current/programs/ab.html
Đến đây sê-ri HTTP và Web sâu khép lại 12 phần: từ một request thô trên dây, header, mã trạng thái/redirect, keep-alive, HTTP/2, cache, cookie/session, nén, TLS handshake, chứng chỉ X.509, reverse proxy, tới tối ưu nginx tĩnh. Sợi chỉ xuyên suốt: web không phải hộp đen — mọi tầng đều xem được bằng công cụ dòng lệnh (curl, openssl, nginx, ab), và đo thật luôn dạy nhiều hơn đọc chay. Nắm được các tầng này, bạn debug được gần như mọi sự cố web, từ "sao request treo" tới "sao HTTPS đỏ" tới "sao trang chậm".