Nginx 的日志子系统看起来只是把请求信息写到磁盘,实际上它必须经过内存缓冲、格式化、系统调用三个环节。内存碎片问题主要出现在缓冲环节。当 access_log 启用缓冲后,每个 worker 进程会先从内存池中申请一块缓冲区,日志先写入缓冲区,达到阈值或超时后再一次性写入文件。如果缓冲区大小设置不当,或者日志写入频率过高,就会产生大量小块内存的分配与释放,这些小块内存很难被后续请求复用,最终形成内存碎片。

一、Nginx 日志内存碎片的成因与表现
Nginx 采用多进程事件驱动模型,每个 worker 进程独立处理请求并独立记录日志。默认情况下,access_log 没有设置 buffer 参数时,每一条访问日志都会触发一次 write 系统调用。虽然这种做法最直接,但高并发下系统调用开销极大,因此生产环境通常会为 access_log 配置 buffer。配置 buffer 后,Nginx 会先在内存中聚合日志,但当缓冲区被填满或达到 flush 时间后,就需要申请新的缓冲区继续写入。如果缓冲区比较小,比如 4K 或 8K,那么在每秒数千甚至数万请求的场景下,缓冲区会频繁被写满并释放,内存分配器需要不停地在堆上寻找合适大小的空闲块,碎片由此产生。
从内存分配器角度看,glibc 的 malloc 对小内存块采用 fastbins、tcache 等机制,虽然能提升分配速度,但在长期运行、频繁申请和释放不同大小内存时,容易出现空闲块过于分散的情况。Nginx 自身的 ngx_pool 内存池虽然对请求生命周期内的内存做了聚合管理,但日志缓冲区的生命周期跨越多个请求,不能完全套用请求级内存池的回收逻辑。因此日志缓冲相关的内存往往直接依赖系统分配器,碎片化概率更高。
碎片最直观的表现是 worker 进程的 RSS 持续增长,即使请求量没有明显增加,内存占用也不会回落到初始水平。可以通过 /proc/pid/status 或 /proc/pid/smaps 查看进程内存细节。当发现 PSS 增长明显高于实际业务数据,或者内存无法及时归还给操作系统时,就应该考虑日志缓冲区的碎片问题。典型场景是压测后 Nginx 内存从几十 MB 涨到几百 MB,重启后恢复正常,但运行一段时间后又缓慢上升。
二、调整日志缓冲与刷盘参数缓解碎片
最直接的整理手段是在 access_log 指令中合理设置 buffer 和 flush 参数。buffer 指定缓冲区大小,flush 指定最长刷新时间。例如下面这行配置:
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access.log main buffer=64k flush=5s;
}这里使用了 64K 的缓冲区,并且每 5 秒强制刷盘一次。在大流量站点,单条访问日志经过格式化后大约在 150 到 300 字节,64K 缓冲可以容纳几百条日志,能够明显减少小块内存的申请次数。缓冲区越大,单位时间内日志写入系统调用的次数越少,内存分配器处理小块分配的压力也越小。但缓冲区不是越大越好,过大的 buffer 会延迟日志的实时性,一旦 worker 崩溃,缓冲区中尚未刷盘的日志会丢失。flush 参数则保证即使缓冲区没有写满,也会在指定时间后刷盘,兼顾可观测性。
如果访问日志量极大,可以适当把 buffer 提高到 128K 甚至 256K,并把 flush 调整到 10 秒或 15 秒。需要结合日志分析系统的延迟容忍度来决定。对于 error_log,它本身不接受 buffer 参数,但可以通过把错误日志输出到 syslog 或关闭不必要级别的日志来减少写入频率。例如把 error_log 设置为 warn 级别,避免 info 和 debug 日志产生过多小块内存分配。
另一个相关参数是 gzip。access_log 支持 gzip 压缩,例如 gzip=6。压缩会消耗 CPU,但能减少磁盘写入量和刷盘频率,间接降低内存缓冲区的更替速度。不过压缩本身也需要临时缓冲区,如果 CPU 已经紧张,则不建议开启。总体来看,调整 buffer 和 flush 是成本最低、见效最快的碎片整理方式。
三、使用 syslog 转发降低 Nginx 自身内存压力
如果 Nginx 本地日志缓冲仍然造成明显的内存碎片,可以考虑把日志直接转发给系统日志服务,由 rsyslog 或 systemd-journald 负责磁盘写入。Nginx 的 access_log 和 error_log 都支持 syslog 协议。配置方式如下:
error_log syslog:server=127.0.0.1:514 warn; access_log syslog:server=127.0.0.1:514 main buffer=32k flush=3s;
在这种方案下,Nginx 进程只负责把日志内容发送到本地 Unix socket 或 UDP 端口,不再直接写文件。由于 syslog 服务通常使用更大的缓冲区和独立的写盘策略,Nginx 内部需要维护的日志缓冲可以更小,同时也能借助 syslog 的异步写盘能力降低阻塞概率。需要注意的是,syslog 传输需要内核网络栈参与,如果使用 UDP 协议,极端情况下可能丢失日志;使用本地 Unix socket 或 TCP 则能提供更高可靠性,但实现稍复杂。
使用 syslog 后,Nginx 侧的内存碎片主要来自发送日志时使用的临时缓冲区。仍然可以通过设置较小的 buffer 和合理的 flush 来控制分配频率。此外,rsyslog 本身也需要调优,例如设置合适的队列大小和写盘模式。这样整体上日志功能的内存压力被分散到专门的日志服务进程中,Nginx worker 的 RSS 增长会明显变缓,碎片整理更简单。
四、借助日志切割、worker 隔离与 jemalloc 深度优化
日志切割虽然不能直接整理内存碎片,但可以配合 Nginx 的日志缓冲策略,减少单个日志文件过大带来的额外开销。使用 logrotate 定期切割日志并发送 USR1 信号让 Nginx 重新打开日志文件,可以防止文件增长到影响文件系统性能的程度。不过要注意,USR1 信号会触发 worker 重新打开文件,如果日志缓冲未满,Nginx 会先刷新缓冲区再切换文件,这个过程可能短暂增加内存分配。
从进程层面看,适当控制 worker_processes 数量能避免多个 worker 各自维护日志缓冲导致的内存叠加。通常 worker 数量等于 CPU 核心数即可,过多 worker 只会增加内存碎片总量。同时可以设置 worker_rlimit_nofile 限制打开文件数,避免日志文件句柄过多影响内存回收。
更彻底的优化是替换内存分配器。Nginx 默认使用系统 glibc malloc,而 jemalloc 对多线程、多进程下的小块内存分配和碎片控制表现更好。编译 Nginx 时可以加入如下参数:
./configure --with-ld-opt=-ljemalloc
编译后使用 jemalloc 的 Nginx 在高并发日志写入场景下,内存碎片增长通常比 glibc 更平缓。可以配合监控脚本定期统计 /proc/pid/smaps 中的 PSS 值和 Glibc 分配器状态。例如:
grep VmRSS /proc/$(cat /run/nginx.pid)/status
grep Pss /proc/$(cat /run/nginx.pid)/smaps | awk '{sum += $2} END {print sum " kB"}'通过对比调整前后同一时间段内的内存增长曲线,可以评估碎片整理效果。如果 RSS 仍然持续增长,还可以检查日志格式中是否包含过多变量,因为每个变量解析都会产生临时字符串,这些字符串在日志缓冲区拼装时也会占用内存。精简 log_format 字段不仅能降低 CPU 消耗,也能减少缓冲区的内存压力。综合运用这些手段,Nginx 日志内存碎片完全可以在不牺牲日志功能的前提下得到有效控制。