Nginx日志切割应避免依赖系统时间,改用文件状态判断、固定命名+原子重命名、size主触发+copytruncate、秒级dateformat、stat校验时间合理性,并从源头限制时间跳变。

系统时间跳变(如 NTP 大步校准、手动修改时间、虚拟机时钟漂移恢复)会导致 Nginx 日志切割错乱:logrotate 按时间判断该不该切,却看到“昨天”变成了“三天前”,或“今天”突然回退;脚本里用 date -d "yesterday" 生成的文件名可能重复、缺失,甚至覆盖旧日志。
核心原则:不依赖系统时间做切割决策
所有基于 date 命令动态计算日期的方案,在时间跳变时都不可靠。真正健壮的做法是:
- 用 文件状态而非系统时间判断是否切割:例如检查 access.log 是否存在、是否非空、最后修改时间是否早于当前小时/天
- 切割动作本身使用 固定格式命名 + 原子重命名,避免因时间倒流导致文件名冲突
- 确保 USR1 信号只在重命名后发送,防止 Nginx 继续往已移动的文件写入
logrotate 配置防跳变关键项
默认的 daily 或 hourly 触发方式会受系统时间影响。应改用更稳定的触发逻辑:
- 启用
size 100M(或合理阈值)作为主触发条件,时间参数仅作兜底:daily改为hourly并配合maxage 24,但核心靠大小驱动 - 必须加
copytruncate:先拷贝再清空原文件,不依赖重命名,规避时间跳变下mv的语义混乱 - 禁用
dateext或改用dateformat -%Y%m%d-%s:加入秒级时间戳,即使日期倒退,文件名仍唯一 - 在
postrotate中增加校验:[ $(stat -c "%Y" /var/log/nginx/access.log 2>/dev/null) -gt $(date -d '1 hour ago' +%s 2>/dev/null) ] || exit 0,防止时间异常时误执行
Shell 脚本切割的容错写法
若不用 logrotate,自定义脚本需主动防御时间跳变:
- 用
stat -c "%y" access.log | cut -d' ' -f1获取日志文件自身最后修改日期,而不是用date算“昨天” - 重命名时强制用当前真实时间戳:
mv access.log access_$(date +\%Y\%m\%d_\%H\%M\%S).log,不拼接任何“昨日”“今日”逻辑 - 添加时间合理性检查:
if [ $(date +%s) -lt $(( $(stat -c "%Y" access.log 2>/dev/null) + 3600 )) ]; then echo "time jump detected, skip"; exit 1; fi - USR1 发送前确认 pid 文件存在且内容合法,避免向不存在的进程发信号
从源头减少跳变影响
预防优于补救。系统时间本身就不该频繁跳变:
- chrony 配置中启用
makestep 1.0 -1,只允许首次启动大步校准,日常必须走 slewing 渐调 - 禁止任何人工执行
date -s或hwclock --set,所有时间调整交由 chronyd 自动完成 - 云主机或虚拟机中,关闭 VMware Tools / Hyper-V Integration Services 的时间同步功能,仅保留 chronyd
- 在 Nginx 日志格式中加入
$msec(毫秒级时间戳),它来自gettimeofday(),不受 NTP slewing 影响,可用于事后校验日志时间线是否连续


















