Python 多进程日志输出到同一个文件并实现日志回滚

简介: Python 多进程想要实现将日志输出到同一个文件中,使用同一个日志句柄,且日志需要按照日期,大小回滚。


Python 多进程想要实现将日志输出到同一个文件中,使用同一个日志句柄,且日志需要按照日期,大小回滚。

出现问题

使用 Python 自带的有回滚功能的日志库 TimedRotatingFileHandler 日志并没有正常打印:在日志回滚时多个进程抢占句柄,最终只有一个进程能够正常打印日志;在多进程中回滚时会互相将日志文件删除,并且多进程同时启动时有小概率将多进程卡死。

注:使用该库的日志滚动只会发生在有新日志输出时,会出现像这样的情况:
假设日志按天回滚,第一天日志正常输出,第二天和第三天没有日志输出,第四天有日志输出,那么在第四天只会回滚第一天的日志,而不会生成第二天和第三天的日志文件。

TimedRotatingFileHandler 回滚 doRollover() 源码(省略初始化和其他函数部分):

class TimedRotatingFileHandler(BaseRotatingHandler):
    def doRollover(self):
        """
        do a rollover; in this case, a date/time stamp is appended to the filename
        when the rollover happens.  However, you want the file to be named for the
        start of the interval, not the current time.  If there is a backup count,
        then we have to get a list of matching filenames, sort them and remove
        the one with the oldest suffix.
        """
        # 关闭向日志中打印的被回滚掉的流,将其流置为空
        if self.stream:
            self.stream.close()
            self.stream = None

        # 获得队列的起始时间,将值赋给TimeTuple
        currentTime = int(time.time())
        dstNow = time.localtime(currentTime)[-1]
        t = self.rolloverAt - self.interval
        if self.utc:
            timeTuple = time.gmtime(t)
        else:
            timeTuple = time.localtime(t)
            dstThen = timeTuple[-1]
            if dstNow != dstThen:
                if dstNow:
                    addend = 3600
                else:
                    addend = -3600
                timeTuple = time.localtime(t + addend)

        # 获得被回滚掉的日志名
        dfn = self.rotation_filename(self.baseFilename + "." +
                                     time.strftime(self.suffix, timeTuple))

        # TODO 在该处多进程会互相删除各自的日志文件
        if os.path.exists(dfn):
            os.remove(dfn)
        self.rotate(self.baseFilename, dfn)

        # 回滚计数器大于0
        if self.backupCount > 0:
            for s in self.getFilesToDelete():
                os.remove(s)
        if not self.delay:
            self.stream = self._open()
        newRolloverAt = self.computeRollover(currentTime)
        while newRolloverAt <= currentTime:
            newRolloverAt = newRolloverAt + self.interval

        # 执行回滚
        if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc:
           # ~~~ 省略 ~~~~

问题原因

问题出在上述代码的 36 ~ 38 行 ,此处并没有加锁,并不是进程安全的。

在日志需要回滚时,多进程的每个进程都调用了 doRollover() 方法,导致多进程抢占日志句柄,最终在日志回滚后只有一个进程能够正常将日志打印到日志文件中。

问题解决

对日志文件的文件描述符加独占锁(fcntl.LOCK_EX),防止多个进程同时同时获取到日志文件描述符。

具体修改代码如下: 该类中的 doRollover() 方法中文件覆盖的部分(上述代码 36 ~ 38 行)重写

class MultiCompatibleTimedRotatingFileHandler(TimedRotatingFileHandler):
    def __init__(self, log_path, interval, when, backupCount, encoding):
        super().__init__(filename=log_path, interval=interval, when=when, backupCount=backupCount, encoding=encoding)

    def doRollover(self):
        # ~~~ 省略,与上面代码保持一致 ~~~

        # if os.path.exists(dfn):
        #     os.remove(dfn)
        # self.rotate(self.baseFilename, dfn)

        # 多进程检测到旧句柄存在则不覆盖,继续向文件续写,避免多进程日志互相覆盖
        if not os.path.exists(dfn) and os.path.exists(self.baseFilename):
            f = open(self.baseFilename, 'a')
            fcntl.lockf(f.fileno(), fcntl.LOCK_EX)
            if not os.path.exists(dfn) and os.path.exists(self.baseFilename):
                os.rename(self.baseFilename, dfn)
            f.close()  # 释放锁 释放老 log 句柄

        # ~~~ 省略,与上面代码保持一致 ~~~
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
2天前
|
存储 Python
Python文件编码概念详解
Python文件编码概念详解
11 1
聊聊python多线程与多进程
为什么要使用多进程与多线程呢? 因为我们如果按照流程一步步执行任务实在是太慢了,假如一个任务就是10秒,两个任务就是20秒,那100个任务呢?况且cpu这么贵,时间长了就是浪费生命啊!一个任务比喻成一个人,别个做高铁,你做绿皮火车,可想而知!接下来我们先看个例子:
|
12天前
|
IDE 开发工具 Python
python中SyntaxError: unexpected EOF while parsing(语法错误:解析时遇到意外的文件结束)
【5月更文挑战第14天】python中SyntaxError: unexpected EOF while parsing(语法错误:解析时遇到意外的文件结束)
39 6
|
2天前
|
Python Windows
一步步教你将包含其他文件的 Python 脚本等打包成 EXE
最近我编写了一个Python脚本,该脚本需要依赖两个同级目录下的文件才能正常运行。然而,当我将脚本打包成EXE程序后,必须将这两个文件放在EXE文件的同级目录下才能正常执行。为了简化部署,我希望能将这两个文件一起打包到EXE文件中,这时候该怎么办呢?
|
2天前
|
Python
Python文件的异常、模块与包
Python文件的异常、模块与包
10 3
|
4天前
|
Python
Python OS 文件/目录方法
Python OS 文件/目录方法
|
4天前
|
开发者 Python
Python File(文件) 方法
Python File(文件) 方法
|
4天前
|
安全 Python
Python 文件I
Python 文件I
|
5天前
|
数据采集 存储 C++
单线程 vs 多进程:Python网络爬虫效率对比
本文探讨了Python网络爬虫中的单线程与多进程应用。单线程爬虫实现简单,但处理速度慢,无法充分利用多核CPU。而多进程爬虫通过并行处理提高效率,更适合现代多核架构。代码示例展示了如何使用代理IP实现单线程和多进程爬虫,显示了多进程在效率上的优势。实际使用时还需考虑代理稳定性和反爬策略。
单线程 vs 多进程:Python网络爬虫效率对比
|
5天前
|
Python
python语法中错误的文件或模块导入
【5月更文挑战第19天】
10 1