是时候丢弃 Python 2.0,将 100 万行的代码迁移到Python 3.0了!
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译者:弯月,责编:屠敏“征稿啦”
CSDN 公众号秉持着「与千万技术人共成长」理念,不仅以「极客头条」、「畅言」栏目在第一时间以技术人的独特视角描述技术人关心的行业焦点事件,更有「技术头条」专栏,深度解读行业内的热门技术与场景应用,让所有的开发者紧跟技术潮流,保持警醒的技术嗅觉,对行业趋势、技术有更为全面的认知。如果你有优质的文章,或是行业热点事件、技术趋势的真知灼见,或是深度的应用实践、场景方案等的新见解,欢迎联系 CSDN 投稿,联系方式:微信(guorui_1118,请备注投稿+姓名+公司职位),邮箱([email protected])。