禁用 Python GC,Instagram 性能提升10%
通过关闭 Python 垃圾收集(GC)机制,该机制通过收集和释放未使用的数据来回收内存,Instagram 的运行效率提高了 10 %。是的,你没听错!通过禁用 GC,我们可以减少内存占用并提高 CPU 中 LLC 缓存的命中率。如果你对为什么会这样感兴趣,带你发车咯!
我们如何运行 Web 服务器的?
Instagram 的 Web 服务器在多进程模式下运行 Django,使用主进程创建数十个工作(worker)进程,而这些工作进程会接收传入的用户请求。对于应用程序服务器来说,我们使用带分叉模式的 uWSGI 来平衡主进程和工作进程之间的内存共享。
为了防止 Django 服务器运行到 OOM,uWSGI 主进程提供了一种机制,当其 RSS 内存超过预定的限制时重新启动工作进程。
了解内存
我们开始研究为什么 RSS 内存在由主进程产生后会迅速增长。一个观察结果是,RSS 内存即使是从 250 MB 开始的,其共享内存也会下降地非常快,在几秒钟内从 250 MB 到大约 140 MB(共享内存大小可以从/ proc / PID / smaps读取)。这里的数字是无趣的,因为它们随时都会变化,但共享内存下降的规模是非常有趣的 – 大约是总内存 1/3 的。接下来,我们想要了解为什么共享内存,在工作器开始产生时是怎样变为每个进程的私有内存的。
我们的猜测:读取时复制
Linux内核具有一种称为写入时复制(Copy-on-Write,CoW)的机制,用作 fork 进程的优化。一个子进程开始于与其父进程共享每个内存页。而仅当该页面被写入时,该页面才会被复制到子进程内存空间中(有关详细信息,请参阅 wiki https://en.wikipedia.org/wiki/Copy-on-write)。
但在Python领域里,由于引用计数的缘故,事情变得有趣。每次我们读取一个Python对象时,解释器将增加其引用计数,这本质上是对其底层数据结构的写入。这导致 CoW 的发生。因此,我们在使用 Python 时,正在做的即是读取时复制(CoR)!
1
2
3
4
5
6
7
8
|
#define PyObject_HEAD
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
...
typedef struct _object {
PyObject_HEAD
} PyObject;
|
所以问题是:我们在写入时复制的是不可变对象如代码对象吗?假定 PyCodeObject 确实是 PyObject 的“子类”,显然也是这样的。我们的第一想法是禁用 PyCodeObject 的引用计数。
第1次尝试:禁用代码对象的引用计数
在 Instagram 上,我们先做一件简单的事情。考虑到这是一个实验,我们对 CPython 解释器做了一些小的改动,验证了引用计数对代码对象没有变化,然后在我们的一个生产服务器运行 CPython。
结果是令人失望的,因为共享内存没有变化。当我们试图找出原因是,我们意识到我们找不到任何可靠的指标来证明我们的黑客行为起作用,也不能证明共享内存和代码对象的拷贝之间的联系。显然,这里缺少一些东西。获得的教训:在行动之前先验证你的理论。
页面错误分析
在对 Copy-on-Write 这个问题谷歌搜索一番以后,我们了解到 Copy-on-Write 与系统中的页面错误是相关联的。每个 CoW 在运行过程中都可能触发页面错误。Linux 提供的 Perf 工具允许记录硬件/软件系统事件,包括页面错误,甚至可以提供堆栈跟踪!
所以我们用到了一个 prod,重新启动该服务器,等待它 fork,继而得到一个工作进程 PID,然后运行如下命令。
1
|
perf record -e page-faults -g -p <PID>
|
然后,当在堆栈跟踪的过程中发生页面错误时,我们有了一个主意。
结果与我们的预期不同。首要嫌疑人是 collect 而非是复制代码对象,它属于 gcmodule.c,并在触发垃圾回收时被调用。在理解了 GC 在 CPython 中的工作原理后,我们有了以下理论:
CPython的 GC 完全是基于阈值而触发的。这个默认阈值非常低,因此它在很早的阶段就开始了。 它维护着许多代的对象链表,并且在进行 GC 时,链表会被重新洗牌。因为链表结构与对象本身一样是存在的(就像 ob_refcount),在链表中改写这些对象会导致页面在写入时被复制,这是一个不幸的副作用。
1
2
3
4
5
6
7
8
9
|
/* GC information is stored BEFORE the object structure. */
typedef union _gc_head {
struct {
union _gc_head *gc_next;
union _gc_head *gc_prev;
Py_ssize_t gc_refs;
} gc;
long double dummy; /* force worst-case alignment */
} PyGC_Head;
|
第2次尝试:让我们试试禁用GC
那么,既然 GC 在暗中中伤我们,那我们就禁用它!
我们在我们的引导脚本添加了一个 gc.disable() 的函数调用。我们重启了服务器,但是再一次的,不走运! 如果我们再次查看 perf,我们将看到 gc.collect 仍然被调用,并且内存仍然被复制。在使用 GDB 进行一些调试时,我们发现我们使用的第三方库( msgpack )显然调用了 gc.enable() 将它恢复了,使得 gc.disable() 在引导程序中被清洗了。
给 msgpack 打补丁是我们最后要做的事情,因为它为其他做同样的事情的库打开了一扇门,在未来我们没注意的时候。首先,我们需要证明禁用 GC 实际上是有帮助。答案再次落在 gcmodule.c 上。 作为 gc.disable 的替代,我们做了 gc.set_threshold(0),这一次,没有库能将其恢复了。
就这样,我们成功地将每个工作进程的共享内存从 140MB 提高到了 225MB,并且每台机器的主机上的总内存使用量减少了 8GB。 这为整个Django 机队节省了 25% 的 RAM。有了这么大头的空间,我们能够运行更多的进程或运行具有更高的 RSS 内存阈值的进程。实际上,这将Django层的吞吐量提高了 10% 以上。
第3次尝试:完全关闭 GC 需要多次往复
在尝试了一系列设置之后,我们决定在更大的范围内尝试:一个集群。 反馈相当快,我们的连续部署终止了,因为在禁用 GC 后,重新启动我们的 Web 服务器变得很慢。通常重新启动需要不到 10 秒,但在 GC 禁用的情况下,它有时需要 60 秒以上。
1
|
2016-05-02_21:46:05.57499 WSGI app 0 (mountpoint='') ready in 115 seconds on interpreter 0x92f480 pid: 4024654 (default app)
|
复制这个 bug 是非常痛苦的,因为它不是确定发生的。经过大量的实验,一个真正的 re-pro 在顶上显示。当这种情况发生时,该主机上的可用内存下降到接近零并跳回,强制清除所有的缓存内存。之后当所有的代码/数据需要从磁盘读取的时候(DSK 100%),一切都变得很缓慢。
这敲响了一个警钟,即 Python 在解释器关闭之前会做一个最后的 GC,这将导致在很短的时间内内存使用量的巨大跳跃。再次,我想先证明它,然后弄清楚如何正确处理它。所以,我注释掉了对 Py_Finalize 在 uWSGI 的 python 插件的调用,问题也随之消失了。
但显然我们不能只是禁用 Py_Finalize。我们有一系列重要的使用 atexit 钩子的清理工具依赖着它。最后我们做的是为 CPython 添加一个运行标志,这将完全禁用 GC。
最后,我们要把它推广到更大的规模。我们在这之后尝试在整个机队中使用它,但是连续部署再次终止了。然而,这次它只是在旧型号 CPU( Sandybridge )的机器上发生,甚至更难重现了。得到的教训:经常性地在旧的客户端/模型做测试,因为它们通常是最容易出问题的。
因为我们的连续部署是一个相当快的过程,为了真正捕获发生了什么,我添加了一个单独的 atop 到我们的 rollout 命令中。我们能够抓住一个缓存内存变的很低的时刻,所有的 uWSGI 进程触发了很多 MINFLT(小页错误)。
再一次地,通过 perf 分析,我们再次看到了 Py_Finalize。 在关机时,除了最终的 GC,Python 还做了一系列的清理操作,如破坏类型对象和卸载模块。这种行为再一次地,破坏了共享内存。
第4次尝试:关闭GC的最后一步的GC:无清除
我们究竟为什么需要清理? 这个过程将会死去,我们将得到另一个替代品。 我们真正关心的是我们的 atexit 钩子,为我们的应用程序清理。至于 Python 的清理,我们不必这样做。 这是我们在自己的 bootstrapping 脚本中以这样的方式结束:
1
2
3
4
5
6
7
|
# gc.disable() doesn't work, because some random 3rd-party library will
# enable it back implicitly.
gc.set_threshold(0)
# Suicide immediately after other atexit functions finishes.
# CPython will do a bunch of cleanups in Py_Finalize which
# will again cause Copy-on-Write, including a final GC
atexit.register(os._exit, 0)
|
这是基于这个事实,即 atexi t函数以注册表的相反顺序运行。atexit 函数完成其他清除,然后在最后一步中调用 os._exit(0) 以退出当前进程。
随着这两条线的改变,我们最终让它在整个机队中得以推行。在小心地调整内存阈值后,我们赢得了 10 % 的全局容量!厦门叉车租赁
回顾
在回顾这次性能提升时,我们有两个问题:
首先,如果没有垃圾回收,是不是 Python 的内存要炸掉,因为所有的分配出去的内存永远不会被释放?(记住,在 Python 内存没有真正的堆栈,因为所有的对象都在堆中分配)。
幸运的是,这不是真的。Python 中用于释放对象的主要机制仍然是引用计数。 当一个对象被解引用(调用 Py_DECREF)时,Python 运行时总是检查它的引用计数是否降到零。在这种情况下,将调用对象的释放器。垃圾回收的主要目的是终止引用计数不起作用的那些引用周期。
1
2
3
4
5
6
7
8
|
#define Py_DECREF(op)
do {
if (_Py_DEC_REFTOTAL _Py_REF_DEBUG_COMMA
--((PyObject*)(op))->ob_refcnt != 0)
_Py_CHECK_REFCNT(op)
else
_Py_Dealloc((PyObject *)(op));
} while (0)
|
增益分析
第二个问题:增益来自哪里?
禁用 GC 的增益来源于两重原因:
- 我们为每个服务器释放了大约 8GB 的 RAM,这些 RAM 我们会用于为内存绑定的服务器生成创建更多的工作进程,或者用于为绑定 CPU 服务器们降低重新生成速率;
- 随着 CPU 指令数在每个周期( IPC)增加了约 10%,CPU吞吐量也得到改善。
1
2
3
4
5
|
# perf stat -a -e cache-misses,cache-references -- sleep 10
Performance counter stats for 'system wide':
268,195,790 cache-misses # 12.240 % of all cache refs [100.00%]
2,191,115,722 cache-references
10.019172636 seconds time elapsed
|
禁用 GC 时,有 2-3% 的缓存缺失率下降,这是 IPC 有 10 % 提升的主要原因。CPU 高速缓存未命中的代价是昂贵的,因为它会阻塞 CPU 流水线。 对 CPU 缓存命中率的小改进通常可以显着提高IPC。使用较少的 CoW,具有不同虚拟地址(在不同的工作进程中)的更加多的 CPU 高速缓存线,指向相同的物理存储器地址,使得高速缓存命中率变得更高。
正如我们所看到的,并不是每个组件都按预期工作,有时,结果会非常令人惊讶。 所以保持挖掘和嗅探,你会惊讶于万物到底是如何运作的! Wu Chenyang 是一名软件工程师,而 Ni Min 则是 Instagram 的工程经理。
禁用 Python GC,Instagram 性能提升10%的更多相关文章
- Web 应用性能提升 10 倍的 10 个建议
转载自http://blog.jobbole.com/94962/ 提升 Web 应用的性能变得越来越重要.线上经济活动的份额持续增长,当前发达世界中 5 % 的经济发生在互联网上(查看下面资源的统计 ...
- Elasticsearch Reindex性能提升10倍+实战
文章转载自: https://mp.weixin.qq.com/s?__biz=MzI2NDY1MTA3OQ==&mid=2247484134&idx=1&sn=750249a ...
- 一次 Spark SQL 性能提升10倍的经历(转载)
1. 遇到了啥问题 是酱紫的,简单来说:并发执行 spark job 的时候,并发的提速很不明显. 嗯,且听我慢慢道来,啰嗦点说,类似于我们内部有一个系统给分析师用,他们写一些 sql,在我们的 sp ...
- 存算分离下写性能提升10倍以上,EMR Spark引擎是如何做到的?
引言 随着大数据技术架构的演进,存储与计算分离的架构能更好的满足用户对降低数据存储成本,按需调度计算资源的诉求,正在成为越来越多人的选择.相较 HDFS,数据存储在对象存储上可以节约存储成本,但与此 ...
- Nacos 2.0 正式发布,性能提升 10 倍!!
3月20号,Nacos 2.0.0 正式发布了! Nacos 简介: 一个更易于构建云原生应用的动态服务发现.配置管理和服务管理平台. 通俗点讲,Nacos 就是一把微服务双刃剑:注册中心 + 配置中 ...
- 如何把 MySQL 备份验证性能提升 10 倍
JuiceFS 非常适合用来做 MySQL 物理备份,具体使用参考我们的官方文档.最近有个客户在测试时反馈,备份验证的数据准备(xtrabackup --prepare)过程非常慢.我们借助 Juic ...
- 如何把Go调用C的性能提升10倍?
目前,当Go需要和C/C++代码集成的时候,大家最先想到的肯定是CGO.毕竟是官方的解决方案,而且简单. 但是CGO是非常慢的.因为CGO其实一个桥接器,通过自动生成代码,CGO在保留了C/C++运行 ...
- 八年技术加持,性能提升10倍,阿里云HBase 2.0首发商用
摘要: 早在2010年开始,阿里巴巴集团开始研究并把HBase投入生产环境使用,从最初的淘宝历史交易记录,到蚂蚁安全风控数据存储,HBase在几代阿里专家的不懈努力下,已经表现得运行更稳定.性能更高效 ...
- Databricks缓存提升Spark性能--为什么NVMe固态硬盘能够提升10倍缓存性能(原创)
我们兴奋的宣布Databricks缓存的通用可用性,作为统一分析平台一部分的 Databricks 运行时特性,它可以将Spark工作负载的扫描速度提升10倍,并且这种改变无需任何代码修改. 1.在本 ...
随机推荐
- ABAP术语-V1 Module
V1 Module 原文;http://www.cnblogs.com/qiangsheng/archive/2008/03/21/1115707.html Function module creat ...
- WebGl 旋转(矩阵变换)
代码1: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF- ...
- Redis(四):解析配置文件redis.conf
解析配置文件redis.conf目录导航: 它在哪 Units单位 INCLUDES包含 GENERAL通用 SNAPSHOTTING快照 REPLICATION复制 SECURITY安全 LIMIT ...
- Rabbitmq(二)
1.安装 Rabbit MQ 是建立在强大的Erlang OTP平台上,因此安装RabbitMQ之前要先安装Erlang. erlang:http://www.erlang.org/download. ...
- 我的 Delphi 学习之路 —— Delphi 助手的安装
标题:我的 Delphi 学习之路 -- Delphi 助手的安装 作者:断桥烟雨旧人伤 Delphi 助手的安装 CnWizards 类似于 VS 中的番茄助手,在编写 Delphi 代码时帮助极大 ...
- colemak,你用了吗?
为了输入代码的感觉更好,我学习了colemak键盘布局,这个布局它是在QWERTY的基础上改了10多个键. 开始的三天,感觉非常不好,每按一个键都要思考很长时间,干脆在网上找了个在线打字的网页去练,感 ...
- python迭代器生成器
1.生成器和迭代器.含有yield的特殊函数为生成器.可以被for循环的称之为可以迭代的.而可以通过_next()_调用,并且可以不断返回值的称之为迭代器 2.yield简单的生成器 #迭代器简单的使 ...
- Linux使用scp命令进行文件远程拷贝详解
前言 scp是 secure copy的缩写, scp是Linux系统下基于ssh登陆进行安全的远程文件拷贝命令.Linux的scp命令可以在Linux服务器之间复制文件和目录. 使用语法: scp ...
- Pytorch之Variable求导机制
自动求导机制是pytorch中非常重要的性质,免去了手动计算导数,为构建模型节省了时间.下面介绍自动求导机制的基本用法. #自动求导机制 import torch from torch.autogra ...
- 20155310 2016-2017-2 《Java程序设计》第四周学习总结
20155310 2016-2017-2 <Java程序设计>第四周学习总结 一周两章新知识的自学与理解真的是很考验和锻炼我们,也对前面几章我们的学习进行了检测,遇到忘记和不懂的知识就再复 ...