当前位置: 首页 > 科技观察

Python这么慢,为什么大公司还在用?

时间:2023-03-12 08:29:05 科技观察

前言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迁移过程中,我就因为迭代器的这个特性被抓了一次。看一下下面的代码:这段代码的目的是将Cython源文件一个一个编译。当他们将环境切换到Python3时,出现了一个奇怪的问题:CYTHON_SOURCES中的第一个文件总是被跳过进行编译。为什么?这都是迭代器的错。在Python3中,map()函数不再返回整个列表,而是一个迭代器。因此,当第二行代码生成builds迭代器时,第三行代码的while循环遍历builds,只取出第一个元素。所以后续的pending对象总会缺少第一个元素。这个问题解决起来很简单,你只需要手动将构建转换成一个列表:builds=list(map(BuildProcess,CYTHON_SOURCES))但是这种bug很难定位。如果用户的订阅源中永远缺少最新的第一个条目,用户将很少注意到。字典的顺序看下面的代码:>>>testdict={'a':1,'b':2,'c':3}>>>json.dumps(testdict)会输出什么?#Python2'{"a":1,"c":3,"b":2}'#Python3.5.1'{"c":3,"b":2,"a":1}'#or'{"c":3,"a":1,"b":2}'#Python3.6'{"a":1,"b":2,"c":3}'在不同的Python中这个json转储的结果在新版本下完全不同。即使在3.5.1中,它也完全随机地返回两个不同的结果。Instagram有一个判断配置文件是否改变的模块,也正是因为这个原因,才会出现问题。这个问题的解决方法是在调用json.dumps时传入sort_keys=True参数:>>>json.dumps(testdict,sort_keys=True)'{"a":1,"b":2,"c":3}'迁移到Python3.6后的性能改进虽然Instagram已经解决了这些奇怪的版本差异,但仍然有一个巨大的难题困扰着他们:性能问题。在Instagram,他们使用两个主要指标来衡量他们服务的性能:CPUinstructionsperrequest(越低越好)Requestspersecond(越高越好)所以当所有迁移工作完成后,他们非常惊喜地发现,:第一个性能指标,每次请求产生的CPU指令数居然下降了12%!!!然而,按理说,第二个指标——每秒请求数——也应该接近12%的提升。但最终的变化是0%。出了什么问题?他们最终确定,由于不同Python版本下的内存优化配置不同,CPU指令数减少带来的性能提升被抵消了。那么为什么不同Python版本下的内存优化配置不一样呢?下面是他们用来检查uwsgi配置的代码:ifuwsgi.opt.get('optimize_mem',None)=='True':optimize_mem()注意...=='True'?在Python3中,这个条件判断永远不会被满足。问题是unicode。将代码中的'True'换成b'True'后(即把text类型换成binary,这个判断在Python2中完全没有区别),问题解决。因此,最终,由于添加了一个小字母“b”,程序的整体性能提高了12%。完美切换今年2月,Instagram后台代码运行环境完全切换到Python3:当所有代码迁移到Python3运行环境时:整体CPU占用节省12%(Django/uwsgi)内存占用节省30%(celery)此外,在整个迁移期间,Instagram的月活跃用户从4亿激增至6亿。产品还发布了评论过滤、直播等诸多新功能。那么,最初促使他们转向Python3的原因是什么?类型注解:Instagram整个代码库中有2%的代码添加了类型注解,他们还开发了一些工具来协助开发者添加类型提示asyncio:他们在单个界面中使用asynio并行地做多件事,从而产生了一个请求延迟减少20-30%。社区:他们与英特尔工程师联手帮助他们更好地调整CPU利用率。同时,开发了很多新的工具来帮助他们进行性能调优。Instagram给我们带来的启发Instagram演讲的视频不长,但是内容却非常丰富。在写这篇文章之前,我从来没有想过最终的文章会这么长。综上所述,Instagram视频能教给我们什么?Python+Django的组合完全可以加载一个拥有数十亿用户的服务。如果你正准备开始一个项目,请随意使用Python!完善的单元测试对于复杂的项目来说是非常必要的。没有那个“成千上万的单元测试”。很难想象Instagram的迁移项目会顺利进行。开发人员和同事也是你产品的用户,用好他们。在发布之前将它们用作对新功能的额外测试。完全基于主分支的开发过程可以给你更快的迭代速度。前提是要有完善的单元测试和持续部署流程。Python3是大势所趋,如果你准备开始一个新的项目,不要犹豫,拥抱Python3!