Instagram 实战 - 探索『写时复制』友好的 Python 垃圾回收机制

file

在 Instagram 上,我们拥有全球最大的 Django web 框架部署,这个框架完全是用 Python 编写的。 我们在早期之所以使用 Python,是因为它的简洁性,但随着我们的业务扩展,为了继续保持它的简洁性,这些年我们也不得不做大量的内部改进。 去年,我们尝试了 废除 Python 垃圾收集 (GC)机制(通过收集和释放未使用的数据来回收内存),并由此增加了10%的容量。 但是,随着我们的工程团队和新特性的数量不断壮大,内存使用量也在不断增长。 最终,我们通过禁用GC而增加的容量也开始捉襟见肘了。

下面的图表显示了我们的内存随着请求的增长而增长,经过3000次的请求后,该进程使用了大约600M以上的内存,而且更重要的是该趋势呈线性增长趋势。

file

从我们的负载测试来看,我们可以看到内存使用已成为我们的瓶颈。启用GC(内存回收机制)可以缓解这个问题并减缓内存增长,但是不希望写时拷贝(COW)仍然会增加整个内存的占用量。所以我们决定看看是否可以让Python GC(内存回收机制)在没有COW的情况下工作,从而减少内存开销。
file

第一次尝试: GC头数据构架重设

如果你仔细阅读我们上一个GC布局, 你会发现COW的元凶在每个python对象的开头部分:

/* 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;

这样的原理是我们每做一次采集, 所有的跟踪对象都会用ob_refcnt更新gc_refs ,但不幸的是,这一写入运算会导致内存中的拷贝。 下一种显而易见的解决方式是把所有的开头部分都移到另一语块内存中并且密集存储。

我们执行一个gc_head结构指针在采集过程中不会改变的版本。

typedef union _gc_head_ptr
{
    struct {
        union _gc_head *head;
    } gc_ptr;
    double dummy; /* force worst-case alignment */
} PyGC_Head_Ptr;

这样有用吗?我们试过随脚本分配内存然后返回一个子进程来测试它:

lists = []
strs = []
for i in range(16000):
    lists.append([])
    for j in range(40):
        strs.append(' ' * 8)

在旧gc_head结构下,子进程的RSS内存占用增加约60MB。在新的有额外指针的数据结构下,其只增加了约0.9MB。所以这样是有用的。

然而,你可能已经注意到在提出的附加数据指针结构介绍了内存开销(16字节 --- 两指针)。他看起来是个小数字,但是考虑到它应用到每一个可分配的Python对象(一个进程中常常包括数百万个对象,每个主机约70个进程),对于每个服务来说都是一个非常巨大的内存管理。

16 bytes 1,000,000 70 = ~1 GB

第二次尝试:从GC中隐藏共享对象

尽管新的 gc_head 数据结构表现出其在减少内存使用上的突出贡献,但它的性能天花板却并不理想。我们想要找的解决方案应是能在不造成明显性能影响的前提下使用 GC。由于我们的问题实际上就出在主进程在其子进程被开启前创建的那些共享对象上,所以我们尝试让 Python 的 GC 机制区别对待这些共享对象。换句话说,如果我们能从 GC 机制中隐藏这些共享对象,那么这些共享对象就不会被 GC 的回收循环所检查到,我们的问题也就迎刃而解了。

为达到这一目的,我们将一个简单的 API gc.freeze() 添加到 Python 的GC 模块下,来从 Python 的 GC 分代列表中移除那些共享对象。在 Python 内部,GC 分代列表用于追踪那些需要被回收的对象。我们已经将这一改进上行给了 Python,这一新 API 将会在 Python3.7 版本中发布(https://github.com/python/cpython/pull/370...)。

static PyObject *
gc_freeze_impl(PyObject *module)
{
    for (int i = 0; i < NUM_GENERATIONS; ++i) {
        gc_list_merge(GEN_HEAD(i), &_PyRuntime.gc.permanent_generation.head);
        _PyRuntime.gc.generations[i].count = 0;
    }
    Py_RETURN_NONE;
}

成功!

我们将这一改进部署在了我们的产品中,并且这次一切表现得如同预期:COW不再发生并且共享内存保持着相同的大小,而每个请求的内存平均增长率下降了大约50%。下图展示了GC的启用是如何借助停止线性增长并使每个进程运行更久来改善内存增长的。

file

Credits

感谢Jiahao Li,Matt Page,David Callahan,Carl S. Shapiro和Chenyang Wu对“写时复制”友好型Python垃圾回收的讨论与贡献。

作者是一名在Instagram工作的基础工程师。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://engineering.instagram.com/copy-o...

译文地址:https://learnku.com/python/t/22973/insta...

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!