通过关闭 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)!

#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,然后运行如下命令。

perf record -e page-faults -g -p

然后,当在堆栈跟踪的过程中发生页面错误时,我们有了一个主意。

结果与我们的预期不同。首要嫌疑人是 collect 而非是复制代码对象,它属于 gcmodule.c,并在触发垃圾回收时被调用。在理解了 GC 在 CPython 中的工作原理后,我们有了以下理论:

CPython的 GC 完全是基于阈值而触发的。这个默认阈值非常低,因此它在很早的阶段就开始了。 它维护着许多代的对象链表,并且在进行 GC 时,链表会被重新洗牌。因为链表结构与对象本身一样是存在的(就像 ob_refcount),在链表中改写这些对象会导致页面在写入时被复制,这是一个不幸的副作用。

/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 秒以上。

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 脚本中以这样的方式结束:

#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 运行时总是检查它的引用计数是否降到零。在这种情况下,将调用对象的释放器。垃圾回收的主要目的是终止引用计数不起作用的那些引用周期。

#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吞吐量也得到改善。

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内存_禁用 Python GC,Instagram 性能提升10%相关推荐

  1. python 监控内存_使用python写一个监控不同机器的使用内存情况并使用flask出图

    使用python写一个监控不同机器的使用内存情况并使用flask出图 基于是自己想要扩展前一篇博客的内容所以直接就把代码传上去了能做出的效果是可以像zabbix那样监控多台主机上的使用内存情况: 出来 ...

  2. python 读取内存_使用Python学习win32库进行内存读写

    前言: 上一周,在52的精华帖中,看到有位大佬用Python制作了鬼泣5的修改器,看完才知道,原来Python也可以对内存进行操作,出于对技术的好奇,看完以后,决定自己也尝试一下. 要用到的工具: C ...

  3. golang MySQL 占内存_使用golang插入mysql性能提升經驗

    前言 golang可以輕易制造高並發,在某些場景很合適,比如爬蟲的時候可以爬的更加高效.但是對應某些場景,如文件讀寫,數據庫訪問等IO為瓶頸的場合,就沒有什么優勢了. 前提基礎 1.golang數據庫 ...

  4. python 时间序列预测_使用Python进行动手时间序列预测

    python 时间序列预测 Time series analysis is the endeavor of extracting meaningful summary and statistical ...

  5. python 概率分布模型_使用python的概率模型进行公司估值

    python 概率分布模型 Note from Towards Data Science's editors: While we allow independent authors to publis ...

  6. python强制释放内存_强制Python释放对象以释放内存

    我运行以下代码:from myUtilities import myObject for year in range(2006,2015): front = 'D:\\newFilings\\' ba ...

  7. python 内存_一行Python解决内存问题

    原标题:一行Python解决内存问题 内存不足是项目开发过程中经常碰到的问题,我和我的团队在之前的一个项目中也遇到了这个问题,我们的项目需要存储和处理一个相当大的动态列表,测试人员经常向我抱怨内存不足 ...

  8. python分词词典_基于python的分词算法的实现(3) – 建立字典 | 学步园

    单词词典里面基本只要保存词的词性的频率,另外考虑到一次性把词典读入内存的消耗太大,必须把词典分块,当有需求的时候才将特定 的块装载进内存中.在这样的需求下,设计采用如下结构的词典: +-------- ...

  9. python高斯求和_利用Python进行数据分析(3)- 列表、元组、字典、集合

    本文主要是对Python的数据结构进行了一个总结,常见的数据结构包含:列表list.元组tuple.字典dict和集合set. image 索引 左边0开始,右边-1开始 通过index()函数查看索 ...

最新文章

  1. JAVA中大小写转化函数_Java-切换大小写,多个大小写调用同一函数
  2. ASP NET 数据库访问
  3. Scala隐式转换之隐式类
  4. 从分布式到云端服务:Google Spanner 成长之路
  5. python提取英文单词 每行显示一个_使用python对文件中的单词进行提取
  6. java futuretask get reject异常_FutureTask的get()方法之异常处理
  7. C#中join 的lambada写法
  8. Access SqLDbHelper
  9. Oracle常见索引扫描方式总结
  10. Y460安装桌面导航
  11. 电信HG2201T网关(部分地区的电信网关2.0)超级密码获取
  12. 大于号html语言怎么写,在HTML中使用大于号、小于号、空格等字符
  13. 怎么样学习平面设计?平面设计难吗?
  14. Preliminary Design Review(初步设计评审(回顾))
  15. css view a if属性,uni-app学习笔记(2)view属性控制css样式
  16. 极路由 支持php,目前极路由4增强版(B70)可用的最新固件整理!Padavan_PandoraBox_灯大_hanwckf_H大...
  17. Nginx最新教程通俗易懂
  18. 计算机在生活中的应用论文2000字,浅议计算机在生活中的应用论文(2)
  19. 英文歌曲:A place nearby (天堂若比邻)
  20. 为什么89C51单片机里面有TH0=(65536-50000)/256;TL0=(65536-50000)%256;

热门文章

  1. 【ros】2.ros的xx.msg编译
  2. Pytorch搭建Faster R-CNN目标检测平台
  3. Oracle编程入门经典 第4章 新9i示例模式
  4. VS2010下安装配置OpenCV2.4.4
  5. 图形处理(六)拖拽式网格融合-Siggraph 2010
  6. 【人脸识别】初识人脸识别
  7. TLD(Tracking-Learning-Detection)学习与源码理解之(六)
  8. 压缩跟踪Compressive Tracking
  9. 数字图像处理:第二章 图象获取、显示、表示与处理
  10. FileReader类型之文字读取