源码分析logrotate切割日志的实现原理

     上个月就想总结写一篇关于日志切割的文章,后来跟进了一篇python logging RotatingFileHandler的日志切割实现,今天再补充一个linux logrotate的实现原理。 有兴趣看logging RotatingFileHandler的文章,附带连接 http://xiaorui.cc/?p=3114

    别问我 logrotate 是啥?  一边去 !   logrotate 是个功能强大的日志切割服务,可以根据自定义的时间及文件大小进行切割打包日志。 并且logrotate也可以做一些收尾的动作。    我想关于logrotate的用法大家都很熟悉,但你们有没有关注过他的实现方法? logrotate怎么做到日志切割的情况下还不影响业务。


该文章写的有些乱,欢迎来喷 ! 另外文章后续不断更新中,请到原文地址查看更新。http://xiaorui.cc/?p=3275

简单看了下logrotate的源码,虽然logrotate.c有2500行,对于我们来说只关心logrotate是如何切割日志的.  下面copyTruncate函数其实就两个动作,一个是copy源日志到新的日志文件里,然后清空源日志文件。

static int copyTruncate(char *currLog, char *saveLog, struct stat *sb,
            int flags)
{
    int fdcurr = -1, fdsave = -1;

    message(MESS_DEBUG, "copying %s to %s\n", currLog, saveLog);

    if (!debug) {
    if ((fdcurr = open(currLog, ((flags & LOG_FLAG_COPY) ? O_RDONLY : O_RDWR) | O_NOFOLLOW)) < 0) {
        message(MESS_ERROR, "error opening %s: %s\n", currLog,
            strerror(errno));
        return 1;
    }

    省略...
    ... ...

    if (flags & LOG_FLAG_COPYTRUNCATE) {
    message(MESS_DEBUG, "truncating %s\n", currLog);

    if (!debug) {
        fsync(fdsave);
        if (ftruncate(fdcurr, 0)) {
        message(MESS_ERROR, "error truncating %s: %s\n", currLog,
            strerror(errno));
        close(fdcurr);
        close(fdsave);
        return 1;
        }
    }
    ...
    
    if (fdcurr >= 0) {
    close(fdcurr);
    }
    if (fdsave >= 0) {
    close(fdsave);
    }
    return 0;
}

这是logrotate的源码地址, https://github.com/logrotate/logrotate/blob/master/logrotate.c 

下面是我用python写的测试代码,逻辑很简单就是写日志, 不停的让日志变大变膨胀. 

#xiaorui.cc
import logging
import requests
import time

def get_logger(LOGFILE):
    logger = logging.getLogger('mylogger')
    logger.setLevel(logging.INFO)
    fh = logging.FileHandler(LOGFILE)
    fh.setLevel(logging.DEBUG)
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    fh.setFormatter(formatter)
    logger.addHandler(fh)
    return logger

logger = get_logger('debug.log')
r = requests.get("http://xiaorui.cc")

while 1:
    logger.info(r.content)
    time.sleep(0.1)

首先我们要知道在linux下所有的文件都有inode号,一个inode号也只能对应一个文件。 

第一种方法,学名 CopyTruncate:

1. Copy entire log file
2. Truncate log file

当我们运行程序不停的写入日志,在我运行logrotate切割日志后,我们通过lsof查看进程的文件描述符,发现debug.log inode还是一个,没有被换掉。

#xiaorui.cc
[ruifengyun@wx-weixin-2 ~]$ ll -i -h
total 656M
11665580 -rw-rw-r-- 1 ruifengyun ruifengyun 6.7M Apr  6 18:18 debug.log
11665833 -rw-rw-r-- 1 ruifengyun ruifengyun 646M Apr  6 18:18 debug.log-20160406

[ruifengyun@wx-weixin-2 ~]$ sudo lsof -p 37001
python  37001 ruifengyun    3w   REG        8,2 579223355 11665580 /home/ruifengyun/debug.log

[ruifengyun@wx-weixin-2 ~]$ ll -i -h
total 656M
11665580 -rw-rw-r-- 1 ruifengyun ruifengyun 6.7M Apr  6 18:18 debug.log
11665833 -rw-rw-r-- 1 ruifengyun ruifengyun 646M Apr  6 18:18 debug.log-20160406

那么他是如何切割的日志,难道是复制过去的?   你说的对!  他还真是copy复制的方式实现的日志切割,copy完成之后,会清空debug.log文件。  对于程序来说是无感知的,照样写入。   

会丢失数据么? 
会的,本来日志有100w行,程序的日志还在不停的写入,当我copy并别名后,再去清空源日志的时候,源日志很有可能现在是100w+了。 后面你懂得….

为什么要copy复制日志文件,而不是mv ?
copy / mv没有改变源文件的信息以及 inode 属性

第二种方法, 方法叫  send signal :


上面的方法保证了文件的inode不变化,这样我们的程序不需要有任何的变动。 其实logrotate还有比较优美的方法,  简单说send signal的实现原理是让logrotate给日志生产者发送重载信号。 在继续详细描述logrotate send signal之前,我们先看看nginx是如何处理日志切割的问题。

#xiaorui.cc

1.  mv access.log access.log.0
2.  kill -USR1 `cat master.nginx.pid`

nginx master处理了signal USR1信号,当nginx收到这信号后会重新创建日志对象,确保日志往新的access.log写入。 

logrotate跟nginx的实现思路是一样的,但logrotate只是个日志消费者,而不是生产者,所以他做了跟上面逻辑一样的事情,针对日志做别名,然后给日志的消费者发送特定信号。  logrotate可以自定义脚本来发送信号。

postrotate指令可以自定义命令,另外如果你的服务端压根不支持日志的重载,那么你发了信号也是徒劳的。 

/usr/local/nginx/logs/*.log {
    daily
    dateext
    compress
    rotate 7
    sharedscripts
    postrotate
        kill -USR1 `cat /var/run/nginx.pid`
    endscript
}

在github和stackoverflow搜到了关于Logrotate lost data的话题。 跟我的想法一样,这些老外也认为反正都是日志,丢就丢吧.   一般日志切割的crontab周期是半夜三更,这时候访问本来就不多,可以说足够规避丢日志的情况.   如果你的日志写入一直都是那么着急忙慌的,又不想丢失日志,可以用第二种的方法。 


大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码,感谢!
另外再次标注博客原地址  xiaorui.cc

3 Responses

发表评论

电子邮件地址不会被公开。 必填项已用*标注