[译] 将一个旧的大型项目迁移到 Python 3

将一个旧的大型项目迁移到 Python 3

一年半前,我们就决定使用 Python 3 了。我们已经讨论了很长时间,现在是时候使用了!现在这个过程已经结束了,我们已经把生产环境的最后部署都迁移到了 Python 3

  • 整个代码库大约有 240 k 行,不包括空行和注解。
  • 这是一个基于 Web 的批处理任务系统。并且只有一个生产,部署环境。
  • 代码库大约有 15 年的历史了。
  • 虽然这是一个 Django 应用程序,但部分代码是先于 Django 公布之前写的。

关于修改 Python 3 的一些基本统计数据,是基于对 git 提交历史的粗略过滤产生的:

  • 275 次提交
  • 4080 次添加代码行
  • 3432 次删除代码行

我发现有 109 个 jira 问题与这个项目相关。

Py2 → six → py3

我们的理念一直是 py2 →py2/py3 → py3 因为我们实在无法在实际生产中实现巨变,这种直觉也以令人惊讶的方式被证明是正确的。这意味着 2 到 3 是不可能的,我认为这很常见。我们尝试过使用 2 to 3 来检测 Python 3 的兼容性问题,但很快这也被发现无法成立。基本上,这样的更改意味着在 Python 2 中的代码将被破坏。这样的改变不可行。

结论是使用 six, 这是一个库,可以方便的构建一个在 Python 2 和 3 中都有效的代码库。

首当其冲的就是更新之前的依赖关系。这项工作需要立刻启动,因为之后会有更多的内容要更新。

现代化

Python-modernize 是我们选择进行迁移的工具。它是一个可以自动将 Py 2 代码库转换为可兼容 six 代码库的工具。我们首先引入一个测试,作为 CI 的一部分,来检查基于 modernize 的新代码是否已经准备好兼容 py3 了。这样做最大的效果的是让那些仍使用 Py 2 语法的人意识到新的处理方法,但这显然对将现有的 240 k 行代码转化到 six 作用不大。我们都有使用旧语法的坏习惯,这可以说是教学上的成功了,即使它对代码行的计数没有什么不同,它也被我们用于实验分支:

实验分支

我新建了一个名为“Python 3 ”的分支,并做了以下操作:

  • 在整个代码库上运行“python-modernize -n -w” 。它会在合适的地方修改代码。我经常做完这步后没有进行第一次提交就开始修复代码。这个错误步骤总是让我后悔,不止一次地迫使我重新开始做整件事情。即使这个阶段出错,最好还是先把它提交。因此将机器和人要做的事情分开显得尤为重要。
  • 将所有用于函数体的依赖项导入到我们还没有修复的 py3。

这里的想法是“run ahead”,即看看如果我们没有使用过时的依赖项,我们会遇到什么问题。这个分支允许我在超级中断状态下可以非常快速地启动应用程序,至少可以运行一些单元测试。 这个分支有很大的不同,但我还是找到了把它应用在适当场景的方法。我使用优秀的 GitUp 来拆分、组合和提交。当一个提交看起来不错的时候,我会把它挑选到一个新的分支,然后发给代码审查。

没有人可以在这个分支上工作,因为它被不断地 rebase ,强制推送,滥用,但是它确实让项目向前推进了,而不用等待所有的依赖项被更新。我强烈推荐使用这种方法!

静态分析

我们添加了预提交钩子,所以如果您编辑了一个文件,就会收到建议将 Python 3 全部进行 modernize 更新的提示。

quote_plus 的手动静态分析: 在处理 quote_plus 和 six 上有一些细微差别。最后,我们创建了自己的包装器,默认代码强制执行使用这个包装器,而不是使用标准库中的包装器,也不使用 six 中包装器。我们还静态检查了您从未给 quote_plus 发送过的字节。

我们修复了每个 diango 应用程序中所有的 python 3 问题,并在 CI 环境中使用一个白名单强制执行了这一点,所以您无法破坏一个曾经修复过的应用程序。

依赖

对于我们来说,解决依赖是最困难的部分。我们有很多依赖,所以花了很多时间,其中有两个依赖关系比较棘手:

  • splunk-lib. 我们依赖于 splunk,但是直到今天,他们仍然忽略所有要求为客户端增加 py3 兼容性的愤怒的客户。我们团队中的一个人 最后自己亲自动手来解决这个问题。Splunk 处理得真的很糟糕,它甚至把这个评论区的这个问题锁上了!这简直让人无法接受。
  • Cassandra. 我们的整个产品都在使用这个数据库,但是我们使用了一个有以前 API 模块的旧的驱动程序。对于我们来说,py3 的迁移过程中,这占据了很大的一部分,因此我们必须逐段重写所有的这些代码。

测试

我们的代码测试覆盖率大约有 65% 包括:单元、集成, 以及 UI 合并。 我们确实编写了更多的测试,但总体数量并没有发生太大的变化。考虑将覆盖率从 65% 提高到 66% ,意味着编写将近2000 行代码的测试,这一点也不奇怪。

我们必须跳过需要 Cassandra 的测试,同时修复这个依赖项。 我发明了一个有趣的小 hack 来使它发挥作用, 并写了这方面的文章.

代码更改

关于代码更改的说明,在如何将 py2 迁移到 six 的文档中并未提及 (也许是我们错过了):

StringIO

我们在代码中大量使用 StringIO 。第一反应就是使用 six。但对于 StringIO 来说,这在几乎所有情况下 (但不是全部!)都被证明是错。基本上,我们必须非常仔细地考虑每一个我们使用 StringIO 的地方,并试图弄清楚我们是否应该用 io.StringIO, io.BytesIO 或者 six.StringIO 来替代它。这里犯错的表现通常为看起来像兼容 py3 的代码准备好了,在 py2 中可以正常运行,却实际上在 py3 中是失效的。

future 中导入unicode_literals

这是一件好坏参半的事情。您可以通过将它添加到许多文件中来发现 bug,但是有时会在 py2 中引入 bug。 当日志突然在奇怪的地方,比如在字符串前写”u”时,它也会变得令人困扰。总的来说,这显然不是我所期望的效果。

str/bytes/unicode

这在很大程度上是您所期望的。我感到惊讶的是,在 py2 和 py3 中需要 str 。如果将来您使用 unicode_literals 导入,那么一些字符串需要从 'foo' 修改为 str('foo')

six.moves

six.moves 的实现是一个非常奇怪的黑客行为,因此它不像它假装的普通 Python 模块那样运行。 我也不同意他们在 six.moves 中不包含 mock 的选择。我们必须使用他们的 API 来自己添加它,但这让我们很难开始工作,而且它要求我们将 from mock import patch 改为 from six.moves import mock 这也意味着 patch 现在变成了 mock.patch

CSV 的解析是不同的

如果你使用 csv 模块,你需要了解 csv342。在我看来,这应该是 six 的一部分。否则就意味着你没有意识到有问题。不过我们在许多地方都没有使用 csv342,所以您这里要做的工作可能会有所不同。

发布顺序

我们首先进行测试:

  • 在 CI 中进行单元测试
  • 在 CI 中进行集成和UI测试(不包括 Cassandra)
  • 在 CI 中进行 Cassandra 测试 (这要晚于之前的步骤!)

接下来就是产品本身了。我们建立一台拥有能一次性切换到 py3 的能力的批处理机器,并且至关重要地是将其切换回来。当在 py3 上发生中断时,这一点就显得很重要了。这对我们来说是很好的,因为我们可以重新排队那些中断的任务,但是我们不能中断太多或者任何实际上是很关键的任务。我们使用 Sentry 来收集奔溃日志,所以很容易查看迁移到 py3 时遇到的所有问题,而且当我们修复了所有的问题时,我们需要再次迁移到 py3,直到我们得到一些问题,如此反复。

我们有如下环境:

  • Devtest: 开发人员在内部使用,所以大多数情况下,这只是用来测试数据库迁移。这个环境非常容易使用,所以这里不经常出问题。
  • IAT (内部验收测试):用于验证更改,并在我们将更改推送到生产之前执行回归测试。
  • UAT (用户接受度测试): 客户可以访问的测试环境。用于需要准备客户系统的变更,或者让客户在上线前查看变更。这个环境在数据库迁移前几天才会迁移。
  • 生产环境

我们按照以下顺序将 Python 3 发布到这些环境中:

  • Devtest 环境
  • 短期 IAT 环境
  • 长期 IAT 环境
  • 一台短期的批处理生产机器
  • 在工作期间使用的一台批处理生产机器
  • 生产 SFTP
  • 占一半生产的批处理机器
  • 生产批次
  • 生产 Web (在测试环境的长时间手动测试运行之后)
  • 生产负载机器。这是批处理的一个特殊子集。它完成了我们产品中 CUP 和内存最多的部分。

负载机器暴露了与 Python 3 不兼容的客户数据配置,因此我们必须在 Python 2 中实现对这些情况的警告,并确保再次打开 Python 3 之前已经修复了它们。这花了几天时间,因为我们每天都会收到客户数据,所以每次都会有一个警告,这又让我们不得不再等一天。

生产中的惊喜

  • 'ß'.upper() 在 py2 中是  'ß' 但是在 py3 中是  'SS' 。当产品的最后一部分迁移到 py3 时,最终导致了产品的崩溃!
  • 在 py2 中对不同类型的对象进行比较和排序是有效的,但这隐藏了大量的 bug 。我们得到了一些令人讨厌的惊喜,因为这种行为以一些不明显的方式从堆栈中泄露出来,特别是在一些排序列表中存在  None 的时候。总的来说,这是一个胜利,因为我们发现了相当多的 bug 。 None 在 py2 的列表中排在第一位,这可能会让人感到惊讶(您可能会期望它被排序到接近于零的地方!), 现在我们只需要来处理它们。
  • '{}'.format(b'asd') 在 Python 2 中是 'asd' , 但是在 Python 3 中是 "b'asd'" 。在 Python 3 中,这里几乎任何其他行为都会更好: 输出为十六进制 ( 结果明显更不一样 ) ,旧的行为 (之前的代码运行),或者抛出异常 (最好的行为!)。
  • int('1_0') 在 py 3 中结果是 10 , 但是在 py2 中无效。这甚至在切换到 py3 之前就困扰了我们。因为这种错配导致了另一个在我们之前使用 py3 的团队给我们发送了我们认为无效而他们认为有效的有效值。我个人认为这个决定是错误的:非常严格的解析是更好的默认方式,我担心这将在未来几年会继续以微妙的方式困扰我们。

结论

最后,我们觉得在这件事上我们真的别无选择: Python 2 的维护将在某个时刻停止,我们的依赖项仅限于 py3,最明显的就是 Django。但是,无论如何,我们还是想要进行这种转换,因为我们经常会被 bytes/Unicode 问题困扰,并且Python 3 仅仅是修复了 Python 2 中的许多小麻烦。这次迁移过程,我们已经在生产过程中发现了一些实际的漏洞/错误配置。我们也期待在任何地方都可以使用 f-string 和有序字典。

掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

    原文作者:python入门
    原文地址: https://juejin.im/post/5a9e3ff06fb9a028d2077434
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞