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

翻译 Summer ⋅ 于 1个月前 ⋅ 390 阅读 ⋅ 原文地址
这是一篇协同翻译的文章,目前翻译进度为 67%,你可以点击『我来翻译』按钮来 参与翻译

file

在Instagram上,我们拥有全球最大的Django web框架部署,这个框架完全是用Python编写的。 我们之所以开始使用Python,是因为它的简单性,但是为了保持简单,我们必须做很多年的黑客工作。 去年,我们试图 解散Python垃圾收集 (GC)机制(通过收集和释放未使用的数据来回收内存),并且增加了10%的容量。 但是,随着我们的工程团队和功能数量不断增长,内存使用量也在不断增长。 最终,我们开始失去了通过禁用GC而取得的成果。

Ellison 翻译于 4周前

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

file

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

pengge 翻译于 2周前

第一次尝试: 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;
MrGo 翻译于 1周前

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

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

MrGo 翻译于 2天前

Second try: hiding shared objects from GC

Even though the new gc_head data structure showed promising gains on memory size, its overhead was not ideal. We wanted to find a solution that could enable the GC without noticeable performance impacts. Since our problem is really only on the shared objects that are created in the master process before the child processes are forked, we tried letting Python GC treat those shared objects differently. In other words, if we could hide the shared objects from the GC mechanism so they wouldn't be examined in the GC collection cycle, our problem would be solved.

For that purpose, we added a simple API as gc.freeze() into the Python GC module to remove the objects from the Python GC generation list that's maintained by Python internal for tracking objects for collection. We have upstreamed this change to Python and the new API will be available in the Python3.7 release (https://github.com/python/cpython/pull/3705).

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

Success!

We deployed this change into our production and this time it worked as expected: COW no longer happened and shared memory stayed the same, while average memory growth per request dropped ~50%. The plot below shows how enabling GC helped the memory growth by stopping the linear growth and making each process live longer.

file

Credits

Thanks to Jiahao Li, Matt Page, David Callahan, Carl S. Shapiro, and Chenyang Wu for their discussions and contributions to the COW-friendly Python garbage collection.

Author is an infrastructure engineer at Instagram.

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

本帖已被设为精华帖!
回复数量: 0
    暂无评论~~
      请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!
    Ctrl+Enter