You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

新增Django Channels后WSGI请求偶发“cannot call this from an async context - use a thread or sync_to_async”错误排查问询

聊聊WSGI请求里出现SynchronousOnlyOperation错误的根源和解决办法

咱们来好好分析下你新增Django Channels后,WSGI请求里出现的这个SynchronousOnlyOperation错误。首先得明确,这个错误本来是Django用来防止同步代码在异步上下文里执行阻塞操作的,但你的情况刚好反过来——同步的WSGI请求居然触发了这个检查,说明当前线程的asyncio事件循环没被正确清理,导致Django误以为现在是异步环境。

结合你的部署架构(WSGI和ASGI分离)和排查过的信息,下面给你拆解几个最可能的原因和对应的解决思路:

1. 最可能:同时配置WSGI_APPLICATION和ASGI_APPLICATION导致上下文混淆

你提到已经在生产环境禁用ASGI配置,这个方向完全正确。虽然你把WSGI和ASGI服务分开部署了,但如果给Gunicorn用的settings里还保留着ASGI_APPLICATION配置,Django启动时会初始化ASGI相关组件,这些组件可能在后台悄悄创建async事件循环,污染WSGI服务的线程上下文。

验证&解决步骤:

  • 给WSGI和ASGI服务用完全独立的配置文件:比如给Gunicorn用settings_wsgi.py(彻底删掉ASGI_APPLICATION配置项),给Daphne用settings_asgi.py(保留ASGI相关配置)。
  • 重启Gunicorn服务后观察错误是否消失,这应该是最快速的验证方式。

2. 线程上下文被污染:Gunicorn Worker复用了带async循环的线程

你安装了gevent,有没有给Gunicorn用gevent worker?如果用了,或者你的部署脚本在启动Gunicorn前跑过Daphne(导致进程继承了async循环上下文),就可能出现这种问题——WSGI的worker线程意外带上了async事件循环,让Django误判了执行环境。

解决思路:

  • 确保WSGI和ASGI服务完全隔离:用不同的用户、进程组启动,启动Gunicorn前彻底清理环境变量和进程上下文,避免继承任何残留的async循环。
  • 如果用了gevent worker,调整Gunicorn的--max-requests参数,比如设置成1000,强制worker处理一定请求后重启,避免上下文长期污染;也可以尝试添加环境变量GEVENT_RESOLVER=ares,提升gevent和Django的兼容性。

3. 要不要怀疑Channels的AuthMiddlewareStack?

你说部署已经分离WSGI和ASGI,所以WSGI请求根本不会经过ASGI的AuthMiddlewareStack,这个可能性极低。不过可以顺便排查下Nginx的转发规则,是不是不小心把部分WSGI请求转到了Daphne的ASGI服务?比如非WebSocket的HTTP请求被错误转发,导致这些请求经过ASGI中间件,污染了后续上下文。

排查步骤:

  • 检查Nginx的location配置,确保只有WebSocket请求(通过Upgrade和Connection头判断)转发到Daphne的8002端口,普通HTTP请求都转发到Gunicorn的端口:
# 处理WebSocket请求
location /ws/ {
    proxy_pass http://127.0.0.1:8002;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
}

# 处理普通HTTP请求(WSGI)
location / {
    proxy_pass http://127.0.0.1:8000; # 替换成你的Gunicorn端口
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
  • 查看Daphne的访问日志,确认没有收到非WebSocket的HTTP请求。

4. Celery的asyncio代码有没有潜在问题?

你说Celery在另一个Docker实例,而且_get_optimised_file没有数据库操作,这个概率几乎为0,但可以优化下Celery的asyncio代码,用更安全的写法避免循环泄漏:
把手动创建循环的代码:

loop = asyncio.new_event_loop()
try:
    asyncio.set_event_loop(loop)
    loop.run_until_complete(asyncio.wait([_get_optimised_file(img, image_uri_map) for img in images_files]))
finally:
    loop.close()

改成用asyncio.run():

asyncio.run(asyncio.wait([_get_optimised_file(img, image_uri_map) for img in images_files]))

asyncio.run()会自动创建和关闭事件循环,避免循环残留的问题。

临时缓解方案

如果错误还在出现,可以在Gunicorn的配置文件gunicorn_conf.py里添加一个钩子,强制每个worker进程fork后重置asyncio上下文:

import asyncio

def post_fork(server, worker):
    # 重置asyncio事件循环,确保每个worker有干净的上下文
    asyncio.set_event_loop(asyncio.new_event_loop())

总结下优先级:先搞定WSGI配置文件的ASGI项移除,再检查Gunicorn的worker配置,最后排查Nginx转发规则。应该能解决这个低概率的上下文污染问题。

内容的提问来源于stack exchange,提问作者user1619801

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 13:18:12