前言PyCon是全球最大的以Python编程语言为主题的技术会议。该会议由Python社区组织,每年举办一次。在Python2017上,Instagram工程师在Instagram做了一个关于Python的主题演讲,还分享了Instagram如何将他们的整个项目运行时升级到Python3的故事。本文是演讲的总结,由Python爱好者朱磊撰写。Instagram是由KevinSystrom和MikeKrieger于2010年创立的手机照片和视频分享软件。Instagram发布后迅速走红。2012年被Facebook以10亿美元收购。当时,Instagram只有13名员工。如今,Instagram拥有30亿总注册用户和超过7亿月活跃用户(相比之下,微信最近披露的月活跃用户为9.38亿)。让人意外的是,如此高的访问量背后,竟然完全由以速度慢着称的Python+Django支持。为什么Python和DjangoInstagram选择Django的原因很简单,Instagram的两位创始人(KevinSystrom和MikeKrieger)都是产品经理。当他们想要创建Instagram时,Django是他们所知道的最稳定、最成熟的技术之一。即使在今天,它也拥有超过30亿的注册用户。Instagram仍然是Python和Django的重度用户。Instagram的工程师HuiDing表示:“直到用户ID已经超过32bitint限制(约20亿),Django本身仍然没有成为我们的瓶颈。”不过,除了使用Django的原生功能,Instagram也已经对Django做了很多定制工作:extendingDjangoModelstosupportSharding(一种数据库分片技术)。手动关闭GC(垃圾回收),提高Python内存管理效率。他们还写了一篇博客来解释这一点:DismissingPythonGarbageCollectionatInstagram。跨位于不同地理位置的多个数据中心部署整个系统。Python语言的优势在于,Instagram的联合创始人MikeKrieger说:“我们的用户不关心Instagram使用什么样的关系数据库,他们当然也不关心Instagram是用什么编程语言开发的。‘所以,Python,一种简单实用的编程语言,最终赢得了Instagram的青睐,他们认为使用像Python这样简单的语言有助于塑造Instagram的工程师文化,即:专注于定位和解决问题——而不是工具本身的各种花花绿绿的功能使用那些已经在市场上得到验证的成熟技术解决方案——不要被工具本身的问题困扰使用Python语言,它仍然很慢,不是吗?然而,这对Instagram来说不是问题,因为他们认为:“Instagram最大的瓶颈在于开发效率,而不是代码执行效率。“在Instagram,我们的瓶颈是开发速度,而不是纯粹的代码执行。所以,最后的结论是:你可以用Python语言来实现一个被超过数十亿用户使用的产品,而不用担心语言或框架本身的性能瓶颈。如何提高运行效率不过,即使你选择Python和Django,它们也有很多好处。在Instagram用户快速增长的过程中,性能问题还是出现了:服务器数量的增长速度已经慢慢超过了用户的增长速度。Instagram如何处理这个问题?他们使用这些工具来缓解性能问题:开发工具来帮助调优:Instagram开发了很多涵盖各个层的工具来帮助他们进行性能调优和寻找性能瓶颈。使用C/C++重写部分组件:使用C或C++重写那些稳定的、对性能最敏感的组件,比如访问memcache的库。使用Cython:Cython也是他们用来提高Python效率的法宝之一。除了上述方法,他们还在探索异步IO和新的PythonRuntime带来的性能可能性。为什么升级到Python3很长一段时间以来,Instagram一直在Python2.7+Django1.3的组合上运行。在这个落后于社区多年的环境下,他们的工程师也做了很多很多的小补丁。他们会永远停留在这个版本上吗?于是,经过一系列的讨论,他们最终做出了一个重大决定:升级到Python3!!事实上,Instagram目前已经完成了向Python3的迁移——他们的整个服务已经在Python3上运行了几个月。那么他们是怎么做到的呢?接下来是Instagram工程师Lisaguo带来的Instagram如何迁移到Python3的故事。对于Instagram,以下因素是他们迁移到Python3的主要原因:新特性:TypeAnnotationsTypeAnnotations看看下面的代码:defcompose_from_max_id(max_id):'''@paramstrmax_id'''Graph是什么类型函数的max_id参数?诠释?元组?还是清单?等等,函数文档说它是str类型。但是,如果这个参数的类型随时间发生变化怎么办?如果一个粗心的工程师修改了代码而忘记更新文档,会给功能的使用者带来很大的麻烦,最后还不如完全没有意见呢。2.性能Instagram的整个DjangoStack运行在uwsgi上,全部使用同步网络IO。这意味着同一个uwsgi进程一次只能接收和处理一个请求。这使得调整应该在每台机器上运行的uwsgi进程的数量变得很麻烦:使用更多进程以获得更好的CPU利用率?但这会消耗大量内存。而进程太少会导致CPU不能被充分利用。为此,他们决定跳过Python2中蹩脚的异步IO实现(可怜的gevent、tornado、扭曲的人),直接升级到Python3,探索标准库中asyncio模块带来的可能性。3、社区由于Python社区已经停止支持Python2,如果整个运行环境升级到Python3,Instagram的工程师们可以更贴近Python社区,更好地回馈社区。迁移解决方案在Instagram中,Python3的迁移必须满足两个先决条件:不停机,不提供服务,因此不可用不会影响新产品功能的开发。但是,在Instagram的开发环境中,必须满足以上两个条件才能完成迁移到Python3.6这样庞大的工程。基于主分支的开发过程即使使用以多分支功能着称的git,Instagram的所有开发工作也主要是在主分支上进行的。Instagram奉行的开发理念是:“无论新功能或代码有多大,每个结构都应该拆解成更小的Commits。”那些被合并到master分支的代码将在一个小时内发布到线上环境。而这发布过程每天会发生数百次,如此频繁的发布频率,如何在满足前两个前提条件的情况下完成迁移变得尤为困难DeprecatedmigrationsolutionCreateanewbranch他们首先想到的是:“让我们创建一个分支,等我们完成开发后再合并分支”。但是以Instagram的高迭代频率,使用单独的分支并不是一个好主意:Instagram的代码库每天都会频繁更新天,并且在Python3分支的开发过程中保持新分支与现有master分支同步的成本很高同时,非常容易出错,最后把改动比较大的Python3分支合并回Master,风险非常大。Python3分支上只有少数工程师负责升级工作,其他想帮忙迁移的工程师无法一一参与。替换接口的另一种选择是将Instagram的API接口一个一个替换。但Instagram的不同界面共享许多通用模块。这个计划的实施难度也很大。微服务的另一种解决方案是将Instagram改造成微服务架构。通过将那些通用模块重写为Python3版本的微服务来逐步迁移。但是这个解决方案需要重新组织很多代码。同时,当进程中的函数调用变成RPC时,整个站点的延迟会增加。此外,更多的微服务会带来更高的部署复杂性。所以,自从Instagram的发展理念是:小步前进,快速迭代。他们最终决定的解决方案是:循序渐进,最终让master分支上的代码同时兼容Python2和Python3。既然官方迁移到Python3就是让整个代码库同时兼容Python2和Python3、首先必须满足这一点的是那些被广泛使用的第三方包。对于第三方包,Instagram做了如下处理:拒绝引入所有不兼容Python3的新包删除所有未使用的包替换那些不兼容Python3的包在代码迁移过程中,他们使用了工具modernize帮助他们。使用modernize时有个小技巧:一次性修复多个文件的一个兼容性问题,而不是一次性修复一个文件的多个兼容性问题。这可以使代码审查过程更加容易,因为审查者一次只需要关注一个问题。对于像Python这样极其灵活的动态语言,除了实际执行代码之外,几乎没有其他更好的检查代码错误的方法。前面说过,所有合并到master的Instagram代码提交都会在一个小时内上线,但这并不是没有前提条件。每个提交都需要通过数千个单元测试才能上线。因此,他们开始添加Python3来执行所有单元测试。起初,只有少数单元测试会在Python3下通过,但随着Instagram工程师继续修复失败的单元测试,最终所有单元测试都会在Python3下成功执行。但是,单元测试也有局限性:Instagram的单元测试并没有达到100%的代码覆盖率。很多第三方模块使用了mock技术,mock行为可能与真实的在线服务不同。因此,当所有的单元测试都被修复后,他们就开始使用Python3来运行服务。这个过程不会在一夜之间发生。首先,所有Instagram工程师开始访问这些使用Python3执行的新服务,然后是所有Facebook员工,然后是0.1%、20%的用户,最终Python3访问了所有Instagram用户。迁移过程中的技术问题Instagram在迁移到Python3时遇到了很多问题,以下是最典型的:Unicode相关的字符串问题。与Python2相比,Python3最大的变化之一是在语言中支持unicode。处理。在Python2中,文本类型(又名unicode)和二进制类型(又名str)之间的界限非常模糊。许多函数的参数可以是文本或二进制。但在Python3中,文本和二进制字符串是完全分开的。因此,下面这段在Python2下可以正常运行的代码在Python3下会报错:mymac=hmac.new('abc')TypeError:key:expectedbytesorbbytearray,butgot'str'[外链图片传输失败,源站可能有一个反盗链机制。建议保存图片直接上传(img-tQT44Q0M-1570179360052)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAIRAEAOw==)]解决方法其实很简单,加个判断:如果value是text类型,则转成binary。像这样:value='abc'ifisinstance(value,six.text_type):valuevalue=value.encode(encoding='utf-8')mymac=hmac.new(value)但是,在整个代码库中,像上面一样有这样的案例很多。作为开发者,如果在调用每个函数的时候需要思考:是编码成二进制还是解码成文本?这将是一个非常沉重的负担。所以Instagram封装了一些辅助函数,名为ensure_str()、ensure_binary()和ensure_text()。开发者只需要先使用这些辅助函数对不确定类型的字符串进行转换即可。mymac=hmac.new(ensure_binary('abc'))[保存外链图片失败,源站可能有防盗链机制,建议保存图片直接上传(img-Ls5jOGEl-1570179360053)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAICRAEAOw==)]Python版本之间的pickle差异Instagram在其代码中大量使用了pickle。例如,用它来序列化一个对象并将其存储在memcache中。如下代码所示:memcache_data=pickle.dumps(data,pickle.HIGHEST_PROTOCOL)data=pickle.loads(memcache_data)问题是Python2和Python3的pickle模块不一样。如果上面的第一行代码恰好被运行在Python3上的服务序列化,它将被存储在memcache中。但是反序列化过程是由Python2执行的,代码运行时会出现如下错误:ValueError:unsupportedpickleprotocol:4这是因为在Python3中,pickle.HIGHEST_PROTOCOL的值为4,而Python2中的pickle中支持的最高版本号是2。那么如何解决这个问题呢?Instagram最终选择让Python2和Python3使用完全不同的命名空间来访问memcache。通过将两者的数据读写完全分离来解决这个问题。Iterator在Python3中,很多内置函数被修改为只返回一个迭代器:map()filter()dict.items()[外链图片传输失败,源站可能有防盗链机制,就是推荐保存图片直接上传(img-GLVPUDc0-1570179360059)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAAALAAAAAABAAEAAAICRAEAOw==)]迭代器有很多好处,最大的好处是使用迭代器不需要一次性分配大量的内存,因此相对来说内存效率更高。但是迭代器有一个天然的特性。当你迭代一个迭代器一次并访问它的内容时,你不能再次访问这些内容。迭代器中的所有内容都只能访问一次。在Instagram的Python3迁移过程中,我就因为迭代器的这个特性被抓了一次。看一下下面的代码:
