天天看點

網易數帆核心團隊:memory cgroup 洩漏問題的分析與解決

memory cgroup 洩露是 K8s(Kubernetes) 叢集中普遍存在的問題,輕則導緻節點記憶體資源緊張,重則導緻節點無響應隻能重新開機伺服器恢複;大多數的開發人員會采用定期 drop cache 或者關閉核心 kmem accounting 進行規避。本文基于網易數帆核心團隊的實踐案例,對 memory cgroup 洩露問題的根因進行分析,同時提供了一種核心層面修複該問題的方案。

背景

運維監控發現部分雲主機計算節點,K8s(Kubernetes) 節點都有出現負載異常沖高的問題,具體表現為系統運作非常卡,load 持續在 40+,部分的 kworker 線程 cpu 使用率較高或者處于 D 狀态,已經對業務造成了影響,需要分析具體的原因。

網易數帆核心團隊:memory cgroup 洩漏問題的分析與解決

問題定位

現象分析

對于 cpu 使用率異常的問題,perf 是必不可少的工具。通過 perf top 觀察熱點函數,發現核心函數 cache_reap 使用率會間歇性沖高。

翻開對應的核心代碼,cache_reap 函數實作如下:

網易數帆核心團隊:memory cgroup 洩漏問題的分析與解決

不難看出,該函數主要是周遊一個全局的 slab_caches 連結清單,該連結清單記錄的是系統上所有的 slab 記憶體對象相關資訊。

通過分析 slab_caches 變量相關的代碼流程發現,每一個 memory cgroup都會對應一個 memory.kmem.slabinfo 檔案。

網易數帆核心團隊:memory cgroup 洩漏問題的分析與解決

該檔案裡面記錄的是各個 memory cgroup 組程序申請的 slab 相關資訊,同時該 memory cgroup 組的 slab 對象也會被統一添加到全局的 slab_caches 連結清單中,莫非是因為 slab_caches 連結清單資料太多,導緻周遊時間比較長,進而導緻 CPU 沖高?

slab_caches 連結清單資料太多,那麼前提肯定是 memory cgroup 數量要特别多,自然而然也就想到要去統計一下系統上存在多少個 memory cgroup,但當我們去統計/sys/fs/cgroup/memory 目錄下的 memory cgroup 組的數量時發現也就隻有一百個不到的 memory cgroup,每個 memory cgroup 裡面 memory.kmem.slabinfo 檔案最多也就包含幾十條記錄,是以算起來 slab_caches 連結清單成員個數最多也不會超過一萬個,是以怎麼看也不會有問題。

網易數帆核心團隊:memory cgroup 洩漏問題的分析與解決

最終還是從最原始的函數 cache_reap 入手,既然該函數會比較消耗 CPU,那麼直接通過跟蹤該函數來分析究竟是代碼裡面什麼地方執行時間比較長。

确認根因

通過一系列工具來跟蹤 cache_reap 函數發現,slab_caches 連結清單成員個數達到了驚人的幾百萬個,該數量跟我們實際計算出來的數量差異巨大。

再通過 cat /proc/cgroup 檢視系統的目前 cgroup 資訊,發現 memory cgroup 數量已經累積到 20w+。在雲主機計算節點上存在這麼多的 cgroup,明顯就不是正常的情況,即便是在 K8s(Kubernetes) 節點上,這個數量級的 cgroup 也不可能是容器業務能正常産生的。

網易數帆核心團隊:memory cgroup 洩漏問題的分析與解決

那麼為什麼/sys/fs/cgroup/memory 目錄下統計到的 memory cgroup 數量和/proc/cgroups 檔案記錄的數量會相差如此之大了?因為 memory cgroup 洩露導緻!

詳細解釋參考如下:

系統上的很多操作(如建立銷毀容器/雲主機、登入主控端、cron 定時任務等)都會觸發建立臨時的 memory cgroup。這些 memory cgroup 組内的程序在運作過程中可能會産生 cache 記憶體(如通路檔案、建立新檔案等),該 cache 記憶體會關聯到該 memory cgroup。當 memory cgroup 組内程序退出後,該 cgroup 組在/sys/fs/cgroup/memory 目錄下對應的目錄會被删除。但該 memory cgroup 組産生的 cache 記憶體并不會被主動回收,由于有 cache 記憶體還在引用該 memory cgroup 對象,是以也就不會删除該 memory cgroup 在記憶體中的對象。

在定位過程中,我們發現每天的 memory cgroup 的累積數量還在緩慢增長,于是對節點的 memory cgroup 目錄的建立、删除進行了跟蹤,發現主要是如下兩個觸發源會導緻 memory cgroup 洩露:

  1. 特定的 cron 定時任務執行 
  2. 使用者頻繁登入和退出節點

這兩個觸發源導緻 memory cgroup 洩漏的原因都是跟 systemd-logind 登入服務有關系,執行 cron 定時任務或者是登入主控端時,systemd-logind 服務都會建立臨時的 memory cgroup,待 cron 任務執行完或者是使用者登出後,會删除臨時的 memory cgroup,在有檔案操作的場景下會導緻 memory cgroup 洩漏。

複現方法

分析清楚了 memory cgroup 洩露的觸發場景,那就複現問題就容易很多:

網易數帆核心團隊:memory cgroup 洩漏問題的分析與解決

核心的複現邏輯就是建立臨時 memory cgroup,并進行檔案操作産生 cache 記憶體,然後删除 memory cgroup 臨時目錄,通過以上的方式,在測試環境能夠很快的複現 40w memory cgroup 殘留的現場。

解決方案

通過對 memory cgroup 洩漏的問題分析,基本搞清楚了問題的根因與觸發場景,那麼如何解決洩露的問題呢?

方案一:drop cache

既然 cgroup 洩露是由于 cache 記憶體無法回收引起的,那麼最直接的方案就是通過“echo 3 > /proc/sys/vm/drop_caches”清理系統緩存。

但清理緩存隻能緩解,而且後續依然會出現 cgroup 洩露。一方面需要配置每天定時任務進行 drop cache,同時 drop cache 動作本身也會消耗大量 cpu 對業務造成影響,而對于已經形成了大量 cgroup 洩漏的節點,drop cache 動作可能卡在清理緩存的流程中,造成新的問題。

方案二:nokmem

kernel 提供了 cgroup.memory = nokmem 參數,關閉 kmem accounting 功能,配置該參數後,memory cgroup 就不會有單獨的 slabinfo 檔案,這樣即使出現 memory cgroup 洩露,也不會導緻 kworker 線程 CPU 沖高了。

不過該方案需要重新開機系統才能生效,對業務會有一定影響,且該方案并不能完全解決 memory cgroup 洩露這個根本性的問題,隻能在一定程度緩解問題。

方案三:消除觸發源

上面分析發現的 2 種導緻 cgroup 洩露的觸發源,都可以想辦法消除掉。

針對第 1 種情況,通過與相應的業務子產品溝通,确認可以關閉該 cron 任務; 

針對第 2 種情況,可以通過 loginctl enable-linger username 将對應使用者設定成背景常駐使用者來解決。

設定成常駐使用者後,使用者登入時,systemd-logind 服務會為該使用者建立一個永久的 memory cgroup 組,使用者每次登入時都可以複用這個 memory cgroup 組,使用者退出後也不會删除,是以也就不會出現洩漏。

到此時看起來,這次 memory cgroup 洩漏的問題已經完美解決了,但實際上以上處理方案僅能覆寫目前已知的 2 個觸發場景,并沒有解決 cgroup 資源無法被徹底清理回收的問題,後續可能還會出現的新的 memory cgroup 洩露的觸發場景。

核心裡的解決方案

正常方案

在問題定位過程中,通過 Google 就已經發現了非常多的容器場景下 cgroup 洩漏導緻的問題,在 centos7 系列,4.x 核心上都有報告的案例,主要是由于核心對 cgroup kernel memory accounting 特性支援的不完善,當 K8s(Kubernetes)/RunC 使用該特性時,就會存在 memory cgroup 洩露的的問題。

而主要的解決方法,不外乎以下的幾種規避方案:

  1. 定時執行 drop cache
  2. 核心配置 nokmem 禁用 kmem accounting 功能
  3. K8s(Kubernetes) 禁用 KernelMemoryAccounting 功能
  4. docker/runc 禁用 KernelMemoryAccounting 功能

我們在考慮有沒有更好的方案,能在核心層面“徹底”解決 cgroup 洩露的問題?

核心回收線程

通過對 memoy cgroup 洩露問題的深入分析,我們看到核心的問題是,systemd-logind 臨時建立的 cgroup 目錄雖然會被自動銷毀,但由于檔案讀寫産生的 cache 記憶體以及相關 slab 記憶體卻沒有被立刻回收,由于這些記憶體頁的存在,cgroup 管理結構體的引用計數就無法清零,是以雖然 cgroup 挂載的目錄被删除了,但相關的核心資料結構還保留在核心裡。

根據對社群相關問題解決方案的跟蹤分析,以及阿裡 cloud linux 提供的思路,我們實作一個簡單直接的方案:

在核心中運作一個核心線程,針對這些殘留的 memory cgroup 單獨做記憶體回收,将其持有的記憶體頁釋放到系統中,進而讓這些殘留的 memory cgroup 能夠被系統正常回收。

這個核心線程具有以下特性:

  1. 隻對殘留的 memory cgroup 進行回收
  2. 此核心線程優先級設定為最低
  3. 每做一輪 memory cgroup 的回收就主動 cond_resched(),防止長時間占用 cpu

回收線程的核心流程如下:

網易數帆核心團隊:memory cgroup 洩漏問題的分析與解決

功能驗證

對合入核心回收線程的核心進行功能與性能測試,結果如下:

  • 在測試環境開啟回收線程,系統殘留的 memory cgroup 能夠被及時的清理;
  • 模拟清理 40w 個洩漏的 memory cgroup,回收線程 cpu 使用率最高不超過 5%,資源占用可以接受;
  • 針對超大規格的殘留 memory cgroup 進行測試,回收 1 個持有 20G 記憶體的 memory cgroup,核心回收函數的執行時間分布,基本不超過 64us;不會對其他服務造成影響; 
網易數帆核心團隊:memory cgroup 洩漏問題的分析與解決

開啟核心回收線程後,正常通過核心 LTP 穩定性測試,不會增加核心穩定性風險。

可以看到通過新增一個核心線程對殘留的 memory cgroup 進行回收,以較小的資源使用率,能夠有效解決 cgroup 洩露的問題,這個方案已經在網易私有雲大量上線使用,有效提升了網易容器業務的穩定性。

總結

以上是我們分享的 memory cgroup 洩露問題的分析定位過程,給出了相關的解決方案,同時提供了核心層面解決該問題的一種思路。

在長期的業務實踐中,深刻的體會到 K8s(Kubernetes)/容器場景對 Linux kernel 的使用和需求是全方位的。一方面,整個容器技術主要是基于 kernel 提供的能力建構的,提升 kernel 穩定性,針對相關子產品的 bug 定位與修複能力必不可少;另一方面, kernel 針對容器場景的優化/新特性層出不窮。我們也持續關注相關技術的發展,比如使用 ebpf 優化容器網絡,增強核心監控能力,使用 cgroup v2/PSI 提升容器的資源隔離及監控能力,這些也都是未來主要推動在網易内部落地的方向。

作者介紹:張亞斌,網易數帆核心專家

繼續閱讀