K8S學習筆記之Pod OOM排查

0x00 概述

在一次系統上線後,我們發現某幾個節點在長時間運行後會出現內存持續飆升的問題,導致的結果就是Kubernetes集羣的這個節點會把所在的Pod進行驅逐OOM;如果調度到同樣問題的節點上,也會出現Pod一直起不來的問題。我們嘗試了殺死Pod後手動調度的辦法(label),當然也可以排除調度節點。但是在一段時間後還會復現,我們通過監控系統也排查了這段時間的流量情況,但應該和內存持續佔用沒有關聯,這時我們意識到這可能是程序的問題。

 

0x01 現象-內存居高不下

發現個別業務服務內存佔用觸發告警,通過 Grafana 查看在沒有什麼流量的情況下,內存佔用量依然拉平,沒有打算下降的樣子:

並且觀測的這些服務,早年還只是 100MB。現在隨着業務迭代和上升,目前已經穩步 4GB,容器限額 Limits 紛紛給它開道,但我想總不能是無休止的增加資源吧,這是一個很大的問題。

 

0x02 Pod頻繁重啓

有的業務服務,業務量小,自然也就沒有調整容器限額,因此得不到內存資源,又超過額度,就會進入瘋狂的重啓怪圈:

重啓將近 200 次,告警通知已經爆炸!

 

0x03 排查

3.1 猜想一:頻繁申請重複對象

出現問題服務的業務特點,那就是基本爲圖片處理類的功能,例如:圖片解壓縮、批量生成二維碼、PDF 生成等,因此就懷疑是否在量大時頻繁申請重複對象,而程序本身又沒有及時釋放內存,因此導致持續佔用。

內存池

想解決頻繁申請重複對象,可以用最常見的 sync.Pool

當多個 goroutine 都需要創建同⼀個對象的時候,如果 goroutine 數過多,導致對象的創建數⽬劇增,進⽽導致 GC 壓⼒增大。

形成 “併發⼤-佔⽤內存⼤-GC 緩慢-處理併發能⼒降低-併發更⼤”這樣的惡性循環。

場景驗證

在描述中關注到幾個關鍵字,分別是併發大,Goroutine 數過多,GC 壓力增大,GC 緩慢。也就是需要滿足上述幾個硬性條件,纔可以認爲是符合猜想的。

通過拉取 PProf goroutine,可得知 Goroutine 數並不高:

沒有什麼流量的情況下,也不符合併發大,Goroutine 數過多的情況,若要更進一步確認,可通過 Grafana 落實其量的高低。

從結論上來講,我認爲與其沒有特別直接的關係,但猜想其所對應的業務功能到導致的間接關係應當存在。

3.2 猜想二:未知的內存泄露

內存居高不下,其中一個反應就是猜測是否存在泄露,而我們的容器中目前只跑着一個進程:

 
顯然其提示的內存使用不高,也不像進程內存泄露的問題,因此也將其排除。

3.3 猜想三:容器環境的機制

既然不是業務代碼影響,也不是GC影響,那是否與環境本身有關呢,我們可以得知容器 OOM 的判別標準是 container_memory_working_set_bytes(當前工作集)。

而 container_memory_working_set_bytes 是由 cadvisor 提供的,對應下述指標:

 

0x04 原因

從 cadvisor/issues/638 可得知 container_memory_working_set_bytes 指標的組成實際上是 RSS + Cache。

而 Cache 高的情況,常見於進程有大量文件 IO,佔用 Cache 可能就會比較高,猜測也與 Go 版本、Linux 內核版本的 Cache 釋放、回收方式有較大關係。

出問題的常見功能,如:

  • 批量圖片解壓縮。

  • 批量二維碼生成。

  • 批量上傳渲染後圖片。

 

0x05 解決方案

在本場景中 cadvisor 所提供的判別標準 container_memory_working_set_bytes 是不可變更的,也就是無法把判別標準改爲 RSS,因此我們只能考慮掌握主動權。

5.1 開發角度

使用類 sync.Pool 做多級內存池管理,防止申請到 “不合適”的內存空間,常見的例子:ioutil.ReadAll:

func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) { 
   …    for {
           if free := cap(b.buf) - len(b.buf); free < MinRead {
               newBuf := b.buf            
               if b.off+free < MinRead {
                   newBuf = makeSlice(2*cap(b.buf) + MinRead)
                   // 擴充雙倍空間                    
                   copy(newBuf, b.buf[b.off:])           
               }              

核心是做好做多級內存池管理,因爲使用多級內存池,就會預先定義多個 Pool,比如大小 100,200,300的 Pool 池,當你要 150 的時候,分配200,就可以避免部分的內存碎片和內存碎塊。

但從另外一個角度來看這存在着一定的難度,因爲你怎麼知道什麼時候在哪個集羣上會突然出現這類型的服務,何況開發人員的預期情況參差不齊,寫多級內存池寫出 BUG 也是有可能的。

讓業務服務無限重啓,也是不現實的,被動重啓,沒有控制,且告警,存在風險。

5.2 運維角度

可以使用定期重啓的常用套路。可以在部署環境可以配合腳本做 HPA,當容器內存指標超過約定限制後,起一個新的容器替換,再將原先的容器給釋放掉,就可以在預期內替換且業務穩定了。

 

0x06 總結

根據上述排查和分析結果,原因如下:

  • 應用程序行爲:文件處理型服務,導致 Cache 佔用高。

  • Linux 內核版本:版本比較低(BUG?),不同 Cache 回收機制。

  • 內存分配機制:在達到 cgroup limits 前會嘗試釋放,但可能內存碎片化,也可能是一次性索要太多,無法分配到足夠的連續內存,最終導致 cgroup oom。

從根本上來講,應用程序需要去優化其內存使用和分配策略,又或是將其抽離爲獨立的特殊服務去處理。並不能以目前這樣簡單未經多級內存池控制的方式去使用,否則會導致內存使用量越來越大。

而從服務提供的角度來講,我們並不知道這類服務會在什麼地方出現又何時會成長起來,因此我們需要主動去控制容器的 OOM,讓其實現優雅退出,保證業務穩定和可控。

 

0x07 轉載

Kubernetes Pod OOM 排查日記

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章