一.遇到的问题
如果在多进程中直接使用RotatingFileHandler
和TimedRotatingFileHandler
,则容易出现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运行的得到的运行时间的统计结果:
可以看出绝大部分的时间还是花在了锁和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.py
中sys.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
的时候,我参考了这几篇博客:
- Python3使用队列进行并发日志处理
- 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是我自定义的日志类的专门由于多进程部分的日志记录器)