背景
需要做一个能够实时输出运行结果的界面,美滋滋的做完后,突然发现,发起websocket请求后,在这个请求未结束之前,无法发起其他请求,不管是http还是websocket,都会阻塞。。。无语凝噎。google无果,官方文档查看也没看到啥有用的东西(也可能是我急于解决问题,没有细看),后来看了下参考的文章,发现其django、channels版本都比我的低,于是尝试降版本测试,可行了。。。我一个小小的运维,何苦遭此折磨。。。
我的版本
Django 3.2
channels 3.0.4
channels-redis 3.4.0
参考版本
Django 2.0.5
channels 2.1.1
channels-redis 2.2.1
推测是channels升级到3.0版本后有了大的改动,使用方法和之前不一样了。。肯定是往好的方向走,但是我并没有找到类似的解决案例。有遇到过知道如何解决的小伙伴,动动小手留下你的解决方案吧!
20220614更新:
好家伙,django 2.0.5有个问题,在create数据时会有如下报错,升级到2.2.28就没问题。。。为啥升到这个版本?因为机智的我先安装了channels2.1.1,这货直接把django3.2给我卸载干成了2.2.28,我一试,您猜怎么着,成了!
File "/home/MonitorTools/python3.8.6/lib/python3.8/site-packages/django/db/backends/oracle/operations.py", line 228, in fetch_returned_insert_id
return int(cursor._insert_id_var.getvalue())
TypeError: int() argument must be a string, a bytes-like object or a number, not 'list'
20220616更新:
本以为没有了后续,谁知问题才刚刚开始。。。降版本后又出现了这种问题,于是又是各种折腾,贴下报错
Application instance <Task pending name='Task-2' coro=<StaticFilesWrapper.__call__() running at /home/python/.pyenv/versions/testChannels/lib/python3.8/site-packages/channels/staticfiles.py:44> wait_for=<Future pending cb=【<TaskWakeupMethWrapper object at 0x7f4af0b59a30>()】>> for connection <WebSocketProtocol client=【'IP地址', 54090】 path=b'/room/123/'> took too long to shut down
是的,发现降版本还报错后我又升回了django3和channels3,经过反复测试,有那么一两会儿是正常的,后来还是阻塞,说这么多,还是贴下代码吧
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
import datetime
import time
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
self.accept()
print('有人来连接了')
def websocket_disconnect(self, message):
print('断开连接')
def websocket_receive(self, message):
# self.cope
msg = message['text']
print('接收消息:' + msg)
for i in range(30):
self.send(msg+ str(i) +'.xasdsfsd')
time.sleep(1)
因为之前写的代码里有些操作是耗时的,这里没有啥好办法去模拟,就直接time.sleep了,排除过程中看了channels配置,消费者写法,网上各种教程,换了百种写法都无果。后来看到了报错中packages/channels/staticfiles.py
,于是点进去想看看报错took too long to shut down
的逻辑,结果没找到。。。于是盲猜是不是静态文件的问题,因为在测试过程中,每次出问题貌似就是我在强制清空缓存后,于是乎在网上找了方法,专门使用daphne来监听websocket,后来测试,果然没问题了。。。你以为这就结束了?是的,测了几把有报错了。。。原汁不原味。。。这次在python3.8/site-packages/channels/routing.py
上报错了。。
2022-06-16 16:48:48,593 WARNING Application instance <Task pending name='Task-49' coro=<ProtocolTypeRouter.__call__() running at /home/user/.pyenv/versions/3.8.6/envs/project/lib/python3.8/site-packages/channels/routing.py:71> wait_for=<Future pending cb=[_chain_future.<locals>._call_check_cancel() at /home/user/.pyenv/versions/3.8.6/lib/python3.8/asyncio/futures.py:360, <TaskWakeupMethWrapper object at 0x7f9aeaa11790>()]>> for connection <WebSocketProtocol client=['IP', 61617] path=b'/ws/clean/'> took too long to shut down and was killed.
后来看到知乎上有个大佬写的一篇python asgi的文章,里面随手带过的一句话引起了我的注意不能使用time.sleep, 会阻塞整个线程
,而我的测试恰恰用的都是time.sleep,于是乎接着搜索,又看到了另一篇文章
https://string.quest/read/16973226, 里面提到通过生产者-消费者模式,每个消费者依次消费队列:从队列中获取>进程>从队列中获取... 在给定项目(或“事件”)的流程阶段,您将消费者置于 sleep 状态,因此该流程在 sleep 完成之前不会完成。您不能消费下一个项目,这就是您的消费者被阻止的原因。
,这么一看,好像问题就是在我用了time.sleep导致的,事实上我在测试时,如果不用time.sleep时确实不会造成阻塞,但是仍会有报错。
再后来有幸在一位大哥的博客上看到类似的经历:
https://j-sui.com/2020/05/20/django-channels-daphne-tricky-exception/
得出的结论是daphne在报错,去看了源码,是因为关闭websocket时超时导致的错误提示。。先写到这里,后面接着测试。。。
20220617更新:
时至今日,终于搞定了。思路参考https://blog.ops-coffee.cn/s/r5spytjrl0jjeauye4q_-q ,大概就是celery执行任务,通过channels_layers实时发送消息至WEB。因为任务执行都放在了celery异步执行,所以对于websocketconsumer来说并没有任何需同步等待的操作,就不会造成阻塞。
阻塞的操作不止是time.sleep,数据库读写、os操作等等可能会耗时的都可能会造成阻塞。官方有相关的说明,基础功底不够扎实,又心浮气躁,没有去细细查阅。感兴趣的可以通读研究一下, https://channels-readthedocs-io.translate.goog/en/stable/topics/databases.html?_x_tr_sl=auto&_x_tr_tl=zh-CN&_x_tr_hl=zh-CN&_x_tr_pto=wapp
贴下我的代码:
以下代码基于环境:
python 3.8.6
django 4.0.5
channels 3.0.4
首先是channels的配置
# settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
]
ASGI_APPLICATION = "project.asgi.application"
# channel配置
CHANNEL_LAYERS = {
"default": {
# This example app uses the Redis channel layer implementation channels_redis
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
# "hosts": [('127.0.0.1', 7779)],
"hosts": [("redis://xxx.xx.xx.xx:8000/2")],
},
},
}
asgi路由
# project/asgi.py settings.py同级目录下
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
django.setup()
from django.core.asgi import get_asgi_application
from django.urls import path
from channels.routing import ProtocolTypeRouter, URLRouter
from clean.consumers import CleanConsumer
application = ProtocolTypeRouter({
"http": get_asgi_application(),
# Just HTTP for now. (We can add other protocols later.)
"websocket":
URLRouter([
path(r'ws/clean/', CleanConsumer.as_asgi()),
]),
})
consumer
# myapp/consumers.py
from channels.generic.websocket import WebsocketConsumer
from myapp.tasks import mytask
class MyConsumer(WebsocketConsumer):
def __init__(self, *args, **kwargs):
super(MyConsumer, self).__init__(*args, **kwargs)
self.redis = get_redis_connection()
def connect(self):
self.accept()
def receive(self, text_data=None, bytes_data=None):
info = json.loads(text_data)
# 调用celery任务
self.result = mytask.delay(info, self.channel_name) # self.channel_name是websocketconsumer本身具有的一个属性
def disconnect(self, close_code):
# self.result.revoke(terminate=True)
def send_message(self, event):
#自定义函数,在celery任务中使用channels_layers会用到
self.send(text_data=json.dumps(event))
celery task
# myapp/tasks.py
from __future__ import absolute_import, unicode_literals
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from project.celery import app
@app.task
def auto_expdp(info, channel_name):
# 通过channels_layers从websocketconsumer函数外部向channels发送消息,
channel_layer = get_channel_layer()
# 异步转同步使用async_to_sync方法,channel_name为频道名,这里直接用的websocketconsumer自带的channel_name属性,type发送的方法,send.message对应前面MyConsumer里的send_message,这里channels是对.进行了内部转化为_,没啥特殊理解的,message就是你要发送的消息
async_to_sync(channel_layer.send)(
channel_name,
{"type": "send.message","message": ''}
)
以上基本上就成型了,可以异步执行任务,然后页面实时输出每个步骤的执行结果。官方给出的其他consumer如SyncConsumer,还未深入研究,待有时间研究后再来更新。
20220725更新:
记录一个channels配置报错的问题,配置好channels后启动django就会报错如下
django.core.exceptions.ImproperlyConfigured: Cannot import ASGI_APPLICATION module 'dbaOps.asgi'
解决方法:
# 进入调试
python manage.py shell
# 尝试导入,根据报错进行处理,这里是某个文件导入某个模块有问题
from dbaOps.asgi import application