本文小編為大家詳細(xì)介紹“禁用Python GC會怎么樣”,內(nèi)容詳細(xì),步驟清晰,細(xì)節(jié)處理妥當(dāng),希望這篇“禁用Python GC會怎么樣”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學(xué)習(xí)新知識吧。
成都創(chuàng)新互聯(lián)公司專注于企業(yè)營銷型網(wǎng)站建設(shè)、網(wǎng)站重做改版、察隅網(wǎng)站定制設(shè)計、自適應(yīng)品牌網(wǎng)站建設(shè)、H5場景定制、電子商務(wù)商城網(wǎng)站建設(shè)、集團公司官網(wǎng)建設(shè)、外貿(mào)網(wǎng)站制作、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁設(shè)計等建站業(yè)務(wù),價格優(yōu)惠性價比高,為察隅等各大城市提供網(wǎng)站開發(fā)制作服務(wù)。
Instagram 的 Web 服務(wù)器在多進程模式下運行 Django,使用主進程創(chuàng)建數(shù)十個工作(worker)進程,而這些工作進程會接收傳入的用戶請求。對于應(yīng)用程序服務(wù)器來說,我們使用帶分叉模式的 uWSGI 來平衡主進程和工作進程之間的內(nèi)存共享。
為了防止 Django 服務(wù)器運行到 OOM,uWSGI 主進程提供了一種機制,當(dāng)其 RSS 內(nèi)存超過預(yù)定的限制時重新啟動工作進程。
我們開始研究為什么 RSS 內(nèi)存在由主進程產(chǎn)生后會迅速增長。一個觀察結(jié)果是,RSS 內(nèi)存即使是從 250 MB 開始的,其共享內(nèi)存也會下降地非???,在幾秒鐘內(nèi)從 250 MB 到大約 140 MB(共享內(nèi)存大小可以從/ proc / PID / smaps讀取)。這里的數(shù)字是無趣的,因為它們隨時都會變化,但共享內(nèi)存下降的規(guī)模是非常有趣的 – 大約是總內(nèi)存 1/3 的。接下來,我們想要了解為什么共享內(nèi)存,在工作器開始產(chǎn)生時是怎樣變?yōu)槊總€進程的私有內(nèi)存的。
Linux內(nèi)核具有一種稱為寫入時復(fù)制(Copy-on-Write,CoW)的機制,用作 fork 進程的優(yōu)化。一個子進程開始于與其父進程共享每個內(nèi)存頁。而僅當(dāng)該頁面被寫入時,該頁面才會被復(fù)制到子進程內(nèi)存空間中(有關(guān)詳細(xì)信息,請參閱 wiki https://en.wikipedia.org/wiki/Copy-on-write)。
但在Python領(lǐng)域里,由于引用計數(shù)的緣故,事情變得有趣。每次我們讀取一個Python對象時,解釋器將增加其引用計數(shù),這本質(zhì)上是對其底層數(shù)據(jù)結(jié)構(gòu)的寫入。這導(dǎo)致 CoW 的發(fā)生。因此,我們在使用 Python 時,正在做的即是讀取時復(fù)制(CoR)!
#define PyObject_HEAD _PyObject_HEAD_EXTRA Py_ssize_t ob_refcnt; struct _typeobject *ob_type; ... typedef struct _object { PyObject_HEAD } PyObject; |
所以問題是:我們在寫入時復(fù)制的是不可變對象如代碼對象嗎?假定 PyCodeObject 確實是 PyObject 的“子類”,顯然也是這樣的。我們的第一想法是禁用 PyCodeObject 的引用計數(shù)。
在 Instagram 上,我們先做一件簡單的事情??紤]到這是一個實驗,我們對 CPython 解釋器做了一些小的改動,驗證了引用計數(shù)對代碼對象沒有變化,然后在我們的一個生產(chǎn)服務(wù)器運行 CPython。
結(jié)果是令人失望的,因為共享內(nèi)存沒有變化。當(dāng)我們試圖找出原因是,我們意識到我們找不到任何可靠的指標(biāo)來證明我們的黑客行為起作用,也不能證明共享內(nèi)存和代碼對象的拷貝之間的聯(lián)系。顯然,這里缺少一些東西。獲得的教訓(xùn):在行動之前先驗證你的理論。
在對 Copy-on-Write 這個問題谷歌搜索一番以后,我們了解到 Copy-on-Write 與系統(tǒng)中的頁面錯誤是相關(guān)聯(lián)的。每個 CoW 在運行過程中都可能觸發(fā)頁面錯誤。Linux 提供的 Perf 工具允許記錄硬件/軟件系統(tǒng)事件,包括頁面錯誤,甚至可以提供堆棧跟蹤!
所以我們用到了一個 prod,重新啟動該服務(wù)器,等待它 fork,繼而得到一個工作進程 PID,然后運行如下命令。
perf record -e page-faults -g -p |
然后,當(dāng)在堆棧跟蹤的過程中發(fā)生頁面錯誤時,我們有了一個主意。
結(jié)果與我們的預(yù)期不同。首要嫌疑人是 collect 而非是復(fù)制代碼對象,它屬于 gcmodule.c,并在觸發(fā)垃圾回收時被調(diào)用。在理解了 GC 在 CPython 中的工作原理后,我們有了以下理論:
CPython的 GC 完全是基于閾值而觸發(fā)的。這個默認(rèn)閾值非常低,因此它在很早的階段就開始了。 它維護著許多代的對象鏈表,并且在進行 GC 時,鏈表會被重新洗牌。因為鏈表結(jié)構(gòu)與對象本身一樣是存在的(就像 ob_refcount),在鏈表中改寫這些對象會導(dǎo)致頁面在寫入時被復(fù)制,這是一個不幸的副作用。
/* 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; |
那么,既然 GC 在暗中中傷我們,那我們就禁用它!
我們在我們的引導(dǎo)腳本添加了一個 gc.disable() 的函數(shù)調(diào)用。我們重啟了服務(wù)器,但是再一次的,不走運! 如果我們再次查看 perf,我們將看到 gc.collect 仍然被調(diào)用,并且內(nèi)存仍然被復(fù)制。在使用 GDB 進行一些調(diào)試時,我們發(fā)現(xiàn)我們使用的第三方庫( msgpack )顯然調(diào)用了 gc.enable() 將它恢復(fù)了,使得 gc.disable() 在引導(dǎo)程序中被清洗了。
給 msgpack 打補丁是我們最后要做的事情,因為它為其他做同樣的事情的庫打開了一扇門,在未來我們沒注意的時候。首先,我們需要證明禁用 GC 實際上是有幫助。答案再次落在 gcmodule.c 上。 作為 gc.disable 的替代,我們做了 gc.set_threshold(0),這一次,沒有庫能將其恢復(fù)了。
就這樣,我們成功地將每個工作進程的共享內(nèi)存從 140MB 提高到了 225MB,并且每臺機器的主機上的總內(nèi)存使用量減少了 8GB。 這為整個Django 機隊節(jié)省了 25% 的 RAM。有了這么大頭的空間,我們能夠運行更多的進程或運行具有更高的 RSS 內(nèi)存閾值的進程。實際上,這將Django層的吞吐量提高了 10% 以上。
在嘗試了一系列設(shè)置之后,我們決定在更大的范圍內(nèi)嘗試:一個集群。 反饋相當(dāng)快,我們的連續(xù)部署終止了,因為在禁用 GC 后,重新啟動我們的 Web 服務(wù)器變得很慢。通常重新啟動需要不到 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) |
復(fù)制這個 bug 是非常痛苦的,因為它不是確定發(fā)生的。經(jīng)過大量的實驗,一個真正的 re-pro 在頂上顯示。當(dāng)這種情況發(fā)生時,該主機上的可用內(nèi)存下降到接近零并跳回,強制清除所有的緩存內(nèi)存。之后當(dāng)所有的代碼/數(shù)據(jù)需要從磁盤讀取的時候(DSK 100%),一切都變得很緩慢。
這敲響了一個警鐘,即 Python 在解釋器關(guān)閉之前會做一個最后的 GC,這將導(dǎo)致在很短的時間內(nèi)內(nèi)存使用量的巨大跳躍。再次,我想先證明它,然后弄清楚如何正確處理它。所以,我注釋掉了對 Py_Finalize 在 uWSGI 的 python 插件的調(diào)用,問題也隨之消失了。
但顯然我們不能只是禁用 Py_Finalize。我們有一系列重要的使用 atexit 鉤子的清理工具依賴著它。最后我們做的是為 CPython 添加一個運行標(biāo)志,這將完全禁用 GC。
最后,我們要把它推廣到更大的規(guī)模。我們在這之后嘗試在整個機隊中使用它,但是連續(xù)部署再次終止了。然而,這次它只是在舊型號 CPU( Sandybridge )的機器上發(fā)生,甚至更難重現(xiàn)了。得到的教訓(xùn):經(jīng)常性地在舊的客戶端/模型做測試,因為它們通常是最容易出問題的。
因為我們的連續(xù)部署是一個相當(dāng)快的過程,為了真正捕獲發(fā)生了什么,我添加了一個單獨的 atop 到我們的 rollout 命令中。我們能夠抓住一個緩存內(nèi)存變的很低的時刻,所有的 uWSGI 進程觸發(fā)了很多 MINFLT(小頁錯誤)。廈門叉車租賃公司
再一次地,通過 perf 分析,我們再次看到了 Py_Finalize。 在關(guān)機時,除了最終的 GC,Python 還做了一系列的清理操作,如破壞類型對象和卸載模塊。這種行為再一次地,破壞了共享內(nèi)存。
我們究竟為什么需要清理? 這個過程將會死去,我們將得到另一個替代品。 我們真正關(guān)心的是我們的 atexit 鉤子,為我們的應(yīng)用程序清理。至于 Python 的清理,我們不必這樣做。 這是我們在自己的 bootstrapping 腳本中以這樣的方式結(jié)束:
# 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函數(shù)以注冊表的相反順序運行。atexit 函數(shù)完成其他清除,然后在最后一步中調(diào)用 os._exit(0) 以退出當(dāng)前進程。
隨著這兩條線的改變,我們最終讓它在整個機隊中得以推行。在小心地調(diào)整內(nèi)存閾值后,我們贏得了 10 % 的全局容量!
在回顧這次性能提升時,我們有兩個問題:
首先,如果沒有垃圾回收,是不是 Python 的內(nèi)存要炸掉,因為所有的分配出去的內(nèi)存永遠(yuǎn)不會被釋放?(記住,在 Python 內(nèi)存沒有真正的堆棧,因為所有的對象都在堆中分配)。
幸運的是,這不是真的。Python 中用于釋放對象的主要機制仍然是引用計數(shù)。 當(dāng)一個對象被解引用(調(diào)用 Py_DECREF)時,Python 運行時總是檢查它的引用計數(shù)是否降到零。在這種情況下,將調(diào)用對象的釋放器。垃圾回收的主要目的是終止引用計數(shù)不起作用的那些引用周期。
#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 的增益來源于兩重原因:
我們?yōu)槊總€服務(wù)器釋放了大約 8GB 的 RAM,這些 RAM 我們會用于為內(nèi)存綁定的服務(wù)器生成創(chuàng)建更多的工作進程,或者用于為綁定 CPU 服務(wù)器們降低重新生成速率;
隨著 CPU 指令數(shù)在每個周期( 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 高速緩存線,指向相同的物理存儲器地址,使得高速緩存命中率變得更高。
讀到這里,這篇“禁用Python GC會怎么樣”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領(lǐng)會,如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。