Django 3.0将是异步的

安德鲁·戈德温(Andrew Godwin)于5月9日发布了DEP 0009:具有异步功能的Django ,并于7月21日获得Django 技术委员会的批准,因此希望在Django 3.0发行之前,他们将有时间做一些有趣的事情。 在Habr的评论中已经提到该消息,但是我决定通过翻译将该消息传达给更广泛的受众-主要是针对那些像我一样不特别关注Django新闻的人。



异步Python已经开发了很多年,在Django生态系统中,我们在Channels中对其进行了试验,主要侧重于Web套接字支持。


随着生态系统的发展,很明显,虽然没有迫切需要扩展Django以支持Web套接字等非HTTP协议,但异步支持将为传统的Django模型-视图-模板框架提供许多好处。


好处在下面的动机部分中进行了描述,但是我得出的一般结论是,我们从异步Django中获得了很多好处,值得付出艰苦的工作。 我还认为,以一种迭代的,由社区支持的方式进行更改非常重要,而这种更改将不依赖可能会耗尽一两个旧的贡献者。


尽管该文档被指定为“功能” DEP,但所有这些都意味着它也部分是过程DEP。 以下提议的更改范围非常大,将它们作为传统的单一功能流程启动可能会失败。


当然,在整个文档中,重要的是要记住Django的理念,即保持一切安全并向后兼容。 该计划不是要删除同步Django,而是要使它保持当前形式,而是将异步添加为认为自己需要额外性能或灵活性的用户的一种选择。


这是一项巨大的工作吗? 当然可以 但是我认为,这使我们能够极大地改变Django的未来-我们有机会采用一个行之有效的框架和一个令人难以置信的社区,并引入一套以前不可能的全新选择。


网络已经发生了变化,Django应该随之发生变化,但是根据我们的理想,负担得起,默认情况下安全且随着项目的发展和需求的变化而灵活。 在云数据仓库,面向服务的体系结构和作为复杂业务逻辑基础的后端的世界中,具有竞争力的能力是关键。


该DEP概述了一个计划,我认为它将带领我们到达那里。 这是我真正相信的愿景,我将与之共同努力,尽一切可能。 同时,进行认真的分析和怀疑是合理的; 我要求您进行建设性的批评以及信任。 Django依赖于人员及其创建的应用程序社区,如果我们需要确定通往未来的道路,则必须共同努力。


简短说明


我们将为Django添加对异步表示,中间件,ORM和其他重要元素的支持。


这将通过在线程中运行同步代码,然后逐步将其替换为异步代码来完成。 同步API将继续存在并得到完全支持,并且随着时间的流逝,它们将变成用于初始异步代码的同步包装器。


ASGI模式将把Django作为本机异步应用程序启动。 每次访问Django时,WSGI模式都会触发一个单独的事件循环,以便异步层与同步服务器兼容。


围绕ORM的多线程是复杂的,并且需要连接上下文和粘性线程的新概念来运行同步ORM代码。


Django的许多部分将继续同步工作,我们的首要任务是支持用户以两种样式编写视图,从而允许他们为正在处理的演示文稿选择最佳样式。


某些功能(例如模板和缓存)将需要它们自己的独立DEP,并需要研究如何使其完全异步。 该DEP主要关注HTTP中间件视图流和ORM。


将具有完全的向后兼容性。 一个标准的Django 2.2项目应该在异步Django(3.0或3.1)中运行而无需更改。


该建议的重点是逐步实现小型迭代部件,并将其逐步放置在master分支中,以避免长寿命的fork出现问题,并允许我们在发现问题时改变路线。


这是吸引新成员的好机会。 我们必须为该项目提供资金,以便更快地进行。 资金应达到我们不习惯的规模。


规格书


总体目标是使Django的每个部分都可以变为异步状态(不仅仅限于CPU绑定的计算)(在没有锁的异步事件循环中运行)。


这包括以下功能:


  • 中间层(中间件)
  • 观看次数
  • ORM
  • 模式
  • 测试中
  • 快取
  • 表格验证
  • 电邮

但是,这不包括国际化之类的事情,因为这是一个CPU密集型任务,而且运行速度很快,或者通过管理命令启动时是单线程的迁移,因此不会带来任何性能提升。


在可预见的将来,每个在内部变为异步的单独函数还将提供一个与当前API(在2.2中)向后兼容的同步接口-我们可以随着时间的推移对其进行更改以使其变得更好,但同步API不会随处可见。


下面概述如何在技术上实现该目标,然后给出特定领域的具体实现细节。 对于Django的所有功能,它并不是穷尽的,但是如果我们达到了这个最初的目标,那么我们将包括几乎所有用例。


本节的最后一部分,“程序”,还讨论了如何逐步并由几个开发团队并行实施这些更改,这对于在合理的时间内借助志愿者完成这些更改非常重要。


技术审查


允许我们并行维护同步和异步实现的原理是在另一种内部运行一种样式的能力。


每个功能将经历三个实施阶段:


  • 仅同步(我们在这里)
  • 异步包装的同步实现
  • 异步包装的异步实现

异步包装器


首先,将现有的同步代码包装在异步接口中,该接口在线程池中运行同步代码。 这将使我们能够相对快速地设计和提供异步接口,而不必重写所有可用的代码以实现异步。


此工具包已经在asgiref中作为sync_to_async函数sync_to_async ,该函数支持诸如异常处理或threadlocals之类的东西(有关此sync_to_async ,请sync_to_async下文)。


在线程中运行代码很可能不会导致生产率的提高-当您仅运行常规线性代码时,出现的开销可能会使其速度有所降低-但这将使开发人员可以竞争性地开始运行某些产品并习惯于新功能。


另外,Django的某些部分很容易在重复访问时从同一线程开始。 例如,在数据库中处理事务。 如果我们将某些代码包装在atomic() ,然后通过从池中获取的随机线程来访问ORM,则事务将无效,因为该事务已绑定到启动事务的线程内部的连接。


在这种情况下,需要一个“粘性线程”,其中异步上下文在同一线程中顺序调用所有同步代码,而不是将其推送到线程池中,同时保持ORM和其他线程敏感部分的正确行为。 我们怀疑需要Django的所有部分(包括整个ORM)都将使用sync_to_async版本(考虑到这一点),因此默认情况下所有内容都是安全的。 用户将能够有选择地禁用此选项以执行有竞争力的查询-有关更多详细信息,请参见下面的“ ORM”。


异步实现


下一步是将函数的实现重写为异步代码,然后通过包装器提供同步接口,该包装器在一次事件循环中执行异步代码。 这已在asgiref中作为async_to_sync函数提供。


无需立即重写所有功能即可快速跳转到第三阶段。 我们可以将精力集中在我们能做得好的部分上,并获得第三方库的支持,同时在需要更多工作来实现本机异步的情况下帮助Python生态系统的其余部分。 这将在下面讨论。


该概述概述了几乎所有应该变为异步的Django函数,除了Python不提供我们已经使用的异步函数等效项的那些地方。 结果要么是Django以异步模式显示其API的方式发生变化,要么是与Python核心开发人员一起帮助开发Python异步功能。


线程本地


需要与下文描述的大多数功能分开提及的Django实现的基本细节之一是threadlocals。 顾名思义,threadlocals在线程中工作,尽管Django将HttpRequest对象保留在threadlocal之外,但我们在其中放置了其他一些内容,例如数据库连接或当前语言。


使用threadlocals可以分为两个选项:


  • “本地上下文”,其中在某些基于堆栈的上下文(例如请求)中需要一个值。 这是设置当前语言所必需的。
  • “ True threadlocals”,其中受保护的代码实际上对于从另一个线程进行调用是不安全的。 这是用于连接数据库。

乍一看,似乎可以使用Python中新的contextvars模块来解决“上下文本地”问题,但是Django 3.0仍必须支持Python 3.6,而该模块出现在3.7中。 另外, contextvars专门设计用来在切换到新流时摆脱上下文的,而我们需要保存这些值以允许sync_to_asyncasync_to_sync函数正常用作包装器。 当Django仅支持3.7及更高版本时,我们可以考虑使用contextvars ,但这将需要在Django中进行大量工作。


这已经通过与协程和线程兼容的asgiref Local得以解决。 现在它不使用contextvars ,但是我们可以在进行一些测试之后将其切换为与backport一起使用,以支持3.6版。


另一方面,真正的threadlocals可以继续在当前线程中继续工作。 但是,我们必须更加小心,以防止此类对象泄漏到另一个流中。 当演示文稿不再在同一线程中执行,而是为每个ORM调用生成一个线程时(在“同步实现,异步包装”阶段),在异步模式下可能无法进行的某些操作。


这将需要特别注意,并且禁止在异步模式下执行某些先前可能的操作; 我们所知道的情况在下面的特定部分中进行了描述。


同时支持同步和异步接口


尝试移植Django时,我们将遇到的主要问题之一是Python不允许您使用同一个名称制作函数的同步和异步版本。


这意味着您不能仅仅采用和制作一个像这样的API:


 #   value = cache.get("foo") #   value = await cache.get("bar") 

这是对Python异步实现方式的不幸限制,并且没有明显的解决方法。 打电话时,您不知道是否会等待,因此无法确定需要退货的内容。


(注意:这是因为Python将异步函数实现为“返回协程的同步可调用函数”,而不是“在对象上调用__acall__方法”之__acall__东西。异步上下文管理器和迭代器不存在此问题,因为他们有单独的方法__aiter____aenter__ 。)


考虑到这一点,我们必须将同步和异步实现的名称空间彼此分开放置,以免它们冲突。 我们可以使用命名的参数sync=True来执行此操作,但这会导致函数/方法的主体混乱,并阻止使用async def ,并且还使您不小心忘记编写此参数。 当您想异步调用同步方法时,随机调用它是很危险的。


Django代码库中大多数地方的建议解决方案是为函数的异步实现的名称提供后缀-例如,同步的cache.get之外的cache.get 。 尽管这是一个丑陋的解决方案,但它使查看代码时检测错误非常容易(您应将_async_async方法一起使用)。


视图和HTTP处理


视图可能是异步有用的基石,我们希望大多数用户在异步和同步代码之间进行选择。


Django将支持两种视图:


  • 到目前为止,由同步函数或带有同步__call__类定义的同步表示
  • 由异步函数(返回协程)或带有异步__call__的类定义的异步表示形式。

它们将由BaseHandler处理, BaseHandler将检查从URL解析器接收的视图并相应地调用它。 基本处理程序应该是Django异步化的第一部分,我们将需要修改WSGI处理程序以使用async_to_sync在其自己的事件循环中调用它。


中间层(中间件)或诸如ATOMIC_REQUESTS设置(将视图包装在非异步安全代码中(例如, atomic()块))将继续起作用,但是其速度将受到影响(例如,禁止在视图内部使用atomic()进行并行ORM调用)


现有的StreamingHttpResponse类将被修改为能够接受同步或异步迭代器,然后其内部实现将始终是异步的。 对于FileResponse同样FileResponse 。 由于这是直接访问Response对象的第三方代码的潜在向后不兼容点,因此我们仍然需要在过渡期间提供同步的__iter__


Django将继续无限地支持WSGI,但是WSGI处理程序将继续运行异步中间件并在其自己的一次性事件循环中进行视图。 这可能会导致性能略有下降,但在最初的实验中并没有太大影响。


所有异步HTTP函数都将在WSGI中运行,包括长轮询和慢响应,但它们的效率将与现在一样低,每个连接占用一个线程/进程。 ASGI服务器将是唯一能够有效支持许多并发请求以及处理非HTTP协议(例如WebSocket)以供Channels扩展使用的服务器。


中间层


上一节主要关注请求/响应路径,而中间件由于其当前设计固有的复杂性而需要单独的一节。


Django中间件现在以堆栈的形式排列,每个中间件都通过get_response来运行下一个顺序的中间件(或堆栈中最低的中间件的视图)。 但是,我们需要维护同步和异步中间件的混合以实现向后兼容性,并且这两种类型将不能彼此本地访问。


因此,为了确保中间件正常工作,我们将不得不使用get_response占位符初始化每个中间件,该占位符将控制权返回给处理程序,并处理中间件和视图之间的数据传输以及异常抛出。 从某种角度上讲,从内部角度来看,它最终看起来像Django 1.0时代的中间件,尽管用户API仍然保持不变。


我们可以宣布同步中间件已过时,但我建议您尽快不要这样做。 如果并且当我们结束其淘汰周期的末尾时,我们可以将中间件实现返回到现在的纯递归堆栈模型。


ORM


就代码大小而言,ORM是Django中最大的部分,也是最难转换为异步的部分。


这主要是由于以下事实:基础数据库驱动程序在设计上是同步的,向一组成熟的,标准化的异步数据库驱动程序的进展会很慢。 相反,我们必须设计一个数据库驱动程序最初将是同步的未来,并为将以迭代方式进一步开发异步驱动程序的贡献者奠定基础。


ORM的问题分为两大类-线程和隐式阻塞。



ORM的主要问题是Django是围绕单个全局connections对象设计的,它神奇地为您提供了当前线程的正确连接。


在异步世界中-所有协程都在同一线程中工作-这不仅令人讨厌,而且很危险。 如果没有任何额外的安全性,用户照常访问ORM可能会通过从几个不同的位置访问ORM来破坏连接对象。


幸运的是,连接对象至少可以在线程之间移植,尽管不能同时从两个线程中调用它们。 Django已经在ORM代码中关注数据库驱动程序的线程安全性,因此我们有一个地方可以更改其行为以使其正常工作。


我们将修改connections对象,以便它既可以理解协程,也可以理解线程-重用asgiref.local一些代码,但还要添加其他逻辑。 连接将在相互调用的异步和同步代码中共享-上下文通过sync_to_asyncasync_to_sync传递-同步代码将被强制在一个粘性线程中顺序执行,因此这将无法工作同时破坏线程安全性。


这意味着我们需要像上下文管理器这样的解决方案来打开和关闭数据库连接,例如atomic() 。 这将允许我们在此上下文中提供一致的调用和粘性线程,并允许用户在要打开多个连接的情况下创建多个上下文。 如果我们想进一步发展,它也为我们提供了摆脱神奇的全球connections的潜在途径。


目前,Django没有独立于处理程序类信号的连接生命周期管理,因此我们将使用它们来创建和清除这些“连接上下文”。 该文档也将进行更新,以使其更清楚如何在请求/响应周期之外正确处理连接; 即使在当前代码中,许多用户也不知道任何长期运行的管理团队必须定期调用close_old_connections才能正常工作。


向后兼容意味着我们必须允许用户随时从任何随机代码访问connections ,但是我们仅允许同步代码使用; 从第一天开始,我们将确保将代码包装在“连接上下文”中(如果它是异步的)。


似乎除了transaction.atomic()之外还添加transaction.atomic()并要求用户在其中之一中运行所有代码似乎很好,但这会导致您对附加的代码产生混乱其中一个在另一个内部。


相反,我建议创建一个新的上下文管理器db.new_connections()来启用此行为,并使其在每次调用时创建一个新连接,并允许在其内部嵌套任意atomic()


每次您new_connections()块时,Django都会使用新的数据库连接设置一个新的上下文。 在区块外执行的所有交易将继续; 块内的任何ORM调用都将与数据库建立新连接,并且从这个角度来看将看到数据库。 如果在数据库中启用了事务隔离(通常默认情况下会这样做),则意味着该块内的新连接可能看不到该块外任何未提交的事务所做的更改。


此外,此new_connections块内的连接本身可以使用atomic()来触发这些新连接上的其他事务。 允许这两个上下文管理器进行任何嵌套,但是每次使用new_connections ,先前打开的事务都会被“挂起”,并且在new_connections新的new_connections块之前不会影响ORM调用。


有关此API外观的示例:


 async def get_authors(pattern): # Create a new context to call concurrently async with db.new_connections(): return [ author.name async for author in Authors.objects.filter(name__icontains=pattern) ] async def get_books(pattern): # Create a new context to call concurrently async with db.new_connections(): return [ book.title async for book in Book.objects.filter(name__icontains=pattern) ] async def my_view(request): # Query authors and books concurrently task_authors = asyncio.create_task(get_authors("an")) task_books = asyncio.create_task(get_books("di")) return render( request, "template.html", { "books": await task_books, "authors": await task_authors, }, ) 

这有点冗长,但目标也是添加高级快捷方式以启用此行为(并涵盖从Python 3.6中的asyncio.create_task到3.7中的asyncio.ensure_future的过渡)。


在同一连接上下文中使用此上下文管理器和粘性流,我们保证默认情况下所有代码都将尽可能安全。 用户有可能使用yield在一个线程中将连接用于请求的两个不同部分,但是现在这yield可能。


隐式锁


当前ORM设计的另一个问题是在模型实例中遇到阻塞(与网络相关的)操作,特别是与读取相关的字段。


如果您采用模型实例,然后访问model_instance.related_field ,则Django将透明地加载关联模型的内容并将其返回给您。 但是,这在异步代码中是不可能的-阻塞代码不应在主线程中执行,并且不能异步访问属性。


幸运的是,Django已经有了解决方法select_related ,它可以预先加载相关字段,而prefetch_related用于多对多关系。 如果异步使用ORM,我们将禁止任何隐式阻止的操作,例如对属性的后台访问,而是返回一个错误,指示您必须首先检索该字段。


这样做还有一个好处,就是可以防止在for循环中执行N个请求的慢速代码,这是许多新Django程序员常见的错误。 这增加了入门障碍,但是请记住,异步Django将是可选的-用户仍然可以根据需要编写同步代码(并且在本教程中会鼓励这样做,因为在同步代码中犯错误要困难得多)。


幸运的是, QuerySet可以轻松实现异步生成器,并透明地支持同步和异步:


 async def view(request): data = [] async for user in User.objects.all(): data.append(await extract_important_info(user)) return await render("template.html", data) 

其他


与模式更改关联的ORM部分将不是异步的; 仅应从管理团队中召集他们。 有些项目已经在提交中称呼它们,但这并不是一个好主意。


模式


现在,模板是完全同步的,并且计划在第一步中以这种方式保留它们。 , , DEP.


, Jinja2 , .


, Django , . Jinja2 , , , .


, render_async , render ; , , .



Django — _async - (, get_async , set_async ).


, API sync_to_async , BaseCache .


, thread-safety API , Django, , . , ORM, , .



, , , ModelForm ORM .


, - clean save , , . , , , DEP.


Email


Django, . send_mail_async send_mail , async - (, mail_admins ).


Django , - SMTP, . , , , , .


测试中


, Django .


ASGI- asgiref.testing.ApplicationCommunicator . assert' .


Django , , . , — , , HTTP event loop, WSGI.


. , , .


, , . async def @async_to_sync , , , Django test runner.


asyncio ( loop' , ) , , , DEBUG=True . — , , .


WebSockets


Django; , Channels , ASGI, .


, Channels, , ASGI.



, , . , .


, . , — .


, , . , ORM, , , .


:


  • ( 3.0)


    • HTTP, ( )
    • async ORM

  • ( 3.1)


    • ORM ( )
    • ( )
    • ( )


    • ORM ( )
    • ( )
    • Email


; , . , , , , .


, - ; , , . , , Django, Django async-only .


, , DEP , , , email . DBAPI — , core Python , , PEP, .



, , , . Django , - , -; .


, - . , — , , .


Python — . - Python , , .


Python asyncio , , . , , , , Django-size .



Django, «»; , , — , Django — .


, . , API , Django - .


, , Django . , Django ORM , , , -.


, , — . - long-poll server-sent events. Django , - .



Django; . , , , .


Django , . ; Django- , , , , , .


, -- Django .



, . « Django», ; , , .


, , , , API Django, , , Python 3, API, Django Python.


Python


Python . Python, , , .


, Django — - Python — , Python, Python . , , , .



, Django, . , Django , .


— , , , , .


, — , , — Django ( , Python ).


Django?


, Django. , Lawrence Journal-World — , , SPA — , , . , , , , .


, Django , - , — — . , , ; , Django , .


, Django . , . , , , — , , .



Django django-developers , , , , DEP.


, , , :


  • : master- Django .
  • : Django , , , , Django .
  • : , , Django, , Django. , , , , .

Channels DEP ; Django , .


, , . DEP , , — Django .


. Django, , , WSGI , event loop , . 10% — , . , .


, , (- , Python ). , Django ; , , , Django master- .


, ( ORM, ..), ; Python.


, , Django, «» . , , , , .


替代品


, , , .


_async


, , (, django.core.cache.cache.get_async ), :


 from django.core.cache_async import cache cache.get("foo") 

, ; , , .


, , ; .


Django


- , ; , , , — .


, , , — , .


Channels


, Channels , «» Django. , - , , Django; ORM, HTTP/middleware flow .


asyncio


event loop' Python, asyncio , Django. await async Python event loop .


, asyncio , ; Django , , . Django ; , , async runtime, , .


Greenlets/Gevent


Gevent, , Python.


, . yield await , API, Django, , . , .


, , , . greenlet-safe Django ORM - new-connection-context, .


, . Django « » gevent, , , , .



DEP.


, — , — , ( ).


, , - . Django Fellows ; — , ( ), , , - .


— , Kickstarter migrations contrib.postgres , MOSS (Mozilla) Channels . , Django, , .


, . — Python, Django — — . , Django/async, .


HTTP/middleware/view flow, , , , « », .


, , , ( , Fellows, / , Channels, , ), , .


, , , , Django, .



, , , , API.


, , , , HTTP/middleware flow. , API, APM, .


, , Django , , . , ORM , , — , ORM .



DEP , ; Django .


, asgiref , , . Django Django.


Channels , Django, Django.


著作权


( ) CC0 1.0 Universal .

Source: https://habr.com/ru/post/zh-CN461493/


All Articles