新增Django Channels后WSGI请求偶发“cannot call this from an async context - use a thread or sync_to_async”错误排查问询
咱们来好好分析下你新增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

