如何解决Python 3.5中Sanic框架多进程的OSError问题?
这个问题我之前在部署Python后台服务时也碰到过,核心原因很明确:当你关闭终端后,后台运行的Sanic主进程以及它用multiprocessing创建的子进程,都会失去与终端的关联——原来的标准输出(stdout)和标准错误(stderr)文件描述符已经变成无效状态,子进程尝试执行sys.stdout.flush()时就会触发OSError: [Errno 5] Input/output error。
下面给你几个实用的解决方案,按优先级排序:
1. 重定向子进程的输出流到文件或/dev/null
直接在子进程启动时,把stdout和stderr重定向到不会随终端关闭失效的位置,比如日志文件或者系统的空设备(/dev/null)。这样子进程就不会再尝试刷新已经无效的终端输出流了。
示例代码:
import os import sys from multiprocessing import Process from sanic import Sanic, json app = Sanic("background_upload_app") def background_upload(file_data): # 重定向stdout和stderr到/dev/null(如果不需要日志可以用这个) # 或者替换成你的日志文件路径,比如'/var/log/sanic_upload.log' target_path = os.devnull with open(target_path, 'w') as f: os.dup2(f.fileno(), sys.stdout.fileno()) os.dup2(f.fileno(), sys.stderr.fileno()) # 执行你的文件上传逻辑 try: # 模拟上传过程 with open(f'/tmp/uploaded_{file_data.name}', 'wb') as out_file: out_file.write(file_data.body) except Exception as e: print(f"Upload failed: {e}") # 现在会写到/dev/null或者指定日志文件 @app.route('/upload', methods=['POST']) async def upload(request): file = request.files.get('file') if file: Process(target=background_upload, args=(file,)).start() return json({'status': 'accepted', 'message': 'Uploading in background'}) return json({'status': 'error', 'message': 'No file provided'}, status=400) if __name__ == '__main__': app.run(host='0.0.0.0', port=8000)
2. 使用logging模块统一管理日志
把所有打印输出替换成Python标准库的logging模块,将日志写入文件而非终端。这种方式更规范,也方便后续排查问题。
示例代码:
import logging from logging.handlers import RotatingFileHandler from multiprocessing import Process from sanic import Sanic, json app = Sanic("background_upload_app") def setup_logging(): # 配置日志处理器,写入文件并自动轮转 logger = logging.getLogger() logger.setLevel(logging.INFO) # 避免重复添加处理器 if not logger.handlers: handler = RotatingFileHandler( '/var/log/sanic_upload.log', maxBytes=10*1024*1024, # 10MB backupCount=5 ) formatter = logging.Formatter( '%(asctime)s - %(processName)s - %(levelname)s - %(message)s' ) handler.setFormatter(formatter) logger.addHandler(handler) def background_upload(file_data): logger = logging.getLogger(__name__) logger.info(f"Starting background upload for file: {file_data.name}") try: with open(f'/tmp/uploaded_{file_data.name}', 'wb') as out_file: out_file.write(file_data.body) logger.info(f"Successfully uploaded file: {file_data.name}") except Exception as e: logger.error(f"Upload failed for {file_data.name}: {str(e)}") @app.route('/upload', methods=['POST']) async def upload(request): file = request.files.get('file') if file: Process(target=background_upload, args=(file,)).start() return json({'status': 'accepted', 'message': 'Uploading in background'}) return json({'status': 'error', 'message': 'No file provided'}, status=400) if __name__ == '__main__': setup_logging() app.run(host='0.0.0.0', port=8000)
3. 用进程管理工具启动应用(推荐生产环境)
在生产环境中,不要直接用&后台运行,而是用nohup或者systemd这类工具来管理进程,它们会自动处理输出流的重定向,避免终端关闭导致的问题。
用nohup启动:
nohup python3 your_app.py > /var/log/sanic_app.log 2>&1 &这条命令会把应用的所有输出(stdout和stderr)写入指定的日志文件,即使终端关闭,进程也会继续运行。
用systemd做成服务:
创建一个/etc/systemd/system/sanic-upload.service文件:[Unit] Description=Sanic Background Upload Service After=network.target [Service] User=your_username WorkingDirectory=/path/to/your/app ExecStart=/usr/bin/python3 your_app.py Restart=always StandardOutput=append:/var/log/sanic_upload.log StandardError=append:/var/log/sanic_upload.log [Install] WantedBy=multi-user.target然后启动并设置开机自启:
sudo systemctl daemon-reload sudo systemctl start sanic-upload sudo systemctl enable sanic-upload
这样配置后,应用会由systemd管理,即使服务器重启也会自动启动,日志也会稳定写入文件,彻底避免终端相关的IO错误。
内容的提问来源于stack exchange,提问作者sunil barve

