FastAPI代理多路视频流至浏览器时出现HTTP请求阻塞问题
FastAPI代理多路视频流至浏览器时出现HTTP请求阻塞问题
看起来你的问题出在异步客户端的连接管理和Uvicorn的默认并发配置上。当代理多个长连接的MJPEG流时,几个关键的限制会导致第6个请求无法被处理,直到某个现有连接关闭。下面是具体的原因分析和解决方案:
核心原因分析
- Uvicorn单Worker的并发瓶颈:默认情况下Uvicorn以单Worker运行,虽然异步框架理论上支持大量长连接,但如果没有明确配置,可能会因为底层资源调度限制导致并发请求被阻塞。
- httpx.AsyncClient重复创建的资源浪费:你的代码里每个流请求都会新建一个
httpx.AsyncClient实例,每个实例维护独立的连接池。对于长连接的MJPEG流来说,这不仅会浪费系统资源,还可能因为连接池的分散管理导致可用连接被快速耗尽。 - 长连接超时参数缺失:httpx的默认超时设置可能会间接引发连接未正确释放的问题,虽然这不是直接阻塞的原因,但会影响长连接的稳定性。
具体解决方案
1. 复用全局的httpx.AsyncClient
不要为每个流创建新的AsyncClient,而是初始化一个全局异步客户端复用连接池,这样能更高效地管理到目标流主机的连接,避免资源分散浪费。
修改后的代码:
import uvicorn import httpx from fastapi import FastAPI from fastapi.responses import StreamingResponse app = FastAPI() # 全局复用的AsyncClient,应用启动时创建,关闭时销毁 client: httpx.AsyncClient | None = None @app.on_event("startup") async def startup_event(): global client # 针对长连接设置无读取超时,连接超时设为10秒(可根据实际调整) client = httpx.AsyncClient(timeout=httpx.Timeout(None, connect=10.0)) @app.on_event("shutdown") async def shutdown_event(): global client if client: await client.aclose() async def proxy_mjpeg_stream(url: str): global client if not client: raise RuntimeError("AsyncClient未初始化") try: async with client.stream("GET", url) as stream: async for chunk in stream.aiter_bytes(): if chunk: yield chunk except httpx.ReadTimeout as er: print(f'ERROR ReadTimeout - {url=} - {er}') except Exception as e: print(f'ERROR 意外异常 - {url=} - {e}') @app.get("/get_stream") async def get_stream(url: str): if not url: return {"error": "必须提供URL参数"} headers = { "Content-Type": 'multipart/x-mixed-replace; boundary=frame', } return StreamingResponse(proxy_mjpeg_stream(url), headers=headers) if __name__ == "__main__": uvicorn.run(app)
2. 调整Uvicorn的并发配置
运行Uvicorn时通过参数增加Worker数量和并发限制,提升整体长连接处理能力:
uvicorn main:app --workers 4 --limit-concurrency 100
--workers 4:启动4个Worker进程,每个Worker独立处理请求,大幅提升并发承载能力--limit-concurrency 100:设置每个Worker的最大并发任务数,确保能容纳更多长连接流
3. 优化httpx连接池配置(可选)
如果所有代理的流都指向同一主机,可以进一步调整连接池参数,优化连接复用效率:
client = httpx.AsyncClient( timeout=httpx.Timeout(None, connect=10.0), limits=httpx.Limits( max_connections=50, # 全局最大连接数 max_keepalive_connections=20, # 最大保持活跃的连接数 keepalive_expiry=300 # 连接保持活跃的超时时间(秒) ) )
4. 检查系统文件描述符限制
如果调整后仍有问题,可以检查系统的文件描述符限制(通过ulimit -n命令查看),默认限制过低的话可以临时调高:
ulimit -n 10240
验证效果
修改完成后,尝试代理更多的流请求,应该不会再出现第6个请求被阻塞的情况。如果还有问题,可以进一步排查目标流主机的连接限制,或者增加Uvicorn的Worker数量。
备注:内容来源于stack exchange,提问作者Eldellano




