PyCon大会Python主题演讲摘要

PyCon 是全国际最大的以 Python 编程言语 为主题的技能大会。大会由 Python 社区组织,每年举行一次。在大会上,来自国际各地的 Python 用户与中心开发者齐聚一堂,共同同享 Python 国际的新鲜事、Python 言语的应用案例、运用技巧等等内容。

Instagram 简介

<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 "="">本文为该次演讲的内容摘要。

Python @Instagram

为什么挑选 Python 和 Django

<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 "="">

Python 言语的优势地点

<p "="">Instagram 的联合创始人 Mike Krieger 说过: 『咱们的用户根本不关心 Instagram 运用了哪种联系数据库,他们当然也不关心 Instagram 是用什么编程言语开发的。』<p "="">所以,Python 这种 简略 并且 实用至上 的编程言语终究赢得了 Instagram 的喜爱。他们以为,运用 Python 这种简略的言语有助于刻画 Instagram 的工程师文明,那就是:<ol "="">

  • 专心于定位问题、处理问题 - 而不是东西自身的各种花花绿绿的特性
  • 运用那些经过商场验证过的成熟技能计划 - 而不用被东西自身的问题所干扰
  • 用户至上:专心于用户所能看到的新特性,为用户带去价值

<p "="">可是,即使运用 Python 言语有这么多优点,它仍是很慢,不是吗?<p "="">不过,这关于 Instagram 不是问题,由于他们以为:『Instagram 的最大瓶颈在于开发功率,而不是代码的履行功率』<blockquote "="">

At Instagram, our bottleneck is development velocity, not pure code execution.

<p "="">所以,终究的结论是:你彻底能够运用 Python 言语来完结一个超越几十亿用户运用的产品,而根本不用忧虑言语或框架自身的功用瓶颈。

怎么提高运转功率

<p "="">可是,即就是选用了具有诸多优点的 Python 和 Django。在 Instagram 的用户数迅速增长的进程中,功用问题仍是呈现了:效劳器数量的增长率现已慢慢的超越了用户增长率。Instagram 是怎么应对这个问题的呢?<p "="">他们运用了这些手法来缓解功用问题:<ul "="">

  • 开发东西来协助调优:Instagram 开发了许多包括各个层面的东西,来协助他们进行功用调优以及找到功用瓶颈。
  • 运用 C/C++ 来重写部分组件:把那些稳定并且对功用最敏感的组件,运用 C 或 C++ 来重写,比方拜访 memcache 的 library。
  • 运用 Cython:Cython 也是他们用来提高 Python 功率的法宝之一。

<p "="">除了上面这些手法,他们还在探索异步 IO 以及新的 Python Runtime 所能带来的功用可能性。

晋级到 Python 3

<p "="">在相当长的一段时间,Instagram 都跑在 Python 2.7 + Django 1.3 的组合之上。在这个现已落后社区许多年的环境上,他们的工程师们还打了十分十分多的小 patch。难道他们要被永久卡在这个版别上吗?<p "="">所以,在经过一系列的评论后,他们终究做出一个重大的决议:晋级到 Python 3!!<p "="">事实上,Instagram 现在现已完结了将运转环境搬迁到 Python 3 的作业 - 他们的整套效劳现已在 Python 3 上跑了好几个月了。那么他们是怎么做到的呢?接下来就是由 Instagram 工程师 Lisa guo 带来的 Instagram 怎么搬迁到 Python 3 的故事。

Instagram 晋级到 Python 3 的故事

为什么要晋级到 Python 3

<p "="">关于 Instagram 来说,下面这些因素是推动他们将运转环境搬迁到 Python 3 的主要原因:

1. 新特性:类型注解 Type Annotations

<p "="">看看下面这段代码:

def compose_from_max_id(max_id): '''@param str max_id''' 

<p "="">图中函数的 max_id 参数究竟是什么类型呢?int?tuple?或是 list? 等等,函数文档里边说它是 str 类型。<p "="">但随着时间推移,万一这个参数的类型发作改变了呢?假如某位大意的工程师修正代码的一起忘了更新文档,那就会给函数的运用者带来很大麻烦,终究还不如没有注释呢。

2. 功用

<p "="">Instagram 的整个 Django Stack 都跑在 uwsgi 之上,全部运用了同步的网络 IO。这意味着同一个 uwsgi 进程在同一时间只能接收并处理一个恳求。这让怎么调优每台机器上应该运转的 uwsgi 进程数成了一个麻烦事:<p "="">为了更好使用 CPU,运用更多的进程数?但那样会消耗许多的内存。而过少的进程数量又会导致 CPU 不能被充分使用。<p "="">为此,他们决议跳过 Python 2 中哪些糟糕的异步 IO 完结 (不幸的 gevent、tornado、twisted 众),直接晋级到 Python 3,去探索规范库中的 asyncio 模块所能带来的可能性。

3. 社区

<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 "="">

  • Instagram 的 Codebase 每天都在频频更新,在开发 Python 3 分支的进程中,让新分支与现有 master 分支坚持同步开支极大,一起极易犯错
  • 终究将 Python 3 分支这个改动十分多的分支兼并回 Master 具有十分高的风险
  • 只需少量几个工程师在 Python 3 分支上专职担任晋级作业,其他想协助搬迁作业的工程师无法参与进来

<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 "="">

  • 拒绝引进一切不兼容 Python 3 的新 package
  • 去掉一切不再运用的 package
  • 替换那些不兼容 Python 3 的 package

<p "="">在代码的搬迁进程中,他们运用了东西 modernize 来协助他们。<p "="">运用 modernize 时,有一个小技巧:每次修复多个文件的一个兼容问题,而不是一下修复一个文件中的多个兼容问题。 这样能够让 Code Review 进程简略许多,由于 Reviewer 每次只需求关注一个问题。

运用单元测试来协助搬迁

<p "="">关于 Python 这种灵活性极强的动态言语来说,除了真实去履行代码外,几乎没有其他比较好的查看代码错误的手法。<p "="">前面提到,Instagram 一切被兼并到 master 的代码提交会在一个小时内上线到线上环境,但这不是没有前提条件的。在上线前,一切的提交都需求经过不计其数个单元测试。<p "="">所以,他们开端参加 Python 3 来履行一切的单元测试。一开端,只需极少量的单元测试能够在 Python 3 环境下经过,但随着 Instagram 的工程师们不断的修复那些失败的单元测试,终究一切的单元测试都能够在 Python 3 环境下成功履行。

单元测试的局限性

<p "="">可是,单元测试也是有局限性的:<ul "="">

  • Instagram 的单元测试没有做到 100% 的代码覆盖率
  • 许多第三方模块都运用了 mock 技能,而 mock 的行为与真实的线上效劳可能会有所不同

<p "="">所以,当一切的单元测试都被修复后,他们开端在线上正式运用 Python 3 来运转效劳。<p "="">这个进程并不是一蹴而就的。首先,一切的 Instagram 工程师开端拜访到这些运用 Python 3 来履行的新效劳,然后是 Facebook 的一切雇员,随后是 0.1%、20% 的用户,终究 Python 3 覆盖到了一切的 Instagram 用户。

图:按部就班的发布流程

搬迁进程的技能问题

<p "="">Instagram 在搬迁到 Python 3 时碰到许多问题,下面是最典型的几个:

Unicode 相关的字符串问题

<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'))

不同 Python 版别的 pickle 差异

<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}' 

搬迁到 Python 3.6 后的功用提高

<p "="">当 Instagram 处理了这些奇古怪怪的版别差异问题后,还有一个巨大的谜题困扰着他们:功用问题。<p "="">在 Instagram,他们运用两个主要指标来衡量他们的效劳功用:<ul "="">

  • 每次恳求发生的 CPU 指令数(越低越好)
  • 每秒能够处理的恳求数(越高越好)

<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 "="">

  • 节省了 12% 的整体 CPU 运用率(Django/uwsgi)
  • 节省了 30% 的内存运用(celery)

<p "="">一起,在整个搬迁期间,Instagram 的月活用户阅历了从 4 亿到 6亿 的巨大增长。产品也发布了评论过滤、直播等十分多新功用。<p "="">那么,那几个最开端驱动他们搬迁到 Python 3 的意图呢?<ul "="">

  • 类型注解:Instagram 的整个 codebase 里现已有 2% 的代码增加上了类型注解,一起他们还开发了一些东西来辅佐开发者增加类型提示
  • asyncio:他们在单个接口中使用 asynio 平行的去做多件作业,终究降低了 20-30% 的恳求推迟。
  • 社区:他们与 Intel 的工程师联合,协助他们更好的对 CPU 使用率进行调优。一起还开发了许多新的东西,协助他们进行功用调优

Instagram 带给咱们的启示

<p "="">Instagram 的演讲视频时间不长,可是内容很丰富,在编写此文前,我彻底没有想到终究的文章会这么长。<p "="">那么,Instagram 的视频能够给咱们哪些启示呢?<ul "="">

  • Python + Django 的组合彻底能够负载用户数以 10 亿记的效劳,假如你正准备开端一个项目,放心运用 Python 吧!
  • 完善的单元测试关于杂乱项目是十分有必要的。假如没有那『不计其数的单元测试』。很难幻想 Instagram 的搬迁项目能够成功进行下去。
  • 开发者和同事也是你的产品用户,使用好他们。用他们为你的新特性发布前多一道测试。
  • 彻底根据主分支的开发流程,能够给你更快的迭代速度。前提是具有完善的单元测试和继续部署流程。
  • Python 3 是大势所趋,假如你正准备开端一个新项目,无需踌躇,拥抱 Python 3 吧!

<p "="">好了,就到这儿吧。Happy Hacking!

猜你喜欢

转载自www.cnblogs.com/blogst/p/10319140.html