Nginx作为高性能的反向代理服务器,本身的事件驱动架构可以轻松支撑数万并发连接。但在实际生产环境中,不少运维同学遇到过一种奇怪的现象:流量高峰期请求延迟突然抖动,worker进程CPU占用不高,网络也没有问题,最后定位下来居然是日志写入环节出了问题。这类由日志引起的处理链路阻塞,通常被称为日志流水线停顿。它不是Nginx崩溃或者报错,而是日志这个本该异步旁路的动作,因为配置不当变成了拖慢整个请求处理的关键路径。

一、理解Nginx日志的写入机制
要弄清楚停顿是怎么产生的,先要知道Nginx写日志的完整流程。当一次请求结束时,Nginx会按照log阶段的组织规则,把变量格式化成一条日志记录。如果access_log指令没有配置buffer参数,这条日志会在请求结束时直接调用write系统调用写到文件里。这意味着每一次请求都要经历一次磁盘IO,哪怕操作系统有页缓存兜底,write调用本身的开销和锁竞争在高并发下依然不可忽视。
配置了buffer之后行为就完全不同了。日志先写入一块用户态的内存缓冲区,缓冲区写满或者达到了flush指定的时间间隔,才会真正落盘。这个设计把成千上万次小写入合并成少量大块写入,极大降低了系统调用的次数。但缓冲区也是停顿问题的第一个来源:当缓冲区恰好写满时,触发写盘的那次请求会同步等待刷盘完成,如果此时磁盘正忙,这个请求的处理时间就被硬生生拉长了。
另外需要区分两条日志链路。access_log记录的是访问日志,走的是上述缓冲机制;而error_log记录的是错误日志,它的写入路径不同,且不受buffer参数控制。error_log的输出级别如果设置得过低,比如debug级别,Nginx会打印海量调试信息,这些信息大多是同步写入的,很容易把worker进程拖住。这也是很多人调优了access_log却依然有停顿的原因之一。
二、造成流水线停顿的常见原因
第一种典型场景是缓冲区配置过小。access_log /var/log/nginx/access.log main buffer=32k;这样的配置在中小流量下没问题,但在每秒数万请求的场景下,32k的缓冲区几毫秒就会被填满,刷盘频率过高,缓冲的意义基本丧失,同时还带来周期性的同步等待。反过来,缓冲区设置得过大也有隐患,flush间隔如果没配,日志可能长时间停留在内存里,排查问题时发现日志迟迟不出来,甚至进程异常退出时丢失大量日志。
第二种场景是磁盘IO本身成为瓶颈。日志文件所在的磁盘如果同时承载着数据库写入、其他服务的读写,或者使用的是低性能的云盘,随机IO延迟会显著上升。Nginx的worker进程在刷盘时被阻塞,而每个worker是一个单线程的事件循环,一旦这个循环被卡住,这个worker负责的所有连接全部停摆。表现出来就是部分请求正常、部分请求超时,非常具有迷惑性。
第三种场景和日志轮转有关。logrotate默认采用移动文件加新建文件的方式,如果脚本里没有通知Nginx重新打开文件句柄,Nginx会继续往已经被改名旧的文件描述符上写,新文件一直是空的,磁盘空间也可能因为旧句柄未释放而异常。更危险的是某些轮转脚本会发送stop再start信号,重启期间请求直接中断,形成明显的服务停顿窗口。
三、排查思路与优化配置
排查时建议先用iostat观察磁盘的util和await指标,确认刷盘时刻是否存在IO等待飙升;再用strace -p <worker_pid> -e trace=write观察worker的write调用频率和耗时。如果看到大量小尺寸的write调用,说明buffer没有生效或者配得太小。
优化日志写入,可以参考下面这套配置思路:
http {
# 日志格式精简,减少每条记录的格式化开销
log_format main '$remote_addr - $upstream_response_time '
'$request_time "$request" $status $body_bytes_sent';
# 缓冲区设为64k,3秒或写满即刷盘,并压缩缓冲区减小内存占用
access_log /var/log/nginx/access.log main buffer=64k flush=3s gzip=1;
# 错误日志级别至少warn,生产环境严禁debug
error_log /var/log/nginx/error.log warn;
# 大流量场景可以直接发往syslog,彻底剥离磁盘IO
# access_log syslog:server=127.0.0.1:514,facility=local7 main;
}
这套配置的核心逻辑有三点:一是用buffer加flush组合,把落盘节奏控制在一个可预期的范围内,避免缓冲区写满触发的同步等待过于频繁;二是把error_log级别收紧,砍掉不必要的调试输出;三是提供syslog方案作为备选,把日志写入完全交给专门的采集进程,Nginx侧只做网络发送,磁盘压力与本机解耦。
日志轮转的正确姿势是配置完轮转后给Nginx发送USR1信号,让它重新打开日志文件:
/var/log/nginx/*.log {
daily
rotate 30
missingok
notifempty
compress
sharedscripts
postrotate
# 通知Nginx重新打开日志文件句柄,不影响正在处理的请求
[ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
endscript
}
除了本机文件方案,规模较大的系统建议在Nginx前面再挂一层日志采集,比如用Filebeat tail日志文件输出到Kafka或者Elasticsearch,采集端自身做背压控制。这样即使后端存储抖动,也不会反过来影响Nginx的写入。对于极端高并发场景,还可以考虑把access_log的记录内容做减法,去掉昂贵的变量比如需要额外计算的$request_body,只保留监控和排障真正需要的字段,从源头上降低日志环节的整体成本。
总结一下,日志流水线停顿本质上是把异步的旁路操作变成了请求关键路径上的同步等待。把缓冲参数调对、控制好error_log级别、规范日志轮转流程、必要时外置日志采集,这几步做下来,绝大多数由日志引发的高峰期抖动都能消除,让Nginx回到它应有的高性能状态。
Nginx日志access_log流水线停顿修改时间:2026-09-15 23:46:45