詳解內(nèi)存管理機制的變更,你需要了解
本文轉(zhuǎn)載自微信公眾號「腦子進煎魚了」,作者陳煎魚。轉(zhuǎn)載本文請聯(lián)系腦子進煎魚了公眾號。
大家好,我是正在學習如何蒸魚的煎魚。
在上一篇 Go1.16 特性介紹的文章中我們有提到,從 v1.16 起,Go 在 Linux 下的默認內(nèi)存管理策略會從MADV_FREE 改回 MADV_DONTNEED 策略。
這時候可能至少分兩撥小伙伴,分別是:
- 知道是什么,被這個問題 “折磨“ 過的,瞬間眼前一亮。
- 不知道是什么,出現(xiàn)了各種疑惑了,這說的都是些什么。
靈魂拷問
你有沒有以下的疑問,或者是否清楚:
- 文中所說的 MADV_FREE 是什么?
- 文中所說的 MADV_DONTNEED 是什么?
- 為什么特指 Go 語言的 Linux 環(huán)境?
- 為什么是說從 MADV_FREE改回 MADV_DONTNEED?
在今天這篇文章中我們都將進一步的展開和說明,讓我們一同來了解這個改來改去的內(nèi)存機制到底是何物。
madvise 愛與恨
在 Linux 系統(tǒng)中,在 Go Runtime 中通過系統(tǒng)調(diào)用 madvise(addr, length, advise) 方法,能夠告訴內(nèi)核如何處理從 addr 開始的 length 字節(jié)。
重點之一就是 ”如何處理“,在 Linux 下 Go 語言中目前支持兩種策略,分別是(via @felix021):
- MADV_FREE:內(nèi)核會在進程的頁表中將這些頁標記為 “未分配”,從而進程的 RSS 就會變小。OS 后續(xù)可以將對應的物理頁分配給其他進程。
- MADV_DONTNEED:內(nèi)核只會在頁表中將這些進程頁面標記為可回收,在需要的時候才回收這些頁面。
所帶來的影響
Go 語言官方恰好就在 2019 年的 Go1.12 做了如下調(diào)整。
- Go1.12 以前。
- Go.12-Go1.15.
Go1.12 以前
Go Runtime 在 Linux 上默認使用的是 MADV_DONTNEED 策略。
- // 沒有任何奇奇怪怪的判斷
- madvise(v, n, _MADV_DONTNEED)
從整體效果來看,進程 RSS 可以下降的比較快,但從性能效率上來看差點。
Go1.12-Go1.15
當前 Linux 內(nèi)核版本 >=4.5 時,Go Runtime 在 Linux 上默認使用了性能更為高效的 MADV_FREE 策略。
- var advise uint32
- if debug.madvdontneed != 0 {
- advise = _MADV_DONTNEED
- } else {
- advise = atomic.Load(&adviseUnused)
- }
- if errno := madvise(v, n, int32(advise)); advise == _MADV_FREE && errno != 0 {
- // MADV_FREE was added in Linux 4.5. Fall back to MADV_DONTNEED if it is
- // not supported.
- atomic.Store(&adviseUnused, _MADV_DONTNEED)
- madvise(v, n, _MADV_DONTNEED)
- }
從整體效果來看,進程RSS 不會立刻下降,要等到系統(tǒng)有內(nèi)存壓力了才會釋放占用,RSS 才會下降。
帶來的副作用
故事往往不是那么的美好,顯然在 Go1.12 起針對 madvise 的 MADV_FREE 策略的調(diào)整非常 “片面”。
來自社區(qū)微信群的小伙伴
結(jié)合社區(qū)里所遇到的案例可得知,該次調(diào)整帶來了許多問題:
- 引發(fā)用戶體驗的問題:Go issues 上總是出現(xiàn)以為內(nèi)存泄露,但其實只是未滿足條件,內(nèi)存沒有馬上釋放的案例。
- 混淆統(tǒng)計信息和監(jiān)控工具的情況:在 Grafana 等監(jiān)控上,發(fā)現(xiàn)容器進程內(nèi)存較高,釋放很慢,告警了,很慌。
- 導致與內(nèi)存使用有關(guān)聯(lián)的個別管理系統(tǒng)集成不良:例如 Kubernetes HPA ,或者自定義了擴縮容策略這類模式,難以評估。
- 擠壓同主機上的其他應用資源:并不是所有的 Go 程序都一定獨立跑在單一主機中,自然就會導致同一臺主機上的其他應用受到擠壓,這是難以評估的。
從社區(qū)反饋來看是問題多多,弊大于利。
官方本想著想著性能更好一些,但是在現(xiàn)實場景中引發(fā)了不少的新問題,甚至有提到和 Android 流程管理不兼容的情況。
有種 “撿了芝麻,丟了西瓜” 的感覺。
Go1.16:峰回路轉(zhuǎn)
既然社區(qū)反饋的問題何其多,有沒有人提呢?有,超級多。
多到提出修改回 MADV_DONTNEED 的 issues 僅花了 1-2 天的時間就討論完畢。
很快得出結(jié)論且合并 CL 關(guān)閉 issues 了。
Go1.16 修改內(nèi)容如下:
- func parsedebugvars() {
- // defaults
- debug.cgocheck = 1
- debug.invalidptr = 1
- if GOOS == "linux" {
- debug.madvdontneed = 1
- }
- ...
- }
直接指定回了 debug.madvdontneed = 1,簡單粗暴。
總結(jié)
在本篇文章中,我們針對 Go 語言在 Linux 下的 madvise 方法的策略調(diào)整進行了歷史介紹和說明,同時針對其調(diào)整所帶來的的副作用及應對措施進行了一一介紹。
本次變更很好的印證了,牽一發(fā)動全身的說法。大家在后續(xù)應用這塊時也要多加注意。
你覺得 Go1.16 這個特性變更怎么樣呢?歡迎在評論區(qū)留言。
參考
runtime: default to MADV_DONTNEED on Linux
踩坑記:go 服務(wù)內(nèi)存暴漲
Go 1.12 關(guān)于內(nèi)存釋放的一個改進