You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Granian多Worker部署下FastAPI服务日志丢失问题排查求助

排查Granian多Worker环境下FastAPI日志丢失问题

首先,你的问题核心在于多个Granian Worker进程(6个)同时操作同一个日志文件,加上自定义的轮转逻辑,引发了多进程竞争条件,导致日志丢失。我来拆解问题原因并给出针对性的解决方案:

问题根源分析

1. 多进程日志写入的句柄不一致

每个Granian Worker是独立的操作系统进程,都会初始化自己的TimedRotatingFileHandler并持有独立的日志文件句柄。当其中一个进程触发日志轮转(比如午夜):

  • 它会压缩并重命名当前日志文件,然后创建新的空文件继续写入
  • 但其他Worker进程的文件句柄仍然指向已被删除/重命名的旧文件
  • 后续这些Worker的日志会写入到一个"幽灵文件"中——磁盘上看不到该文件(因为原文件已被删除),但进程还持有句柄,直到Worker重启才会释放,这部分日志永久丢失

2. 自定义轮转逻辑的多进程冲突

虽然你的_gzip_rotator用了原子替换来避免半拉文件,但这只解决了单个进程内的原子性。如果多个进程同时触发轮转(比如时间同步有微小偏差),会出现:

  • 多个进程同时尝试压缩同一个源文件,导致文件被多次删除或压缩失败
  • 部分日志写入操作被中断,直接丢失

解决方案

方案一:用Docker统一收集日志(最简单可靠)

放弃应用内的文件日志处理,直接将所有日志输出到stdout/stderr,让Docker负责日志的轮转和存储。这完全规避了多进程写文件的竞争问题。

步骤1:修改日志配置,只保留控制台输出

修改你的LoggerConfig.setup_logger方法,移除文件相关的handler:

@classmethod
def setup_logger(
    cls,
    name: str = "my_app",
    level: int = logging.INFO,
) -> logging.Logger:
    logger = logging.getLogger(name)
    logger.setLevel(level)
    logger.propagate = False
    if logger.handlers:
        return logger

    formatter = logging.Formatter(cls._LOG_FMT)

    # 只保留控制台输出
    stream_handler = logging.StreamHandler()
    stream_handler.setLevel(level)
    stream_handler.setFormatter(formatter)
    logger.addHandler(stream_handler)

    return logger

步骤2:配置Docker Compose的日志轮转

在docker-compose.yml中添加日志驱动配置,让Docker自动处理轮转和压缩:

services:
  my-app:
    build: .
    container_name: my-app
    network_mode: "host"
    restart: unless-stopped
    logging:
      driver: "json-file"
      options:
        max-size: "100m"  # 单个日志文件最大100MB
        max-file: "30"    # 最多保留30个轮转文件
        compress: "true"  # 自动压缩旧日志

方案二:用QueueHandler + QueueListener实现进程安全日志

让所有Worker将日志发送到一个统一的队列,由主进程单独处理文件写入和轮转,确保只有一个进程操作日志文件。

步骤1:主进程初始化队列和Listener

在应用启动的入口(比如src.my_app.main),先启动一个QueueListener:

import logging
import logging.handlers
from multiprocessing import Queue
from pathlib import Path
from your_log_config_module import LoggerConfig

# 全局日志队列(Granian的Worker会继承这个队列)
log_queue = Queue(-1)

def init_logging_listener():
    # 创建文件Handler(复用你原有的轮转配置)
    log_path = Path("logs/my_app.log")
    log_path.parent.mkdir(parents=True, exist_ok=True)
    
    file_handler = logging.handlers.TimedRotatingFileHandler(
        filename=str(log_path),
        when="midnight",
        interval=1,
        backupCount=365,
        encoding="utf-8",
        errors="backslashreplace",
        delay=True,
    )
    file_handler.suffix = "%Y-%m-%d"
    file_handler.namer = LoggerConfig._gzip_namer
    file_handler.rotator = LoggerConfig._gzip_rotator
    file_handler.setFormatter(logging.Formatter(LoggerConfig._LOG_FMT))
    
    # 启动QueueListener
    listener = logging.handlers.QueueListener(log_queue, file_handler)
    listener.start()
    return listener

# 启动应用前初始化Listener
log_listener = init_logging_listener()

# 然后定义FastAPI app
from fastapi import FastAPI
app = FastAPI()

# ... 你的路由逻辑 ...

步骤2:Worker进程配置QueueHandler

修改LoggerConfig.setup_logger,让Worker使用QueueHandler发送日志到队列:

@classmethod
def setup_logger(
    cls,
    name: str = "my_app",
    level: int = logging.INFO,
    log_queue=None,
) -> logging.Logger:
    logger = logging.getLogger(name)
    logger.setLevel(level)
    logger.propagate = False
    if logger.handlers:
        return logger

    formatter = logging.Formatter(cls._LOG_FMT)

    # 控制台输出保留(方便调试)
    stream_handler = logging.StreamHandler()
    stream_handler.setLevel(level)
    stream_handler.setFormatter(formatter)
    logger.addHandler(stream_handler)

    # 添加QueueHandler,将日志发送到全局队列
    if log_queue is not None:
        queue_handler = logging.handlers.QueueHandler(log_queue)
        queue_handler.setFormatter(formatter)
        logger.addHandler(queue_handler)

    return logger

在Worker初始化时,传入全局的log_queue即可(Granian的Worker会继承主进程的全局变量,所以可以直接在路由或依赖中调用setup_logger(log_queue=log_queue))。

方案三:临时排查验证

如果想先确认问题是不是多进程导致的,可以临时把Granian的--workers改成1,运行一段时间观察日志是否还丢失。如果单Worker下日志正常,那100%是多进程共享日志文件的问题。

另外,可以在容器内执行lsof | grep deleted,查看是否有已被删除但仍被进程持有的日志文件——如果有,说明确实是Worker在写入已轮转的旧文件。


内容的提问来源于stack exchange,提问作者Geo48

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 06:40:09