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-limiter
的memory.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.high
。memory.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