Python 2 vs Python 3,究竟谁是性能之王?

Python 2 vs Python 3,究竟谁是性能之王?前段时间,Hackermoon 上一位叫 Anthony Shaw 的作者为我们做了一些测试,最终得出结论,虽然 Python 2 在加密和启动时间测试过程中,比 Python 3 的速度更胜一筹,但整体而言,Python 3 更快。 而这是否就意味着我们还是将项目代码迁移到 Python 3.0 的好?接下来,本文来自全球著名的桌面应用之一的 Dropbox 将分享他们要弃用 Python 2.0 的真实原因,以及如何将百万行的代码成功迁移至 Python 3。

Dropbox 是世界上流行的桌面应用之一,你可以安装在 Windows、macOS 和部分的 Linux 发行版上。但你可能不知道,这个应用大部分是用 Python 写的。实际上,Drew 给 Dropbox 写下的第一行代码就是用的 Windows 版 Python,用的是老牌的 pywin32 等库。

虽然我们靠着 Python 2 支撑了这么多年(我们用过的最新版本是 Python 2.7),但我们从 2015 年就开始向 Python 3 转换了。今天我们终于完成了转换,你现在再装 Dropbox 的话,那么它用的是 Dropbox 定制版本的 Python 3.5。本文将介绍这次史无前例的 Python 3 转换的计划、执行和发布过程。

为什么选择 Python 3?

Python 3 的接受度在 Python 社区一直是热门话题。现在虽然 Python 3 已经广为接受(http://py3readiness.org/),一些非常流行的项目如 Django 甚至完全放弃了 Python 2 的支持,但这个话题的热度依然存在。对于我们来说,影响我们决定进行转换的几个关键因素有:

引人入胜的新功能

Python 3 的创新十分迅速。除了一长列(http://whypy3.com/)正常的改进(如 str 和 bytes 的讨论),还有几个功能吸引了我们的眼球:

类型标注语法:我们的代码量非常大,所以类型标注对于开发的效率非常重要。在 Dropbox 我们很喜欢 MyPy(http://mypy-lang.org/),因此原生的类型标注支持对我们很有吸引力。

并行函数语法:许多功能都极度依赖线程和消息传递,我们采用的是 Actor 模式,使用了 Future 模块。而 asyncio 项目及其 async/await 语法有时能避免回调函数,从而获得更干净的代码。

过老的工具链

随着 Python 2 日久年深,最初适合部署的工具链也大部分过时了。由于这些因素,继续使用 Python 2 会带来一系列的维护负担:

过老的编译器和运行时使得我无法们升级一些重要更新。

例如,我们在 Windows 和 Linux 上使用 Qt,而最新版本的 Qt 包含了 Chromium(通过 QtWebEngine 实现),因此需要更现代的编译器。

我们与操作系统的集成越来越深,而无法使用新版本的工具链,导致使用新版 API 的成本增大。

例如,理论上 Python 2 依然需要 Visual Studio 2008 (http://stevedower.id.au/blog/building-for-python-3-5/)。但这个版本微软已经不再支持了,也与 Windows 10 SDK 不兼容。

冻结和脚本

当初,我们依靠“冻结”脚本为我们支持的每个平台创建原生应用程序。但是,我们并没有直接使用原生的工具链,如 macOS 的 Xcode,而是将创建各个平台上的二进制文件的任务交给其他程序去做,Windows 下是 py2exe,macOS 下是 py2app,Linux 下是 bbfreeze。这个完全面向 Python 的构建系统收到了 distutils 的启发,因为我们的应用最初只不过是个 Python 包,所以只需要一个类似于 setup.py 的脚本来构建。

随着时间的流逝,我们的代码量越来越大。现在,我们的开发已经不仅仅使用 Python 开发了。实际上,我们的代码现在由 TypeScript/HTML、Rust 和Python 混合组成,某些平台上还用了 Objective-C 和 C++。为支持所有组件,setup.py 脚本(内部的名字为 build-all.py)越来越大,越来越难以管理。

导火索就是我们与各个操作系统集成的方式。首先,我们越来越多地引入高级的 OS 扩展,如 Smart Sync 的内核组件等,这些组件不能,通常也不会使用 Python 编写。其次,像微软和苹果等供应商对部署应用提出了新的需求,因此经常需要用到新的、更复杂的工具,这些工具经常是这些供应商独有的(比如代码签名等)。

例如在 macOS 上,10.10 版本引入了新的应用扩展以便与 Finder 进行集成,就是FinderSync(https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/Finder.html)。它并不只是个 API,而是个完整的应用程序包(.appex),有自己的生存中崛起规则(即它由 OS 启动),而且对于进程间通信的要求更严格。换句话说,使用 Xcode 就很容易集成这些扩展,但 py2app 根本不支持它们。

因此,我们面临着两个问题:

由于我们使用 Python 2,因此无法使用新的工具链,所以集成新的 API 的代价更高(比如使用Windows 10的Windows Runtime)。

我们的冻结脚本使得部署原生代码的代价更高(例如在 macOS 上构建应用扩展)。

当我们计划转换成 Python 3 时,我们面临着两个选择:一是改进冻结脚本中的依赖,以支持 Python 3(从而支持现代编译器)和平台相关的功能(如应用程序扩展),二是不再使用以 Python 为中心的构建系统,完全放弃冻结脚本。我们选择了后者。

关于 pyinstaller 的一点:我们认真地思考过在项目早期使用 pyinstaller,但当时它不支持 Python 3,而且更重要的是,它和其他冻结脚本有类似的限制。不管怎样,这个项目本身很不错,我们只是觉得不适合我们而已。

嵌入 Python

为了解决构建和部署的问题,我们决定使用新的架构,在原生应用中嵌入 Python 运行时。我们不再将构建过程交给冻结脚本处理,而是使用各个平台自己的工具链(比如 Windows 下使用 Visutal Studio)来构建各种入口点。进一步,我们将 Python 代码抽象到一个库中,从而为多种语言“混合”的方式提供更直接的支持。

这样我们就可以直接使用各个平台的 IDE 和工具链了(例如可以直接添加原生的构建目标,如 macOS 上的 FinderSync),同时保留使用 Python 编写大部分应用程序逻辑的能力。

我们最后采用了下面的结构:

原生入口点:这些与各个平台的应用程序模型兼容。

其中包括应用程序扩展,如 Windows 下的 COM 组件和 macOS 下的应用程序扩展。

共享库可以使用多种语言编写(包括 Python)。

表面上,这个应用能够更接近平台的要求,而在各个库的背后,我们可以有更大的灵活性来选择自己喜欢的语言和工具。

这种架构能提高模块性,同时还带来一个关键的副作用:现在可以同时部署 Python 2 库和 Python 3 库了。联系到 Python 3 转换工作,我们的转换过程就需要两步:第一,给 Python 2 实现新的架构;第二,利用它将 Python 2 替换成 Python 3。

第一步:“解冻”

第一步就是停止使用冻结脚本。目前,bbfreeze 和 pywin32 都不支持 Python 3,所以我们别无选择。我们从 2016 年开始逐步进行这项改变。

首先,我们将配置 Python 运行时的工作抽象化,将 Python 的东西放到一个新的库中,名为 libdropbox_bootstrap。这个库会代替一些冻结脚本提供的功能。尽管我们不再需要这些脚本,但它们仍然提供了一些运行 Python 代码所需的最基本的东西:

打包代码以便在设备上执行

这样我们才能发布编译好的 Python 字节码,而不用发布 Python 源代码。由于以前的每个冻结脚本在各个平台上有各自的格式,我们利用这个机会引入了一种新的格式,用于在所有平台上打包代码使用:

所有 Python 模块的 Python 的字节码 .pyc 都放在单一的 zip 文档中(如 python-packages-35.zip)。

原生扩展. pyd / .so 由于是平台相关的原生动态链接库,他们必须安装在特定的位置,保证应用程序能毫无障碍地加载。

Windows 下,这些文件与入口点(即 Dropbox.exe)放在一起。

打包通过优秀的 modulegraph(作者是 py2app 和 PyObjC 的作者 Ronald Oussoren)实现。

隔离 Python 解释器

这样能阻止我们的应用程序在设备上运行其他的 Python 源代码。有意思的是,Python 3 使得这种嵌入变得容易得多了。例如,新的 Py_SetPath 函数(https://docs.python.org/3/c-api/init.html#c.Py_SetPath)能够让我们将代码隔离,不需要再像 Python 2 时代在冻结脚本中进行某种复杂的隔离操作了。为了在 Python 2 中支持这一功能,我们在定制版本的 Python 2 中向下移植了这一功能。

其次,我们使用了平台相关的入口点Dropbox.exe、Dropbox.app和dropboxd 来使用这个库。这些入口点都是用各自平台的“标准”工具编译的,即 Visual Studio、Xcode 和 make,没有使用 distutils。这样我们就可以去掉冻结脚本带来的大量修补工作了。例如,在 Windows 下,这一步大大简化,只需为 Dropbox.exe 配置 DEP/NX 即可,就能将应用程序装箱单和资源嵌入了。

关于 Windows 的一点说明:现在,继续使用 Visual Studio 2008 的代价已经非常高了。为了正确地转换,我们需要一个能同时支持 Python 2 和 Python 3 的版本,最终我们采用了 Visual Studio 2013。为支持它,我们进一步修改了定制版本的 Python 2,使之能正确在 Visual Studio 2013 下编译。这些修改的代价进一步证明,我们转换到 Python 3 的决定是正确的。

第二步:混合

成功地转换如此之大(包含大约 100 万行 Python 代码)、安装量如此之高(大约有几亿安装)的应用程序需要逐步进行。我们不能简单地在某次发布中“改变一个开关”来实现转换,特别是我们的发布过程要求每两个星期给所有用户发布一个新版本。因此,必须找到一种办法,将 Python 3 的部分转换发布给一小部分用户,以便检测并修改 Bug。

为达到这一点,我们决定实现用 Python 2 和 Python 3 同时编译 Dropbox。这要求做到以下两点:

能够同时发布 Python 2 和 Python 3 的“包”,包括字节码和扩展,两者必须能够并存。

在转换过程中强制使用混合的 Python 2 / 3 语法。

我们采用上一步引入的嵌入式设计来实现:将 Python 代码抽象到库和包中,就能很容易地引入另一个版本。这样入口点程序(即 Dropbox.exe)就可以在初始化的早期控制选择哪个 Python 版本了。

我们通过手动连接入口点程序到 libdropbox_bootstrap 来实现这一点。例如在 macOS 和 Linux 下,我们在 Python 版本确定之后使用 dlopen/dlsym 来加载。在 Windows 下,使用 LoadLibrary 和 GetProcAddress。

对 Python 解释器的选择必须在 Python 加载之前完成,因此为了使之更顺畅,我们实现了命令行参数 /py3 用于开发,和一个保存在硬盘上的永久设置,以便通过我们的功能切换系统Stormcrow(https://blogs.dropbox.com/tech/2017/03/introducing-stormcrow/)来控制。

有了这些,我们就能在启动 Dropbox 客户端时动态选择 Python 版本了。这样就可以在 CI 基础设施中设置额外的任务来针对 Python 3 运行单元测试和集成测试。我们还在提交队列中增加了自动检查,以防止提交会破坏 Python 3 支持的改动。

通过自动测试确保没问题之后,我们就开始将 Python 3 的改动推送给真正的用户。我们通过远程的功能开关来将新功能逐渐开放给用户。首先对 Dropbox 推送改动,这样我们就能找出并改正大部分主要的底层问题。然后将范围扩大到 Beta 用户,他们的 OS 版本问题更加芜杂。然后最终扩展到稳定版。7 个月之后,所有的 Dropbox 都已经在运行 Python 3 了。为了尽可能提高质量,我们要求所有与转换相关的 bug 必须进行深入调查并彻底修复,才能扩大推送的范围。

逐渐推送到 Beta 版

逐渐推送到稳定版

到了版本 52 时,这个过程终于完成了。我们可以完全从 Dropbox 的桌面客户端中删掉 Python 2 了。

写在最后

一篇文章很难完整概括我们将代码迁移至 Python 3.0 的完整过程,这其中还有许多可以讨论的东西。接下来,我们还会在以后的文章中讨论:

我们怎样在 Windows 和 macOS 上报告崩溃,并利用这些信息调试原生和 Python 代码;

怎样维护 Python 2 和 Python 3 混合代码,用到了哪些工具?

整个 Python 3 转换过程中最值得讨论的 Bug 和故事。

敬请期待,也欢迎在下方留言分享你对迁移过程的看法。

原文:https://blogs.dropbox.com/tech/2018/09/how-we-rolled-out-one-of-the-largest-python-3-migrations-ever/ 作者:Max Bélanger和Damien DeVille 译者:弯月,责编:屠敏


原文发布于微信公众号 - 我就是马云飞(coding_ma)

原文发表时间:2018-09-30

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏CDA数据分析师

案例分析:基于消息的分布式架构

美国计算机科学家,LaTex的作者Leslie Lamport说:“分布式系统就是这样一个系统,系统中一个你甚至都不知道的计算机出了故障,却可能导致你自己的计算...

30880
来自专栏技术换美食换不换

picu后端架构总结

首先我们项目的定位是一个图片,音频为主体的分享应用,于是服务器对于大资源的存储有了常规数据库,nginx静态资源存储和对象存储服务的选型问题.常规数据库(如my...

13920
来自专栏程序员的SOD蜜

唯一不变的就是一直在变”--“数据”的华丽“变身术”

 系列文章索引: [WCF邮件通信系统应用 之 数据同步程序 之 设计内幕 之 一] 同步一个数据库要发多少个数据包? [WCF邮件通信系统应用 之 数据同步...

22060
来自专栏不止是前端

实用主义:前后端分离MOCK数据

361110
来自专栏技术翻译

共享MongoDB主机的五大好处

共享主机是在云中部署MongoDB的最具成本效益且易于设置的选项之一,并被全球数千家公司用于托管其数据库。在这篇文章中,我们概述了使用共享MongoDB主机的五...

19500
来自专栏沃趣科技

沃趣科技火线救援某公安系统核心业务数据

求助电话 只剩下键盘敲打声的办公室,被一个突如其来的电话打破了宁静。电话那头,是某公安客户的紧急求助。 案发现场 其核心数据库,由于存储突然断电,导致数据库实例...

36470
来自专栏乐沙弥的世界

Percona XtraDB Cluster集群节点重启及故障转移

要重新启动集群节点,请关闭MySQL并重新启动它。该节点将离开集群(并且法定人数的总计数应该减少)。发布命令 systemctl restart mysql

11220
来自专栏ThoughtWorks

聊一聊契约测试 | 洞见

如果从契约产生的阶段来说,现有资料表明最早要追溯到西周时期的《周恭王三年裘卫典田契》,将契约文字刻写在器皿上,就是为了使契文中规定的内容得到多方承认、信守,“万...

15050
来自专栏数据和云

【全局出发,追根溯源】一则集群故障案例分析

作者简介: ? 董冰,混迹DBA圈子十余载的闲云野鹤,曾服务过政府行业、银行数据中心、互联网游戏上市公司,辗转蛰伏于中国铁塔,励志做一个社会主义的螺丝钉。 故...

36660
来自专栏phodal

这些奇技浮巧,助你优化前端应用性能

我开始写前端应用的时候,并不知道一个 Web 应用需要优化那么多的东西。编写应用的时候,运行在本地的机器上,没有网络问题,也没有多少的性能问题。可当我把自己写的...

346100

扫码关注云+社区

领取腾讯云代金券