简介:一直以来,Python 的性能是大家诟病最多的地方,不少最初采用 Python 的项目甚至开始迁移到其他语言,Duolingo 就是其中一例。而整个 Python 社区最成功的框架莫过于 PyPy,但 Python 使用大户 Dropbox 并没有采用,相反,他们另起炉灶写了一个 Pyston。关于 Python 几个老生常谈的话题,Kevin Modzelewski 有话要说。

一直以来,Python 的性能是大家诟病最多的地方,不少最初采用 Python 的项目甚至开始迁移到其他语言,Duolingo 就是其中一例。而整个 Python 社区最成功的框架莫过于 PyPy,但 Python 使用大户 Dropbox 并没有采用,相反,他们另起炉灶写了一个 Pyston。关于 Python 几个老生常谈的话题,Kevin Modzelewski 有话要说。

“出来混迟早要还的”技术债

Duolingo(多邻国)是一个完全免费的多国语言学习工具,致力于为全球用户提供一个平等接受语言教育的机会。目前用户数超过 1.5 亿人(笔者是用户之一),其创始人 Luis von Ahn 是卡内基梅伦大学的教授、图灵测试(CAPTCHAs,俗称验证码)的发明人、麦克阿瑟家族的一员。

不久前 Duolingo 工程师 Andrekhorie 在官方技术博客上发表了一篇文章,阐述了对 Duolingo 这个复杂系统重构的种种心得。在 Duolingo 后端,超过 88 个课程系统通过机器学习的不断优化后提供给用户使用。Duolingo 原来的系统是用 Python 编写的,后来用 Scala 重写了一遍。虽然重构系统耗费了大量的人力、物力(工程师们不得不中断新的研发计划转而用几个月的时间来重写整个系统),但这一切都是值得的——重构后的系统时延从 750ms 降到 14ms,uptime 则从 99.9% 提高到了 100%。

系统重构

没有什么例外,所有的公司都会在成长初期欠下这样的技术债——就像你欠银行的钱,随着时间的推移它会像滚雪球一样不断变大。有句话说的好,出来混迟早是要还的。这种痛苦将伴随着企业的成长,直至你不得不面对它。

在系统设计之初,由于经常需要共享数据,Duolingo 的系统架构如下图所示:

[attach]683311[/attach]

这个架构的问题是有太多的严重依赖关系,在这种情况下,越来越多的网络请求使得系统延时越来越大。为了解决这个问题,重新设计架构的主导思想是尽可能解耦所有的依赖,使系统变得简单和健壮(如下图所示)。在新的架构下,经常被共享使用的课程数据离线存储在 AWS S3 上,Duolingo 只需做一些轻量级的缓存即可。

[attach]683312[/attach]

重写代码

Duolingo 做的最大的一个决定是用 Scala 重写了会话生成器。Duolingo 后端的会话生成器最初是用 Python 编写的。Python 的优点就不多说了,但它的缺点也是显而易见的。

  1. 性能:例如 Python 比C或者 Java 要慢很多。
  2. 内存管理:Python 的 GIL 制约了开发人员对内存的管理和利用。
  3. 动态类型:对于复杂系统,这导致部署过程中不得不重复大量的 bug 修复工作,反而减慢了项目的实施进度。

作为一门函数式编程语言,Scala 有很多十分吸引人的特性。它十分精炼,编写的代码可读性更强、调试维护也更方便。Scala 吸取了其前辈编程语言的优点,并解决了那些语言无法克服的痛点,有更好的容错机制,bug 也更少。此外,Scala 基于 Java Virtual Machine 意味着有丰富的 Java 类库可用。在很多复杂度不比 Duolingo 差的大数据项目中都使用了 Scala,这看起来是个不错的选择。最重要的是在复杂系统编程语言中,Scala 的学习曲线比较容易。

Duolingo 的思考

通过这次重构,Duolingo 写了所有的测试用例,一方面提高了系统的稳定性,另一方面积累了许多开发文档。在这个过程中他们无缝集成了许多互相独立的开发组件。

本次重构花费的时间低于 Duolingo 的预期,并且重构后的系统更健壮,代码库也更可读、可维护。在整个重构过程中也遇到了一些痛点,比如 Scala 的一些库缺乏可用的文档;在与 Java 集成时也遇到了一些麻烦,某些 Scala 的特性不能被很好地支持。

性能数据方面,重构后的系统在最初的几个月里高可用达到 100%,而这一数据最初是 99%,大部分的延时都发生在从 S3 的缓存中下载没有被缓存命中的课程数据文件。

经常吐槽 Python 的同学“你不懂”

鉴于最近大家对关于 Pyston 的未来讨论较多,恰好近日 Pyston 发布了 0.6.1 版,最新版的 Pyston 在基准测试中的性能比 CPython 快 95%,并使 Dropbox 的性能提升了 10%(Dropbox 内部有很多项目是用 Python 编写的,Pyston 是 Dropbox 发起的一个开源项目,目标是使用 LLVM 和现代 JIT 技术开发一款高性能的 Python 实现)。Pyston 项目的核心开发者 Kevin Modzelewski 写了一篇文章,谈了他对 Python 的一些看法。

为什么要有 JIT 以及为什么要重复造轮子

时光倒流至 2013 年,当时 Dropbox 网站的 Web 服务器 90% 的 CPU 资源被各种访问请求消耗掉了,服务器采购的速度让所有人都心惊肉跳。当时普遍认为 Python 的瓶颈是I/O,由于这个问题在整个系统中广泛存在,问题相当棘手。放眼望去当时并无成熟的技术解决方案——你不能指望也无法想象几百万行代码的 PyPy 能够与 Dropbox 自身的大量扩展兼容。时至今日 Kevin Modzelewski 依然坚信,当初的选择是正确的。人们既不想放弃 Python 这个生态,又想提高性能,唯有想方设法改进 Python,这是不二之选。

另一个常见的抱怨是,当初为什么不采用 PyPy 或 CPython 的代码库?这个问题确实不好回答。兼容性是 Dropbox 首先要考虑的,其次是合理的性能提升。Dropbox 的需求与 PyPy 在设计理念和技术实现两方面都背道而驰,在 Kevin Modzelewski 看来,这也是 PyPy 项目继续取得更大成就的瓶颈所在。对 PyPy 的小修小补固然能有所改进,但像内存使用、支持C扩展、性能稳定等问题已经固化到 PyPy 的架构中去,这显然不是修修补补能解决的问题。Kevin Modzelewski 对一个为了适用于 Dropbox 而改造过的 PyPy 到底还能不能再称之为 PyPy 表示怀疑。

至于 CPython,这更多的是一个务实的决定,Dropbox 当时的目标是尽可能多地利用 CPython。Kevin Modzelewski 也承认,时至今日,90% 的 Pyston 的代码库都是 CPython 代码。从这一点上来说,Pyston 显然是基于 CPython 的实现。但是,在项目的早期阶段,Dropbox 的首要任务是验证其战略是否正确——利用 LLVM 开发一款高性能的 Python 实现,CPython 并不适合进行这样的实验。尽管走了一些弯路、重复造了一些轮子,Kevin Modzelewski 认为 Dropbox 折腾一番的价值在于他们更好地理解了这些技术,未来也许还会遇到类似的情况,由于前面的经验相信他们能做的更好。

一些大家没想到的点

在 Kevin Modzelewski 看来,没有调查就没有发言权,但是一些人比较热衷于发表不切实际的看法。比如说,V8 引擎使得浏览器运行 JS 的速度变得更快,Kevin Modzelewski 在想的是如何使 Python 也能获得这样的速度。Kevin Modzelewski 考虑的是,跟动态语言相比,为什么 Python 比较慢呢?而不是说,你看 Lua 的速度更快,所以用 Lua 会更好。Kevin Modzelewski 希望人们理解,Python 之所以慢是因为其丰富的对象模型,而不是它的动态类型和动态范围。问题是,每一个 Python 操作都有很多关键点,由于很多特性被广泛使用,因此用户就忽略掉了这些地方。

比如,当一个包含局部变量的框架退出后,如何平滑地立即修改对应的函数?在缺少动态语言如 JS 或 Lua 那样的模拟机制的前提下,这些特性由于应用广泛所以必须支持。但是人们显然忽略掉了这些。

另一方面,Python 的兼容性也跟大家理解的不一样。例如,Kevin Modzelewski 发现 Dropbox 的代码库太庞大以至于兼容性问题不可避免。当从引用计数切换到跟踪垃圾收集器甚至切换到排序字典时,都会引起无数的中断。最终 Dropbox 不得不分离并重新执行这两个实现以匹配 CPython 的行为。

特别是在 Web 应用程序领域内存使用是大家诟病 Python 最多的地方。一部分是 GIL 的机制问题——类似于多线程,多进程肯定会占用更多的内存。不管出于怎样的考虑,Python 禁止不同的进程之间共享其内存。这里的内存使用不是大家经常讨论的在 Python 空间(MicroPython 除外)中的内存使用,而且这也是 PyPy 不适用于 Dropbox 的另一个原因。Dropbox 系统中的许多地方实际上也是有这种限制,其中一个关键指标是“每 GB 内存的每秒请求次数”。Kevin Modzelewski 一度认为,当请求次数以 50% 的速度递增,而内存以 2 倍的速度扩展是可行的,实际上这在内存绑定类服务中根本不可行。

Kevin Modzelewski 最后表示,尽管有上述问题,但这在当时都是经过考量的选择,如果再来一次他相信能做的更好。亲爱的读者,你对 Python 的认识是否因此得以刷新了呢?