使用 logrotate 滚动 Docker 容器内的 Nginx 的日志
Nginx
没有提供开箱即用的日志滚动功能,而是将其交给使用者自己实现。你既可以按照官方文档的建议通过编写脚本实现,也可以使用 logrotate
管理日志。但是和在普通场景下不同,在使用 Docker
运行 Nginx
时,你可能需要额外考虑一点细节。本文记录了在为 Docker
中的 Nginx
的日志文件配置滚动功能过程中遇到的一些问题和思考。
Nginx 滚动日志
官方文档
In order to rotate log files, they need to be renamed first. After that USR1 signal should be sent to the master process. The master process will then re-open all currently open log files and assign them an unprivileged user under which the worker processes are running, as an owner. After successful re-opening, the master process closes all open files and sends the message to worker process to ask them to re-open files. Worker processes also open new files and close old files right away. As a result, old files are almost immediately available for post processing, such as compression.
根据官方文档的解释,滚动日志文件的流程应如下,你可以自己编写 Shell
脚本配合 crontab
实现定时滚动功能。
- 首先重命名日志文件
- 之后发送
USR1
信号给Nginx
主进程,Nginx
将重新打开日志文件 - 对日志文件进行后处理,比如压缩(可选)
1 | mv access.log access.log.0 |
说明
- 在没有执行
kill
命令前,即便已经重命名了日志文件,Nginx
还是会向重命名后的文件写入日志。因为在Linux
系统中,系统内核是根据文件描述符定位文件的。 USR1
是自定义信号,软件的作者自己确定收到该信号后做什么。在Nginx
中,主进程收到信号后,会重新打开所有当前打开的日志文件并将它们分配给一个非特权用户作为所有者,工作进程就是在该所有者下运行的。成功重新打开后,主进程关闭所有打开的文件并向工作进程发送消息,要求它们重新打开文件。工作进程也打开新文件并立即关闭旧文件。
使用 logrotate
logrotate is designed to ease administration of systems that generate large numbers of log files. It allows automatic rotation, compression, removal, and mailing of log files. Each log file may be handled daily, weekly, monthly, or when it grows too large.
logrotate
旨在简化生成大量日志文件的系统的管理。它允许自动滚动、压缩、删除和邮寄日志文件。每个日志文件可以在每天、每周、每月或当它变得太大时处理。Linux
一般默认安装了 logrotate
。
默认配置文件
查看默认配置文件:cat /etc/logrotate.conf
。
1 | # see "man logrotate" for details |
配置信息所在目录
查看日志滚动的配置信息所在的目录:ls /etc/logrotate.d/
。
1 | alternatives apport apt bootlog btmp dpkg rsyslog ubuntu-advantage-tools ufw unattended-upgrades wtmp |
为 Nginx 新增配置
为 Nginx
新增日志滚动配置,vim /etc/logrotate.d/nginx
。
1 | /path/to/your/nginx/logs/*.log { |
验证配置和测试
测试配置文件是否有错误,logrotate -d /etc/logrotate.d/nginx
。
强制滚动:logrotate -f /etc/logrotate.d/nginx
一些坑
/var/lib/logrotate/ 权限问题
当你使用校验过配置文件的正确性后,尝试强制滚动时,可能会遇到报错。
1 | logrotate -f /etc/logrotate.d/nginx |
这是因为 logrotate 会在 /var/lib/logrotate/
目录下创建 status
文件。查看目录权限可知,需要以 root
身份运行 logrotate
。
1 | ll /var/lib/logrotate |
事实上,logrotate -d /etc/logrotate.d/nginx
命令也会读取 /var/lib/logrotate/status
,但是 other
对该目录也有 r
读取权限,所以没有报错。
1 | logrotate -d /etc/logrotate.d/nginx |
日志文件夹的权限
即使你使用 root
身份运行 logrotate
,你可能还会遇到以下报错
1 | logrotate -f /etc/logrotate.d/nginx |
你需要在配置文件中,使用 su <user> <group>
指定日志所在文件夹所属的用户和组,logrotate
才能正确读写。
由宿主机还是容器主导
首先 Nginx
的日志文件夹通过挂载映射到宿主机,日志滚动既可以由宿主机主导,也可以由容器主导,不过不论如何我们都需要向 Docker
容器内的 Nginx
发送 USR1
信号。有人倾向于在容器内完成所有工作,和宿主机几乎完全隔离;我个人更青睐于由宿主机主导,因为容器内的环境并不总是拥有你想要使用的软件(除非你总是定制自己使用的镜像),甚至标准镜像往往非常精简。
在 logrotate
配置中的 postrotate
部分添加脚本,使用 docker exec
在容器内执行命令,完成向 Nginx
发送信号的工作。脚本的处理逻辑大概是“如果存在 /var/run/nginx.pid
,就执行 kill -USR1 \`cat /var/run/nginx.pid\`
命令,并打印成功的消息”。但是我看到很多文章中分享的配置类似下面这样:
1 | docker exec nginx sh -c "if [ -f /var/run/nginx.pid ]; then kill -USR1 $(docker exec nginx cat /var/run/nginx.pid); fi" |
经过测试都会有以下报错,我不清楚是否大多是抓取发布的文章,也不清楚他们是否测试过,对于 Shell
脚本写得不多的我来说,半夜测试反复排查错误真是头昏脑胀。在我原先的理解里,-c
后面的脚本是整体发到容器内部执行的,后来我才意识到,我对脚本内部的命令在宿主机还是容器里执行的理解是错误的。
1 | cat: /var/run/nginx.pid: No such file or directory |
继续写入旧日志文件
在使用 logrotate -f /etc/logrotate.d/nginx
测试通过后的第二天,发现虽然创建了新的日志文件,但是 Nginx
继续写入到旧的日志文件。这不同于网上很多文章提到的“没有发送 USR1
信号给 Nginx
”的情况。
尝试手动发送信号,观察效果。
1 | docker exec nginx sh -c "kill -USR1 `docker exec nginx cat /var/run/nginx.pid`" |
发现虽然终止了继续写入到旧的文件,但是在宿主机读取日志时,提示没有权限。
1 | cat access.log |
查看日志文件权限,发现不对劲。新建的 access.log
的权限为 600
,所属用户从 moralok
变为 systemd-resolve
,失去了只有所属用户才拥有的 rw
权限。
此时虽然注意到没有成功创建新的
error.log
,但是只是以为对于空的日志文件不滚动。并且在后续重现问题时发现此时其实可以在容器里看到日志开始写入新的日志文件。
1 | ll |
这个时候我的想法是,既然成功创建了新的日志文件,肯定是 Nginx
接收到了 USR1
信号。怎么会出现“crontab
触发时发送信号有问题,手动发送却没问题”的情况呢?难道是两者触发的执行方式有所不同?还是说宿主机创建的文件会有问题?注意到新的日志文件所属的用户和组和原日志文件所属的用户和组不同,我开始怀疑创建文件的过程有问题。在反复测试尝试重现问题后,我把关注点放到了 create
配置上。其实在最开始,我就关注了它,我想既然在默认的配置文件中已经设置而且我也不修改,那么就不在 /etc/logrotate.d/nginx
添加了。我甚至花了很多时间浏览文档,确认它在缺省后面的权限属性时会使用原日志文件的权限属性。当时我还专门记录了一个疑问,“文档说在运行 postrotate
脚本前创建新文件,可是在测试验证时,新文件是 Nginx
接收 USR1
信号后重新打开文件时创建的,在脚本执行报错或者脚本中并不发送信号时,不会产生新文件”。现在想来,都是坑,坑里注定要灌满眼泪!
在 /etc/logrotate.d/nginx
添加 create
后,成功重现问题。
1 | ... |
可见 logrotate -f /etc/logrotate.d/nginx
,并没有使用到默认配置 /etc/logrotate.conf
,而 crontab
触发 logrotate
时使用到了。修改为 create 0644 moralok moralok
,成功解决问题。可以确认 create
在缺省权限属性的时候,如果日志文件因为挂载到容器中而被修改了所属用户,logrotate
按照原文件的权限属性创建新文件时会报错,从而导致脚本不能正常执行,Nginx
不会收到信号,error.log
也不会继续滚动。按此原因推理,在配置文件中,加入 nocreate
也可以解决问题,并且更加符合官方文档建议的流程。
枯坐一下午,百思不得其解,想到抓狂。不得不说真的很讨厌这类问题,特定条件下奇怪的问题,食之无味,弃之还不行!如果照着网上的文章,一开始就添加配置真的不会遇到啊。可是不喜欢不明不白地修改配置来解决问题,也不喜欢一次性加很多设置却不知道各个配置的功能,特别是在我的理解里这个默认配置似乎没问题的情况下。明明想要克制住不知重点还不断深入探索细节的坏习惯,却还是被一个
Bug
带着花费了大量的时间和精力,解决了一个照着抄就不会遇到的问题。虽然真的有收获,真的解决了前一晚留下的疑问,可是不甘心啊,气气气!而且为什么这样会有问题,我还是不懂!
logrotate 备忘
命令参数
1 | -d, --debug : 打开调试模式,这意味着不会对日志进行任何更改,并且 logrotate 状态文件不会更新。仅打印调试消息。 |
常用配置文件参数
参数 | 说明 |
---|---|
daily | 周期:每天 |
weekly | 周期:每周 |
monthly | 周期:每月 |
yearly | 周期:每年 |
dateext | 使用日期代替数字作为扩展名(YYYYMMDD),如:access.log-20231202 |
dateformat | 必须配合 dateext 使用,只支持 %Y %m %d %s 这四个参数 |
compress | 使用 gzip 压缩旧版本日志文件 |
nocompress | 不压缩 |
delaycompress | 延迟到下一次滚动周期再压缩 |
nodelaycompress | 不延迟压缩,覆盖 delaycompress |
create mode owner group | 滚动后(运行 postrotate 脚本前),立即创建日志文件,指定权限属性 |
nocreate | 不创建新的日志文件,覆盖 create |
copytruncate | 创建副本后就地截断原始日志文件,用于一些无法被告知关闭日志文件的程序,可能会丢失复制和截断之间的日志 |
nocopytruncate | 不截断始日志文件,覆盖 copytruncate |
ifempty | 即使是空文件也滚动(默认),覆盖 notifempty |
missingok | 如果日志丢失,不报错继续滚动下一个日志 |
notifempty | 如果是空文件,不滚动,覆盖 ifempty |
sharedscripts | 在所有日志都滚动后统一执行一次脚本。如果没有配置,每个日志滚动后都会执行一次脚本 |
prerotate/endscript | 存放在滚动以前需要执行的脚本,两者必须单独成行 |
postrotate/endscript | 存放在滚动以后需要执行的脚本,两者必须单独成行 |
rotate count | 日志文件在删除或邮寄前滚动的次数,0 指没有备份 |
size log-size | 当日志文件到达指定的大小时才滚动,默认单位 byte,可以使用 k、M、G |