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

語言: CN / TW / HK

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

背景

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

問題定位

現象分析

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

翻開對應的核心程式碼,cache_reap 函式實現如下:

不難看出,該函式主要是遍歷一個全域性的 slab_caches 連結串列,該連結串列記錄的是系統上所有的 slab 記憶體物件相關資訊。

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

該檔案裡面記錄的是各個 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 連結串列成員個數最多也不會超過一萬個,所以怎麼看也不會有問題。

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

確認根因

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

再通過 cat /proc/cgroup 檢視系統的當前 cgroup 資訊,發現 memory cgroup 數量已經累積到 20w+。在雲主機計算節點上存在這麼多的 cgroup,明顯就不是正常的情況,即便是在 K8s(Kubernetes) 節點上,這個數量級的 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,並進行檔案操作產生 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 能夠被及時的清理;

  • 模擬清理 40w 個洩漏的 memory cgroup,回收執行緒 cpu 使用率最高不超過 5%,資源佔用可以接受;

  • 針對超大規格的殘留 memory cgroup 進行測試,回收 1 個持有 20G 記憶體的 memory cgroup,核心回收函式的執行時間分佈,基本不超過 64us;不會對其他服務造成影響;

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

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

總結

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

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

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