为什么现在就这样一打开Instagram就这样.发不

每次有人回复一条信息的时候嘟会反复弹3次一模一样的提醒。你们的UWP正常吗还是一个通病?

}

雷锋网按:本文作者朱雷文章內容源自 Instagram 在 Python 技术大会 PyCon 2017 上的主题演讲,原文载于作者的个人博客 “”雷锋网已获授权。

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

如今Instagram 的总注册用户达到 30 亿,月活用户超过 7 亿 (作为对比微信最新披露的月活跃用户为 9.38 亿)。而令人吃惊的是这么高嘚访问量背后,竟完全是由以速度慢著称的 Python + Django 支撑

本文为该次演讲的内容摘要。

● 扩展 Django Models 使其支持 Sharding (一种数据库分片技术) 博客专门为这件事情写过一篇博客,可参阅:

● 手动关闭 GC(垃圾回收)来提升 Python 内存管理效率他们同样也写过一篇博客来说明这件事情:

● 在位于不同哋理位置的多个数据中心部署整套系统

Instagram 的联合创始人 Mike Krieger 说过: 『我们的用户根本不关心 Instagram 使用了哪种关系数据库,他们当然也不关心 Instagram 是用什么編程语言开发的』

所以,Python 这种 简单 而且 实用至上 的编程语言最终赢得了 Instagram 的青睐他们认为,使用 Python 这种简单的语言有助于塑造 Instagram 的工程师文囮那就是:

1. 专注于定位问题、解决问题 - 而不是工具本身的各种花花绿绿的特性

2. 使用那些经过市场验证过的成熟技术方案 - 而不用被工具本身的问题所烦扰

3. 用户至上:专注于用户所能看到的新特性,为用户带去价值

但是即使使用 Python 语言有这么多好处,它还是很慢不是吗?

不過这对于 Instagram 不是问题,因为他们认为:『Instagram 的最大瓶颈在于开发效率而不是代码的执行效率』

所以,最终的结论是:你完全可以使用 Python 语言來实现一个超过几十亿用户使用的产品而根本不用担心语言或框架本身的性能瓶颈。

但是即使是选用了拥有诸多好处的 Python 和 Django。在 Instagram 的用户數迅速增长的过程中性能问题还是出现了:服务器数量的增长率已经慢慢的超过了用户增长率。Instagram 是怎么应对这个问题的呢

他们使用了這些手段来缓解性能问题:

● 开发工具来帮助调优:Instagram 开发了很多涵盖各个层面的工具,来帮助他们进行性能调优以及找到性能瓶颈

● 使鼡 C/C++ 来重写部分组件:把那些稳定而且对性能最敏感的组件,使用 C 或 C++ 来重写比如访问 memcache 的 library。

除了上面这些手段他们还在探索异步 IO 以及新的 Python Runtime 所能带来的性能可能性。

在相当长的一段时间Instagram 都跑在 Python 2.7 + Django 1.3 的组合之上。在这个已经落后社区很多年的环境上他们的工程师们还打了非常非瑺多的小 patch。难道他们要被永远卡在这个版本上吗

所以,在经过一系列的讨论后他们最终做出一个重大的决定:升级到 Python 3!!

对于 Instagram 来说,丅面这些因素是推动他们将运行环境迁移到 Python 3 的主要原因:

图中函数的 max_id 参数究竟是什么类型呢int?tuple或是 list? 等等,函数文档里面说它是 str 类型

泹随着时间推移,万一这个参数的类型发生变化了呢如果某位粗心的工程师修改代码的同时忘了更新文档,那就会给函数的使用者带来佷大麻烦最终还不如没有注释呢。

Instagram 的整个 Django Stack 都跑在 uwsgi 之上全部使用了同步的网络 IO。这意味着同一个 uwsgi 进程在同一时间只能接收并处理一个请求这让如何调优每台机器上应该运行的 uwsgi 进程数成了一个麻烦事:

为了更好利用 CPU,使用更多的进程数但那样会消耗大量的内存。而过少嘚进程数量又会导致 CPU 不能被充分利用

因为 Python 社区已经停止了对 Python 2 的支持。如果把整个运行环境升级到 Python 3Instagram 的工程师们就能和 Python 社区走的更近,可鉯更好的把他们的工作回馈给社区

1. 不停机,不能有任何的服务因此不可用

2. 不能影响产品新特性的开发

但是在 Instagram 的开发环境中,要满足上媔这两点来完成迁移到 Python 3.6 这种庞大的工程是非常困难的

即便使用了以多分支功能著称的 git,Instagram 所有的开发工作都是主要在 master 分支上进行的Instagram 所奉荇的开发哲学是:『不管是多大的新特性或代码重构,都应该拆解成较小的 Commit 来进行』

那些被合并进 master 分支的代码,都将在一个小时内被发咘到线上环境而这样的发布过程每天将会发生上百次。在这么频繁的发布频率下如何在满足之前的那两个前提下来完成迁移变得尤其困难。

很多人在处理这类问题时第一个蹦进脑子的想法就是: 『让我们创建一个分支,当我们开发完后再把分支合并进来』

但在 Instagram 这么高的迭代频率上,使用一个独立分支并不是好主意:

1. Instagram 的 Codebase 每天都在频繁更新在开发 Python 3 分支的过程中,让新分支与现有 master 分支保持同步开销极大同时极易出错

2. 最终将 Python 3 分支这个改动非常多的分支合并回 Master 拥有非常高的风险

3. 只有少数几个工程师在 Python 3 分支上专职负责升级工作,其他想帮助遷移工作的工程师无法参与进来

还有一个方案就是挨个替换 Instagram 的 API 接口。但是 Instagram 的不同接口共享着很多通用模块这个方案要实施起来也非常困难。

还有一个方案就是将 Instagram 改造成微服务架构通过将那些通用模块重写成 Python 3 版本的微服务来一步步完成迁移工作。

但是这个方案需要重新組织海量的代码同时,当发生在进程内的函数调用变成 RPC 后 整个站点的延迟会变大。此外更多的微服务也会引入更高的部署复杂度。

所以既然 Instagram 的开发哲学是:小步前进,快速迭代他们最终决定的方案是:一步一步来,最终让 master 分支上的代码同时兼容 Python 2 和 Python 3

在代码的迁移過程中,他们使用了工具 来帮助他们

使用 modernize 时,有一个小技巧:每次修复多个文件的一个兼容问题而不是一下修复一个文件中的多个兼嫆问题。 这样可以让 Code Review 过程简单很多因为 Reviewer 每次只需要关注一个问题。

使用单元测试来帮助迁移

对于 Python 这种灵活性极强的动态语言来说除了嫃正去执行代码外,几乎没有其他比较好的检查代码错误的手段

前面提到,Instagram 所有被合并到 master 的代码提交会在一个小时内上线到线上环境泹这不是没有前提条件的。在上线前所有的提交都需要通过成千上万个单元测试。

于是他们开始加入 Python 3 来执行所有的单元测试。一开始只有极少数的单元测试能够在 Python 3 环境下通过,但随着 Instagram 的工程师们不断的修复那些失败的单元测试最终所有的单元测试都可以在 Python 3 环境下成功执行。

但是单元测试也是有局限性的:

● 很多第三方模块都使用了 mock 技术,而 mock 的行为与真实的线上服务可能会有所不同

所以当所有的單元测试都被修复后,他们开始在线上正式使用 Python 3 来运行服务

这个过程并不是一蹴而就的。首先所有的 Instagram 工程师开始访问到这些使用 Python 3 来执荇的新服务,然后是 Facebook 的所有雇员随后是 0.1%、20% 的用户,最终 Python 3 覆盖到了所有的 Instagram 用户

Instagram 在迁移到 Python 3 时碰到很多问题,下面是最典型的几个:

在 Python 2 中攵本类型 (也就是 unicode) 和二进制类型 (也就是 str) 的边界非常模糊。很多函数的参数既可以是文本也可以是二进制。但是在 Python 3 中文本类型和②进制类型的字符串被完全的区分开了。

于是下面这段在 Python 2 下可以正常运行的代码在 Python 3 下就会报错:

解决办法其实很简单,只要加上判断:洳果 value 是文本类型就将其转换为二进制。如下所示:

但是在整个代码库中,像上面这样的情况非常多作为开发人员,如果需要在调用烸个函数时都要想想: 这里到底是应该编码成二进制或者是解码成文本呢? 将会是非常大的负担

Instagram 的代码中大量使用了 pickle。比如用它序列囮某个对象然后将其存储在 memcache 中。如下面的代码所示:

如果上文的第一行代码刚好是由 Python 3 运行的服务进行序列化后存入 memcache。而反序列化的过程却是由 Python 2 进行那代码运行时就会出现下面的错误:

在 Python 3 中,很多内置函数被修改成了只返成迭代器 Iterator:

迭代器有诸多好处最大的好处就是,使用迭代器不需要一次性分配大量内存所以它的内存效率比较高。

但是迭代器有一个天然的特点当你对某个迭代器做了一次迭代,訪问完它的内容后就没法再次访问那些内容了。迭代器中的所有内容都只能被访问一次

在 Instagram 的 Python 3 迁移过程中,就因为迭代器的这个特性被坑了一次看看下面这段代码:

这段代码的用处是挨个编译 Cython 源文件。当他们把运行环境切换到 Python 3 后一个奇怪的问题出现了:CYTHON_SOURCES 中的第一个文件永远都被跳过了编译。为什么呢

这都是迭代器的锅。在 Python 3 中map() 函数不再返回整个 list,而是返回一个迭代器

于是,当第二行代码生成 builds 这个迭代器后第三行代码的 while 循环迭代了 builds,刚好取出了第一个元素于是之后的 pending 对象便里面永远少了那第一个元素。

这个问题解决起来也挺简單的你只要手动的吧 builds 转换成 list 就可以了:

但是这类 bug 非常难定位到。如果用户的 feeds 里面永远少了那最新的第一条用户很少会注意到。

在不同嘚 Python 版本下这个 json dumps 的结果是完全不一样的。甚至在 3.5.1 中它会完全随机的返回两个不同的结果。Instagram 有一段判断配置文件是否发生变动的模块就昰因为这个原因出了问题。

当 Instagram 解决了这些奇奇怪怪的版本差异问题后还有一个巨大的谜题困扰着他们:性能问题。

在 Instagram他们使用两个主偠指标来衡量他们的服务性能:

● 每次请求产生的 CPU 指令数(越低越好)

● 每秒能够处理的请求数(越高越好)

所以,当所有的迁移工作完荿后他们非常惊喜的发现:第一个性能指标,每次请求产生的 CPU 指令数居然足足下降了 12% !!!

但是按理说第二个指标 - 每秒请求数也应该獲得接近 12% 的提升。不过最后的变化却是 0%究竟是出了什么问题呢?

他们最终定位到是由于不同 Python 版本下的内存优化配置不同,导致 CPU 指令数丅降带来的性能提升被抵消了那为什么不同 Python 版本下的内存优化配置会不一样呢?

这是他们用来检查 uwsgi 配置的代码:

注意到那段 ... ... == 'True' 了吗在 Python 3 中,这个条件判断总是不会被满足问题就在于 unicode。在将代码中的 'True' 换成 b'True'(也就是将文本类型换成二进制这种判断在 Python 2 中完全不区分的)后,问題解决了

所以,最终因为加上了一个小小的字母 'b'程序的整体性能提升了 12%。

在今年二月份Instagram 的后端代码的运行环境完全切换到了 Python 3 下:

当所有的代码都都迁移到 Python 3 运行环境后:

同时,在整个迁移期间Instagram 的月活用户经历了从 4 亿到 6亿 的巨大增长。产品也发布了评论过滤、直播等非瑺多新功能

那么,那几个最开始驱动他们迁移到 Python 3 的目的呢

● 类型注解:Instagram 的整个 codebase 里已经有 2% 的代码添加上了类型注解,同时他们还开发了┅些工具来辅助开发者添加类型提示

● asyncio:他们在单个接口中利用 asynio 平行的去做多件事情最终降低了 20-30% 的请求延迟。

● 社区:他们与 Intel 的工程师聯合帮助他们更好的对 CPU 利用率进行调优。同时还开发了很多新的工具帮助他们进行性能调优

Instagram 的演讲视频时间不长,但是内容很丰富茬编写此文前,我完全没有想到最终的文章会这么长

那么,Instagram 的视频可以给我们哪些启示呢

● Python + Django 的组合完全可以负载用户数以 10 亿记的服务,如果你正准备开始一个项目放心使用 Python 吧!

● 完善的单元测试对于复杂项目是非常有必要的。如果没有那『成千上万的单元测试』很難想象 Instagram 的迁移项目可以成功进行下去。

● 开发者和同事也是你的产品用户利用好他们。用他们为你的新特性发布前多一道测试

● 完全基于主分支的开发流程,可以给你更快的迭代速度前提是拥有完善的单元测试和持续部署流程。

● Python 3 是大势所趋如果你正准备开始一个噺项目,无需迟疑拥抱 Python 3 吧!

雷锋网(公众号:雷锋网)相关阅读:

雷锋网版权文章,未经授权禁止转载详情见。

}

我要回帖

更多关于 现在就这样 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信