修补msgpack是我们要做的最后一件事,因为它意味着我们没有注意到其他库在未来也会做同样的事情。首先,我们需要证实禁用GS实际上是很有帮助的。答案存在于gcmodule.c中。作为gc.disable的替代,我们做了gc.set_threshold(0),这一次,没有任何库被恢复过来。
这样,我们成功地将每个工作进程的共享内存从140MB提高到225MB,并且每台机器在主机上的总内存使用量减少了8GB。这就为整个Django集群节约了25%的内存。有了这么大的头部空间,,我们能够运行更多的进程或运行带有更高的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-top在顶部显示了。当发生这种情况时,主机上的可用内存骤降到接近零并跳回,强迫所有的高速缓存内存撤出。然后到所有的代码/数据需要从磁盘读取(DSK 100%)的时刻,一切都慢吞吞的。
听上去很奇怪,Python会在关闭解释器之前做最后一个GC,这会在很短的时间内,导致内存使用量产生巨大的飞跃。再者就是,我想先证明它,然后弄清楚如何正确处理它。因此,我在uWSGI的python插件中注释掉Py_Finalize的调用,问题就消失了。
但显然的是,我们不能对Py_Finalize只是一禁了之。因为我们有一堆重要的清理,要用到依赖它的atexit钩子。最后,我们所做的就是,在CPython添加一个运行时标志,来完全禁用GC。
最后,我们开始将这个做法推广到更大的规模。此后,我们在整个集群进行尝试,但是,连续部署再次被中断了。不过,这次它只是在旧CPU型号(Sandybridge)的机器上中断了,甚至更难重现。经验教训:要多测试旧式客户端/旧型号,因为他们最容易被中断。
因为我们的连续部署是一个相当快的过程,为了真正捕获发生了什么,我在rollout命令添加了一个单独的atop。这样我们就能够抓住高速缓存内存真的很低的一个时刻。所有uWSGI进程触发了很多MINFLT(minor page faults,小页面错误)。
(点击放大图像)
再次通过perf得出的概要,我们再次看到了Py_Finalize。在关机时,除了最终的GC,Python做了一堆清理操作,如破坏类型对象和卸载模块。这又一次损害了共享内存。
(点击放大图像)
尝试4:关闭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)基于这个事实,atexit函数以注册表的相反顺序运行。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,用于为内存绑定服务器生成创建更多的工作进程,或者降低CPU绑定服务器生成的工作程序刷新率;