PyCon 是全国际最大的以 Python 编程言语 为主题的技能大会。大会由 Python 社区组织,每一年举行一次。在大会上,来自国际各地的 Python 用户与中心开发者齐聚一堂,共同同享 Python 国际的新鲜事、Python 言语的应用案例、运用技巧等等内容。python
<p "="">Instagram 是一款移动端的照片与视频同享软件,由 Kevin Systrom 和 Mike Krieger 在 2010 年创办。Instagram 在发布后开端快速流行。于 2012 年被 Facebook 以 10 亿美圆的价格收买。而其时 Instagram 的员工仅有区区 13 名。<p "="">现在,Instagram 的总注册用户到达 30 亿,月活用户超越 7 亿 (做为对比,微信最新披露的月活泼用户为 9.38 亿)。而使人吃惊的是,这么高的拜访量背后,竟完全是由以速度慢著称的 Python + Django 支撑。<p "="">在 Python 2017 上,Instagram 的工程师们带来了一个有关 Python 在 Instagram 的主题演讲,一块儿还同享了 Instagram 怎么将整个项目运转环境晋级到 Python 3 的故事。<p "="">本文为该次演讲的内容摘要。git
<p "="">Instagram 挑选 Django 的缘由很简略,Instagram 的两位创始人 (Kevin Systrom and Mike Krieger) 都是产品经理出身。在他们想要创造 Instagram 时,Django 是他们所知道的最稳定和成熟的技能之一。<p "="">时至今日,即便现已具备超越 30 亿的注册用户。Instagram 依然是 Python 和 Django 的重度运用者。Instagram 的工程师 Hui Ding 说到: 『一直到用户 ID 现已超越了 32bit int 的限额(约为 20 亿),Django 自身依然没有成为我们的瓶颈地点。』<p "="">不过,除了运用 Django 的原生功用外,Instagram 还对 Django 作了许多定制化做业:<ul "="">数据库
<p "="">Instagram 的联合创始人 Mike Krieger 说过: 『我们的用户根本不关心 Instagram 运用了哪一种联系数据库,他们固然也不关心 Instagram 是用什么编程言语开发的。』<p "="">因此,Python 这种 简略 而且 实用至上 的编程言语终究赢得了 Instagram 的喜好。他们觉得,运用 Python 这种简略的言语有助于刻画 Instagram 的工程师文明,那就是:<ol "="">编程
<p "="">但是,即便运用 Python 言语有这么多优势,它还是很慢,不是吗?<p "="">不过,这关于 Instagram 不是问题,因为他们觉得:『Instagram 的最大瓶颈在于开发功率,而不是代码的履行功率』<blockquote "="">json
At Instagram, our bottleneck is development velocity, not pure code execution.后端
<p "="">因此,终究的结论是:你完全可以运用 Python 言语来完结一个超越几十亿用户运用的产品,而根本不用忧虑言语或框架自身的功用瓶颈。微信
<p "="">但是,即就是选用了具备诸多优势的 Python 和 Django。在 Instagram 的用户数迅速增加的进程中,功用问题还是呈现了:效劳器数量的增加率现已慢慢的超越了用户增加率。Instagram 是怎么应对这个问题的呢?<p "="">他们运用了这些手法来缓解功用问题:<ul "="">网络
<p "="">除了上面这些手法,他们还在探索异步 IO 以及新的 Python Runtime 所能带来的功用可能性。架构
<p "="">在至关长的一段时间,Instagram 都跑在 Python 2.7 + Django 1.3 的组合之上。在这个现已落后社区许多年的环境上,他们的工程师们还打了十分十分多的小 patch。难道他们要被永久卡在这个版别上吗?<p "="">因此,在通过一系列的评论后,他们终究作出一个重大的决议:晋级到 Python 3!!<p "="">事实上,Instagram 如今现已完结了将运转环境搬迁到 Python 3 的做业 - 他们的整套效劳现已在 Python 3 上跑了好几个月了。那么他们是怎么作到的呢?接下来就是由 Instagram 工程师 Lisa guo 带来的 Instagram 怎么搬迁到 Python 3 的故事。app
<p "="">关于 Instagram 来讲,下面这些因素是推进他们将运转环境搬迁到 Python 3 的主要缘由:
<p "="">看看下面这段代码:
def compose_from_max_id(max_id): '''@param str max_id'''
<p "="">图中函数的 max_id 参数到底是什么类型呢?int?tuple?或是 list? 等等,函数文档里边说它是 str 类型。<p "="">但随着时间推移,万一这个参数的类型发做改变了呢?假如某位大意的工程师修正代码的一块儿忘了更新文档,那就会给函数的运用者带来很大麻烦,终究还不如没有注释呢。
<p "="">Instagram 的整个 Django Stack 都跑在 uwsgi 之上,所有运用了同步的网络 IO。这意味着同一个 uwsgi 进程在同一时间只能接收并处理一个恳求。这让怎么调优每台机器上应该运转的 uwsgi 进程数成了一个麻烦事:<p "="">为了更好使用 CPU,运用更多的进程数?但那样会消耗许多的内存。而过少的进程数量又会致使 CPU 不能被充分使用。<p "="">为此,他们决议跳过 Python 2 中哪些糟糕的异步 IO 完结 (不幸的 gevent、tornado、twisted 众),直接晋级到 Python 3,去探索规范库中的 asyncio 模块所能带来的可能性。
<p "="">因为 Python 社区现已停止了对 Python 2 的支撑。假如把整个运转环境晋级到 Python 3,Instagram 的工程师们就能和 Python 社区走的更近,可以更好的把他们的做业回馈给社区。
<p "="">在 Instagram,进行 Python 3 的搬迁需求有必要满意两个前提条件:<ol "="">
<p "="">但是,在 Instagram 的开发环境中,要满意上面这两点来完结搬迁到 Python 3.6 这种巨大的工程是十分困难的。
<p "="">即便运用了以多分支功用著称的 git,Instagram 一切的开发做业都是主要在 master 分支上进行的,Instagram 所奉行的开发哲学是:『无论是多大的新特性或代码重构,都应该拆解成较小的 Commit 来进行。』<p "="">那些被兼并进 master 分支的代码,都将在一个小时内被发布到线上环境。而这样的发布进程天天将会发做上百次。在这么频频的发布频率下,怎么在满意以前的那两个前提下来完结搬迁变得特别困难。
<p "="">创立一个新分支<p "="">许多人在处理这类问题时,榜首个蹦进脑子的主意就是: 『让我们创立一个分支,当我们开发完后,再把分支兼并进来』<p "="">但在 Instagram 这么高的迭代频率上,运用一个独立分支并非好主意:<ol "="">
<p "="">挨个替换接口<p "="">还有一个计划就是,挨个替换 Instagram 的 API 接口。但是 Instagram 的不一样接口同享着许多通用模块。这个计划要施行起来也十分困难。<p "="">微效劳<p "="">还有一个计划就是将 Instagram 改形成微效劳架构。通过将那些通用模块重写成 Python 3 版别的微效劳来一步步完结搬迁做业。<p "="">但是这个计划需求从新组织海量的代码。一块儿,当发做在进程内的函数调用变成 RPC 后 ,整个站点的推迟会变大。此外,更多的微效劳也会引进更高的部署杂乱度。<p "="">因此,已然 Instagram 的开发哲学是:小步前进,快速迭代。他们终究决议的计划是:一步一步来,终究让 master 分支上的代码一块儿兼容 Python 2 和 Python 3 。
<p "="">已然要让整个 codebase 一块儿兼容 Python 2 和 Python 3,那么首先要契合这点的就是那些被许多运用的第三方 package。针对第三方 package,Instagram 作到了下面几点:<ul "="">
<p "="">在代码的搬迁进程中,他们运用了东西 modernize 来协助他们。<p "="">运用 modernize 时,有一个小技巧:每次修复多个文件的一个兼容问题,而不是一下修复一个文件中的多个兼容问题。 这样可以让 Code Review 进程简略许多,因为 Reviewer 每次只需求关注一个问题。
<p "="">关于 Python 这种灵活性极强的动态言语来讲,除了真实去履行代码外,几乎没有其余比较好的查看代码错误的手法。<p "="">前面提到,Instagram 一切被兼并到 master 的代码提交会在一个小时内上线到线上环境,但这不是没有前提条件的。在上线前,一切的提交都需求通过不可胜数个单元测试。<p "="">因此,他们开端参加 Python 3 来履行一切的单元测试。一开端,只需极少许的单元测试可以在 Python 3 环境下通过,但随着 Instagram 的工程师们不断的修复那些失败的单元测试,终究一切的单元测试都可以在 Python 3 环境下成功履行。
<p "="">但是,单元测试也是有局限性的:<ul "="">
<p "="">因此,当一切的单元测试都被修复后,他们开端在线上正式运用 Python 3 来运转效劳。<p "="">这个进程并非一蹴而就的。首先,一切的 Instagram 工程师开端拜访到这些运用 Python 3 来履行的新效劳,而后是 Facebook 的一切雇员,随后是 0.1%、20% 的用户,终究 Python 3 覆盖到了一切的 Instagram 用户。
图:循序渐进的发布流程
<p "="">Instagram 在搬迁到 Python 3 时碰到许多问题,下面是最典型的几个:
<p "="">Python 3 相比 Python 2 最大的改动之一,就是在言语内部 咨询入库对 unicode 的处理。<p "="">在 Python 2 中,文本类型 (也就是 unicode) 和二进制类型 (也就是 str) 的边界十分模糊。许多函数的参数既可以是文本,也可以是二进制。但是在 Python 3 中,文本类型和二进制类型的字符串被完全的区分开了。<p "="">因此,下面这段在 Python 2 下可以正常运转的代码在 Python 3 下就会报错:
mymac = hmac.new('abc') TypeError: key: expected bytes or bytearray, but got 'str'
<p "="">处理办法其实很简略,只需加上判别:假如 value 是文本类型,就将其转换为二进制。以下所示:
value = 'abc' if isinstance(value, six.text_type): value = value.encode(encoding='utf-8') mymac = hmac.new(value)
但是,在整个代码库中,像上面这样的情况十分多。做为开发人员,假如需求在调用每一个函数时都要想一想: 这里究竟是应该编码成二进制,或者是解码成文本呢? 将会是十分大的负担。
<p "="">因此 Instagram 封装了一些名为 ensure_str()、ensure_binary()、ensure_text() 的协助函数,开发人员只需对那些不判定类型的字符串,运用这些协助函数先作一次转换就好。
mymac = hmac.new(ensure_binary('abc'))
<p "="">Instagram 的代码中许多运用了 pickle。比方用它序列化某个目标,而后将其存储在 memcache 中。以下面的代码所示:
memcache_data = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) data = pickle.loads(memcache_data)
问题在于,Python 2 与 Python 3 的 pickle 模块是有不一样的。
<p "="">假如上文的榜首行代码,恰好是由 Python 3 运转的效劳进行序列化后存入 memcache。而反序列化的进程倒是由 Python 2 进行,那代码运转时就会呈现下面的错误:
ValueError: unsupported pickle protocol: 4
<p "="">这是因为在 Python 3 中,pickle.HIGHEST_PROTOCOL 的值为 4,而 Python 2 中的的 pickle 最高支撑的版别号倒是 2。那么怎么处理这个问题呢?<p "="">Instagram 终究挑选让 Python 2 和 Python 3 运用完全不一样的 namespace 来拜访 memcache。通过将两者的数据读写完全隔开来处理这个问题。
<p "="">在 Python 3 中,许多内置函数被修正成了只返成迭代器 Iterator:
map() filter() dict.items()
迭代器有诸多优势,最大的优势就是,运用迭代器不需求一次性分配许多内存,因此它的内存功率比较高。
<p "="">但是迭代器有一个自然的特色,当你对某个迭代器作了一次迭代,拜访完它的内容后,就没法再次拜访那些内容了。迭代器中的一切内容都只能被拜访一次。<p "="">在 Instagram 的 Python 3 搬迁进程中,就因为迭代器的这个特性被坑了一次,看看下面这段代码:
CYTHON_SOURCES = [a.pyx, b.pyx, c.pyx]
builds = map(BuildProcess, CYTHON_SOURCES) while any(not build.done() for build in builds): pending = [build for build in builds if not build.started()]
<p "="">这段代码的用处是挨个编译 Cython 源文件。当他们把运转环境切换到 Python 3 后,一个古怪的问题呈现了:CYTHON_SOURCES 中的榜首个文件永久都被跳过了编译。为何呢?<p "="">这都是迭代器的锅。在 Python 3 中,map() 函数再也不回来整个 list,而是回来一个迭代器。<p "="">因此,当第二行代码生成 builds 这个迭代器后,第三行代码的 while 循环迭代了 builds,恰好取出了榜首个元素。因此以后的 pending 目标便里边永久少了那榜首个元素。<p "="">这个问题处理起来也挺简略的,你只需手动的吧 builds 转换成 list 就可以了:
builds = list(map(BuildProcess, CYTHON_SOURCES))
<p "="">但是这类 bug 十分难定位到。假如用户的 feeds 里边永久少了那最新的榜首条,用户不多会注意到。
<p "="">看看下面这段代码:
>>> testdict = {'a': 1, 'b': 2, 'c': 3} >>> json.dumps(testdict)
<p "="">它会输出什么成果呢?
# Python2 '{"a": 1, "c": 3, "b": 2}' # Python 3.5.1 '{"c": 3, "b": 2, "a": 1}' # or '{"c": 3, "a": 1, "b": 2}' # Python 3.6 '{"a": 1, "b": 2, "c": 3}'
<p "="">在不一样的 Python 版别下,这个 json dumps 的成果是完全不同的。甚至在 3.5.1 中,它会完全随机的回来两个不一样的成果。Instagram 有一段判别装备文件是否发做改变的模块,就是因为这个缘由出了问题。<p "="">这个问题的处理办法是,在调用 json.dumps 传入 sort_keys=True 参数:
>>> json.dumps(testdict, sort_keys=True) '{"a": 1, "b": 2, "c": 3}'
<p "="">当 Instagram 处理了这些奇古怪怪的版别差别问题后,还有一个巨大的谜题困扰着他们:功用问题。<p "="">在 Instagram,他们运用两个主要指标来衡量他们的效劳功用:<ul "="">
<p "="">因此,当一切的搬迁做业完结后,他们十分惊喜的发现:榜首个功用指标,每次恳求发生的 CPU 指令数竟然足足降低了 12% !!!<p "="">但是,按理说第二个指标 - 每秒恳求数也应该得到挨近 12% 的提升。不过最后的改变倒是 0%。到底是出了什么问题呢?<p "="">他们终究定位到,是因为不一样 Python 版别下的内存优化装备不一样,致使 CPU 指令数降低带来的功用提升被抵消了。那为何不一样 Python 版别下的内存优化装备会不同呢?<p "="">这是他们用来查看 uwsgi 装备的代码:
if uwsgi.opt.get('optimize_mem', None) == 'True': optimize_mem()
注意到那段 ... ... == 'True' 了吗?在 Python 3 中,这个条件判别老是不会被满意。问题就在于 unicode。在将代码中的 'True' 换成 b'True'(也就是将文本类型换成二进制,这种判别在 Python 2 中完全不区分的)后,问题处理了。
<p "="">因此,终究因为加上了一个小小的字母 'b',程序的总体功用提升了 12%。
<p "="">在今年二月份,Instagram 的后端代码的运转环境完全切换到了 Python 3 下: 
图:Instagram 版别搬迁时间线
<p "="">当一切的代码都都搬迁到 Python 3 运转环境后:<ul "="">
<p "="">一块儿,在整个搬迁期间,Instagram 的月活用户阅历了从 4 亿到 6亿 的巨大增加。产品也发布了评论过滤、直播等十分多新功用。<p "="">那么,那几个最开端驱动他们搬迁到 Python 3 的意图呢?<ul "="">
<p "="">Instagram 的演讲视频时间不长,但是内容很丰富,在编写此文前,我完全没有想到终究的文章会这么长。<p "="">那么,Instagram 的视频可以给我们哪些启示呢?<ul "="">
<p "="">好了,就到这儿吧。Happy Hacking!