上个月我们帮一位跑 SaaS 后台的客户排障:2 核 4G 的 VPS,CPU 常年 20%,但每到下午三点网站就卡成幻灯片。云厂商监控里出站带宽曲线顶格,套餐 100 Mbps 被吃满整整四十分钟。他第一反应是「该升配了」——我们打开 Nginx 访问日志,发现 70% 的流量是未压缩的 JSON API 响应和带 ?v= 随机参数的静态 JS,同一批资源被重复全量下发。
调完 Gzip、静态缓存头和 upstream keepalive 之后,峰值带宽降到 35 Mbps,页面加载时间从 8 秒缩到 1.2 秒,一分钱没多花。这类故事在中小 VPS 上太常见了:瓶颈在出口带宽,而不在算力。Nginx 作为反向代理站在用户和后端之间,天然是「省带宽」的最佳杠杆——前提是你要知道该拧哪几颗螺丝。
本文不讲 Nginx 安装入门,而是把我们生产环境里验证过的一套带宽导向调优清单整理出来:先诊断、再压缩、再缓存、最后优化连接复用与缓冲。前提是你已用 Nginx 做 443 反代(若还没配防火墙,建议先看 云服务器 UFW 防火墙配置指南,只开 22/80/443 再谈优化)。
为什么带宽会先爆,而不是 CPU?
云 VPS 的计费带宽通常是双向不对称的:小套餐给 100 Mbps 峰值、月流量包 1–2 TB,看起来不少,但一个未压缩的 500 KB JSON 接口,每秒 200 次请求就是 800 Mbps 的理论需求——瞬间打穿。与此同时,Nginx 处理静态文件和简单反代,CPU 占用往往很低,所以监控图上会出现「CPU 很闲、带宽很满」的错位。
另一个常见误区是把所有流量都打到源站。没有 CDN、没有浏览器缓存、没有 proxy_cache,每个用户每次刷新都从 VPS 重新拉一遍 bundle.js 和 logo.png。Nginx 官方文档里列出的 http 模块,本质上都是帮你「少传字节」或「少建连接」的工具箱——关键是按场景组合使用,而不是抄一份万能配置就上线。
| 症状 | 常见根因 | 优先动作 |
|---|---|---|
| API 响应慢、带宽高 | JSON/HTML 未压缩 | 开启 gzip / brotli |
| 刷新页面流量大 | 静态资源无 Cache-Control | expires + 指纹文件名 |
| 后端连接数飙高 | 每请求新建 upstream TCP | keepalive 连接池 |
| 大文件下载占满出口 | 源站直传、无分段缓存 | CDN 或 proxy_cache |
第一步:先诊断,别盲目升带宽
动手改配置前,花十分钟确认「带宽花在哪儿」。我们在服务器上常用这组命令:
# 实时按连接看流量(需 apt install iftop) sudo iftop -i eth0 # 按小时/天统计(vnstat) vnstat -h vnstat -d # Nginx 访问日志:找体积最大的 URL awk '{print $7, $10}' /var/log/nginx/access.log | \ awk '{a[$1]+=$2} END {for(i in a) print a[i], i}' | sort -rn | head -20 # 当前 ESTABLISHED 连接数 ss -s ss -tn state established | wc -l
若日志里排前几名的是 /api/ 且响应体动辄几百 KB,压缩是第一优先级;若是 .js、.css、.woff2 且状态码全是 200(没有 304),说明缓存头没配好。还要区分入站和出站:DDoS 或爬虫会把入站打满,那是另一类问题(限速、WAF、fail2ban),本文聚焦正常业务下的出站优化。
云控制台里的带宽图有 1–5 分钟聚合延迟,排障时以服务器本机 iftop 为准。若 SSH 都变卡,参考我们的 Linux 服务器 SSH 连接被拒排查指南,先确认不是网络层故障。
第二步:Gzip / Brotli 压缩——最低成本的带宽削减
文本类响应(JSON、HTML、JS、CSS、SVG、XML)压缩率通常在 60%–80%。Nginx 内置 gzip 模块,开起来只需几行配置,对 CPU 的额外开销在大多数 VPS 上完全可以接受。
gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 5; # 6–9 收益递减,5 是性价比甜点 gzip_min_length 256; # 太小的响应不必压 gzip_types text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml font/woff2;
几个实战细节:第一,gzip_proxied any 确保反代后端返回的内容也会压缩(默认只压直连响应)。第二,图片(JPEG/PNG/WebP)和视频不要再 gzip——已经压缩过的二进制,gzip 几乎无效还浪费 CPU。第三,用 curl -H 'Accept-Encoding: gzip' -I https://your.site/api/foo 检查响应头是否有 Content-Encoding: gzip。
若编译时带了 Brotli 模块(部分发行版或自编译版本),同等质量下 Brotli 比 gzip 再省 15%–20%,适合文本 API 密集型站点。没有模块也不必强求,先把 gzip 配稳。
第三步:静态资源缓存与 sendfile
前端构建工具(Vite、Webpack)会给文件名加 content hash,例如 app.a3f2b1.js。这种文件可以长期缓存,用户第二次访问根本不走网络。问题出在很多团队只做了构建侧 hash,Nginx 侧却没发 Cache-Control,浏览器每次仍发全量请求。
location ~* \.(js|css|woff2?|ttf|ico|svg)$ {
root /var/www/app/dist;
expires 30d;
add_header Cache-Control "public, immutable";
access_log off; # 静态命中可关 access_log 减 IO
}
# 全局 http 块建议开启
sendfile on;
tcp_nopush on;
tcp_nodelay on;
sendfile 让内核直接把文件页缓存送到 socket,减少用户态拷贝,对大静态文件和高并发下载都有帮助。注意:sendfile 与 AIO、线程池等高级特性组合时要测一遍,默认 trio(sendfile + tcp_nopush + tcp_nodelay)对绝大多数站点够用。
HTML 和 index.html 不要用 immutable,否则发版后用户可能看到旧壳子。通常策略是:hash 资源 30 天,HTML no-cache(允许协商缓存)。
第四步:反向代理缓存(proxy_cache)
对读多写少的 API(配置列表、商品目录、文章详情),可以在 Nginx 层做短 TTL 缓存,源站带宽直接除以缓存命中率。这是 proxy 模块里最被低估的能力之一。
# http 块 proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=apicache:50m max_size=2g inactive=60m use_temp_path=off; upstream backend { server 127.0.0.1:3000; keepalive 32; # 见下一节 } server { location /api/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_cache apicache; proxy_cache_valid 200 5m; proxy_cache_key "$scheme$request_method$host$request_uri"; add_header X-Cache-Status $upstream_cache_status; proxy_buffering on; proxy_buffers 16 16k; proxy_busy_buffers_size 64k; } }
$upstream_cache_status 会显示 HIT、MISS 或 BYPASS,排障时一眼可知缓存是否生效。写操作(POST/PUT/DELETE)务必 bypass 缓存;带 Cookie 的个性化接口也要谨慎,避免把 A 用户的数据缓存给 B 用户。
proxy_buffering 开启时,Nginx 会先把后端响应收进缓冲区再发给客户端——后端慢、客户端快时,能避免后端连接长时间占用;若后端是流式 SSE/大文件上传,要对特定 location 设 proxy_buffering off。
第五步:upstream keepalive 减少 TCP 握手开销
默认情况下,Nginx 对每个客户端请求可能新建一条到后端的 TCP 连接。高 QPS 时,大量 TIME_WAIT 和三次握手会吃掉 CPU 和本地端口,间接让响应变慢、有效带宽下降。upstream keepalive 让 Nginx 维护一个到后端的连接池,请求完成后连接归还池中复用。
upstream backend {
server 127.0.0.1:8080;
keepalive 64; # 池大小,按 QPS 调整
}
location / {
proxy_pass http://backend;
proxy_http_version 1.1; # HTTP/1.1 才能 keepalive
proxy_set_header Connection ""; # 清除 Connection: close
}
缺少 proxy_http_version 1.1 或 Connection "" 任一,keepalive 都不会生效——这是我们在客户环境里见过最多的「配了等于没配」案例。池大小 keepalive 64 对日 PV 百万级以下通常够用,过大反而占后端文件描述符。
第六步:worker、连接数与限速
带宽打满时,还要确认 Nginx 没有成为连接数瓶颈。核心参数在 nginx.conf 顶层:
worker_processes auto;
worker_rlimit_nofile 65535;
events {
worker_connections 4096;
use epoll;
multi_accept on;
}
# 可选:按 IP 限速,防单用户占满带宽
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn:10m;
server {
limit_req zone=perip burst=20 nodelay;
limit_conn conn 50;
}
worker_connections × worker_processes 要大于峰值并发连接数。2 核机器设 worker_processes auto 通常是 2,4096 × 2 = 8192 对中小站点绰绰有余。同时把系统 fs.file-max 和 worker_rlimit_nofile 对齐,否则日志里会出现 too many open files。
limit_rate 可对单连接限速(大文件下载场景),limit_req 防刷接口。这些不直接「增加」带宽,但能防止少数异常流量拖垮全体用户——属于带宽治理的最后一道闸。
第七步:什么时候该上 CDN?
当优化做完、带宽仍周期性顶格,就该考虑 CDN 或升级套餐了。判断标准很简单:
- 静态资源占出站 60% 以上 → CDN 缓存 JS/CSS/图片,源站只回源 HTML 和 API
- 用户分布跨多个地区 → 边缘节点缩短物理距离,等效降低源站带宽压力
- 流量有突发(营销活动、直播) → CDN 扛峰值,避免源站按峰值带宽买套餐
上 CDN 后 Nginx 侧记得:源站只接受 CDN 回源 IP(防火墙白名单),并透传真实客户端 IP(X-Forwarded-For / real_ip 模块)。否则攻击者直连源站 IP,CDN 的防护和缓存就 bypass 了。
完整 server 块示例(可直接改域名部署)
把上文零散配置合成一份最小可运行示例,适合 Node / Python 后端监听 127.0.0.1:3000 的场景:
upstream app {
server 127.0.0.1:3000;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
gzip on;
gzip_types application/json application/javascript text/css text/plain;
gzip_min_length 256;
location /assets/ {
alias /var/www/app/dist/assets/;
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
}
}
改完后执行 sudo nginx -t && sudo systemctl reload nginx,用浏览器开发者工具 Network 面板确认静态资源有 30 天缓存、API 有 gzip。我们习惯在发版 checklist 里加一条:curl -sI https://app.example.com/assets/main.js | grep -i cache。
验证与持续监控
调优不是一锤子买卖。建议在 Grafana / 云监控里同时看这四条线:出站带宽、Nginx active connections、upstream 响应时间、缓存命中率(若开了 proxy_cache)。发版或流量活动前做一次对比压测:ab -n 1000 -c 50 https://app.example.com/ 或 hey -n 5000 -c 100,记录调优前后的带宽峰值和 P95 延迟。
若带宽降下来了但延迟仍高,瓶颈可能转到后端数据库或磁盘 IO——那是另一个话题。但至少你可以确定:钱应该花在加索引上,而不是盲目买 500 Mbps 套餐。
调优之外:选对节点同样省带宽
Nginx 能帮你「少传字节」,但用户到源站的物理距离没法用配置消除。若你的受众在亚太,却买了一台美东 VPS,RTT 高、重传多,有效吞吐天然打折。很多团队会把构建与预览放在云端 Mac 或离用户更近的节点,静态资源走对象存储 + CDN,源站只跑 API——带宽账单会好看很多。
VPSSPark 的 Mac mini M4 云端节点适合跑 CI 打包、内网预览和跳板运维:macOS 原生环境,Terminal 与 OpenSSH 开箱即用;待机功耗约 4W,适合 7×24 挂着当中转或构建机,比再开一台 x86 小主机更省电。配合前文 UFW 只开 443、应用绑 127.0.0.1 的架构,攻击面和带宽浪费都能压住。
如果你正在规划一套「省带宽、好运维」的部署架构, VPSSPark 云端 Mac mini 是低功耗跳板与构建节点的务实选择—— 立即了解套餐方案 ,让 Nginx 调优成果不被错误的节点选址抵消。