Python向来以慢著称,为啥Instagram却唯独钟爱它
PyCon是全世界最大的以Python编程语言为主题的技术大会,大会由Python社区组织,每年举办一次。在Python2017上,Instagram的工程师们带来了一个有关Python在Instagram的主题演讲,同时还分享了Instagram如何将整个项目运行环境升级到Python3的故事。本文为该次演讲的内容摘要,由Python爱好者朱雷撰写,聊聊架构经授权发布。
Instagram是一款移动端的照片与视频分享软件,由KevinSystrom和MikeKrieger在2010年创办。Instagram在发布后开始快速流行。于2012年被Facebook以10亿美元的价格收购。而当时Instagram的员工仅有区区13名。
如今,Instagram的总注册用户达到30亿,月活用户超过7亿(作为对比,微信最新披露的月活跃用户为9.38亿)。而令人吃惊的是,这么高的访问量背后,竟完全是由以速度慢著称的Python+Django支撑。
为什么选择Python和Django
相关厂商内容
布局大前端!UC浏览器容器化架构艰难演进之路
Twitch如何通过软硬件优化降低80%视频成本?
不改一行代码:左耳朵看微服务调度的三大件
AI时代,我到底该如何跟上技术潮流?
获取面向人工智能、机器学习和深度学习的最新工具、框架
相关赞助商
Instagram选择Django的原因很简单,Instagram的两位创始人(KevinSystromandMikeKrieger)都是产品经理出身。在他们想要创造Instagram时,Django是他们所知道的最稳定和成熟的技术之一。
时至今日,即使已经拥有超过30亿的注册用户。Instagram仍然是Python和Django的重度使用者。Instagram的工程师HuiDing说到:『一直到用户ID已经超过了32bitint的限额(约为20亿),Django本身仍然没有成为我们的瓶颈所在。』
不过,除了使用Django的原生功能外,Instagram还对Django做了很多定制化工作:
扩展DjangoModels使其支持Sharding(一种数据库分片技术)。
手动关闭GC(垃圾回收)来提升Python内存管理效率,他们同样也写过一篇博客来说明这件事情:DismissingPythonGarbageCollectionatInstagram。
在位于不同地理位置的多个数据中心部署整套系统。
Python语言的优势所在
Instagram的联合创始人MikeKrieger说过:『我们的用户根本不关心Instagram使用了哪种关系数据库,他们当然也不关心Instagram是用什么编程语言开发的。』
所以,Python这种简单而且实用至上的编程语言最终赢得了Instagram的青睐。他们认为,使用Python这种简单的语言有助于塑造Instagram的工程师文化,那就是:
专注于定位问题、解决问题-而不是工具本身的各种花花绿绿的特性
使用那些经过市场验证过的成熟技术方案-而不用被工具本身的问题所烦扰
用户至上:专注于用户所能看到的新特性,为用户带去价值
但是,即使使用Python语言有这么多好处,它还是很慢,不是吗?
不过,这对于Instagram不是问题,因为他们认为:『Instagram的最大瓶颈在于开发效率,而不是代码的执行效率』。
AtInstagram,ourbottleneckisdevelopmentvelocity,notpurecodeexecution.
所以,最终的结论是:你完全可以使用Python语言来实现一个超过几十亿用户使用的产品,而根本不用担心语言或框架本身的性能瓶颈。
如何提升运行效率
但是,即使是选用了拥有诸多好处的Python和Django。在Instagram的用户数迅速增长的过程中,性能问题还是出现了:服务器数量的增长率已经慢慢的超过了用户增长率。Instagram是怎么应对这个问题的呢?
他们使用了这些手段来缓解性能问题:
开发工具来帮助调优:Instagram开发了很多涵盖各个层面的工具,来帮助他们进行性能调优以及找到性能瓶颈。
使用C/C++来重写部分组件:把那些稳定而且对性能最敏感的组件,使用C或C++来重写,比如访问memcache的library。
使用Cython:Cython也是他们用来提升Python效率的法宝之一。
除了上面这些手段,他们还在探索异步IO以及新的PythonRuntime所能带来的性能可能性。
为什么要升级到Python3
在相当长的一段时间,Instagram都跑在Python2.7+Django1.3的组合之上。在这个已经落后社区很多年的环境上,他们的工程师们还打了非常非常多的小patch。难道他们要被永远卡在这个版本上吗?
所以,在经过一系列的讨论后,他们最终做出一个重大的决定:升级到Python3!!
事实上,Instagram目前已经完成了将运行环境迁移到Python3的工作-他们的整套服务已经在Python3上跑了好几个月了。那么他们是怎么做到的呢?接下来便是由Instagram工程师Lisaguo带来的Instagram如何迁移到Python3的故事。
对于Instagram来说,下面这些因素是推动他们将运行环境迁移到Python3的主要原因:
1.新特性:类型注解TypeAnnotations
看看下面这段代码:
defcompose_from_max_id(max_id):'''@paramstrmax_id'''
图中函数的max_id参数究竟是什么类型呢?int?tuple?或是list?等等,函数文档里面说它是str类型。
但随着时间推移,万一这个参数的类型发生变化了呢?如果某位粗心的工程师修改代码的同时忘了更新文档,那就会给函数的使用者带来很大麻烦,最终还不如没有注释呢。
2.性能
Instagram的整个DjangoStack都跑在uwsgi之上,全部使用了同步的网络IO。这意味着同一个uwsgi进程在同一时间只能接收并处理一个请求。这让如何调优每台机器上应该运行的uwsgi进程数成了一个麻烦事:
为了更好利用CPU,使用更多的进程数?但那样会消耗大量的内存。而过少的进程数量又会导致CPU不能被充分利用。
为此,他们决定跳过Python2中哪些蹩脚的异步IO实现(可怜的gevent、tornado、twisted众),直接升级到Python3,去探索标准库中的asyncio模块所能带来的可能性。
3.社区
因为Python社区已经停止了对Python2的支持。如果把整个运行环境升级到Python3,Instagram的工程师们就能和Python社区走的更近,可以更好的把他们的工作回馈给社区。
迁移方案
在Instagram,进行Python3的迁移需要必须满足两个前提条件:
不停机,不能有任何的服务因此不可用
不能影响产品新特性的开发
但是,在Instagram的开发环境中,要满足上面这两点来完成迁移到Python3.6这种庞大的工程是非常困难的。
基于主分支的开发流程
即便使用了以多分支功能著称的git,Instagram所有的开发工作都是主要在master分支上进行的,Instagram所奉行的开发哲学是:『不管是多大的新特性或代码重构,都应该拆解成较小的Commit来进行。』
那些被合并进master分支的代码,都将在一个小时内被发布到线上环境。而这样的发布过程每天将会发生上百次。在这么频繁的发布频率下,如何在满足之前的那两个前提下来完成迁移变得尤其困难。
被弃用的迁移方案
创建一个新分支
很多人在处理这类问题时,第一个蹦进脑子的想法就是:『让我们创建一个分支,当我们开发完后,再把分支合并进来』。但在Instagram这么高的迭代频率上,使用一个独立分支并不是好主意:
Instagram的Codebase每天都在频繁更新,在开发Python3分支的过程中,让新分支与现有master分支保持同步开销极大,同时极易出错
最终将Python3分支这个改动非常多的分支合并回Master拥有非常高的风险
只有少数几个工程师在Python3分支上专职负责升级工作,其他想帮助迁移工作的工程师无法参与进来
挨个替换接口
还有一个方案就是,挨个替换Instagram的API接口。但是Instagram的不同接口共享着很多通用模块。这个方案要实施起来也非常困难。
微服务
还有一个方案就是将Instagram改造成微服务架构。通过将那些通用模块重写成Python3版本的微服务来一步步完成迁移工作。
但是这个方案需要重新组织海量的代码。同时,当发生在进程内的函数调用变成RPC后,整个站点的延迟会变大。此外,更多的微服务也会引入更高的部署复杂度。
所以,既然Instagram的开发哲学是:小步前进,快速迭代。他们最终决定的方案是:一步一步来,最终让master分支上的代码同时兼容Python2和Python3。
正式迁移到Python3
既然要让整个codebase同时兼容Python2和Python3,那么首先要符合这点的就是那些被大量使用的第三方package。针对第三方package,Instagram做到了下面几点:
拒绝引入所有不兼容Python3的新package
去掉所有不再使用的package
替换那些不兼容Python3的package
在代码的迁移过程中,他们使用了工具modernize来帮助他们。
使用modernize时,有一个小技巧:每次修复多个文件的一个兼容问题,而不是一下修复一个文件中的多个兼容问题。这样可以让CodeReview过程简单很多,因为Reviewer每次只需要关注一个问题。
对于Python这种灵活性极强的动态语言来说,除了真正去执行代码外,几乎没有其他比较好的检查代码错误的手段。
前面提到,Instagram所有被合并到master的代码提交会在一个小时内上线到线上环境,但这不是没有前提条件的。在上线前,所有的提交都需要通过成千上万个单元测试。
于是,他们开始加入Python3来执行所有的单元测试。一开始,只有极少数的单元测试能够在Python3环境下通过,但随着Instagram的工程师们不断的修复那些失败的单元测试,最终所有的单元测试都可以在Python3环境下成功执行。
但是,单元测试也是有局限性的:
Instagram的单元测试没有做到100%的代码覆盖率
很多第三方模块都使用了mock技术,而mock的行为与真实的线上服务可能会有所不同
所以,当所有的单元测试都被修复后,他们开始在线上正式使用Python3来运行服务。
这个过程并不是一蹴而就的。首先,所有的Instagram工程师开始访问到这些使用Python3来执行的新服务,然后是Facebook的所有雇员,随后是0.1%、20%的用户,最终Python3覆盖到了所有的Instagram用户。
迁移过程的技术问题
Instagram在迁移到Python3时碰到很多问题,下面是最典型的几个:
Unicode相关的字符串问题
Python3相比Python2最大的改动之一,就是在语言内部对unicode的处理。
在Python2中,文本类型(也就是unicode)和二进制类型(也就是str)的边界非常模糊。很多函数的参数既可以是文本,也可以是二进制。但是在Python3中,文本类型和二进制类型的字符串被完全的区分开了。
于是,下面这段在Python2下可以正常运行的代码在Python3下就会报错:
mymac=hmac.new('abc')
TypeError:key:expectedbytesorbytearray,butgot'str'
解决办法其实很简单,只要加上判断:如果value是文本类型,就将其转换为二进制。如下所示:
value='abc'
ifisinstance(value,six.text_type):value=value.encode(encoding='utf-8')
mymac=hmac.new(value)
但是,在整个代码库中,像上面这样的情况非常多。作为开发人员,如果需要在调用每个函数时都要想想:这里到底是应该编码成二进制,或者是解码成文本呢?将会是非常大的负担。
于是Instagram封装了一些名为ensure_str()、ensure_binary()、ensure_text()的帮助函数,开发人员只需对那些不确定类型的字符串,使用这些帮助函数先做一次转换就好。
mymac=hmac.new(ensure_binary('abc'))
不同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使用完全不同的namespace来访问memcache。通过将二者的数据读写完全隔开来解决这个问题。
迭代器
在Python3中,很多内置函数被修改成了只返成迭代器Iterator:
map()
filter()
dict.items()
迭代器有诸多好处,最大的好处就是,使用迭代器不需要一次性分配大量内存,所以它的内存效率比较高。
但是迭代器有一个天然的特点,当你对某个迭代器做了一次迭代,访问完它的内容后,就没法再次访问那些内容了。迭代器中的所有内容都只能被访问一次。
在Instagram的Python3迁移过程中,就因为迭代器的这个特性被坑了一次,看看下面这段代码:
CYTHON_SOURCES=[a.pyx,b.pyx,c.pyx]
builds=map(BuildProcess,CYTHON_SOURCES)
whileany(notbuild.done()forbuildinbuilds):pending=[buildforbuildinbuildsifnotbuild.started()]<dosomework>
这段代码的用处是挨个编译Cython源文件。当他们把运行环境切换到Python3后,一个奇怪的问题出现了:CYTHON_SOURCES中的第一个文件永远都被跳过了编译。为什么呢?
这都是迭代器的锅。在Python3中,map()函数不再返回整个list,而是返回一个迭代器。
于是,当第二行代码生成builds这个迭代器后,第三行代码的while循环迭代了builds,刚好取出了第一个元素。于是之后的pending对象便里面永远少了那第一个元素。
这个问题解决起来也挺简单的,你只要手动的吧builds转换成list就可以了:
builds=list(map(BuildProcess,CYTHON_SOURCES))
但是这类bug非常难定位到。如果用户的feeds里面永远少了那最新的第一条,用户很少会注意到。
字典的顺序
看看下面这段代码:
>>>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版本下,这个jsondumps的结果是完全不一样的。甚至在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,他们使用两个主要指标来衡量他们的服务性能:
每次请求产生的CPU指令数(越低越好)
每秒能够处理的请求数(越高越好)
所以,当所有的迁移工作完成后,他们非常惊喜的发现:第一个性能指标,每次请求产生的CPU指令数居然足足下降了12%!!!
但是,按理说第二个指标-每秒请求数也应该获得接近12%的提升。不过最后的变化却是0%。究竟是出了什么问题呢?
他们最终定位到,是由于不同Python版本下的内存优化配置不同,导致CPU指令数下降带来的性能提升被抵消了。那为什么不同Python版本下的内存优化配置会不一样呢?
这是他们用来检查uwsgi配置的代码:
ifuwsgi.opt.get('optimize_mem',None)=='True':optimize_mem()
注意到那段......=='True'了吗?在Python3中,这个条件判断总是不会被满足。问题就在于unicode。在将代码中的'True'换成b'True'(也就是将文本类型换成二进制,这种判断在Python2中完全不区分的)后,问题解决了。
所以,最终因为加上了一个小小的字母'b',程序的整体性能提升了12%。
完美切换
在今年二月份,Instagram的后端代码的运行环境完全切换到了Python3下:
当所有的代码都都迁移到Python3运行环境后:
节约了12%的整体CPU使用率(Django/uwsgi)
节约了30%的内存使用(celery)
同时,在整个迁移期间,Instagram的月活用户经历了从4亿到6亿的巨大增长。产品也发布了评论过滤、直播等非常多新功能。
那么,那几个最开始驱动他们迁移到Python3的目的呢?
类型注解:Instagram的整个codebase里已经有2%的代码添加上了类型注解,同时他们还开发了一些工具来辅助开发者添加类型提示
asyncio:他们在单个接口中利用asynio平行的去做多件事情,最终降低了20-30%的请求延迟。
社区:他们与Intel的工程师联合,帮助他们更好的对CPU利用率进行调优。同时还开发了很多新的工具,帮助他们进行性能调优
Instagram带给我们的启示
Instagram的演讲视频时间不长,但是内容很丰富,在编写此文前,我完全没有想到最终的文章会这么长。
那么,Instagram的视频可以给我们哪些启示呢?
Python+Django的组合完全可以负载用户数以10亿记的服务,如果你正准备开始一个项目,放心使用Python吧!
完善的单元测试对于复杂项目是非常有必要的。如果没有那『成千上万的单元测试』。很难想象Instagram的迁移项目可以成功进行下去。
开发者和同事也是你的产品用户,利用好他们。用他们为你的新特性发布前多一道测试。
完全基于主分支的开发流程,可以给你更快的迭代速度。前提是拥有完善的单元测试和持续部署流程。
Python3是大势所趋,如果你正准备开始一个新项目,无需迟疑,拥抱Python3吧!
Python培训,就选光环大数据Python培训机构!python学习地址:http://hadoop.aura.cn/python/
大数据培训、人工智能培训、Python培训、大数据培训机构、大数据培训班、数据分析培训、大数据可视化培训,就选光环大数据!光环大数据,聘请专业的大数据领域知名讲师,确保教学的整体质量与教学水准。讲师团及时掌握时代潮流技术,将前沿技能融入教学中,确保学生所学知识顺应时代所需。通过深入浅出、通俗易懂的教学方式,指导学生更快的掌握技能知识,成就上万个高薪就业学子。 更多问题咨询,欢迎点击------>>>>在线客服!