大带宽服务器的Nginx/Apache新手避坑指南 - 20261002
大带宽服务器本身不骗人,10Gbps 就是 10Gbps。骗人的是配置里那几个看起来像"性能开关"的参数——它们大部分时候要么互相打架,要么被你用错了地方。下面六个坑,全是拿真机跑出来的。
一、sendfile、aio、directio:三个都开,等于三个都没生效
<!-- 新手最爱抄的"性能全家桶" -->
location /download/ {
sendfile on;
aio on;
directio 4m;
output_buffers 4 256k;
tcp_nopush on;
}
这五行在很多"Nginx 调优大全"里都能见到,抄的人大概觉得开关越多性能越好。真实情况是这三个东西在 Linux 上互相踩。
规则就两条:
directio一旦对某个文件生效,该文件就以O_DIRECT打开,IO 绕开 page cache、数据直接落到用户态缓冲区——这条路径上根本没有 sendfile 什么事,sendfile 被自动旁路。aio on在 Linux 上只对 O_DIRECT 打开的文件有效。你只写aio on不写directio,它就是个空开关,内核收不到任何io_submit。
所以上面那段配置的实际效果是:小于 4MB 的文件走 sendfile,aio on 白写;大于 4MB 的文件既不进 page cache 也不走 sendfile,而是把数据读进用户态再 write 到 socket——大带宽下这一趟多出来的内存拷贝非常显眼,perf top 里能看到 copy_user_enhanced_fast_string 一路飙升。
想证实这件事,别猜,直接数系统调用(strace 本身有开销,在测试机上做):
# 一边下载一个 1GB 文件,一边跑这个 timeout 10 strace -f -c -p $(cat /run/nginx.pid) 2>&1 | egrep 'sendfile|write|read'
如果 sendfile 计数是 0、write 上千次,说明当前压根没在搞零拷贝。用 bpftrace 看得更干净:
bpftrace -e 'tracepoint:syscalls:sys_enter_sendfile64 { @[comm] = count(); }'
正确写法是按数据特征分两个 location,别混着来:
# A. 热数据集能整个塞进 page cache —— 走零拷贝
location /static/ {
sendfile on;
tcp_nopush on;
}
# B. 单文件几百 MB 起,反复冲刷 page cache —— 绕开缓存,别让 worker 阻塞
location /big/ {
sendfile on; # 小于阈值的文件继续享受零拷贝
directio 32m; # 超过 32MB 走 O_DIRECT
directio_alignment 4k; # ext4/XFS 上给 4k,硬 RAID 上给 512k
aio threads; # 线程池,不是内核 AIO,worker 不阻塞在 read 上
}
注意 aio threads 和 aio on 完全不是一回事:前者是 Nginx 自己的线程池扛阻塞读,后者才是走 io_submit。大文件顺序读用线程池就够了,io_submit 的队列深度在单流顺序读上帮不上什么忙。
二、HTTPS 一开,前面的 sendfile 就是白写的
这条知道的人不多:Nginx 对 SSL 连接从来不走 sendfile。原因不复杂——加密必须在用户态做,数据得先从内核搬进 OpenSSL 的缓冲区,加密结果再 write 回内核,两趟拷贝省不掉。
# 统计 10 秒内 sendfile64 的调用次数,先打 HTTP 端口 perf stat -e 'syscalls:sys_enter_sendfile64' -a -- sleep 10 # 换成压 443,计数大概率是 0
这不是 bug,是物理限制。绝大多数发行版编译出来的 Nginx 也不带 kTLS,所以别指望了。HTTPS 大带宽下真正该调的是这个:
server {
listen 443 ssl http2;
ssl_buffer_size 64k; # 默认 16k
}
ssl_buffer_size 决定单条 TLS record 最大能装多少明文,默认 16k。发一个 100MB 的文件,意味着至少 6400 次 OpenSSL 写操作;调到 64k 后这个数字降到 1600 左右,系统调用开销立刻下来。
但要把话说全:大 record 是有代价的。小响应场景下它会攒着数据不发,TTFB 直接变差。纯 API 服务反而应该调小:ssl_buffer_size 4k;。静态分发和 API 网关,这两个值不要共用一份 conf。
三、先跪的通常是存储,不是网络
10Gbps ≈ 1250MB/s。在你动 Nginx 任何一个参数之前,先测一下盘到底能不能吐出这么多字节。
# 别用 dd 默认的 512 字节块,直接上 fio 打顺序读
fio --name=seqread --rw=read --bs=1M --direct=1 --iodepth=32 \
--numjobs=4 --size=8G --filename=/data/fio.test \
--runtime=30 --time_based --group_reporting
同时看 iostat,重点不是 %util(NVMe 上意义有限),是 rMB/s 和 await:
iostat -xm 1 5 # rMB/s = 实测顺序读带宽 # await = 单次 IO 平均延迟 # aqu-sz = 平均队列深度
如果 rMB/s 卡在 200 上下、await 却在两位数毫秒,那么 10Gbps 端口对你来说就是个装饰品,Nginx 参数调到天亮也没用。
最阴的一类情况是云盘:不少产品的吞吐是按容量配额下发的,容量小、吞吐上限就低,和你的带宽包之间毫无关联。我们线上那批轻云互联的 10Gbps 机器之所以敢直接上生产压测,原因就在这里——NVMe 裸读能稳定跑到 3GB/s 以上,端口才真正喂得满。存储这一层如果先天不足,后面所有优化都是在给漏水的桶擦地。
四、gzip_types 里加 application/octet-stream,是在给 CPU 上刑
gzip on; gzip_types text/plain application/json application/octet-stream; # ← 灾难在这行 gzip_comp_level 6;
application/octet-stream 会命中 zip、7z、mp4、iso 这些本来就熵饱和的数据。Deflate 对它们几乎压不动,CPU 却要全功率跑一遍。实测一下就很直观:
head -c 200M /dev/urandom > /tmp/rand.bin gzip -6 -c /tmp/rand.bin | wc -c # 输出在 200000000 上下浮动,等于一个字节没省
更糟的是 gzip_comp_level:默认值 1 和手动调到 6 相比,普通文本也就多省 3~5% 的体积,CPU 却多烧好几倍。大带宽服务器上带宽本来就不贵,CPU 才是稀缺资源,这笔账怎么算都是亏。
正确姿势是运行时零压缩——预压缩 + gzip_static:
gzip on; gzip_min_length 1k; gzip_comp_level 2; gzip_types text/plain text/css text/xml application/json application/javascript image/svg+xml; # 视频 / 压缩包 / ISO 一律排除 gzip_static on; # 直接发同目录下预压好的 .gz,不现压 gzip_vary on; # 前面挂 CDN 时必须有,否则缓存串味
五、Apache 的 EnableMMAP,送你一个 SIGBUS
# 默认就是这样,绝大多数人从没改过 EnableMMAP On EnableSendfile On
Apache 的 EnableMMAP On 会让子进程把文件 mmap 进地址空间。平时没问题,但只要文件在传输过程中被原子替换或截断——rsync 的默认行为、CI 部署的默认行为——已经映射了旧 inode 的子进程去访问越界页,内核直接甩一个 SIGBUS 过来,子进程当场死。prefork 模式下就是一片子进程重启风暴,日志里全是 Bus error。
复现只要两条命令:
while true; do curl -s -o /dev/null http://127.0.0.1/big.iso; done & for i in $(seq 1 200); do cp /tmp/new.iso /var/www/html/big.iso; done grep -iE "SIGBUS|Bus error|SIGSEGV" /var/log/httpd/error_log
修法没有别的选择,关掉:
EnableMMAP Off # 在某些文件系统上(overlayfs、NFS、部分 FUSE 挂载)sendfile 会返回 EINVAL, # 表现为请求直接 500,error_log 持续刷 "sendfile: Invalid argument" EnableSendfile Off
如果你的 error_log 里出现 sendfile: Invalid argument,说明这个挂载点压根不支持 sendfile,别硬扛,关掉用普通 read/write。关掉之后多出来的那点拷贝开销,比请求 500 便宜一万倍。
六、access_log 不加 buffer,最先炸的是系统盘
# 默认无缓冲,每个请求一次 write 系统调用 access_log /var/log/nginx/access.log main;
大带宽服务器上如果跑的是小文件高 QPS,这一行能把系统盘写穿——很多云主机系统盘 IOPS 本来就紧巴巴的。加上 buffer 立刻不一样:
access_log /var/log/nginx/access.log main buffer=64k flush=5s; open_log_file_cache max=1000 inactive=20s valid=1m min_uses=2;
代价要说清楚:flush 周期内进程被 kill -9,缓冲区里的日志会丢(正常退出会刷新)。对审计有要求的场景用 flush=1s 折中,别直接关 buffer。
还有一点常被忽略:日志字段本身也是 IO。$http_user_agent 和 $http_referer 平均能占掉单行一半以上的体积,静态分发场景里这两个字段基本没人看,从 log_format 里删掉,同样的 QPS 下日志盘写入量直接砍半。
大带宽服务器的调优顺序永远是:先确认存储吐得出来 → 再确认协议栈路径对得上 → 最后才去抠那些零碎参数。反过来做,你会花一整晚改 nginx.conf,然后发现瓶颈在盘上。