多帐号协作时炸日志
多进程多帐号协作,运行一段时间后提示:
--- Logging error ---
Traceback (most recent call last):
File "W:\Anaconda\Lib\logging\handlers.py", line 74, in emit
self.doRollover()
File "W:\Anaconda\Lib\logging\handlers.py", line 446, in doRollover
self.rotate(self.baseFilename, dfn)
File "W:\Anaconda\Lib\logging\handlers.py", line 115, in rotate
os.rename(source, dest)
PermissionError: [WinError 32] 另一个程序正在使用此文件,进程无法访问。: 'R:\\NapCatBot\\logs\\bot_2025_07_11.log' -> 'R:\\NapCatBot\\logs\\bot_2025_07_11.log.2025-07-11
原因盲猜是读写日志没加锁导致的
DeepSeek 提供了以下方案,由于 ncatbot3xx 设计时未考虑多账号协作的问题,我也不知道采用哪个方案合适,如果可以希望您可以提出一些建议
解决多进程日志文件轮转时的权限错误问题
您遇到的问题是在多进程环境下进行日志文件轮转时出现的 PermissionError,这是因为当一个进程正在写入日志文件时,另一个进程尝试重命名该文件导致的冲突。
问题分析
-
错误原因:在多进程环境中,多个进程可能同时尝试写入同一个日志文件,当日志轮转发生时,一个进程可能正在写入文件,而另一个进程尝试重命名它,导致权限冲突。
-
TimedRotatingFileHandler 的限制:标准的
TimedRotatingFileHandler不是为多进程环境设计的,它在轮转时使用os.rename()操作,这在多进程环境下容易出现问题。
解决方案
方案1:使用 ConcurrentRotatingFileHandler
替换 TimedRotatingFileHandler 为 ConcurrentRotatingFileHandler,这是专门为多进程环境设计的日志处理器:
from logging.handlers import RotatingFileHandler
from cloghandler import ConcurrentRotatingFileHandler
# 替换原有的文件处理器
file_handler = ConcurrentRotatingFileHandler(
filename=file_path,
mode='a',
maxBytes=100*1024*1024, # 100MB
backupCount=backup_count,
encoding='utf-8'
)
需要先安装依赖:pip install ConcurrentLogHandler
方案2:使用文件锁实现安全的日志轮转
修改您的日志配置,使用文件锁来确保安全的日志轮转:
import fcntl
from logging.handlers import TimedRotatingFileHandler
class MultiProcessSafeTimedRotatingFileHandler(TimedRotatingFileHandler):
def doRollover(self):
"""
重写doRollover方法,加入文件锁
"""
if self.stream:
self.stream.close()
self.stream = None
# 获取文件锁
lock_file = self.baseFilename + ".lock"
with open(lock_file, 'w') as lock_fd:
try:
fcntl.flock(lock_fd, fcntl.LOCK_EX)
# 执行原始轮转逻辑
super().doRollover()
finally:
fcntl.flock(lock_fd, fcntl.LOCK_UN)
os.remove(lock_file)
if not self.delay:
self.stream = self._open()
# 使用自定义的处理器
file_handler = MultiProcessSafeTimedRotatingFileHandler(
filename=file_path,
when="midnight",
interval=1,
backupCount=backup_count,
encoding="utf-8",
)
方案3:每个进程使用独立的日志文件
为每个进程创建独立的日志文件:
import multiprocessing
# 修改文件路径,包含进程ID
file_name = f"bot_{datetime.now().strftime('%Y_%m_%d')}_{multiprocessing.current_process().pid}.log"
file_path = os.path.join(log_dir, file_name)
方案4:使用日志服务
考虑使用集中式日志服务如 syslog、logstash 或 Sentry 来处理日志,避免文件系统级别的冲突。
推荐方案
对于您的具体情况,我推荐方案1或方案2:
- 方案1最简单直接,使用专门为多进程设计的日志处理器。
- 方案2提供了更多的控制权,但实现稍复杂。
完整修改建议
以下是修改后的日志配置部分:
def setup_logging():
"""设置日志"""
# ... (前面的代码保持不变)
# 创建日志目录
os.makedirs(log_dir, exist_ok=True)
file_path = os.path.join(log_dir, datetime.now().strftime(file_name))
# 配置根日志器
logger = logging.getLogger()
logger.setLevel(logging.DEBUG) # 全局最低级别设为DEBUG
# 控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(console_log_level)
console_handler.setFormatter(ColoredFormatter(log_format))
# 文件处理器 - 使用多进程安全的处理器
try:
from cloghandler import ConcurrentRotatingFileHandler
file_handler = ConcurrentRotatingFileHandler(
filename=file_path,
mode='a',
maxBytes=100*1024*1024, # 100MB
backupCount=backup_count,
encoding='utf-8'
)
except ImportError:
warnings.warn("ConcurrentLogHandler not installed, falling back to standard handler")
file_handler = TimedRotatingFileHandler(
filename=file_path,
when="midnight",
interval=1,
backupCount=backup_count,
encoding="utf-8",
)
file_handler.setLevel(file_log_level)
file_handler.setFormatter(logging.Formatter(file_format))
# 初始化并添加处理器
logger.handlers = []
logger.addHandler(console_handler)
logger.addHandler(file_handler)
其他建议
-
日志文件命名:考虑在日志文件名中包含进程ID,便于调试:
file_name = f"bot_{datetime.now().strftime('%Y_%m_%d')}_{os.getpid()}.log" -
日志轮转策略:考虑使用基于大小的轮转而非基于时间的轮转,可以减少冲突机会。
-
错误处理:在日志记录中添加更详细的错误处理,特别是在多进程环境下。
通过以上修改,您应该能够解决多进程环境下日志轮转时的权限冲突问题。
补充:这玩意是非致命性错误,除了炸日志之外不会导致关键性停止…… (但是并不影响它亟待解决这个事就是了)