正文
3. 社区
因为 Python 社区已经停止了对 Python 2 的支持。如果把整个运行环境升级到 Python 3,Instagram 的工程师们就能和 Python 社区走的更近,可以更好的把他们的工作回馈给社区。
确定迁移方案
在 Instagram,进行 Python 3 的迁移需要必须满足两个前提条件:
-
不停机,不能有任何的服务因此不可用
-
不能影响产品新特性的开发
但是,在 Instagram 的开发环境中,要满足上面这两点来完成迁移到 Python 3.6 这种庞大的工程是非常困难的。
基于主分支的开发流程
即便使用了以多分支功能著称的 git,Instagram 所有的开发工作都是主要在 master 分支上进行的,Instagram 所奉行的开发哲学是:
『不管是多大的新特性或代码重构,都应该拆解成较小的 Commit 来进行。』
那些被合并进 master 分支的代码,都将在一个小时内被发布到线上环境。
而这样的发布过程每天将会发生上百次。
在这么频繁的发布频率下,如何在满足之前的那两个前提下来完成迁移变得尤其困难。
被弃用的迁移方案
创建一个新分支
很多人在处理这类问题时,第一个蹦进脑子的想法就是:
『让我们创建一个分支,当我们开发完后,再把分支合并进来』
但在 Instagram 这么高的迭代频率上,使用一个独立分支并不是好主意:
-
Instagram 的 Codebase 每天都在频繁更新,在开发 Python 3 分支的过程中,让新分支与现有 master 分支保持同步开销极大,同时极易出错
-
最终将 Python 3 分支这个改动非常多的分支合并回 Master 拥有非常高的风险
-
只有少数几个工程师在 Python 3 分支上专职负责升级工作,其他想帮助迁移工作的工程师无法参与进来
挨个替换接口
还有一个方案就是,挨个替换 Instagram 的 API 接口。但是 Instagram 的不同接口共享着很多通用模块。这个方案要实施起来也非常困难。
微服务
还有一个方案就是将 Instagram 改造成微服务架构。通过将那些通用模块重写成 Python 3 版本的微服务来一步步完成迁移工作。
但是这个方案需要重新组织海量的代码。同时,当发生在进程内的函数调用变成 RPC 后 ,整个站点的延迟会变大。此外,更多的微服务也会引入更高的部署复杂度。
所以,既然 Instagram 的开发哲学是:
小步前进,快速迭代
。他们最终决定的方案是:
一步一步来,最终让 master 分支上的代码同时兼容 Python 2 和 Python 3 。
开始迁移工作
既然要让整个 codebase 同时兼容 Python 2 和 Python 3,那么首先要符合这点的就是那些被大量使用的第三方 package。针对第三方 package,Instagram 做到了下面几点:
在代码的迁移过程中,他们使用了工具
modernize
来帮助他们。
使用 modernize 时,有一个小技巧:
每次修复多个文件的一个兼容问题,而不是一下修复一个文件中的多个兼容问题。
这样可以让 Code Review 过程简单很多,因为 Reviewer 每次只需要关注一个问题。
使用单元测试来帮助迁移
对于 Python 这种灵活性极强的动态语言来说,除了真正去执行代码外,几乎没有其他比较好的检查代码错误的手段。
前面提到,Instagram 所有被合并到 master 的代码提交会在一个小时内上线到线上环境,但这不是没有前提条件的。
在上线前,所有的提交都需要通过成千上万个单元测试。
于是,他们开始加入 Python 3 来执行所有的单元测试。一开始,只有极少数的单元测试能够在 Python 3 环境下通过,但随着 Instagram 的工程师们不断的修复那些失败的单元测试,最终所有的单元测试都可以在 Python 3 环境下成功执行。
单元测试的局限性
但是,单元测试也是有局限性的:
所以,当所有的单元测试都被修复后,他们开始在线上正式使用 Python 3 来运行服务。
这个过程并不是一蹴而就的。首先,所有的 Instagram 工程师开始访问到这些使用 Python 3 来执行的新服务,然后是 Facebook 的所有雇员,随后是 0.1%、20% 的用户,最终 Python 3 覆盖到了所有的 Instagram 用户。
图:循序渐进的发布流程
迁移过程的技术问题
Instagram 在迁移到 Python 3 时碰到很多问题,下面是最典型的几个:
Unicode 相关的字符串问题
Python 3 相比 Python 2 最大的改动之一,就是在语言内部对 unicode 的处理。
在 Python 2 中,文本类型
(也就是 unicode)
和二进制类型
(也就是 str)
的边界非常模糊。很多函数的参数既可以是文本,也可以是二进制。但是在 Python 3 中,文本类型和二进制类型的字符串被完全的区分开了。
于是,下面这段在 Python 2 下可以正常运行的代码在 Python 3 下就会报错:
mymac = hmac.new('abc')
TypeError: key: expected bytes or bytearray, but got 'str'
解决办法其实很简单,只要加上判断:如果 value 是文本类型,就将其转换为二进制。如下所示:
value = 'abc'
if isinstance(value, six.