Normal view

There are new articles available, click to refresh the page.
Yesterday — 9 January 2025Main stream

关于 Gunicorn + Flask 在多进程中,日志轮转的一个 BUG

By: Koril
9 January 2025 at 15:30
Koril:

语言、框架、环境

Python3.11

Gunicorn + Flask + logging

Debian 12


问题

我在开发一个小型的 Python Web 应用,选用的是 Gunicorn + Flask 的方案,日志采用了官方自带的 logging 库。

业务及其简单,但接口请求量比较大,日志记录比较多,我看到官方提供了一个logging.handlers.TimedRotatingFileHandler的日志轮转处理器,就直接用了。

但是过了一段时间,我发现了日志丢失的问题:

假设 Gunicorn 启动了 3 个 worker 进程,进程号分别是 1001 、1002 和 1003 ,一开始启动 Gunicorn 时,3 个进程的日志都能正确的写入到 app.log 中,但是一旦发生了日志轮转,最终只有一个进程(比如 1001 )能够写入到新的 app.log 中,另外的 1002 和 1003 的日志就再也没有写入成功了。


我的猜测

我猜应该是和多进程日志处理和日志轮转相关的问题,轮转的时候,只有一个进程在切换 app.log ,其他进程找不到文件了,日志就丢失了?(我的猜测很粗糙,我不太理解原理)

当然,官方文档也提到了这点:

https://docs.python.org/zh-cn/3/howto/logging-cookbook.html#logging-to-a-single-file-from-multiple-processes

文档的建议是,使用 SocketHandler 或者 QueueHandler ,总之是单独使用一个进程处理日志。


提问

  1. 生产环境下,有什么好的解决方案?

  2. 刚刚上面的轮转日志丢失,更加具体的,本质的原理是什么?


代码

日志配置文件 logging.yaml 如下:

version:
  1

formatters:
  brief:
    format: '%(asctime)s - %(levelname)s - %(name)s - %(message)s'
  detail:
    format: '%(asctime)s - %(levelname)s - %(process)d - %(processName)s - %(name)s - %(filename)s - %(funcName)s - %(message)s'

handlers:
  console_handler:
    class: logging.StreamHandler
    level: DEBUG
    formatter: brief
    stream: ext://sys.stdout

  info_handler:
    class: logging.handlers.TimedRotatingFileHandler
    level: INFO
    formatter: detail
    filename: logs/app.log
    when: midnight
    backupCount: 2
    encoding: utf-8

  error_handler:
    class: logging.handlers.TimedRotatingFileHandler
    level: ERROR
    formatter: detail
    filename: logs/error.log
    when: midnight
    backupCount: 2
    encoding: utf-8

loggers:
  study-flask:
    level: DEBUG
    handlers: [console_handler, info_handler, error_handler]
    propagate: False

root:
  level: DEBUG
  handlers: [console_handler]

app.py 中关于日志配置的代码:

def log_config(log_config_file):
    dict_config = yaml.load(
        open(log_config_file, encoding='utf-8'),
        Loader=yaml.FullLoader
    )
    Path.mkdir(Path.cwd().joinpath("logs"), parents=True, exist_ok=True)
    logging.config.dictConfig(dict_config)


def create_app(config_mode):
    app = Flask(__name__)

    log_config('./logging.yaml')
    
    # ... 省略其他代码

❌
❌