目前我们使用 tomcat 作为中间件来运行业务,期间遇到一个问题:日志文件 catalina.out 不会自动分割,一直堆积在同一个文件中。找了好几种解决方案,最后还是决定使用 logratate 来进行分割。

Logratate 原理

logrotate是怎么做到滚动日志时不影响程序正常的日志输出呢?logrotate提供了两种解决方案。

Linux文件操作机制

介绍一下相关的Linux下的文件操作机制。

Linux文件系统里文件和文件名的关系如下图。

inodes

目录也是文件,文件里存着文件名和对应的inode编号。通过这个inode编号可以查到文件的元数据和文件内容。文件的元数据有引用计数、操作权限、拥有者ID、创建时间、最后修改时间等等。文件件名并不在元数据里而是在目录文件中。因此文件改名、移动,都不会修改文件,而是修改目录文件。

借《UNIX环境高级编程》里的图说一下进程打开文件的机制。

file pointer

进程每新打开一个文件,系统会分配一个新的文件描述符给这个文件。文件描述符对应着一个文件表。表里面存着文件的状态信息(O_APPEND/O_CREAT/O_DIRECT…)、当前文件位置和文件的inode信息。系统会为每个进程创建独立的文件描述符和文件表,不同进程是不会共用同一个文件表。正因为如此,不同进程可以同时用不同的状态操作同一个文件的不同位置。文件表中存的是inode信息而不是文件路径,所以文件路径发生改变不会影响文件操作。

create

默认方案没有名字,姑且叫它create吧。因为这个方案会创建一个新的日志文件给程序输出日志,而且第二个方案名copytruncate是个配置项,与create配置项是互斥的。

这个方案的思路是重命名原日志文件,创建新的日志文件。详细步骤如下:

  1. 重命名程序当前正在输出日志的程序。因为重命名只会修改目录文件的内容,而进程操作文件靠的是inode编号,所以并不影响程序继续输出日志。
  2. 创建新的日志文件,文件名和原来日志文件一样。虽然新的日志文件和原来日志文件的名字一样,但是inode编号不一样,所以程序输出的日志还是往原日志文件输出。
  3. 通过某些方式通知程序,重新打开日志文件。程序重新打开日志文件,靠的是文件路径而不是inode编号,所以打开的是新的日志文件。

什么方式通知程序我重新打开日志呢,简单粗暴的方法是杀死进程重新打开。很多场景这种作法会影响在线的服务,于是有些程序提供了重新打开日志的接口,比如可以通过信号通知nginx。各种IPC方式都可以,前提是程序自身要支持这个功能。

有个地方值得一提,一个程序可能输出了多个需要滚动的日志文件。每滚动一个就通知程序重新打开所有日志文件不太划得来。有个sharedscripts的参数,让程序把所有日志都重命名了以后,只通知一次。

copytruncate

如果程序不支持重新打开日志的功能,又不能粗暴地重启程序,怎么滚动日志呢?copytruncate的方案出场了。

这个方案的思路是把正在输出的日志拷(copy)一份出来,再清空(trucate)原来的日志。详细步骤如下:

  1. 拷贝程序当前正在输出的日志文件,保存文件名为滚动结果文件名。这期间程序照常输出日志到原来的文件中,原来的文件名也没有变。
  2. 清空程序正在输出的日志文件。清空后程序输出的日志还是输出到这个日志文件中,因为清空文件只是把文件的内容删除了,文件的inode编号并没有发生变化,变化的是元信息中文件内容的信息。

结果上看,旧的日志内容存在滚动的文件里,新的日志输出到空的文件里。实现了日志的滚动。

这个方案有两个有趣的地方。

  1. 文件清空并不影响到输出日志的程序的文件表里的文件位置信息,因为各进程的文件表是独立的。那么文件清空后,程序输出的日志应该接着之前日志的偏移位置输出,这个位置之前会被0填充才对。但实际上logroate清空日志文件后,程序输出的日志都是从文件开始处开始写的。这是怎么做到的?这个问题让我纠结了很久,直到某天灵光一闪,这不是logrotate做的,而是成熟的写日志的方式,都是用O_APPEND的方式写的。如果程序没有用O_APPEND方式打开日志文件,变会出现copytruncate后日志文件前面会被一堆0填充的情况。
  2. 日志在拷贝完到清空文件这段时间内,程序输出的日志没有备份就清空了,这些日志不是丢了吗?是的,copytruncate有丢失部分日志内容的风险。所以能用create的方案就别用copytruncate。所以很多程序提供了通知我更新打开日志文件的功能来支持create方案,或者自己做了日志滚动,不依赖logrotate。

我的配置文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[root@centos ~] # cat /etc/logrotate.d/tomcat  
# 要分割的日志文件  
/data/tomcat/logs/catalina.out{  
    # mode owner group 转储文件,使用指定的文件模式创建新的日志文件  
    create 664 owner group  
    # 轮训日志时切换设置的用户/用户组来执行(默认是root)  
    # 如果设置的user/group 没有权限去让文件容用 create 选项指定的拥有者 ,会触发错误。  
    su user group  
    # 如果是空文件的话,不转储  
    notifempty  
    # 创建新的文件副本后,截断源文件文件  
    # 拷贝和清空之间有一个时间差,可能会丢失部分日志数据。  
    copytruncate  
    # 按天进行分割  
    daily  
    # 保留副本数量  
    rotate 30  
    # 在日志轮循期间,任何错误将被忽略,例如“文件无法找到”之类的错误。  
    missingok  
    # 压缩分割的文件  
    compress  
    #  delaycompress 和 compress 一起使用时,转储的日志文件到下一次转储时才压缩  
    delaycompress  
    # 当文件大于1G 时,就分割  
    size 1024M  
    # 切割后的日志文件以当前日期为格式结尾  
    dateext  
}  

常用配置参数

  • daily :指定转储周期为每天
  • weekly :指定转储周期为每周
  • monthly :指定转储周期为每月
  • rotate count :指定日志文件删除之前转储的次数,0 指没有备份,5 指保留 5 个备份
  • tabooext [+] list:让 logrotate 不转储指定扩展名的文件,缺省的扩展名是:.rpm-orig, .rpmsave, v, 和~
  • missingok:在日志轮循期间,任何错误将被忽略,例如 “文件无法找到” 之类的错误。
  • size size:当日志文件到达指定的大小时才转储,bytes (缺省) 及 KB (sizek) 或 MB (sizem)
  • compress: 通过 gzip 压缩转储以后的日志
  • nocompress: 不压缩
  • copytruncate:用于还在打开中的日志文件,把当前日志备份并截断
  • nocopytruncate: 备份日志文件但是不截断
  • create mode owner group : 转储文件,使用指定的文件模式创建新的日志文件
  • nocreate: 不建立新的日志文件
  • delaycompress: 和 compress 一起使用时,转储的日志文件到下一次转储时才压缩
  • nodelaycompress: 覆盖 delaycompress 选项,转储同时压缩。
  • errors address : 专储时的错误信息发送到指定的 Email 地址
  • ifempty :即使是空文件也转储,这个是 logrotate 的缺省选项。
  • notifempty :如果是空文件的话,不转储
  • mail address : 把转储的日志文件发送到指定的 E-mail 地址
  • nomail : 转储时不发送日志文件
  • olddir directory:储后的日志文件放入指定的目录,必须和当前日志文件在同一个文件系统
  • noolddir: 转储后的日志文件和当前日志文件放在同一个目录下
  • prerotate/endscript: 在转储以前需要执行的命令可以放入这个对,这两个关键字必须单独成行

参考文章: