Python 异步与性能迷思

今天在讨论 Python 的异步编程的时候提到的问题:到底为什么现在 Python 的异步数据库那么少呢?到底针对 Python 而言,什么是影响 Python 性能最大的门槛呢?

为了搞清楚这个问题,我对线上环境应用使用了性能探针系统。性能出人意料 (CPython 2.7.10):

线上性能

似乎真实来说,Python 本身的性能问题才是影响 Python 性能的真凶。

在此以前,我们谈这个之前,还是先复习一下什么是 GIL 吧。无论是 CPython(普通的 Python 2.7.x 和 Python 3.5.x) 还是 pypy,都是有一个名曰『Global Interpreter Lock』的东西限制了它的性能,用一张图可以生动的表现带 GIL 的 Python 在多核下的 CPU 压力:

CPU 性能演示

GIL 限制了 Python 的 bytecode 只能在一个线程中运行,使用这种 GIL 避免多线程编程时隐含并发访问对象可能带来的潜在问题,但是也让 Python 在多核下性能表现感人。这种实现方式一直备受诟病,这也导致 Python 在某些高并发场景时会出现较严重的性能下滑问题。

在 Python 性能提升上,比较常见的性能提升的方式,是将多线程转化为多进程方式,以充分利用 CPU 多核心。但是这样带来直接的问题是进程间通讯会严重影响整体程序的吞吐量。一个简单的 CPU 密集 Python socket 的通讯程序,非 Block 状态下,吞吐量能有大概 10 倍以上的差距。(参考 Python Concurrency From the Ground Up )

不过相对来说,这样虽然影响了最大吞吐量,但是在利用多核效率上提升明显,即便是 CPU 密集的情况,也能保证程序的整体吞吐量不会出现线程模式的成百上千倍的性能差距。

那么,这些与异步又有什么关系呢?

对异步情况而言,对 Python 的代码执行并没有影响,我们所提到的异步,能够提升的性能,也仅仅限于针对针对于较长 IO 等待的问题,比如常见的就是请求远程 API 接口 (http query) 等等情况。对这种情况而言,当进入 Blocking 的 socket 等待时间时,将 CPU 时间释放,用于去做更有意义的其他操作是完全合理的。对于数据库操作而言,则也需要根据不同的情况进行对待。比如连接远程网络数据库,那么使用异步则根据实际的性能消耗时间而定,如果是本地的数据库,那么我想回到第一张图,相信根本无需使用异步库就可以解决这个问题。

从一个 CPU 密集的例子,我们也能看出,其实,使用 JIT 带来的性能提升比较明显。然而,对异步库的异步队列仍是按顺序单个处理的,当队列较长时,同样会带来更严重的性能损耗:因为对那些快速的网络连接而言,Python GIL 则更加限制了 Python 的执行性能。