uvicorn本身不挂起请求,真正导致事件循环卡住的是async函数中缺失await或混入阻塞操作;例如未await的requests.get()、time.sleep()或无超时的redis连接,会使协程无法让出控制权,阻塞其他请求处理。

Uvicorn 本身不会“挂起”请求——它只是把请求交给你的应用协程去处理;真正挂起的,是你写的 async 函数里没写好 await,或者混进了阻塞操作。
为什么 await 缺失会导致整个事件循环卡住
Uvicorn 底层靠 asyncio 事件循环调度协程。每个请求进来,都会启动一个协程(比如你写的 @app.get("/data") async def handler())。如果这个协程里有耗时同步操作(如 time.sleep(2)、requests.get()、未加 await 的数据库查询),它就不会主动让出控制权,事件循环就只能干等它结束,无法切换去处理其他请求。
表现就是:并发稍高(比如 5 路以上)时,新请求明显延迟,甚至超时;日志里看不到新请求进入记录;top 看 CPU 不高,但 uvicorn 进程状态是 S(sleeping)而非 R(running)。
- 典型错误写法:
requests.get("https://api.example.com")—— 必须换成httpx.AsyncClient().get(...)并await - 常见陷阱:
json.loads()没问题,但open("file.json").read()是阻塞的,得用asyncio.to_thread(open, "file.json", "r")包一层 - 调试技巧:在可疑函数开头加
print(f"start {time.time()}"),结尾加print(f"done {time.time()}"),看是否真卡在某处
uvicorn.run() 在 Jupyter 里“假挂起”的真实原因
这不是请求挂起,而是启动方式冲突导致的进程卡死。Jupyter 内核自身已运行一个事件循环,而 uvicorn.run() 内部调用 asyncio.run(),后者发现 loop 已存在,既不报错也不退出,就停在那儿了——单元格一直显示 In [*],端口被占,但服务根本没起来。
Uvicorn 0.52.1 是 Python ASGI Web 服务器,提供异步应用服务及 WebSocket 支持,常用于 FastAPI、Starlette 等应用的开发和部署。本下载为官方 PyPI 通用 wheel 包,可使用 pip 安装,并由包管理器处理所需依赖。
这种情况和你代码逻辑无关,纯属环境不兼容。
- 别再试
uvicorn.run(app, ...)—— 它在 Jupyter 里注定失败 - 替代方案只有两个:
nest_asyncio.apply()(临时打补丁,仅限开发)或改用threading.Thread(target=uvicorn.run, ...).start()(需手动管理生命周期) - 更推荐的做法:把 FastAPI 逻辑拆成纯函数,在 Notebook 里直接调用测试;真正要跑服务,就切到终端执行
uvicorn main:app
Redis/DB 连接未设 timeout 导致的隐性挂起
Uvicorn 不会等你一辈子。如果你用的异步 Redis 客户端(如 redis-py 的 asyncio 版本)或数据库驱动没设连接/读取超时,网络抖动或服务端无响应时,协程就会无限期等待,表现和挂起一模一样。
尤其注意生产环境:Redis 实例重启、防火墙策略变更、DNS 解析失败,都可能触发这种“静默挂起”。
- 必须显式配置:
redis.Redis(..., socket_connect_timeout=2, socket_timeout=3) - 数据库连接池也要设
pool_recycle=3600和pool_pre_ping=True(SQLAlchemy) - 验证方法:在服务运行时,手动
iptables -A OUTPUT -d your-redis-ip -j DROP,看是否在 3 秒内报错,而不是卡住
真正难排查的挂起,往往不是代码写错了,而是你默认信任了某个库的“异步”标签,结果它底层悄悄开了线程、用了同步 socket、或者压根没实现超时——得一层层确认依赖的异步行为是否真实可靠。










