在 Django中使用 Redis和Celery处理异步任务
介绍
在本教程中,我将大致介绍为什么celery消息队列是有价值的,以及如何在Django应用程序中使用celery和Redis。为了演示实现细节,我将构建一个最小化的图像处理应用程序,它将生成用户提交的图像的缩略图。
本文将包含以下主题:
- Celery消息队列和Redis的背景知识
- 在本地开发环境中设置Django、Celery和Redis
- 在Celery任务中创建图像缩略图
- 部署到Ubuntu服务器
如果你只想直接跳到一个功能完整的应用程序,那你可以在GitHub(https://github.com/amcquistan/image_parroter )上找到这个例子的代码,以及安装和设置说明,不然的话,在本文的其余部分中,我将为你介绍如何从头构建所有东西。
Celery消息队列和 Redis的背景知识
Celery是一个基于Python的任务队列软件包,它支持执行异步计算工作负载,这些工作负载由应用程序代码(本例中为Django)发送给Celery任务队列的消息所驱动。Celery还可以用来执行重复性的、周期性的(即计划好的)任务,但这不是本文的重点。
Celery最好与经常被称为消息代理的存储解决方案一起结合使用。和Celery经常一起使用的消息代理是Redis,它是一个基于内存的持久化的键-值数据存储系统。具体来说,Redis用于存储应用程序代码生成的消息,这些消息描述了Celery任务队列中要完成的工作。Redis还用作Celery队列返回的结果的存储,以便Celery队列的消费者稍后进行检索。
在本地开发环境中设置Django、Celery和Redis
我将从最困难的部分开始,首先是安装Redis。
在Windows上安装Redis
- 下载Redis zip文件并解压到某个目录中
- 找到名为redis-server.exe的文件,双击在命令窗口中启动服务器
- 类似地,找到另一个名为redis-clil .exe的文件,双击它,在一个单独的命令窗口中打开这个程序
- 在运行cli客户端的命令窗口中进行测试,以确保客户端可以通过发出命令ping与服务器进行通信,如果一切正常,服务器会返回一个PONG响应
在Mac OSX / Linux上安装Redis
- 下载Redis tarball文件并将其解压到某个目录中
- 使用make install命令运行make文件来构建程序
- 打开终端窗口并运行redis-server命令
- 在另一个终端窗口中运行redis-cli
- 在运行cli客户端的终端窗口中进行测试,以确保客户机可以通过发出ping命令与服务器通信,如果一切正常,服务器会返回一个PONG响应
安装Python虚拟环境和依赖项
现在,我可以继续创建Python3虚拟环境,并安装这个项目所需的依赖项。
首先,我将创建一个名为image_parroter的目录作为项目的主目录,然后在其中创建虚拟环境。从这里开始,所有的命令都将只有unix风格,但是,大多数(如果不是所有的话)命令对于windows环境来说都是相同的。
现在激活虚拟环境后,我就可以安装以下Python包了。
- Pillow是一个用于图像处理的与celery无关的Python包,我将在本教程的后面使用它来演示celery任务的实际用例。
- Django Widget Tweaks是一个Django插件,提供了表单输入呈现的灵活性。
设置Django项目
接下来,我创建了一个名为image_parroter的Django项目,然后是一个名为thumbnailer的Django App。
此时的目录结构如下:
为了在这个Django项目中集成Celery,我按照Celery文档中描述的约定添加了一个新的imageparroter/imageparrroter/celery.py模块。在这个新的Python模块中,我导入了os包和Celery类。
os模块用于将Celery环境变量DJANGO_SETTINGS_MODULE与Django项目的settings模块关联起来。然后,我实例化Celery类的一个实例来创建celery_app实例变量。然后,我将在settings文件中添加很多以'CELERY_'开头的设置,这些设置可以更新Celery应用程序的配置。最后,我告知新创建的celery_app实例去自动发现项目中的任务。
完成的celery.py模块如下所示:
现在,在项目的settings.py模块的最底部,我为celery设置定义了一个部分,并添加了如下所示的设置。这些设置告诉Celery使用Redis作为消息代理,以及在哪里连接到它。它们还告诉Celery消息会在Celery任务队列之间来回传递,而Redis 消息代理将会以application/json的mime类型进行传递。
接下来,我需要确保前面创建和配置的celery应用程序在运行时会被注入Django应用程序。这是通过在Django项目的主__init__ .py脚本中导入Celery应用程序,并在“image_parroter”Django包中显式地将其注册为一个带名称空间的符号来实现的。
我继续遵循文档建议的约定,在“thumbnailer”应用程序中添加了一个名为tasks.py的新模块。在tasks.py模块中,我导入了shared_tasks函数装饰器,并使用它来定义一个名为adding_task的celery任务函数,如下所示。
最后,我需要将thumbnailer应用程序添加到image_parroter项目的settings.py模块中的INSTALLED_APPS列表中。在这里,我还应该添加“widget_tweaks”应用程序,用于控制表单输入的呈现,稍后我将使用它来允许用户上传文件。
现在,我可以在三个终端上使用一些简单的命令来进行测试。
在一个终端中,我需要将redis-server运行,像这样:
在第二个终端中,在先前安装的Python虚拟环境实例激活的情况下,在项目的根包目录中(与包含manage.py模块的目录相同),我启动了celery程序。
在第三个也是最后一个终端中,同样是在Python虚拟环境激活的情况下,我可以启动Django Python shell并测试我的adding_task,如下所示:
注意在adding_task对象上使用的.delay(…)方法。这是将任何必要的参数传递给处理它们的任务对象的常用方法,也是将任务对象发送到消息代理和任务队列的常用方法。调用.delay(…)方法的结果是一个类型为celery.result.AsyncResult的promise-like的返回值。这个返回值包含任务的id、执行状态和任务状态等信息,以及通过.get()方法访问任务生成的任何结果的能力,如示例所示。
在Celery任务中创建图像缩略图
现在,将一个Redis支持的Celery实例集成到Django应用程序的boiler plate setup(套路设置)已经完成,我可以继续使用前面提到的thumbnailer应用程序来演示一些更有用的功能。
回到tasks.py模块中,我从PIL包中导入了Image类,然后添加一个名为make_thumbnails的新任务,它接受一个图像文件路径和一个2元组(宽度和高度尺寸)的列表来创建缩略图。
上面的缩略图任务只是将输入的图像文件加载到一个Pillow图像实例中,然后对传递给任务的尺寸列表进行循环,并为每个尺寸创建缩略图,将每个缩略图添加到一个zip归档文件中,同时清理中间文件。最后,它会返回一个简单的字典,其中包含了下载缩略图zip文档的URL。
定义了celery任务之后,我继续构建Django视图来提供一个带有文件上传表单的模板。
首先,我为Django项目提供了一个MEDIA_ROOT位置,其中可以放置图像文件和zip存档(我在上面的示例任务中使用了这个位置),并指定了可以提供内容的MEDIA_URL。在image_parroter/settings.py模块中,我添加了MEDIA_ROOT、MEDIA_URL、IMAGES_DIR设置位置,并提供了当这些位置不存在时创建它们的逻辑。
在thumbnailer/views.py模块中,我导入了django.views.View 类,并使用它创建了一个包含get和post方法的HomeView类,如下所示。
get方法只简单地返回一个home.html模板,并向它传递一个包含ImageField 字段的FileUploadForm,如HomeView类上面所示。
post方法使用请求中发送的数据来构造FileUploadForm对象,并检查其有效性, 如果它是有效的,post方法就将它上传的文件保存到IMAGES_DIR 中,并启动一个make_thumbnails任务,同时抓取任务id和状态传递给模板,或者将带有错误提示的表单返回给home.html模板。
在HomeView类的下面,我放置了一个TaskView类,我们将使用它通过一个AJAX请求来检查make_thumbnails任务的状态。在这里,你会注意到我已经从celery包中导入了current_app对象,并使用它从请求中检索与task_id相关联的任务的AsyncResult对象。我创建了一个包含任务状态和id的response_data词典,然后如果状态表示任务已经成功执行,我将通过调用AsynchResult对象的get()方法来获取结果,同时将结果分配给response_data的results键,该response_data将会以JSON的形式被返回给HTTP请求者。
在创建模板UI之前,我需要将上面的Django视图类映射到一些合理的URL上。首先,我在thumbnailer应用程序中添加一个urls.py模块,并定义以下URL:
然后在项目的主URL配置中,我需要包含应用程序级的url,并让它识别到媒体URL,像这样:
接下来,我开始构建一个简单的模板视图,让用户提交一个图像文件,并检查提交的make_thumbnails任务的状态,并开始下载生成的缩略图。开始之前,我需要在thumbnailer目录中创建一个目录来存放这个模板,如下所示:
然后,我在这个templates/thumbnailer目录中添加了一个名为home.html的模板。在home.html中,我首先加载“widget_tweaks”模板标记,然后通过导入一个名为bulma CSS的CSS框架和一个名为Axios.js的JavaScript库来定义这个HTML。在这个HTML页面的body中,我提供了一个标题、一个用于显示进度消息中结果的占位符和一个文件上传表单。
在body元素的底部,我添加了JavaScript来提供一些额外的行为。首先,我创建了一个对文件输入框的引用,并注册了一个更改监听器,一旦文件被选中,它就会将所选文件的名称添加到UI中。
接下来是更相关的部分。我使用Django的模板逻辑if操作符检查从HomeView类视图中传递的task_id是否存在。这表示make_thumbnails任务被提交后的一个响应。然后,我使用Django url模板标记来构造一个适当的任务状态检查URL,并使用前面提到的Axios库开始对该URL发起一个间隔计时AJAX请求。
如果一个任务状态被报告为“SUCCESS”,我会将一个下载链接注入DOM并使其启动,触发下载并清除间隔计时器。如果任务状态是“FAILURE”,我就只需要清除这个interval(间隔),如果状态既不是“SUCCESS”也不是“FAILURE”,那么在调用下一个间隔之前我什么都不做。
此时,我可以打开另一个终端,再次启用Python虚拟环境,并启动Django开发服务器,如下所示:
- 前面描述的redis-server和celery任务终端也需要运行,如果从添加make_thumbnails任务之后,你还没有重新启动Celery 工作程序,那你可能需要按Ctrl + C来停止该工作程序,然后再次发送celery worker -A image_parroter --loglevel=info命令来重新启动它。每次对celery 任务相关的代码做了更改之后,Celery工作程序就必须重新启动。
现在,我可以在浏览器中访问http://localhost:8000 来加载home.html视图,提交一个图像文件,这个应用程序应该会返回一个results.zip归档文件,其中包含原始图像和一个128x128像素的缩略图。
部署到Ubuntu 服务器上
为了完成本文,我将演示如何在Ubuntu v18 LTS服务器上安装和配置这个Django应用程序,并使用Redis和Celery来执行异步后台任务。
一旦通过SSH连接上了服务器,我就对服务器进行更新,然后安装必要的包。
我还创建了一个名为“webapp”的用户,它为我提供了一个安装Django项目的主目录。
输入用户数据之后,我将webapp用户添加到sudo和www-data组中,并切换到webapp用户,然后用cd切换到到其主目录中。
在web应用程序目录中,我可以克隆image_parroter GitHub仓库, cd到这个仓库中,创建一个Python虚拟环境,并激活它,然后从requirements.txt文件安装依赖项。
除了我刚刚安装的依赖项之外,我还想为uwsgi web应用程序容器添加一个新的依赖项,它将为Django应用程序提供服务。
在继续之前,最好先更新一下settings.py文件,将DEBUG值切换为False,并将IP地址添加到ALLOWED_HOSTS列表中。
在此之后,进入Django image_parroter项目目录(包含wsgi.py模块的目录),并添加一个用于保存uwsgi配置设置的新文件,将其命名为uwsgi.ini,并向其中加入以下内容:
在我忘记之前,我应该继续添加日志目录,并给它适当的权限和所有者。
接下来,我创建了一个systemd服务文件来管理位于/etc/systemd/system/uwsgi.service的uwsgi应用程序服务器,并包含以下内容:
现在,我可以启动uwsgi服务了,检查它的状态是否正常,并启用它,以便在引导时自动启动。
至此,Django应用程序和uwsgi服务就已经设置好了,我可以继续配置redis-server。
我个人更喜欢使用systemd服务,所以我将通过将supervised参数设置为systemd来编辑/etc/redis/redis.conf配置文件。然后重启redis-server,检查它的状态,并使它能够在引导时启动。
接下来就是配置Celery。我为Celery创建了一个日志位置,并给这个位置适当的权限和所有者,就像这样:
接着,我在与前面描述的uwsgi.ini文件相同的目录中添加了一个名为celery.conf 的Celery配置文件,并向其中加入以下内容:
为了完成对celery的配置,我在/etc/systemd/system/celery.service添加了它自己的systemd服务文件,并在其中添加以下内容:
最后要做的是将nginx配置为uwsgi/django应用程序的反向代理,并提供媒体目录中的内容。为此,我在/etc/nginx/sites-available/image_parroter中添加了一个nginx配置文件,其中包含以下内容:
接下来,我删除了默认的nginx配置,允许我使用server_name _;去捕获80端口上的所有http通信,然后在我刚刚在“sites-available”目录中添加的配置文件和与之相邻的“sites-enabled”目录之间创建一个符号链接。
这样做之后,我就可以重新启动nginx,检查它的状态,并允许它开机启动。
此时,我可以将浏览器转向这个Ubuntu服务器的IP地址,并测试这个thumbnailer应用程序。
结论
本文描述了为什么要使用,以及如何使用Celery来实现启动异步任务的常见目的,该异步任务将连续运行直至完成。这将显著改善用户体验,减少长时间运行的代码路径对web应用程序服务器处理进一步请求的影响。
我已经尽力地去详细说明从设置开发环境、实现celery任务、在Django应用程序代码中生成任务到通过Django和一些简单的JavaScript处理结果的整个过程。
感谢你的阅读,请随时在下面进行评论或批评指正。
英文原文:https://stackabuse.com/asynchronous-tasks-in-django-with-redis-and-celery/
译者:测试