Django 3.1新带来的异步视图实例学习

新发布得到Django 3.1中,提供了对步视图的支持。在附带的官方教程提供了一个有关Django异步视图示例演示在调用时的异步执行asyncio.sleep。但是对此很多人会疑惑,这个sleep能干什么呢?本文我们就一起来学习一下 Django中的异步视图就能干啥。

Django 3.1新带来的异步视图实例学习

Django异步视图

Django现在允许用户编写可以异步运行的视图view。Django中一个简单且最小的同步视图刷新内存的示例:

def index(request):

return HttpResponse("This is a page.")


该例子接受一个请求对象并返回一个响应对象。在实际项目中,视图承担着很多工作,包括从数据库中获取记录,调用服务或渲染模板。在目前的情况下,它们都是同步工作得,需要按照顺序一个接一个地来执行。

在Django的MTV(Model Template View,模型-模板-视图)体系结构中,视图比其他部件更强大(大略感觉相当于MVC架构中的控制器)。在视图中,几乎可以执行创建响应所需的任何逻辑。这就是为什么异步视图的重要性,可以让我们同时做更多的事情。

编写异步视图非常容易,只需在一般的函数前面增加个async。例如,上述最小示例的异步版本为:

async def index_async(request):
    return HttpResponse("This is a asynchronously page!")


但是这样定义的看上去和函数很像,但是她是协程而不是函数。我们不能直接调用,而要创建一个事件循环来执行。

请注意,此特定视图不是异步调用任何内容。如果Django以传统的WSGI模式运行,则将创建(自动)新的事件循环来运行此协程。因此,在这种情况下,它可能会比同步版本慢一些。但这是因为没有使用它来同时运行任务。

那么,为什么还要麻烦编写异步视图呢?同步视图的局限性只有在访问规模很大才显现出其瓶颈。当涉及到大型Web应用程序时,比如FaceBook。

FaceBook的视图

Facebook发布了静态分析工具pysa来检测和预防Python中的安全问题。在关注其代码时候,发现其示例都是异步的写法。

Django 3.1新带来的异步视图实例学习

Django 3.1新带来的异步视图实例学习

可以肯定,虽然这不是Django,但是肯定是类似的框架。

综合考虑,Django将目前默认同步执行的视图改为异步还是非常有意义的。虽然等待I/O操作数微秒时,但是这会阻塞。如果换成异步就不会任何阻塞,可以同时处理其他任务,从而以较低的延迟处理更多的请求。这尤其对Facebook这样的大型网站性能改善而言。线程调度程序可能会在破坏性的共享资源更新之间中断,从而导致难以调试竞争条件。与线程相比,协程可以以更少的开销实现更高级别的并发。

误导性sleep例子

Django异步视图教程中都只简单提供了一个涉及sleep的示例。甚至正式的Django发行说明也包含以下示例:

async def my_view(request):
    await asyncio.sleep(0.5)
    return HttpResponse('Hello, async world!')

对于绝大多数人来说,这代码在可能会有误导。同步或异步发生的sleep对最终用户没有啥意义和效果。开链接到该视图的URL,需要等待0.5秒,然后它才会返回一个 "Hello, async world!"。如果是一位新手,则可能会期望立即得到答复。这与time.sleep()视图中的同步对象相比,没有啥意义。

异步世界中的大多数事情一样,在事件循环中。如果事件循环中还有其他任务等待运行,则该半秒窗口将为其他任务提供运行该任务的机会。协程假定每个人都能快速工作,并迅速将控件移交给事件循环。

一些命令行界面使用sleep来给用户足够的时间以使其消失之前阅读消息。但这对于Web应用程序是相反的-来自Web服务器的更快响应是改善用户体验的关键。

更好的实例

编写异步视图之前要记住的经验法则是检查它是受I/O密集型还是受CPU密集型。大部分时间花费CPU密集型任务中的视图(例如,矩阵乘法或图像处理)实际上不会从异步视图中受益,而专注于I/O绑定的活动。

调用微服务

目前大多数大型Web应用程序正从单一架构转型到有很多微服务组成的架构。渲染视图可能需要许多内部或外部服务的结果。

比如这样一个示例,在书籍电子商务网站显示推荐书籍,为登录用户量身定制了首页。推荐引擎通常被实现为单独的微服务,该微服务基于过去的购买历史以及通过了解过去的推荐的成功程度来进行一些机器学习来做出推荐。

在这种情况下,还需要另一个微服务的结果,该服务决定将哪些促销横幅显示为旋转横幅或幻灯片显示给用户。这些标语不是为登录用户量身定制的,而是根据当前销售的商品(有效的促销活动)或日期而变化。这样一个实例的同步版本:

def sync_home(request):
    context = {}
    try:
        response = httpx.get(PROMO_SERVICE_URL)
        if response.status_code == httpx.codes.OK:
            context["promo"] = response.json()
        response = httpx.get(RECCO_SERVICE_URL)
        if response.status_code == httpx.codes.OK:
            context["recco"] = response.json()
    except httpx.RequestError as exc:
        print(f"An error occurred while requesting {exc.request.url!r}.")
return render(request, "index.html", context)

使用httpx库来代替流行的Python请求库,因为它支持发出同步和异步Web请求。接口几乎是相同的。

该视图的问题在于,由于这些服务顺序发生,因此调用这些服务所花费的时间加在一起。Python进程被挂起,直到第一个服务响应,在最坏的情况下这可能需要很长时间。

让尝试使用简单(且无效)的await调用并发运行它们:

async def async_home_inefficient(request):
    context = {}
    try:
        async with httpx.AsyncClient() as client:
            response = await client.get(PROMO_SERVICE_URL)
            if response.status_code == httpx.codes.OK:
                context["promo"] = response.json()
            response = await client.get(RECCO_SERVICE_URL)
            if response.status_code == httpx.codes.OK:
                context["recco"] = response.json()
    except httpx.RequestError as exc:
        print(f"An error occurred while requesting {exc.request.url!r}.")
return render(request, "index.html", context)

请注意,视图已从函数更改为协程(由于async def关键字)。另请注意,实例中两个地方等待每种服务的响应。不必尝试在这里理解每一行,因为将通过一个更好的示例进行解释。

有趣的是,该视图不能同时工作,并且所花费的时间与同步视图相同。如果熟悉异步编程,可能已经猜到只是等待协程并不会使其同时运行其他事情,只会将控制权交还给事件循环。视图仍然被暂停。

让我们看一下同时运行事务的正确方法:

async def async_home(request):
    context = {}
    try:
        async with httpx.AsyncClient() as client:
            response_p, response_r = await asyncio.gather(
                client.get(PROMO_SERVICE_URL), client.get(RECCO_SERVICE_URL)
            )
             if response_p.status_code == httpx.codes.OK:
                context["promo"] = response_p.json()
            if response_r.status_code == httpx.codes.OK:
                context["recco"] = response_r.json()
    except httpx.RequestError as exc:
        print(f"An error occurred while requesting {exc.request.url!r}.")
return render(request, "index.html", context)

如果我们正在调用的两个服务具有相似的响应时间,那么与同步版本相比,此视图应在_half _time中完成。这是因为调用可以同时发生。

有一个外部try ... except块可以在进行任何HTTP调用时捕获请求错误。然后是一个内部async ... with块,它提供了一个包含客户端对象的上下文。

最重要的一行是asyncio.gather调用,其中包含两个client.get调用创建的协程。collect调用将同时执行它们,并且仅在它们都完成时才返回。结果将是响应的元组,将其分解为两个变量response_p和response_r。如果没有错误,则将这些响应填充到发送的用于模板渲染的上下文中。

微服务通常是组织内部的,因此响应时间短且变化少。但是,绝对不依赖同步调用在微服务之间进行通信永远不是一个好主意。随着服务之间的依赖性增加,它会创建一长串的请求和响应调用。这样的连锁会减慢服务速度。

还有一很实际的例子就是Web抓取的问题,因为有许多异步示例使用它们。这样同时获取和抓取多个外部网站或网站中的页面以获取实时股票市场(或比特币)价格等信息的情况。该实现将与我们微服务示例中看到的非常相似。

但这是非常危险的,因为视图应能尽快将响应返回给用户。因此,尝试获取具有这种随着信息时间变化的的站点可能会导致获取过时的信息。而微服务调用通常是内部的,因此可以通过适当的SLA来控制响应时间。

理想情况下,抓取应在安排为定期运行的单独过程中进行(使用celery或)。该视图应仅选择已采集的值并将其显示给用户。

文件服务

通常有一个需求,需要通过动态内容来提供文件服务。文件通常位于基于磁盘的存储(较慢的)中。尽管使用Python可以很容易地完成此文件操作,但就大型文件的性能而言,它可能会很昂贵。无论文件大小如何,这都是一个潜在的阻塞I/O操作,可用于同时运行另一个任务。

假设我们需要在Django视图中提供PDF证书。但是,出于某种原因(可能用于标识和验证),需要将下载证书的日期和时间存储在PDF文件的元数据中。

该示例中我们使用aiofiles库进行异步文件I/O。该API与熟悉的Python内置文件API几乎相同。下面异步视图的编写方式:

async def serve_certificate(request):
    timestamp = datetime.datetime.now().isoformat()
    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = "attachment; filename=certificate.pdf"
    async with aiofiles.open("homepage/pdfs/certificate-template.pdf", mode="rb") as f:
        contents = await f.read()
        response.write(contents.replace(b"%timestamp%", bytes(timestamp, "utf-8")))
    return response

该例子说明了为什么我们需要在Django中进行异步模板渲染。但是在实现之前,只能使用aiofiles库来提取本地文件拍。直接使用本地文件而不是Django的staticfiles有不利之处。

处理上传

另一方面,上传文件也可能是很长的阻塞操作。出于安全和组织方面的原因,Django将所有上传的内容存储在单独的"媒体"目录中。

如果有一种允许上传文件的表单,那么我们需要预料到一些讨厌的用户会上传一个不可能很大的文件。值得庆幸的是,Django将文件以一定大小的块传递给视图。结合aiofile异步写入文件的功能,我们可以支持高度并发的上传。

async def handle_uploaded_file(f):
    async with aiofiles.open(f"uploads/{f.name}", "wb+") as destination:
        for chunk in f.chunks():
            await destination.write(chunk)
async def async_uploader(request):
    if request.method == "POST":
        form = UploadFileForm(request.POST, request.FILES)
        if form.is_valid():
            await handle_uploaded_file(request.FILES["file"])
            return HttpResponseRedirect("/")
    else:
        form = UploadFileForm()
    return render(request, "upload.html", {"form": form})

同样,这绕过了Django的默认文件上传机制,因此需要注意安全隐患。

总结

Django Async项目具有完全向后兼容性,这是其主要目标之一。因此,可以继续使用旧的同步视图,而无需将其重写为异步视图。异步视图也并不是解决所有性能问题的灵丹妙药,因此大多数项目仍将会继续使用同步代码,因为它们非常容易推理。

实际上,可以在同一项目中同时使用异步视图和同步视图。Django将负责以适当的方式调用视图。但是,如果使用的是异步视图,建议将应用程序部署在ASGI服务器上。

展开阅读全文

页面更新:2024-04-14

标签:视图   示例   应用程序   函数   实例   例子   对象   模板   两个   版本   事件   上传   操作   文件   时间   用户   科技

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top