Python中的异步编程

Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发。

bb4dd0a9e135.png

Quora的使命就是分享和增加全世界的知识,并且为了达到这个使命,我们不断地推出改进来让Quora对于我们的读者和作者来说更快。在上一篇,缩短绘制时间,我们讨论了对于客户端性能的最新优化,本文,我们将通过异步编程框架来讨论服务器端性能的新发展。
为了同时优化页面加载和用户操作的性能,我们积极使用缓存,以确保持续快速访问常用数据。在Quora中,对于存储在较慢的存储系统如MySQL中的数据,我们使用memcached作为主缓存层。例如,当我们呈现那些给答案投票者的名单时,需要从存储层取回用户ID到用户名称的映射。每个请求直接访问MySQL的话,数据库将很快超载,所以使用memcached存储这个映射来替代。尽管为每一个用户ID分配一个单独的memcached请求会比查询MySQL更快,但是通过一个单一的批量的多请求取回所有数据会更快。  
因为网络通常是memcached请求最昂贵的部分(在我们的环境中占总时间的80%以上),适当的批量缓存请求对于保持Quora快速是很重要的。然而,如果开发者必须手动指定所有数据如何从memcached批量检索,它将是单调易错的。因此,我们已经开发了一个抽象概念叫Asynq,它使开发者很容易编写批量的缓存请求,如今它已经开源。
Priming
在我们开发Asynq之前,使用一个叫做priming的方法来给memcached发送批量请求。每次开发者编写访问数据的函数时,也要编写一个单独的priming函数来指定该函数可以访问的所有数据。例如,假设有一个检索给定用户ID列表并返回用户标识列表的函数,如下:

2.png

相应的priming函数会看起来如下:

4.png

调用get_names的代码将负责先调用prime_get_names,它将使用多请求从memecached获取所有必要的数据。然后,该数据将被存储在服务器的本地缓存中,所以当真正的函数(本例是get_names)运行时,它不会调用任何网络请求!本质上,每个prime函数表示的都是一个函数的依赖项,或者说它依赖的memecached键。
当我们需要调用一个缓存函数来决定从memcached中获得什么额外数据时,priming会变得更复杂。考虑如下模板函数:

1.png

在本例中,prime_get_upvoter_names 需要知道upvoter_uids,为了那些uids调用prime_name_of_user ,但由于upvoters_of_answer 被存储了,它也必须被启动。因此,相应的prime函数将明显更加复杂:

1.png

通过恰当地启用我们所有的模型调用, 我们看到惊人的速度改进,因此,我们使用静态分析工具来实施规则,即Quora上所有需要呈现的数据首先要被primed。然而,这些高速增长带来了明显的开发费用,因为实质上开发者需要编写(并调用)所有的模版函数两次。随着我们代码库的增长,priming变得冗余、难理解且易出错。
Asynq
为了解决priming带来的复杂性,我们创建一个叫做Asynq的框架,它在底层采用类似的方法,但是改进了API,把缓存请求集成到模板代码本身。Asynq中,所有需要数据访问的函数通过调度程序运行,调度程序记录跟踪它们的依赖项。当一个函数需要通过调用其它函数来获取数据时,它不再控制调度程序,而是指示需要取回的数据。然后调度程序停止执行该函数直到它解决了这个函数所有的依赖项。
Asynq中,get_names 函数早期看起来如下:
n2.png

不需要额外的priming 函数——所有代码都包含在模版函数本身。因此,开发者不仅不再需要编写一个完全独立的priming函数,他们也不需要记忆每次模版函数被调用时调用priming函数。之前更复杂的get_upvoter_names在Asynq中也更简单了:
n1.png

可以把Async 函数理解成创建一个依赖关系图:在它的第一个yield中,get_upvoter_names 依赖于upvoters_of_answer 的完成。类似地,upvoters_of_answer 可能有它自己的依赖项。Asynq调度程序分解该依赖关系图执行async函数,直到所有目前执行的函数从memcached中获取数据时阻塞。然后调度程序使用一个单独的多请求从memcached取回数据,并继续执行直到async函数完成。
假定我们有一个异步函数称为model_call(),它有三个依赖项,每个依赖项都会读取多个memcached键值。直接实现将会使用3个多请求(或者6个单一获取),每个依赖项函数一个,而异步调度程序分解的依赖图看起来如下所示:

2.png

我们异步编程的方法不同于Python中其它的异步库如asyncioTwistedgeventTornado。这些库侧重于异步I/O,而Asynq却侧重于高效的批处理。例如,一个典型缓存使用memcached和asyncio的实现将分别解决缓存依赖项,因此每个memcached请求都会对memcached产生一个单独的请求。在Asynq中,依赖项将会成批进入一个单独的memcached多请求,这可以减少I/O阻塞的总时间。另外,Asynq允许函数被同步或异步调用(通过增加的.async属性),而asyncio需要所有的async def函数被asyncio.get_event_loop()显式调度。使用Asynq的批处理,我们花费很少时间阻塞在I/O,因此采用其它异步I/O库对于我们来说不是优先选择。
与priming相比,Asynq提供一个更通用的、简明的、有原则的方式来支持批处理。因为逻辑仅需要被实现一次,Asynq明显比priming花费更少的开发费用。 减少priming的重复逻辑也提升了性能,正如我们所见,服务器端的速度取胜,由于我们迁移更多代码库从priming到Asynq。
迁移和学习
开发出第一个版本Asynq后,通过迁移代码库的几个小部分从priming到Asynq,我们着手验证我们的设计和实现。在这样做的过程中,我们发现并修复了各种问题:

  • Python2.7中,生成器不能返回值,所以以上代码片段实际上在Python2.7中是无效的。在Asynq的第一个版本中,异步函数产生的最后一个值将会被解析为函数的返回值。然而,这意味着yield关键字意义的超载,这使得代码很难阅读。作为一个替代解决方案,我们从生成器中返回值——PEP 380中有介绍——从Python 3到Python 2.7,并且在代码库中必要的地方,我们目前正在使用Python 2.7的一个补丁版本。(Asynq也支持Python 2.7的未打补丁版本,通过使用一个result函数,该函数抛出一个异常,该异常被解析为返回值。)

  • 最初,使用@async()装饰器使一个函数变为异步函数完全改变了它的接口——所有的调用者不得不使用一个特殊语法来调用异步函数。这个决策使得priming和Asynq在我们的代码库中更难共存,由于所有的开发者需要意识到这种差异。为了修复这个问题,我们更新@async()装饰器,给所有的异步函数增加了一个新的.async属性,这样直接调用一个装饰器函数将仍旧返回结果。

  • 起初测试异步函数具有挑战性。在单元测试中,我们广泛使用Python的mock模块,但是很难模拟异步函数,因为这样做需要特殊处理.async的属性并返回值。作为这个问题的解决方案,我们创建一个专用的模拟函数,asynq.mock.patch,它自动负责模拟,这使得模拟异步函数更轻松。

实现上述改进后(和其它很多改进),我们决定将我们的代码库完全从priming迁移到Asynq。让这两种抽象概念同时存在于我们的代码库中不利于开发速度,因为工程师需要在两种API中根据他们正在编辑哪种模型进行上下文切换。我们利用现有的静态分析工具自动进行迁移,因此工程师只需要去核实脚本的输出并做一点细小的改变,而不是手动迁移代码。
在开始大规模迁移整个代码库之前,不同团队的工程师完成一些较小的“演习”,作为细化我们自动迁移工具的手段,并建立精确的范围估计。完成几个演练后,我们由大约30个工程师(即我们工程师团队的50%)进行了一次有协调性的迁移,在此期间仅用了4天时间,我们迁移了Quora代码库的15,000多行priming代码。
如今,我们的整个代码库仅使用Asynq,并且服务器端开发速度更快且更不易出错。
开源Asynq(和朋友们)
现在可以在GitHub和PyPI上获得Asynq。你可以阅读源码或者通过pip install asynq安装Asynq。和Asynq一起,我们也将QCore(Asynq的唯一依赖项)开源,它是一个助手集,用于整个Quora代码库,包括一个装饰器框架,一个枚举实现,测试助手,和一个事件实现。Asynq和QCore同时兼容Python2.7和Python3,关于Asynq的更详细文档可以在GitHub仓库查看。
未来,我们将继续致力于使Quora对我们的用户来说更快,使新特性对于我们的工程师来说更容易进行开发。我们目前正在招聘Platform工程师来开发像Asynq的核心框架和抽象概念,所以看看我们的职业页面,如果你想和我们一起分享和增加全世界的知识!

英文原文:https://engineering.quora.com/Asynchronous-Programming-in-Python?srid=hST?utm_source=mybridge&utm_medium=email&utm_campaign=read_more
译者:蒲公英
 

2月15日11:00到13:00网站停机维护,13:00前恢复
iPy智能助手 双击展开
查看更多聊天记录
(Ctrl+回车)换行