一.遇到的问题

  如果在多进程中直接使用RotatingFileHandlerTimedRotatingFileHandler,则容易出现PermissionError: [WinError 32] 另一个程序正在使用此文件,进程无法访问的问题,这是由于主线程和多进程争抢写入log文件导致的问题。

二.解决方案

1. 使用ConcurrentRotatingFileHandler

  这个是网上比较多的关于解决RotatingFileHandler报错的方案,使用pip install concurrent-log-handler安装。按照之前看的博客上的教程,其原本是使用pip install ConcurrentLogHandler安装的库,但是由于在windows使用时会因锁机制导致卡死问题,所以有另一个大佬 Preston Landers 写了新的这个库https://github.com/Preston-Landers/concurrent-log-handler来解决卡死的问题。

  但是即使如此,当我使用这个方法的时候还是遇到了同样的问题,如我这个问答python多进程下,使用ConcurrentRotatingFileHandler后还是很慢所示,通过profile运行的得到的运行时间的统计结果:

python 起一个进程写日志 python 多进程日志_开发语言


  可以看出绝大部分的时间还是花在了锁和io上面,这导致我正常应该刷屏的日志输出变成了5秒才能输出一条日志,这也侧面反应出,我本来应该快速运行的多进程代码部分变得非常的卡,需要隔上很长一段时间才能执行一条logging语句,从而使我的多进程代码效率急速下降。

  由于我实力不够,无法解决这个问题,所以我尝试了第二种的方案,也就是nb_log

2.使用nb_log

  这个库使用pip install nb-log安装,pypi官网https://pypi.org/project/nb-log/,对于这个库的使用说明在这里写的挺详细的(由于太长了,而且本人太懒并没全看,不过可以肯定的是写的极其极其用心)。当看到网上很多人吹捧这个库并且这个库是国人开发的时候,我心动了,这绝对是我必须使用在项目中的日志库。但是当我尝试使用了一番之后,我发现好像和我的项目有点不合。
  因为这个库在每次使用时会直接在项目根目录生成一个nb_log_config.py文件。
  第一,即使他说明使用pycharm时不需要专门设置项目根路径,但是我在使用过程中还是出现了根路径跑到c盘pycharm安装路径于是没有写入权限的问题,只有当我把源码set_nb_log_config.pysys.path[1]改成sys.path[0]之后,才能成功生成在我的项目根路径下。这个情况,让我不确定是否可以在pyinstaller打包之后使用。
  第二,我正在写的是一个pyqt项目,我是准备将其打包成exe使用,但是我无法确定当我打包成exe后,这个库是否还能正常运行(一方面我在网上并没有找到将其用于pyqt项目,并且打包后可以运行的文献,另一方面由于技术不行,我也不敢去改源码来去除生成这个配置代码)。
  第三,我认为这种直接在别人项目根目录下生成代码的方式并不友好,有点360自动安装各种软件的味道,而且对于对代码分类有强迫症的人来说,这太折磨人了,需要配置一些参数都可以理解,但是这种方式去配置,我不是很认可。
  所以最后,我只好忍痛割爱放弃了他。(主要还是自己能力不够,驾驭不了)

3.使用QueueHandler

  这个东西是logging自带的,不需要额外安装啥东西,直接使用from logging.handlers import QueueHandler导入即可。
  最终我选择的是这种方式,使用QueueHandler来处理日志,这个handler需要在初始化时为handler添加一个queue(貌似可以是普通队列也可以是多进程队列,我因为需要在多进程使用,我使用的是多进程队列)。初始化完之后,每当使用这个handler的logger时,不会直接输出日志,而是将日志内容做成一个LogRecord加入队列,这时,只需要单独搞个线程一直不断的从队列中取LogRecord通过logging.getLogger('asyncqt').handle(record)执行就好了。
  也许由于队列的原因和主线程的logging不能完全按照先后顺序执行(我没过多了解,不清楚会不会导致这个情况,但是按照queue的逻辑来想,应该会),不过总体来说这个方法非常丝滑的将所有日志输出,并且不会对多进程的代码逻辑有任何影响。唯一麻烦的地方就是需要搞个多线程去取日志打印日志,但是这对于已经用多进程的我来说,不就是再写个线程吗,也不麻烦多少。
在尝试使用QueueHandler的时候,我参考了这几篇博客:

  1. Python3使用队列进行并发日志处理
  2. python logging QueueHandler使用

三.使用QueueHandler的部分代码

import logging
from multiprocessing import Queue

from src.logic_base.business.logger import logger
from src.logic_base.manager.message_queue_manager import message_queue_manager
from src.logic_base.thread.base_thread import BaseThread


class MultiProcessLogThread(BaseThread):
    """
    多进程日志线程
    """

    def __init__(self):
        super().__init__()

        self.need_stop: bool = False
        self.thread_active: bool = True

        self.setName('多进程日志线程')
        self.setDaemon(True)
        return

    def stop(self) -> None:
        self.need_stop: bool = True
        super().stop()
        return

    def startup(self) -> None:
        logging.info(f"{self.getName()}启动。")
        pass

    def shutdown(self) -> None:
        pass

    def handle(self) -> None:
        try:
            log_queue: Queue = message_queue_manager.log_queue
            while not self.need_stop:
                if not log_queue.empty():
                    record: logging.LogRecord = log_queue.get()
                    if record is None:
                        continue
                    if record.levelno >= logging.WARNING:
                        logger.err_logger.handle(record)
                    else:
                        logging.getLogger('asyncqt').handle(record)
        except Exception as e:
            logger.err_logger.exception("多进程日志线程出错。")
            self.thread_active: bool = False
        self.stop()
        return
import logging
import os
from logging.handlers import RotatingFileHandler, QueueHandler
from multiprocessing import Queue


class Logger:
    """
    日志模块
    """

    def __init__(self):
        self.err_logger = logging.getLogger("err_logger")
        self.mp_logger = logging.getLogger("mp_logger")
        self.mp_init_flag: bool = False
        self.init_logging()
        return

    def init_logging(self):
        """
        初始化日志
        :return:
        """
        self.create_log_dir_and_file()
        formatter: logging.Formatter = logging.Formatter("%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %("
                                                         "message)s")
        rotating_file_handler = RotatingFileHandler(filename="./logs/xxx.log", maxBytes=1000000,
                                                    backupCount=100, encoding="utf-8")
        rotating_file_handler.setLevel(level=logging.INFO)       # 保存到日志文件的日志等级

        # 错误日志记录器的初始化配置
        err_logger_rotating_file_handler = RotatingFileHandler(filename="./logs/xxx_err.log",
                                                               maxBytes=1000000, backupCount=100, encoding="utf-8")
        err_logger_rotating_file_handler.setFormatter(formatter)       # 保存到日志文件的日志等级
        self.err_logger.setLevel(logging.WARNING)
        self.err_logger.addHandler(err_logger_rotating_file_handler)

        stream_handler = logging.StreamHandler()
        stream_handler.setLevel(logging.INFO)                       # 打印在命令行的日志等级
        logging.basicConfig(
            level=logging.INFO,
            format="%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s",
            handlers=[rotating_file_handler, stream_handler]
        )
        asyncqt_logger = logging.getLogger('asyncqt')
        asyncqt_logger.setLevel(logging.WARNING)

        logging.info("初始化日志完成。")
        return

    def create_log_dir_and_file(self):
        """
        创建日志文件夹及文件
        :return:
        """
        base_path: str = os.getcwd()
        log_path: str = os.path.join(base_path, "logs")
        log_file_path: str = os.path.join(log_path, "xxx.log").replace("\\", "/")
        err_log_file_path: str = os.path.join(log_path, "xxx_err.log").replace("\\", "/")
        self.init_dir(path=log_path)
        if not os.path.exists(log_file_path):
            with open(log_file_path, mode="w", encoding="utf-8") as f:
                pass
        if not os.path.exists(err_log_file_path):
            with open(err_log_file_path, mode="w", encoding="utf-8") as f:
                pass
        return

    @staticmethod
    def init_dir(path):
        """
        初始化文件夹
        :param path:
        :return:
        """
        if not os.path.exists(path):
            os.makedirs(path)
        return

    def init_mp_logger(self, queue: Queue):
        """
        初始化多进程日志
        """
        formatter: logging.Formatter = logging.Formatter("%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %("
                                                         "message)s")
        # 多进程日志记录器的初始化配置
        mp_logger_queue_handler = QueueHandler(queue=queue)
        mp_logger_queue_handler.setFormatter(formatter)
        self.mp_logger.setLevel(logging.WARNING)
        self.mp_logger.addHandler(mp_logger_queue_handler)
        self.mp_init_flag: bool = True
        return


logger: Logger = Logger()

在多进程中就使用logger.mp_logger.info(f"")来打印就好了(注:logger.mp_logger是我自定义的日志类的专门由于多进程部分的日志记录器)