日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > python >内容正文

python

python gc内存_禁用 Python GC,Instagram 性能提升10%

發布時間:2025/3/21 python 18 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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%的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。