Flask(Gunicorn)多次刷新后无法提供MJPEG视频流服务问题咨询
问题分析与解决方案
问题背景
测试一个基于Flask的MJPEG视频流Web应用,前端通过<img src="/video_src">加载流,后端用生成器返回multipart数据。使用Gunicorn(1 worker,3 threads)启动后,在aarch64树莓派上出现以下问题:
- 首次访问正常,gen函数以单线程ID输出日志
- 刷新页面后出现交替线程ID,旧线程一段时间后停止
- 第三次刷新后页面无法加载,gen日志停止,15分钟后超时恢复
- 增加线程数可延长可刷新次数,疑似线程耗尽未回收
- x86_64 Mac上无此问题,两者Flask(3.0.0)、Werkzeug(3.0.1)版本一致
核心代码:
server = Flask(__name__) camera = Camera("path to network camera stream") # 全局Camera对象仅实例化一次 @server.route("/video_src") def video_src(): return Response(camera.gen(), mimetype="multipart/x-mixed-replace; boundary=frame") # Camera类的gen函数,self.frame由独立线程更新 def gen(self): while True: print(f"id: {threading.get_ident()}") yield ( b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + self.frame + b"\r\n\r\n" ) time.sleep(0.25)
启动命令:
gunicorn --threads 3 --workers 1 --bind 0.0.0.0:5000 app:server
原因分析
- 生成器未感知客户端断开:用户刷新页面时,旧HTTP连接会被主动关闭,但后端生成器仍在执行
while True循环,time.sleep(0.25)会阻塞线程,直到sleep结束后,yield操作才会发现连接已断开,此时线程才会退出。 - 架构差异导致线程回收延迟:x86_64 Mac环境中,Gunicorn或系统线程调度能更早检测到连接关闭并终止生成器线程;但aarch64树莓派环境中,阻塞的
time.sleep会延迟线程回收,导致线程被持续占用。 - Gunicorn默认超时机制:Gunicorn默认超时为15分钟,当所有线程被阻塞的生成器占用时,新请求无法得到处理,直到超时后Gunicorn强制回收线程,服务才恢复。
修复方案
1. 让生成器主动捕获客户端断开异常
修改gen函数,捕获连接断开相关的异常,在客户端断开时立即终止循环,释放线程:
import time import threading from flask import Flask, Response class Camera: def __init__(self, stream_path): self.stream_path = stream_path self.frame = b"" # 此处添加启动独立线程更新self.frame的逻辑 # ... def gen(self): try: while True: print(f"id: {threading.get_ident()}") yield ( b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + self.frame + b"\r\n\r\n" ) time.sleep(0.25) except (GeneratorExit, IOError, ConnectionResetError, BrokenPipeError): # 客户端断开连接,退出生成器 print(f"Thread {threading.get_ident()} exited (client disconnected)") return
当客户端断开连接时,yield操作会抛出上述异常,捕获后立即退出循环,线程被快速回收。
2. 确保self.frame的线程安全
由于self.frame由独立线程更新,添加锁保护避免读取不完整的JPEG数据:
import threading class Camera: def __init__(self, stream_path): self.stream_path = stream_path self.frame = b"" self.frame_lock = threading.Lock() # 启动更新线程 # ... def update_frame(self): # 模拟更新frame的逻辑 while True: # 实际从摄像头流读取新的JPEG数据 new_frame = b"" with self.frame_lock: self.frame = new_frame time.sleep(0.05) def gen(self): try: while True: print(f"id: {threading.get_ident()}") with self.frame_lock: current_frame = self.frame yield ( b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + current_frame + b"\r\n\r\n" ) time.sleep(0.25) except (GeneratorExit, IOError, ConnectionResetError, BrokenPipeError): print(f"Thread {threading.get_ident()} exited (client disconnected)") return
3. 调整Gunicorn配置(辅助优化)
- 缩短超时时间,避免线程被长时间占用:
将超时设为30秒,即使生成器未主动退出,Gunicorn也会在30秒后回收线程。gunicorn --threads 4 --workers 1 --bind 0.0.0.0:5000 --timeout 30 app:server - 适当增加线程数,但核心还是要靠生成器主动回收线程,否则线程数再多也会耗尽。
总结
核心问题是生成器未及时感知客户端断开,导致线程被阻塞占用。通过捕获连接异常主动终止生成器,配合线程安全的frame读取,再辅助调整Gunicorn超时配置,即可解决树莓派上的线程耗尽问题。
内容的提问来源于stack exchange,提问作者troy
相关产品推荐
相关产品推荐

