最近我在回顧思考(寫 PPT),整理了現狀,發現了這個問題存在多時,通過一番波折,最終肯定了元兇和相對可行的解決方案,所以也在這裏分享一下排查歷程。git
時間線:github
在上 Kubernetes 的前半年,只是用 Kubernetes,開發沒有權限,業務服務極少,忙着寫新業務,風平浪靜。golang
在上 Kubernetes 的後半年,業務服務較少,偶爾會階段性被運維喚醒,問之 「爲何大家的服務內存佔用這麼高,趕忙查」。此時你們還在爲新業務衝刺,猜想也許是業務代碼問題,但沒有調整代碼去嘗試解決。安全
在上 Kubernetes 的第二年,業務服務逐漸增多,廣泛增長了容器限額 Limits,出現了好幾個業務服務是內存小怪獸,所以若是不限制的話,服務過分佔用會致使驅逐,所以反饋語也就變成了:「爲何大家的服務內存佔用這麼高,老被 OOM Kill,趕忙查」。據聞也有幾個業務大佬有去排查(由於 OOM 反饋),彷佛沒得出最終解決方案。bash
不由讓咱們思考,爲何個別 Go 業務服務,Memory 老是提示這麼高,常常達到容器限額,以致於被動 OOM Kill,是否是有什麼安全隱患?併發
發現個別業務服務內存佔用挺高,觸發告警,且經過 Grafana 發如今凌晨(沒有什麼流量)的狀況下,內存佔用量依然拉平,沒有打算降低的樣子,高峯更是不得了,像是個內存炸彈:運維
而且我所觀測的這個服務,早年還只是 100MB。如今隨着業務迭代和上升,目前已經穩步 4GB,容器限額 Limits 紛紛給它開道,但我想總不能是無休止的增長資源吧,這是一個大問題。工具
有的業務服務,業務量小,天然也就沒有調整容器限額,所以得不到內存資源,又超過額度,就會進入瘋狂的重啓怪圈:post
重啓將近 300 次,很是不正常了,更不用提所接受到的告警通知。優化
出現問題的個別業務服務都有幾個特色,那就是基本爲圖片處理類的功能,例如:圖片解壓縮、批量生成二維碼、PDF 生成等,所以就懷疑是否在量大時頻繁申請重複對象,而 Go 自己又沒有及時釋放內存,所以致使持續佔用。
基本上想解決 「頻繁申請重複對象」,咱們大多會採用多級內存池的方式,也能夠用最多見的 sync.Pool,這裏可參考全成所借述的《Go 夜讀》上關於 sync.Pool 的分享,關於這類狀況的場景:
當多個 goroutine 都須要建立同⼀個對象的時候,若是 goroutine 數過多,致使對象的建立數⽬劇增,進⽽致使 GC 壓⼒增大。造成 「併發⼤-佔⽤內存⼤-GC 緩慢-處理併發能⼒下降-併發更⼤」這樣的惡性循環。
在描述中關注到幾個關鍵字,分別是併發大,Goroutine 數過多,GC 壓力增大,GC 緩慢。也就是須要知足上述幾個硬性條件,才能夠認爲是符合猜測的。
經過拉取 PProf goroutine,可得知 Goroutine 數並不高:
另外在凌晨長達 6 小時,沒有什麼流量的狀況下,也不符合併發大,Goroutine 數過多的狀況,若要更進一步確認,可經過 Grafana 落實其量的高低。
從結論上來說,我認爲與其沒有特別直接的關係,但猜測其所對應的業務功能到致使的間接關係應當存在。
內存居高不下,其中一個反應就是猜想是否存在泄露,而咱們的容器中目前只跑着一個 Go 進程,所以首要看看該 Go 應用是否有問題。這時候能夠藉助 PProf heap(可使用 base -diff):
顯然其提示的內存使用不高,那會不會是 PProf 出現了 BUG 呢。接下經過命令也可肯定 Go 進程的 RSS 並不高,但 VSZ 卻相對 「高」 的驚人,我在 19 年針對此寫過一篇《Go 應用內存佔用太多,讓排查?(VSZ篇)》 ,此次 VSZ 太高也給我留下了一個念想。
從結論上來說,也不像 Go 進程內存泄露的問題,所以也將其排除。
在 Go1.12 之前,Go Runtime 在 Linux 上使用的是 MADV_DONTNEED
策略,可讓 RSS 降低的比較快,就是效率差點。
在 Go1.12 及之後,Go Runtime 專門針對其進行了優化,使用了更爲高效的 MADV_FREE
策略。但這樣子所帶來的反作用就是 RSS 不會馬上降低,要等到系統有內存壓力了纔會釋放佔用,RSS 纔會降低。
查看容器的 Linux 內核版本:
$ uname -a
Linux xxx-xxx-99bd5776f-k9t8z 3.10.0-693.2.2.el7.x86_64
複製代碼
但 MADV_FREE
的策略改變,須要 Linux 內核在 4.5 及以上(詳細可見 go/issues/23687),顯然不符合,所以也將其從猜想中排除。
會不會是 Grafana 的圖表錯了,Kubernetes OOM Kill 的判別標準也錯了呢,顯然不大可能,畢竟咱們擁抱雲,阿里雲 Kubernetes 也運行了好幾年。
但在此次懷疑中,我瞭解到 OOM 的判斷標準是 container_memory_working_set_bytes 指標,所以有了下一步猜測。
既然不是業務代碼影響,也不是 Go Runtime 的直接影響,那是否與環境自己有關呢,咱們能夠得知容器 OOM 的判別標準是 container_memory_working_set_bytes(當前工做集)。
而 container_memory_working_set_bytes 是由 cadvisor 提供的,對應下述指標:
從結論上來說,Memory 換算過來是 4GB+,石錘。接下來的問題就是 Memory 是怎麼計算出來的呢,顯然和 RSS 不對標。
從 cadvisor/issues/638 可得知 container_memory_working_set_bytes 指標的組成其實是 RSS + Cache。而 Cache 高的狀況,常見於進程有大量文件 IO,佔用 Cache 可能就會比較高,猜想也與 Go 版本、Linux 內核版本的 Cache 釋放、回收方式有較大關係。
而各業務模塊常見功能,如:
只要是涉及有大量文件 IO 的服務,基本上是這個問題的老常客了,寫這類服務基本寫一箇中一個,由於這是一個混合問題,像其它單純操做爲主的業務服務就很 「正常」,不會出現內存居高不下。
在本場景中 cadvisor 所提供的判別標準 container_memory_working_set_bytes 是不可變動的,也就是沒法把判別標準改成 RSS,所以咱們只能考慮掌握主動權。
首先是作好作多級內存池管理,能夠緩解這個問題的症狀。但這存在難度,從另一個角度來看,你怎麼知道何時在哪一個集羣上會忽然出現這類型的服務,況且開發人員的預期狀況良莠不齊,寫多級內存池寫出 BUG 也是有可能的。
讓業務服務無限重啓,也是不現實的,被動重啓,沒有控制,且告警,存在風險。
所以爲了掌握主動權,能夠在部署環境能夠配合腳本作 「手動」 HPA,當容器內存指標超過約定限制後,起一個新的容器替換,再將原先的容器給釋放掉,就能夠在預期內替換且業務穩定了。
雖然這問題時間跨度比較長,總體來說都是階段性排查,本質上能夠說是對 Kubernetes 的不熟悉有關。但綜合來說這個問題涉及範圍比較大,由於內存居高不下的可能性有不少種,要一個個排查,開發權限有限,費時費力。
基本排查思路就是:
很是感謝在這大段時間內被我諮詢的各位大佬們,感受就是隔了一層紗,捅穿了就很快就定位到了,你們若是有其它解決方案也歡迎隨時溝通。
原文地址:爲何容器內存佔用居高不下,頻頻 OOM