Cgroup之內存子系統

Memory 子系統主要完成兩件事:

(1)控制一組進程使用內存資源的行爲;

(2)統計 cgroup 內進程使用內存資源的信息。在實際業務場景中,主要是爲了避免某些應用大量佔用內存資源(可能是由於內存泄漏導致)從而導致其他進程不可用。當 cgroup 中的進程組佔用內存資源達到設置的閾值後,系統會首先嚐試回收 buffer/cache,如果回收之後 cgroup 剩餘的可用內存資源仍然不足,則會觸發 OOM killer。

cgroup V1

Memory 子系統的特性:

  • 統計匿名頁、文件緩存、swap 緩存的使用情況並進行限制;

  • cgroup 中的所有內存頁放在單獨的 LRU 鏈表,不存在系統全局 LRU;

  • 可以統計和限制 memory+swap 的總量;

  • 可以在層級(hierarchical)進行統計;

  • 軟限制;

  • 移動任務到其它 cgroup 時,同時移動(重新計算)相關的統計信息;

  • 內存使用量達到閾值後進行通知;

  • 內存資源不足時進行通知;

  • 可以禁用 oom-killer 和 oom-notifier

  • Root cgroup 沒有資源限制。

Memory 子系統包含的文件如下:

cgroup.clone_children
cgroup.event_control
cgroup.procs
memory.failcnt
memory.force_empty
memory.kmem.failcnt
memory.kmem.limit_in_bytes
memory.kmem.max_usage_in_bytes
memory.kmem.slabinfo
memory.kmem.tcp.failcnt
memory.kmem.tcp.limit_in_bytes
memory.kmem.tcp.max_usage_in_bytes
memory.kmem.tcp.usage_in_bytes
memory.kmem.usage_in_bytes
memory.limit_in_bytes
memory.max_usage_in_bytes
memory.memsw.failcnt
memory.memsw.limit_in_bytes
memory.memsw.max_usage_in_bytes
memory.memsw.usage_in_bytes
memory.move_charge_at_immigrate
memory.numa_stat
memory.oom_control
memory.pressure_level
memory.soft_limit_in_bytes
memory.stat
memory.swappiness
memory.usage_in_bytes
memory.use_hierarchy
notify_on_release
tasks

用戶態內存資源限制和統計

  • memory.limit_in_bytes:可讀可寫,可以設置和查看當前 cgroup 的可用內存資源(包括文件頁緩存)閾值,如果進程組使用的內存資源總量超過 limit_in_bytes,會觸發 OOM-killer,殺死或暫停執行進程。默認單位是 bytes,可以通過 K、M、G(不區分大小寫)後綴指定單位,-1 表示不限制。

  • memory.soft_limit_in_bytes:可讀可寫,可用內存資源(包括文件頁緩存)的軟限制,如果進程組使用的內存資源總量超過 soft_limit_in_bytes,系統會盡量回收已經超過軟限制的 cgroup,使其小於軟限制設置的值,從而滿足其它未達到軟限制的 cgroup。默認單位是 bytes,可以通過 K、M、G(不區分大小寫)後綴指定單位,-1 表示不限制。由於 limit_in_bytes 設置的值是硬限制,因此,爲了避免觸發 OOM-killer,soft_limit_in_bytes 的值需要小於 limit_in_bytes。軟限制在啓用 CONFIG_PREEMPT_RT 的系統(內核支持完全搶佔,在某些場景可以提升性能)中不可用。

  • memory.max_usage_in_bytes:記錄 cgroup 創建以來使用內存資源的的峯值,單位 bytes。

  • memory.usage_in_bytes:cgroup 中所有任務在當前時刻使用內存資源的總量,單位 bytes。

  • memory.failcnt:cgroup 中所有任務使用內存資源的總量達到 memory.limit_in_bytes 的次數。

  • memory.numa_stat:每個 numa 節點的內存使用量,單位 bytes,以下是一個樣例:

> cat memory.numa_stat
total=5940 N0=5944 N1=220
file=1947 N0=1980 N1=220
anon=66 N0=1 N1=0
unevictable=3927 N0=3963 N1=0
hierarchical_total=5940 N0=5944 N1=220
hierarchical_file=1947 N0=1980 N1=220
hierarchical_anon=66 N0=1 N1=0
hierarchical_unevictable=3927 N0=3963 N1=0

anno:匿名頁和 swap 緩存的大小;

file:文件頁的大小;

unevictable:不可回收的內存頁大小;

total:anno + file + unevictable 的值。

  • memory.use_hierarchy:是否允許內核從 cgroup 所在層級(hierarchy)回收內存,默認值爲 0,不允許從層級中的其它任務回收內存。目前已被廢棄,參考:https://lwn.net/Articles/835983。

  • memory.stat:統計 cgroup 的內存使用信息。

> cat memory.stat
cache 0
rss 0
rss_huge 0
shmem 0
mapped_file 0
dirty 0
writeback 0
swap 0
pgpgin 0
pgpgout 0
pgfault 28215
pgmajfault 0
inactive_anon 0
active_anon 0
inactive_file 0
active_file 0
unevictable 0
hierarchical_memory_limit 9223372036854771712
hierarchical_memsw_limit 9223372036854771712
total_cache 0
total_rss 0
total_rss_huge 0
total_shmem 0
total_mapped_file 0
total_dirty 0
total_writeback 0
total_swap 0
total_bgd_reclaim 0
total_pgpgin 0
total_pgpgout 0
total_pgfault 28215
total_pgmajfault 0
total_inactive_anon 0
total_active_anon 0
total_inactive_file 0
total_active_file 0
total_unevictable 0

cache:緩存頁,包括 tmpfs(shim),單位 bytes。

rss:匿名頁和 swap 空間,包括透明大頁,不包括 tmpfs(shim),單位 bytes。

rss_huge:透明大頁,單位 bytes。

shmem:共享內存頁,單位 bytes。

mapped_file:文件映射頁(包括 tmpfs 和 shmem),單位 bytes。

dirty:髒頁(等待被寫入磁盤的頁),單位 bytes。

writeback:隊列中即將被同步到磁盤的文件頁和匿名頁,單位 bytes。

swap:被使用的交換空間,單位 bytes。

pgpgin:將數據從磁盤讀入內存的次數。(匿名頁 RSS 或 緩存頁)

pgpgout:將內存頁從內存寫入磁盤的次數。

pgfault:發生缺頁中斷(內核必須爲進程虛擬內存地址空間分配和初始化物理內存)的次數。

pgmajfault:發生 major 類型(內核在分配和初始化內存頁之前必須主動釋放物理內存) pagefalut 的次數。

inactive_anon:處於 inactive LRU 列表中的匿名頁和 swap 頁面總和,包括 tmpfs,單位 bytes。

active_anon:處於 active LRU 列表中的匿名頁和 swap 頁面總和,包括 tmpfs,單位 bytes。

inactive_file:處於 inactive LRU 列表中文件頁,單位 bytes。

active_file:處於 active LRU 列表中文件頁,單位 bytes。

unevictable:不可回收的內存頁,單位 bytes。

以下內容在開啓 hierarchy(memory.use_hierarchy 文件的值爲 1)時纔有:

hierarchical_memory_limit:當前 cgroup 層級(包括所有子 cgroup)的可用內存資源閾值。

hierarchical_memsw_limit:當前 cgroup 層級(包括所有子 cgroup)的可用內存資源 + swap 空間閾值。

total_xxx:當前 cgroup 層級(包括所有子 cgroup)的值。例如:total_cache,含義和 cache 相同。

  • memory.move_charge_at_immigrate:當移動 cgroup 中的任務到其它 cgroup 中時,是否移動該任務使用內存頁的統計信息。關閉此功能,任務進入新的 cgroup 記錄的內存使用量從 0 開始。開啓此功能,會在原有值的基礎上累加,同時清理原 cgroup 中的相關數據。目前已被廢棄。

  • memory.pressure_level:內存壓力通知。

  • memory.force_empty:觸發強制內存回收操作。當設置爲 0 時,cgroup 中所有任務使用的內存頁會被回收,只有在 cgroup 中沒有任務時纔可使用。典型的應用是移除 cgroup 的時候,分配的頁緩存會一直保留,直到內存壓力較大的時候才被回收,如果想避免這種情況,可以使用 memory.force_empty 主動觸發。

交換空間大小限制和統計

  • memory.memsw.limit_in_bytes:可用內存資源+交換空間總量的閾值。默認單位是 bytes,可以通過 K、M、G(不區分大小寫)後綴指定單位,-1 表示不限制。爲了避免 OOM 錯誤,memory.limit_in_bytes 的值需要小於 memory.memsw.limit_in_bytes,同時,memory.memsw.limit_in_bytes 的值需要小於可用交換分區總量。如果 memory.memsw.limit_in_bytes 的值等於 memory.limit_in_bytes,相當於禁用交換分區。同時,這兩個參數的設置順序也很重要,如果在設置 memory.limit_in_bytes 之前設置 memory.memsw.limit_in_bytes 的值可能會導致錯誤,因爲,memory.memsw.limit_in_bytes 只有在內存消耗達到 memory.limit_in_bytes 設置的值纔會生效。

⚠️ 注意:爲什麼是 memory + swap 而不是直接限制 swap 呢?

系統中的內核線程 kswapd 根據全局 LRU 可以換出任意頁面。換出的時候,內存頁面會被換出到交換分區,也就是說,換出之後,memory + swap 的總量還是沒變。如果希望在不影響全局 LRU 的情況下限制 swap 的使用,從操作系統的角度看,限制 memory + swap 比直接限制 swap 更好。

  • memory.memsw.max_usage_in_bytes:和 memory.max_usage_in_bytes 類似,記錄使用 memory + swap 的峯值,單位 bytes。

  • memory.memsw.usage_in_bytes:cgroup 中所有任務在當前時刻使用 memory + swap 的總量,單位 bytes。

  • memory.memsw.failcnt:記錄 cgroup 所有任務使用 memory + swap 的總量達到 memory.memsw.limit_in_bytes 的次數。

  • memory.swappiness:和 Linux 內核參數 vm.swappiness 定義類似,控制換出運行時內存的相對權重。可設置的範圍爲 0 到 100,值越小,表示讓內核交換越少內存頁到交換空間,值越大,表示讓內核更多地使用交換空間。默認和/proc/sys/vm/swappiness的值相同。0 不會阻止內存頁被換出,當內存資源不足時,仍然可能被換出到交換分區。

⚠️ 注意:不能修改 root cgroup 中的 memory.swappiness,它繼承 /proc/sys/vm/swappiness的值。如果有子 cgroup,也不能修改其 memory.swappiness 的值。

內核態內存資源限制和統計

  • memory.kmem.limit_in_bytes:可用內核內存資源的閾值。默認單位是 bytes,可以通過 K、M、G(不區分大小寫)後綴指定單位,-1 表示不限制。已被棄用, kmem: further deprecate kmem.limit_in_bytes[1],從 linux v5.16-rc1[2] 開始,設置 memory.kmem.limit_in_bytes 的值會返回 -ENOTSUPP 錯誤。

  • memory.kmem.max_usage_in_bytes:使用內核內存資源的峯值,單位 bytes。

  • memory.kmem.usage_in_bytes:當前時刻 cgroup 中所有任務使用內核內存的總量,單位 bytes。

  • memory.kmem.failcnt:記錄 cgroup 中所有任務使用內核內存的總量達到 memory.kmem.limit_in_bytes 的次數。

  • memory.kmem.tcp.limit_in_bytes:cgroup 中所有任務使用 TCP 協議的內存資源總量閾值。默認單位是 bytes,可以通過 K、M、G(不區分大小寫)後綴指定單位,-1 表示不限制。

  • memory.kmem.tcp.max_usage_in_bytes:記錄 cgroup 中所有任務使用 TCP 協議的內存資源總量峯值,單位 bytes。

  • memory.kmem.tcp.failcnt:記錄使用 TCP 協議的內存資源總量達到 memory.kmem.tcp.limit_in_bytes 的次數。

  • memory.kmem.slabinfo:記錄內核 slab 分配器的狀態。slab 分配器是內核管理 cgroup 內存分配的一種機制,從 buddy 分配器中申請內存,然後將申請的內存進行更細粒度(以字節爲單位)管理。

OOM 控制

  • memory.oom_control:是否啓用 OOM-killer,如果不啓用(oom_kill_disable 的值爲 1),當觸發 OOM 時,進程會 hang 住(進程處於 D 狀態),等待有足夠的內存資源後繼續運行。如果啓用,觸發 OOM 之後會直接終止進程以釋放內存。默認啓用(oom_kill_disable 的值爲 0)。以下是 memory.oom_control 的示例:
> cat memory.oom_control
oom_kill_disable 0
under_oom 0
oom_kill 0


oom_kill_disable:是否禁用 OOM-killer,直接寫 memory.oom_control 文件會更新該值。

under_oom:當前時刻是否處於 OOM 狀態。如果值爲 1,表示正處於 OOM 狀態,進程可能被終止。

oom_kill:cgroup 中被 OOM-killer 殺死的進程數量。

Event 控制

  • cgroup.event_control:提供 event_fd() 的接口。有 3 種用途:

  • 內存閾值:memory cgroup 通過 cgroups notification API 實現內存閾值功能,允許設置多個 memory 和 memory + swap 閾值,當 cgroup 中任務使用內存達到閾值之後通知用戶態進程。註冊閾值的步驟:(1)通過 eventfd(2) 系統調用創建 event_fd;(2)打開 memory.usage_in_bytes 或 memory.memsw.usage_in_bytes 文件,得到 fd;(3)向 cgroup.event_control 文件寫入 "<event_fd> <usage_in_bytes 文件的 fd> <閾值>" 。

  • OOM 控制:上文提到的 memory.oom_control 文件是爲了 OOM 通知等其它控制,通過 cgroup notification API,可以註冊多個 OOM 通知客戶端,在觸發 OOM 的時候得到通知。註冊通知器的步驟:(1)通過 eventfd(2) 系統調用創建 event_fd;(2)打開 memory.oom_control 文件,得到 fd;(3)向 cgroup.event_control 文件寫入 "<event_fd> <memory.oom_control 文件的 fd>" 。

  • 內存壓力:memory.pressure_level 可以用於監控內存分配開銷,基於內存壓力,應用可以實現不同的策略用於管理內存資源。

    內存壓力的定義:

  • low:系統正在回收內存資源以滿足新的分配請求。應用程序收到通知後,可以分析 vmstat 並提前採取措施(關閉不重要的服務)

  • medium:系統處於中等內存壓力下,可能正在向交換分區換出內存頁,將文件頁緩存寫入磁盤等。此時,應用程序可以進一步分析 vmstat/zoneinfo/memcg 或內部內存使用信息,釋放任何容易重建或從磁盤重新讀取的資源。

  • critical:系統正在積極進行內存回收,即將達到 OOM 狀態並且觸發 OOM-killer。此時,應用程序再分析 vmstat 等信息已經晚了,應該立即採取行動。

默認情況下,通知事件是向上傳播的,直到被處理。例如,有 3 個 cgroup A、B、C,A 是 B 的父 cgroup,B 是 C 的父 cgroup。假如 C 的內存壓力較大,只有 C 能收到通知,A 和 B 不會收到。B 只有在 C 中沒有通知的時候才能收到通知。

傳播行爲有 3 種模式:

  • default:與不設置參數的效果一樣,爲了兼容而保留。

  • hierarchy:事件總是向上傳播到 root cgroup,與默認行爲類似,只是每一級無論是否有事件監聽器,傳播都會繼續。對於上面的例子,A 和 B 都會收到通知。

  • local:只有註冊了事件通知器的 cgroup 纔會收到通知,對於上面的例子,如果 C 註冊了 local 通知器,將會收到通知,但是對於 B,無論 C 是否註冊事件通知器,B 都不會收到通知。內存壓力等級和通知模式通過逗號分隔組成字符串,例如:"low,hierarchy" 表示內存壓力處於 low 等級時向層級的 root cgroup 傳播通知。

cgroup.event_control 在啓用 CONFIG_PREEMPT_RT 的系統(內核支持完全搶佔,在某些場景可以提升性能)中不可用。

Demo 體驗

我們以 memory 爲例,使用memhog工具模擬使用內存資源。可以通過以下命令安裝memhog

sudo apt install numactl


使用 memhog 使用 100 MB 內存資源:

memhog 100M


使用以下腳本,每 2 秒使用 100 MB 內存資源:

while true; do memhog 100M; sleep 2; done


使用cgroup-tools工具管理 cgroup,安裝方式:

sudo apt install cgroup-tools


Demo 的步驟如下:

  • 使用cgroup-tools創建新的 cgroup
❯ cgcreate -g memory:memhog-limiter
❯ pwd
/sys/fs/cgroup/memory/memhog-limiter
❯ ls
cgroup.clone_children           memory.kmem.tcp.max_usage_in_bytes  memory.oom_control
cgroup.event_control            memory.kmem.tcp.usage_in_bytes      memory.pressure_level
cgroup.procs                    memory.kmem.usage_in_bytes          memory.soft_limit_in_bytes
memory.failcnt                  memory.limit_in_bytes               memory.stat
memory.force_empty              memory.max_usage_in_bytes           memory.swappiness
memory.kmem.failcnt             memory.memsw.failcnt                memory.usage_in_bytes
memory.kmem.limit_in_bytes      memory.memsw.limit_in_bytes         memory.use_hierarchy
memory.kmem.max_usage_in_bytes  memory.memsw.max_usage_in_bytes     memory.watermark
memory.kmem.slabinfo            memory.memsw.usage_in_bytes         memory.watermark_scale_factor
memory.kmem.tcp.failcnt         memory.move_charge_at_immigrate     notify_on_release
memory.kmem.tcp.lim


  • 設置 cgroup 的最大可用內存資源閾值
❯ cgset -r memory.limit_in_bytes=50M memhog-limiter
❯ cat memory.limit_in_bytes
52428800


該命令將memhog-limitermemory.limit_in_bytes設置爲50M,和直接修改memory.limit_in_bytes文件內容的效果一樣。

  • 使用memhog模擬內存分配,並將使用內存的進程加入 cgroup
tee memhogtest.sh <<- EOF
echo "PID: \$\$"
while true; do memhog 100M; sleep 2; done
EOF
chmod +x memhogtest.sh

# 清理 dmesg 消息
dmesg -C
# 在目標 cgroup 中運行進程
cgexec -g memory:memhog-limiter ./memhogtest.sh
# 查看 dmesg 消息
dmesg


可以看到,系統觸發了 OOM-killer:

[615794.049343] memhog invoked oom-killer: gfp_mask=0xcc0(GFP_KERNEL), order=0, oom_score_adj=0
[615794.052039] CPU: 1 PID: 3530984 Comm: memhog Kdump: loaded Tainted: G           OE     5.4.143.bsk.8-amd64 #5.4.143.bsk.8
[615794.055448] Hardware name: OpenStack Nova, BIOS
[615794.057145] Call Trace:
[615794.058352]  dump_stack+0x66/0x81
[615794.059625]  dump_header+0x4a/0x1d8
[615794.061334]  oom_kill_process.cold.36+0xb/0x10
[615794.063582]  out_of_memory+0x1a8/0x4a0
[615794.065409]  mem_cgroup_out_of_memory+0xbe/0xd0
[615794.067044]  try_charge_memcg+0x67d/0x6e0
[615794.068879]  ? __alloc_pages_nodemask+0x163/0x310
[615794.070705]  mem_cgroup_charge+0x88/0x250
[615794.072616]  handle_mm_fault+0xa78/0x12e0
[615794.074560]  do_user_addr_fault+0x1ce/0x4d0
[615794.076534]  do_page_fault+0x30/0x110
[615794.078265]  async_page_fault+0x3e/0x50
[615794.079908] RIP: 0033:0x7f43b701c55d
[615794.081798] Code: 01 00 00 48 83 fa 40 77 77 c5 fe 7f 44 17 e0 c5 fe 7f 07 c5 f8 77 c3 66 0f 1f 44 00 00 c5 f8 77 48 89 d1 40 0f b6 c6 48 89 fa <f3> aa 48 89 d0 c3 66 66 2e 0f 1f 84 00 00 00 00 00 66 90 48 39 d1
[615794.087942] RSP: 002b:00007ffc8bf8cca8 EFLAGS: 00010206
[615794.089897] RAX: 00000000000000ff RBX: 0000000000a00000 RCX: 0000000000072000
[615794.092202] RDX: 00007f43b32bd000 RSI: 00000000000000ff RDI: 00007f43b3c4b000
[615794.094799] RBP: 0000000003200000 R08: 00007f43b707c8c0 R09: 00007f43b6ebd740
[615794.097459] R10: 000000000000002e R11: 0000000000000246 R12: 00007f43b0abd000
[615794.100303] R13: 0000000000a00000 R14: 00007ffc8bf8ce08 R15: 0000000000000000
[615794.103284] memory: usage 51200kB, limit 51200kB, failcnt 994
[615794.105697] memory+swap: usage 51200kB, limit 9007199254740988kB, failcnt 0
[615794.107948] kmem: usage 260kB, limit 9007199254740988kB, failcnt 0
[615794.109792] Memory cgroup stats for /memhog-limiter:
[615794.109849] anon 52445184
                file 0
                kernel_stack 0
                sock 0
                shmem 0
                file_mapped 0
                file_dirty 0
                file_writeback 0
                anon_thp 0
                inactive_anon 0
                active_anon 52445184
                inactive_file 0
                active_file 0
                unevictable 0
                slab_reclaimable 0
                slab_unreclaimable 0
                slab 0
                bgd_reclaim 0
                workingset_refault 0
                workingset_activate 0
                workingset_nodereclaim 0
                pgfault 449823
                pgmajfault 0
                pgrefill 0
                pgscan 0
                pgsteal 0
                pgactivate 0
                pgdeactivate 0
                pglazyfree 0
                pglazyfreed 0
                thp_fault_alloc 0
                thp_collapse_alloc 0
[615794.152864] Tasks state (memory values in pages):
[615794.154685] [  pid  ]   uid  tgid total_vm      rss pgtables_bytes swapents oom_score_adj name
[615794.157657] [3529173]     0 3529173      595      377    45056        0             0 sh
[615794.160614] [3530984]     0 3530984    26184    13040   151552        0             0 memhog
[615794.163519] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=/,mems_allowed=0-1,oom_memcg=/memhog-limiter,task_memcg=/memhog-limiter,task=memhog,pid=3530984,uid=0
[615794.168572] Memory cgroup out of memory: Killed process 3530984 (memhog) total-vm:104736kB, anon-rss:50704kB, file-rss:1456kB, shmem-rss:0kB, UID:0 pgtables:148kB oom_score_adj:0
[615794.180304] oom_reaper: reaped process 3530984 (memhog), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB


通過memory.failcnt文件可以看到觸發 OOM 的次數:

❯ cat memory.failcnt
1128


cgroup V2

在 cgroup v2 中,需要通過父 cgroup 的 cgroup.subtree_control開啓 memory controller,爲當前 cgroup 開啓 memory controller 之後,會新增 memory 相關的文件(memory.pressure在沒開啓 memory controller 的情況下也存在):

❯ cat cgroup.controllers
memory
❯ ls memory.*
memory.current
memory.drop_cache
memory.events
memory.events.local
memory.high
memory.low
memory.max
memory.min
memory.numa_stat
memory.oom.group
memory.pressure
memory.reclaim
memory.stat
memory.swap.current
memory.swap.events
memory.swap.max

對於 cgroup 子系統中的數據,單位都是 bytes,如果寫入的值沒有按 PAGE_SIZE 對齊,則會增加到最近 PAGE_SIZE 對齊的值。

用戶態內存資源限制和統計

  • memory.current:當前 cgroup 及子 cgroup 使用的內存總量。

  • memory.high:可讀可寫,存在於非 root cgroup 中。當前 cgroup 中所有進程使用內存的上限。當內存使用總量超過memory.high之後,此時還不會觸發 OOM,kernel 會盡力回收內存(回收壓力較大),確保內存使用總量低於memory.highmemory.high可以用在有外部進程監控當前 cgroup 的場景下,以便主動回收內存,從而減輕 kernel 回收內存的壓力。

  • memory.low:可讀可寫,存在於非 root cgroup 中,默認值爲 0。如果當前 cgroup 內存使用總量低於有效 low 邊界,盡力不回收內存,除非在未受保護 cgroup 中沒有可回收的內存(也就是說,即使內存使用總量低於memory.low,也有可能被回收)。如果當前 cgroup 內存使用總量高於這個值,kernel 會按比例回收內存頁,以減少回收壓力。

有效 low 邊界由祖先 cgroup 的 memory.low決定,如果子 cgroup 的memory.low值大於祖先 cgroup(需要更多受保護的內存),則每個子 cgroup 將獲得與其實際內存使用量成比例的父 cgroup 保護內存。

低於 memory.low 的內存儘量不被回收,因此,在使用過程中,不適合設置爲比實際使用量更高的值。

  • memory.max:可讀可寫,默認爲 max(不限制)。內存使用量的硬限制,如果 cgroup 中所有進程總的內存使用量高於這個值並且不能減小,將會觸發 OOM-killer,在某些場景下,可能會短時間超過限制。

默認情況下,分配內存總是會成功,除非當前進程被 OOM-killer 選中。某些情況下,可能不會觸發 OOM-killer,而是返回-ENOMEM給用戶態進程,此時,用戶可以重新嘗試分配內存。

  • memory.min:可讀可寫,存在於非 root cgroup 中。硬內存保護,如果內存使用量低於有效 min 邊界,無論如何,這部分內存都不會被回收。如果沒有不受保護並且可回收的內存,則會觸發 OOM-killer。內存使用量高於有效 min 邊界,內存頁會按比例被回收。

有效 min 邊界由祖先 cgroup 決定,如果子 cgroup 的memory.min值大於祖先 cgroup(需要更多受保護的內存),則每個子 cgroup 將獲得與其實際內存使用量成比例的父 cgroup 保護內存。

不鼓勵將memory.min設置爲比實際使用量更多的值,這可能會導致持續的 OOM,如果 cgroup 中沒有進程,memory.min會被忽略。

high,low,max,min 的關係:

  • memory.stat:只讀,存在非 root cgroup 中。包含不同類型內存的詳細信息,以及內存管理系統的狀態和事件信息。單位都是 bytes。如果某個條目沒有 node 級別的統計,使用 npn(non-per-node)標記,不會出現在memory.numa_stat文件中。
❯ cat memory.stat
anon 0
file 0
kernel_stack 0
sock 0
shmem 0
file_mapped 0
file_dirty 0
file_writeback 0
anon_thp 0
inactive_anon 0
active_anon 0
inactive_file 0
active_file 0
unevictable 0
slab_reclaimable 0
slab_unreclaimable 0
slab 0
bgd_reclaim 0
workingset_refault_anon 0
workingset_refault_file 0
workingset_activate_anon 0
workingset_activate_file 0
workingset_restore_anon 0
workingset_restore_file 0
workingset_nodereclaim 0
pgscan 0
pgsteal 0
pgscan_kswapd 0
pgscan_direct 0
pgsteal_kswapd 0
pgsteal_direct 0
pgfault 0
pgmajfault 0
pgrefill 0
pgactivate 0
pgdeactivate 0
pglazyfree 0
pglazyfreed 0
thp_fault_alloc 0
thp_collapse_alloc 0


anon:在匿名映射中使用的內存,如 brk()、sbrk() 和 mmap(MAP_ANONYMOUS)。

file:用於緩存文件系統數據的內存,包括臨時文件和共享內存。

kernel_stack:分配給內核棧的內存。

sock:用於網絡傳輸緩衝區的內存使用量。

shmem:支持 swap 的緩存文件系統數據,例如 tmpfs、shm 段、共享匿名 mmap()。

file_mapped:用 mmap() 映射的緩存文件系統數據。

file_dirty:被修改但尚未寫回磁盤的緩存文件系統數據。

file_writeback:緩存到文件系統的數據,該數據已被修改,目前正被寫回磁盤。

anon_thp:由透明 hugepages 支持的匿名映射使用的內存。

inactive_anon、active_anon、inactive_file、active_file、unevictable:頁面回收算法管理內部內存管理列表使用的內存,包括支持 swap 和支持文件系統的內存。

slab_reclaimable:slab 中可能被回收的部分,如 dentries 和 inodes。

slab_unreclaimable:slab 中不可被回收的部分。

slab(npn):用於存儲內核內數據結構的內存量。

workingset_refault_anon:被驅逐的匿名頁再次被訪問觸發 fault 的次數。

workingset_refault_file:被驅逐的文件頁再次被訪問觸發 fault 的次數。

workingset_activate_anon:再次訪問觸發 fault 的且立即處於 actived 狀態的匿名頁數量。

workingset_activate_file:再次訪問觸發 fault 的且立即處於 actived 狀態的文件頁數量。

workingset_restore_anon:被恢復的匿名頁數量。

workingset_restore_file:被恢復的文件頁數量。

workingset_nodereclaim:影子節點被回收的次數。

pgscan(npn):已掃描頁面的數量(在非活動的 LRU 列表中)。

pgsteal(npn):回收的頁面數量。

pgscan_kswapd(npn):kswapd 掃描的頁數(在非活動的 LRU 列表中)。

pgscan_direct(npn):直接掃描的頁面數量(在非活動的 LRU 列表中)。

pgsteal_kswapd(npn):kswapd 回收的頁面數量。

pgsteal_direct(npn):直接回收的頁面數量。

pgfault(npn):觸發 pagfault 的數量。

pgmajfault(npn):發生 major pagefault 的數量。(被訪問的數據不在虛擬地址空間,也不在物理內存中,需要從慢速存儲介質讀取。)

pgrefill(npn):被掃描的頁面數量(位於 active LRU 列表)。

pgactivate(npn):被移動到 active LRU 列表中的頁面數量。

pgdeactivate(npn):移動到 inactive LRU 列表中的頁面數量。

pglazyfree(npn):在內存壓力下被推遲釋放的頁面數量。

pglazyfreed(npn):已經被回收的“推遲迴收頁面”的數量。

thp_fault_alloc(npn):爲響應 pagefault 而分配的透明 hugepages 數量。如果沒有設置 CONFIG_TRANSPARENT_HUGEPAGE,不存在這個計數器。

thp_collapse_alloc(npn):爲允許摺疊現有頁面範圍而分配的透明 hugepages 數量。如果沒有設置 CONFIG_TRANSPARENT_HUGEPAGE,不存在這個計數器。

  • memory.numa_stat:只讀,存在非 root cgroup 中。包含不同類型內存的詳細信息,以及每個 node 的其它內存管理信息。
❯ cat memory.numa_stat
anon N0=0
file N0=0
kernel_stack N0=0
shmem N0=0
file_mapped N0=0
file_dirty N0=0
file_writeback N0=0
anon_thp N0=0
inactive_anon N0=0
active_anon N0=0
inactive_file N0=0
active_file N0=0
unevictable N0=0
slab_reclaimable N0=0
slab_unreclaimable N0=0
workingset_refault_anon N0=0
workingset_refault_file N0=0
workingset_activate_anon N0=0
workingset_activate_file N0=0
workingset_restore_anon N0=0
workingset_restore_file N0=0
workingset_nodereclaim N0=0


交換空間大小限制和統計

  • memory.swap.current:當前 cgroup 及子 cgroup 使用交換分區(swap)的總量。

  • memory.swap.events:swap 相關限制觸發的事件數量。

> cat memory.swap.events
max 0
fail 0


max:當前 cgroup 的 swap 使用量即將超過memory.swap.max而導致 swap 分配失敗的次數。

fail:swap 分配失敗的次數,原因是系統 swap 已經用完或達到設置的memory.swap.max限制。

  • memory.swap.max:swap 的硬限制。如果當前 cgroup 的 swap 使用量達到memory.swap.max,匿名內存將不會被交換出去。

內核態內存資源限制和統計

cgroup v2 沒有專門提供關於內核內存資源限制和統計的接口文件,部分統計信息已經包括在memory.stat文件中。

OOM 控制

  • memory.oom.group:可讀可寫,存在於非 root cgroup,默認值爲 0。如果設置爲 1,當觸發 OOM 後當前 cgroup 及子孫 cgroup 內的所有進程(除了 oom_score_adj 爲 -1000 的進程)都會被殺死,或者都不會被殺死。這是爲了保證 cgroup 中負載的完整性(有些負載在部分進程被殺死的情況下不能正常工作)。如果 OOM-killer 在當前 cgroup 被觸發,無論祖先 cgroup 的memory.oom.group如何,都不會殺死當前 cgroup 之外的任何進程。

Event 控制

  • memory.drop_cache:可寫文件,用於手動釋放內存緩存。

  • memory.reclaim:存在於所有 cgroup 中,用於觸發目標 cgroup 的內存回收。文件接收的值是需要回收的內存數量。例如:

echo "1G" > memory.reclaim


Kernel 實際回收的內存數量可能不等於期望值,如果少於指定的值,將返回-EAGAIN

  • memory.events:只讀文件,存在非 root cgroup 中,統計內存限制相關事件的觸發次數。包括當前 cgroup 和所有子 cgroup 觸發事件的總和,當前 cgroup 的事件觸發情況在memory.events.local文件。
❯ cat memory.events
low 0
high 0
max 0
oom 0
oom_kill 0


low:內存使用量低於memory.low,由於高內存壓力而觸發了內存回收的次數。通常是 low 設置的值過大導致的情況。

high:內存使用量高於 high 被節流並進行直接內存回收的次數。

max:內存使用量即將超過memory.max的次數,如果回收沒有成功,則會進入 OOM 狀態。

oom:內存使用量達到限制並進入 OOM 狀態的次數,此時嘗試分配內存將會失敗。

oom_kill:cgroup 中的進程被 OOM-killer 殺死的次數。

  • memory.events.local:同上,只包含當前 cgroup 的數據。

Demo 體驗

使用 cgroup v1 demo 用於定時分配內存的腳本:

❯ ./memhogtest.sh
PID: 746644


加入 cgroup,同時設置 cgroup 的 memory.max 爲 100M:

❯ echo 746644 > cgroup.procs && echo 100M > memory.max

進程由於觸發 OOM 被 kill:

Kernel 日誌:

[1367090.223722] memhog invoked oom-killer: gfp_mask=0xcc0(GFP_KERNEL), order=0, oom_score_adj=0
[1367090.226156] CPU: 2 PID: 748251 Comm: memhog Kdump: loaded Tainted: G           OE     5.4.210.bsk.4-amd64 #5.4.210.bsk.4
[1367090.229922] Hardware name: OpenStack Nova, BIOS
[1367090.232181] Call Trace:
[1367090.233575]  dump_stack+0x66/0x81
[1367090.235116]  dump_header+0x4a/0x1d8
[1367090.236588]  oom_kill_process.cold.39+0xb/0x10
[1367090.238146]  out_of_memory+0x1a8/0x4d0
[1367090.239713]  mem_cgroup_out_of_memory+0xbe/0xd0
[1367090.241454]  try_charge_memcg+0x67d/0x6e0
[1367090.243018]  ? __alloc_pages_nodemask+0x163/0x310
[1367090.244723]  mem_cgroup_charge+0x88/0x250
[1367090.246296]  handle_mm_fault+0xa78/0x12e0
[1367090.247784]  do_user_addr_fault+0x1ce/0x4d0
[1367090.249285]  do_page_fault+0x30/0x110
[1367090.250635]  async_page_fault+0x3e/0x50
[1367090.252166] RIP: 0033:0x7ffb41f158bd
[1367090.253617] Code: 01 00 00 48 83 fa 40 77 77 c5 fe 7f 44 17 e0 c5 fe 7f 07 c5 f8 77 c3 66 0f 1f 44 00 00 c5 f8 77 48 89 d1 40 0f b6 c6 48 89 fa <f3> aa 48 89 d0 c3 66 66 2e 0f 1f 84 00 00 00 00 00 66 90 48 39 d1
[1367090.258600] RSP: 002b:00007fff630afe68 EFLAGS: 00010206
[1367090.260620] RAX: 00000000000000ff RBX: 0000000000a00000 RCX: 000000000006c000
[1367090.263249] RDX: 00007ffb413b6000 RSI: 00000000000000ff RDI: 00007ffb41d4a000
[1367090.265448] RBP: 0000000006400000 R08: 00007ffb41f758c0 R09: 00007ffb41db6740
[1367090.267754] R10: 000000000000002e R11: 0000000000000246 R12: 00007ffb3b9b6000
[1367090.269798] R13: 0000000000a00000 R14: 00007fff630affc8 R15: 0000000000000000
[1367090.272007] memory: usage 102400kB, limit 102400kB, failcnt 570
[1367090.274263] swap: usage 0kB, limit 9007199254740988kB, failcnt 0
[1367090.276337] Memory cgroup stats for /test/child:
[1367090.276471] anon 104620032
                 file 0
                 kernel_stack 73728
                 sock 0
                 shmem 0
                 file_mapped 0
                 file_dirty 0
                 file_writeback 0
                 anon_thp 0
                 inactive_anon 0
                 active_anon 104484864
                 inactive_file 0
                 active_file 0
                 unevictable 0
                 slab_reclaimable 0
                 slab_unreclaimable 135288
                 slab 135288
                 bgd_reclaim 0
                 workingset_refault 0
                 workingset_activate 0
                 workingset_nodereclaim 0
                 pgscan 0
                 pgsteal 0
                 pgscan_kswapd 0
                 pgscan_direct 0
                 pgsteal_kswapd 0
                 pgsteal_direct 0
                 pgfault 745041
                 pgmajfault 0
                 pgrefill 0
                 pgactivate 0
                 pgdeactivate 0
                 pglazyfree 0
                 pglazyfreed 0
                 thp_fault_alloc 0
                 thp_collapse_alloc 0
[1367090.306459] Tasks state (memory values in pages):
[1367090.307975] [  pid  ]   uid  tgid total_vm      rss pgtables_bytes swapents oom_score_adj name
[1367090.310435] [ 746644]  1001 746644      595      406    45056        0             0 sh
[1367090.312420] [ 748251]  1001 748251    26184    25838   249856        0             0 memhog
[1367090.314790] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=test,mems_allowed=0-1,oom_memcg=/test/child,task_memcg=/test/child,task=memhog,pid=748251,uid=1001
[1367090.319170] Memory cgroup out of memory: Killed process 748251 (memhog) total-vm:104736kB, anon-rss:101924kB, file-rss:1428kB, shmem-rss:0kB, UID:1001 pgtables:244kB oom_score_adj:0


查看 cgroup.events:

❯ cat memory.events.local
low 0
high 0
max 177
oom 21
oom_kill 21


參考資料

[1] kmem: further deprecate kmem.limit_in_bytes: https://github.com/torvalds/linux/commit/58056f77502f3567b760c9a8fc8d2e9081515b2d

[2] v5.16-rc1: https://github.com/torvalds/linux/releases/tag/v5.16-rc1

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