python gc内存_禁用 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)!
#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%的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql 高性能引擎_《高性能MySQ
- 下一篇: 安卓mysql导出excel_Andro