Nginx 日志切割两种方案对比:外部脚本 vs 配置文件原生切割收藏(二丫讲梵)备查
版本说明
以下配置在 Nginx 1.18+ 验证有效,$time_iso8601 等内置变量多年未变,可用于任意近期版本。
Nginx 的访问日志和错误日志默认不会自动切割,放任不管一天天变大,等到几个 G 才发现,排查问题时打开都费劲。切割日志有两种思路,选哪种取决于 Nginx 是怎么部署的。
# 1. 方案一:外部脚本 + logrotate 思路(传统部署)
如果 Nginx 是直接跑在宿主机上的传统部署方式,最简单的做法是写一个脚本,配合 cron 定时执行:
#!/bin/bash
Date=`date -d '-1 day' '+%Y-%m-%d'`
cd /var/log/nginx && mkdir logs/$Date
for i in access.log error.log
do
gzip -c $i > logs/$Date/"$i"_"$Date".gz
echo " " > $i
find logs/ -ctime +30 | xargs rm -rf
done
2
3
4
5
6
7
8
9
这个脚本做三件事:把当前日志压缩归档到按日期命名的目录,清空原日志文件(echo " " > $i 而不是删除文件——删除会导致 Nginx 仍然写向已被删除的 inode,日志实际上并未真正切换),再清理 30 天前的旧归档。
坑:echo " " > $i 只是清空文件内容,Nginx 进程持有的文件描述符没有改变,这种"清空法"对追加写入(open 一次、之后一直 write)的日志是安全的;但如果用 mv/rm 之后新建同名文件,Nginx 还在往旧 inode 写,新文件永远是空的,必须额外给 Nginx 发 USR1 信号让它重新打开日志文件(这也是标准 logrotate + nginx 配置里 postrotate 阶段做的事)。
这个方案的局限:如果 Nginx 是容器化部署、日志只是 volume 挂载出来的,logrotate/外部脚本切割时还得想办法给容器内的 Nginx 进程发平滑重载信号,跨容器发信号并不直接,操作起来不优雅。
# 2. 方案二:Nginx 配置文件原生切割(容器化部署更友好)
容器场景下更干净的做法是完全不依赖外部工具,用 Nginx 自己的变量在配置层面直接生成按时间分文件的日志路径——这样每次日志滚动本质上就是"换了个文件名",不存在文件描述符失效的问题。
按天切割:
if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})") {
set $year $1;
set $month $2;
set $day $3;
}
access_log /var/log/nginx/$year-$month-$day-access.log;
2
3
4
5
6
基于内置变量 $time_iso8601(ISO8601 格式时间戳)用正则捕获出年月日,拼进 access_log 的文件路径里——Nginx 每次写日志时按当前请求的时间戳决定写入哪个文件,日期变化时自动"切"到新文件,全程不需要重载进程。
如果想切得更细(比如按小时甚至按秒),把正则和变量都扩展到时分秒:
if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})") {
set $year $1;
set $month $2;
set $day $3;
set $hour $4;
set $minutes $5;
set $seconds $6;
}
access_log /data/log/test/nginx-access-$year-$month-$day-$hour-$minutes-$seconds.log json;
2
3
4
5
6
7
8
9
坑:if 指令在 Nginx 配置里非常受限(俗称"if is evil"),这里的用法只能放在 server 块内,不能直接写在 http 全局块,否则会报配置解析错误。
# 结合自定义日志格式,按域名+小时切分
多域名共用一个 Nginx 实例时,可以把日志格式定义抽成公共文件,配合按小时切割,方便按站点单独归档分析:
$ vim /usr/local/nginx/conf/log_format.conf
log_format json escape=json '{"remote_addr": "$remote_addr",'
'"@timestamp": "$time_iso8601",'
'"request_uri": "$request_uri",'
'"verb": "$request_method",'
'"httpversion": "$server_protocol",'
'"response": "$status", '
'"body_bytes_sent": "$body_bytes_sent", '
'"referrer": "$http_referer", '
'"user_agent": "$http_user_agent", '
'"http_x_forwarded_for": "$http_x_forwarded_for", '
'"server_name": "$host",'
'"request_time": "$request_time",'
'"upstream_response_time": "$upstream_response_time",'
'"realpath_root": "$realpath_root",'
'"request_body": "$request_body",'
'"nginx_version": "$nginx_version",'
'"scheme": "$scheme"}';
if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})") {
set $year $1;
set $month $2;
set $day $3;
set $hour $4;
}
access_log /home/nginx/logs/${server_name}-${year}-${month}-${day}-${hour}_access.log json;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
再在具体 server 块里引用:
server {
listen 80;
server_name example.com;
charset utf-8;
include log_format.conf;
location / {
try_files /_not_exists_ @backend;
}
location @backend {
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://<PUBLIC_IP>:8180;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
${server_name} 变量让不同虚拟主机自动生成各自的日志文件,配合 json 格式方便直接喂给 ELK/Loki 这类日志系统解析。
验证:配置生效后如果日志没有新生成,先检查 Nginx 进程对目标目录是否有写权限,再实际发一次请求触发日志写入——nginx -t 语法检查通过不代表运行时权限没问题。
# 3. 该选哪种
| 外部脚本 + logrotate | 配置文件原生切割 | |
|---|---|---|
| 适用场景 | 传统宿主机部署 | 容器化 / 日志只是 volume 挂载 |
| 切割粒度 | 通常按天,改粒度要改脚本+cron | 配置里改正则粒度即可到秒级 |
| 依赖 | cron、外部脚本、给 Nginx 发信号 | 无外部依赖,Nginx 自身完成 |
| 归档/清理旧日志 | 脚本里顺手做(find -ctime 清理) | 需要额外的清理机制(如日志系统的保留策略) |
# 4. 可复用要点
- 判断依据只有一条:Nginx 是否容器化部署。宿主机部署选外部脚本方案更简单直接;容器场景选配置文件原生切割,避免跨容器发信号的麻烦。
- 外部脚本切割日志文件时,"清空文件内容"和"删除重建文件"效果完全不同——后者必须配合给 Nginx 发
USR1重新打开日志句柄。 - Nginx 配置里的
if指令能用的场景很受限,日志路径这种用法必须写在server块内。
- 02
- ORDER BY 配合 LIMIT 触发的索引选择陷阱 原创08-07
- 03
- Nginx 运维知识地图:从配置基础到反向代理实战 原创07-29