关于 Python 的几点思考

2,350 阅读9分钟
原文链接: www.infoq.com

简介:一直以来,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的系统架构如下图所示:

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

重写代码

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,这是不二之选。

另一个常见的抱怨是,当初为什么不采用PyPyCPython的代码库?这个问题确实不好回答。兼容性是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的认识是否因此得以刷新了呢?