首页 > 解决方案 > 为什么 Python 一直在努力跟上不断生成的异步任务?

问题描述

我有一个带有服务器的 Python 项目,该服务器将工作分配给一个或多个客户端。每个客户端都被分配了一些任务,其中包含用于查询目标 API 的参数。这包括他们可以使用给定 API 密钥每秒发出的最大请求数。客户端处理响应并将结果发送回服务器以存储到数据库中。

服务器和客户端都使用Tornado进行异步网络。我对客户端的初始实现依赖于PeriodicCallback确保对 API 的 n 次调用。我认为这工作正常,因为我的测试将持续 1-2 分钟。

我添加了一些遥测来收集有关性能的统计数据,并注意到客户端实际上在运行几乎 2 分钟后就遇到了问题。我已将 API 请求设置为每秒 20 个(API 本身允许的最大值),客户端可以可靠地命中。但是,2 分钟后,性能会在每秒 12 到 18 个请求之间波动。活动任务的数量稳步增加,直到达到服务器提供的最大活动分配数量(100),并且 Tornado 报告给 API 的 HTTP 请求时间从 0.2-0.5 秒变为 6-10 秒。如果我每秒只执行 14 个请求,性能是稳定的。任何高于 15 个请求的请求都会在开始后 2-3 分钟出现问题。日志可以在这里看到. 请注意“活动查询”列在 01:19:26 之前是如何稳定的。我已经截断了日志来演示

我认为问题在于使用客户端上的单个进程来处理与服务器和 API 的通信。我继续将主要过程分成几个不同的过程。一个处理与服务器的所有通信,一个(或多个)处理对 API 的查询,另一个处理 API 响应到一个扁平类,最后是一个多处理队列管理器。性能问题仍然存在。

我认为,也许,Tornado 是瓶颈,并决定重构。我选择了aiohttpuvloop。我以与上一次尝试类似的方式拆分主要过程。不幸的是,性能问题没有改变。

我采用了两种重构方法,使它们能够将工作分成几个查询过程。但是,无论你如何拆分工作,在 2-3 分钟后仍然会遇到问题。


我在 MacOS 和 Linux 上同时使用 Python 3.7 和 3.8。

在这一点上,它似乎不是单个包的限制。我想过以下几点:

我怀疑这是真的,因为不同的库声称每秒能够同时处理数千条消息。此外,我们可以在一开始就达到每秒 20 个请求,并且结果非常一致。

这不太可能,因为我不是 API 的唯一用户,如果我超额订阅从 API 查询的进程,我可以在很长一段时间内相当一致地每秒请求 20 次。

我已经尝试过 MacOS 和 Debian,它们产生了相同的结果。这可能是 *nix 的问题。

有时来自 API 的响应会在 0.2 到 1.2 秒之间增长和缩小。返回的活动任务数asyncio.all_tasks在遥测数据中保持一致。如果这是真的,我们就不会每次都在同一时间遇到这个问题。

尽管 CPU 温度飙升,但 MacOS 和 Linux 都没有在日志中报告任何热节流。我们在单个内核上的 CPU 利用率不会超过 80%。


在这一点上,我不确定是什么原因造成的,并考虑将客户端重构为另一种语言(可能是带有 Boost 库的 C++)。在我深入研究如此愚蠢的事情之前,我想问一下我是否遗漏了一些简单的事情。

标签: python-3.xpython-asyncio

解决方案


结论

性能似乎因一天中的不同时间而有很大差异。很可能是API。

这个结论是如何得出的

我创建了一个新项目来展示其功能asyncio并确定它是否是瓶颈。该项目采用两个网站,一个作为基线,另一个作为目标 API,并通过不同的测试方法运行:

  1. 每个核心生成一个进程,传递一个信号量,每秒最多查询 n 次
  2. 创建单个事件循环并每秒创建 n 个任务
  3. 创建多个进程,每个进程都有一个事件循环来分配工作,每个循环每秒执行(n 个/进程)任务

(请注意,除非使用具有 12 个或更多内核的高端桌面处理器,否则生成过程非常缓慢并且经常被注释掉)

基线网站每秒最多可查询 50 次。asyncio可以在很长一段时间内可靠地每秒完成 30 个任务,每个任务在 0.01 到 0.02 秒内完成它们的运行。反应非常一致。

目标网站每秒最多查询 20 次。asyncio尽管情况相同(JSON 处理、将响应数据转储到队列、立即返回、没有 CPU 绑定处理),有时也会遇到困难。但是,不同测试的结果各不相同,并且无法始终重现。最初的响应时间不到 0.4 秒,但很快增加到每个请求 4-10 秒。每秒 10-20 个请求将完整返回。

作为替代方法,我为目标网站选择了父 URI。此 URI 不会对其数据库进行大型查询,而是通过静态 JSON 响应返回。响应在 0.06 秒到 2.5-4.5 秒之间反弹。但是,每秒将完成 30-40 个响应。

使用自己的事件循环跨进程拆分请求将在上限范围内将响应时间减少近一半,但仍然需要超过一秒才能完成。

每次都无法从目标网站重现一致的结果表明这是他们最终的性能问题。


推荐阅读