ncatbot icon indicating copy to clipboard operation
ncatbot copied to clipboard

多帐号协作时炸日志

Open WFLing-seaer opened this issue 5 months ago • 2 comments

多进程多帐号协作,运行一段时间后提示:

--- 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

原因盲猜是读写日志没加锁导致的

WFLing-seaer avatar Jul 12 '25 12:07 WFLing-seaer

DeepSeek 提供了以下方案,由于 ncatbot3xx 设计时未考虑多账号协作的问题,我也不知道采用哪个方案合适,如果可以希望您可以提出一些建议

解决多进程日志文件轮转时的权限错误问题

您遇到的问题是在多进程环境下进行日志文件轮转时出现的 PermissionError,这是因为当一个进程正在写入日志文件时,另一个进程尝试重命名该文件导致的冲突。

问题分析

  1. 错误原因:在多进程环境中,多个进程可能同时尝试写入同一个日志文件,当日志轮转发生时,一个进程可能正在写入文件,而另一个进程尝试重命名它,导致权限冲突。

  2. TimedRotatingFileHandler 的限制:标准的 TimedRotatingFileHandler 不是为多进程环境设计的,它在轮转时使用 os.rename() 操作,这在多进程环境下容易出现问题。

解决方案

方案1:使用 ConcurrentRotatingFileHandler

替换 TimedRotatingFileHandlerConcurrentRotatingFileHandler,这是专门为多进程环境设计的日志处理器:

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:使用日志服务

考虑使用集中式日志服务如 sysloglogstashSentry 来处理日志,避免文件系统级别的冲突。

推荐方案

对于您的具体情况,我推荐方案1方案2

  1. 方案1最简单直接,使用专门为多进程设计的日志处理器。
  2. 方案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)

其他建议

  1. 日志文件命名:考虑在日志文件名中包含进程ID,便于调试:

    file_name = f"bot_{datetime.now().strftime('%Y_%m_%d')}_{os.getpid()}.log"
    
  2. 日志轮转策略:考虑使用基于大小的轮转而非基于时间的轮转,可以减少冲突机会。

  3. 错误处理:在日志记录中添加更详细的错误处理,特别是在多进程环境下。

通过以上修改,您应该能够解决多进程环境下日志轮转时的权限冲突问题。

huan-yp avatar Aug 02 '25 15:08 huan-yp

补充:这玩意是非致命性错误,除了炸日志之外不会导致关键性停止…… (但是并不影响它亟待解决这个事就是了)

WFLing-seaer avatar Aug 09 '25 15:08 WFLing-seaer