编程语言
首页 > 编程语言> > 耗时3年,Dropbox史上最大规模Python 3迁移

耗时3年,Dropbox史上最大规模Python 3迁移

作者:互联网

为什么选择 Python 3?

是否应该采用 Python 3 一直是 Python 社区争论的焦点话题。虽然 Python 3 现在得到了广泛的支持,一些非常受欢迎的项目(如 Django)已经完全放弃了 Python 2,但这个争论在一定程度上仍然存在。对于我们来说,有一些关键因素影响着我们的决定:

令人兴奋的新特性

Python 3 带来了快速的创新。除了一长串一般性改进之外,一些特别的特性引起了我们的注意:

老旧的工具链

随着 Python 2 变得老旧,相应的工具链在很大程度上也已经过时了。因此,继续使用 Python 2 将伴随着日益增加的维护负担:

freezer 和脚本

最初,我们依靠“freezer”脚本为各个平台创建原生应用程序。但是,我们不是直接使用原生工具链(例如在 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)变得庞大而混乱,难以维护。

转折点来自于我们与每个操作系统集成的转变:首先,我们开始逐步引入越来越先进的操作系统扩展(如 Smart Sync 内核组件),这些扩展通常不是使用 Python 编写的。其次,微软和苹果开始强制使用更复杂的新工具(通常是专有工具,例如代码签名)来部署应用程序。

例如,macOS 10.10 引入了一个新的应用程序扩展 FinderSync,用于与 Finder 集成。FinderSync 扩展不仅仅是一个 API,它还是一个完整的应用程序包(.appex),它具有自定义生命周期规则(由操作系统启动)以及对进程间通信更严格的要求。通过 Xcode 可以很容易地利用这些扩展,但 py2app 并不完全支持它们。

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

要迁移到 Python 3,我们需要做出选择:修改 freezer 依赖项,增加对 Python 3(以及现代编译器)和平台特定特性(如 app 扩展)的支持,或者抛弃以 Python 为中心的构建系统,彻底废除“freezer”。我们选择了后者。

至于 pyinstaller,我们在项目早期考虑过使用它,但它当时不支持 Python 3,更重要的是,它与 freezer 一样也存在类似的限制。它是个好东西,只是不符合我们的要求。

嵌入 Python

为了解决这个构建和部署问题,我们决定将 Python 运行时嵌入到原生应用程序中。我们没有将这个过程委托给 freezer,而是使用每个平台特定的工具(例如 Windows 上的 Visual Studio)来构建各种入口点。此外,我们将 Python 代码代码抽离成一个库,以便更直接地支持与其他语言的“混合和匹配”。

这样我们就可以直接使用每个平台的 IDE 和工具链(例如在 macOS 上添加 FinderSync),同时仍然可以使用 Python 编写应用程序逻辑。

我们采用了以下结构:

从表面上看,应用程序将更加类似于平台所期望的,但在各种库背后,我们的团队可以更灵活地使用他们喜欢的编程语言或工具。

这种架构所带来的模块化能力还产生了一个关键的副作用:现在可以同时部署 Python 2 和 Python 3 库。在 Python 3 迁移中使用这种方法需要两个步骤:第一,围绕 Python 2 实现新架构,第二,使用 Python 3 代替 Python 2。

第 1 步:“Anti-freeze”

我们的第一步是停止使用 freezer 脚本。bbfreeze 和 pywin32 都缺乏对 Python 3 的支持,所以我们别无选择。从 2016 年开始,我们开始逐步做出这一改变。

首先,我们将配置 Python 运行时和启动 Python 线程的工作抽离到一个叫作 libdropbox_bootstrap 的库中。这个库可以完成之前由 freezer 脚本完成的一些工作。虽然我们在很大程度上已经不再需要依赖这些脚本,但仍然需要为运行 Python 代码的提供一些基本的东西:

打包代码,以便在设备上运行

这需要确保我们提供的是经过编译的 Python“字节码”,而不是 Python 源代码。之前,每个 freezer 脚本都有自己的存储格式,我们借这个机会引入了一种单一的格式,用于在所有平台上打包我们的代码:

隔离 Python 解释器

这样可以防止应用程序执行设备上的其他 Python 代码。有趣的是,Python 3 让这种类型的嵌入变得更加容易。例如,借助新的 Py_SetPath 函数,我们可以很容易地隔离代码,避免了在 Python 2 中隔离 freezer 脚本需要做的那些比较复杂的工作。为了能够在 Python 2 中进行隔离,我们将这个函数反向移植到自定义的分支代码库中。

其次,我们引入了特定于各个平台的入口点,如 Dropbox.exe、Dropbox.app 和 dropboxd,并让这些入口点使用这个库。这些入口点是使用每个平台的“标准”工具构建的:Visual Studio、Xcode 和 make,这样我们就可以删除 freezer 脚本中的大部分自定义拼凑代码。例如,在 Windows 上,这极大地简化了为 Dropbox.exe 配置 DEP/NX 以及嵌入应用程序清单和包含资源文件。

关于 Windows 的说明:在这个时间点上,继续使用 Visual Studio 2008 的成本变得非常高。我们需要一个能够同时支持 Python 2 和 Python 3 的版本,于是我们选择了 Visual Studio 2013。为了支持它,我们对 Python 2 的自定义分支进行了大量修改,以便能够正常编译。为这些变化所做出的努力进一步增强了我们的信念:转向 Python 3 是正确的决定。

第 2 步:Hydra

进行这么大规模的转换(我们的应用程序包含超过 100 万个 Python LOC,并被安装超过数亿次)是一个渐进的过程:我们不能简单粗暴地在一个版本中搞定一切。当然,这个与我们的发布流程也有关系,我们每两周向所有用户发布一个新版本。我们需要找到一种方法,让少量用户先用上 Python 3,便于及早发现和修复 bug。

为实现这一目标,我们决定同时使用 Python 2 和 Python 3 来构建 Dropbox。这要求:

我们利用了前一个步骤引入的嵌入式设计:将 Python 抽离为单独的库,可以很容易地引入另一个版本的变体。然后,在入口点(例如 Dropbox.app)初始化期间,可以选择要使用的 Python 版本。

这是通过手动将入口点链接到 libdropbox_bootstrap 来实现的。例如,在 macOS 和 Linux 上,在选定了 Python 版本之后,我们就使用 dlopen/dlsym。在 Windows 上,我们使用 LoadLibrary 和 GetProcAddress。

在加载 Python 之前需要选择运行时 Python 解释器,在开发时使用命令行参数 /py3,在实际部署时使用磁盘文件来设置,这样就可以通过我们的功能门控系统 Stormcrow 来控制它。

因此,我们在启动 Dropbox 客户端时能够动态地选择 Python 版本。我们也因此能够在 CI 基础设施上设置额外的作业来运行针对 Python 3 的单元测试和集成测试。我们还对代码提交队列进行自动检查,防止某些代码变更出现回退。

在通过自动化测试获得了足够的信心之后,我们开始向真实用户推出基于 Python 3 的版本。我们通过远程功能门控系统来逐步向用户推出新客户端。我们先是将新版本推给 Dropboxer,这样就能找出并修复大多数潜在的问题。然后,我们向一小部分用户推出 Beta 版本,并最终扩展到了稳定版渠道:在 7 个月内,所有 Dropbox 用户都开始使用 Python 3 版本。为了最大限度地提高质量,我们制定了一个策略,在向更大的用户群推出新版本之前,必须修复所有与迁移相关的问题。

直到版本 52,我们才完成了整个迁移过程:Python 2 已经从 Dropbox 的桌面客户端中彻底移除


标签:Python,freezer,耗时,Windows,应用程序,使用,Dropbox,我们
来源: https://blog.51cto.com/15060462/2681412