深入剖析 Linux Cgroups 子系统:资源精细管理
本章主要演示以下 cgroups 下各個 subsystem 的作用。
根據難易程度,依次演示了 pids 、cpu 和 memory 3 個 subsystem 的使用。
注:本文所有操作在 Ubuntu20.04 下進行。
如果你對云原生技術充滿好奇,想要深入了解更多相關的文章和資訊,歡迎關注微信公眾號。
搜索公眾號【探索云原生】即可訂閱
1. pids
pids subsystem 功能是限制 cgroup 及其所有子孫 cgroup 里面能創建的總的 task 數量。
注意:這里的 task 指通過 fork 和 clone 函數創建的進程,由于 clone 函數也能創建線程(在 Linux 里面,線程是一種特殊的進程),所以這里的 task 也包含線程。
本文統一以進程來代表 task,即本文中的進程代表了進程和線程>
創建子 cgroup
創建子 cgroup,取名為 test
#進入目錄/sys/fs/cgroup/pids/并新建一個目錄,即創建了一個子cgroup
lixd /home/lixd $ cd /sys/fs/cgroup/pids
lixd /sys/fs/cgroup/pids $ sudo mkdir test
再來看看 test 目錄下的文件
lixd /sys/fs/cgroup/pids $ cd test
#除了上一篇中介紹的那些文件外,多了兩個文件
lixd /sys/fs/cgroup/pids/test $ ls
cgroup.clone_children cgroup.procs notify_on_release pids.current pids.events pids.max tasks
下面是這兩個文件的含義:
- pids.current: 表示當前 cgroup 及其所有子孫 cgroup 中現有的總的進程數量
- pids.max: 當前 cgroup 及其所有子孫 cgroup 中所允許創建的總的最大進程數量
限制進程數
首先是將當前 bash 加入到 cgroup 中,并修改pids.max的值,為了便于測試,這里就限制為 1:
#--------------------------第一個shell窗口----------------------
# 將當前bash進程加入到該cgroup
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/pids/test# echo $$ > cgroup.procs
#將pids.max設置為1,即當前cgroup只允許有一個進程
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/pids/test# echo 1 > pids.max
由于 bash 已經占用了一個進程,所以此時 bash 中已經無法創建新的進程了:
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/pids/test# ls
bash: fork: retry: Resource temporarily unavailable
bash: fork: retry: Resource temporarily unavailable
bash: fork: retry: Resource temporarily unavailable
創建新進程失敗,于是命令運行失敗,說明限制生效。
打開另一個 shell 查看
lixd /mnt/c/Users/意琦行 $ cd /sys/fs/cgroup/pids/test
lixd /sys/fs/cgroup/pids/test $ ls
cgroup.clone_children cgroup.procs notify_on_release pids.current pids.events pids.max tasks
lixd /sys/fs/cgroup/pids/test $ cat pids.current
1
果然,pids.current 為 1,已經到 pids.max 的限制了。
當前 cgroup 和子 cgroup 之間的關系
當前 cgroup 中的 pids.current 和 pids.max 代表了當前 cgroup 及所有子孫 cgroup 的所有進程,所以子孫 cgroup 中的 pids.max 大小不能超過父 cgroup。
如果子 cgroup 中的 pids.max 設置的大于父 cgroup 里的值,會怎么樣?
答案是子 cgroup 中的進程不光受子 cgroup 限制,還要受其父 cgroup 的限制。
#繼續使用上面的兩個窗口
#--------------------------第二個shell窗口----------------------
#將pids.max設置成2
dev@dev:/sys/fs/cgroup/pids/test$ echo 2 > pids.max
#在test下面創建一個子cgroup
dev@dev:/sys/fs/cgroup/pids/test$ mkdir subtest
dev@dev:/sys/fs/cgroup/pids/test$ cd subtest/
#將subtest的pids.max設置為5
dev@dev:/sys/fs/cgroup/pids/test/subtest$ echo 5 > pids.max
#將當前bash進程加入到subtest中
dev@dev:/sys/fs/cgroup/pids/test/subtest$ echo $$ > cgroup.procs
#--------------------------第三個shell窗口----------------------
#重新打開一個bash窗口,看一下test和subtest里面的數據
#test里面的數據如下:
dev@dev:~$ cd /sys/fs/cgroup/pids/test
dev@dev:/sys/fs/cgroup/pids/test$ cat pids.max
2
#這里為2表示目前test和subtest里面總的進程數為2
dev@dev:/sys/fs/cgroup/pids/test$ cat pids.current
2
dev@dev:/sys/fs/cgroup/pids/test$ cat cgroup.procs
3083
#subtest里面的數據如下:
dev@dev:/sys/fs/cgroup/pids/test$ cat subtest/pids.max
5
dev@dev:/sys/fs/cgroup/pids/test$ cat subtest/pids.current
1
dev@dev:/sys/fs/cgroup/pids/test$ cat subtest/cgroup.procs
3185
#--------------------------第一個shell窗口----------------------
#回到第一個窗口,隨便運行一個命令,由于test里面的pids.current已經等于pids.max了,
#所以創建新進程失敗,于是命令運行失敗,說明限制生效
dev@dev:/sys/fs/cgroup/pids/test$ ls
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: Resource temporarily unavailable
#--------------------------第二個shell窗口----------------------
#回到第二個窗口,隨便運行一個命令,雖然subtest里面的pids.max還大于pids.current,
#但由于其父cgroup “test”里面的pids.current已經等于pids.max了,
#所以創建新進程失敗,于是命令運行失敗,說明子cgroup中的進程數不僅受自己的pids.max的限制,還受祖先cgroup的限制
dev@dev:/sys/fs/cgroup/pids/test/subtest$ ls
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: retry: No child processes
-bash: fork: Resource temporarily unavailable
pids.current > pids.max 的情況
并不是所有情況下都是 pids.max >= pids.current,在下面兩種情況下,會出現 pids.max < pids.current 的情況:
- 設置 pids.max 時,將其值設置的比 pids.current 小
- 將其他進程加入到當前 cgroup 有可能會導致 pids.current > pids.max
- 因為 pids.max 只會在當前 cgroup 中的進程 fork、clone 的時候生效,將其他進程加入到當前 cgroup 時,不會檢測 pids.max,所以可能觸發這種情況
小結
作用:pids subsystem 用于限制 cgroups 下能夠創建的 task(進程和線程)數。
原理:在調用 fork 和 clone 時對比 subsystem 中配置的 pids.max 和 pids.current 值來判斷當前是否能夠繼續創建 task。
用法:配置 pids.max 防止容器消耗完 pid。
2. cpu
在 cgroup 里面,跟 CPU 相關的子系統有 cpusets、cpuacct 和 cpu。
-
其中 cpuset 主要用于設置 CPU 的親和性,可以限制 cgroup 中的進程只能在指定的 CPU 上運行,或者不能在指定的 CPU 上運行,同時 cpuset 還能設置內存的親和性。設置親和性一般只在比較特殊的情況才用得著,所以這里不做介紹。
-
cpuacct 包含當前 cgroup 所使用的 CPU 的統計信息,信息量較少,有興趣可以去看看它的文檔,這里不做介紹。
本節只介紹 cpu 子系統,包括怎么限制 cgroup 的 CPU 使用上限及相對于其它 cgroup 的相對值。
創建子 cgroup
通用是創建子目錄即可。
#進入/sys/fs/cgroup/cpu并創建子cgroup
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu# cd /sys/fs/cgroup/cpu
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu# mkdir test
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu# cd test/
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu/test# ls
cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release
cgroup.procs cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks
看起來文件比 memory subsystem 還是少一些。
cpu.cfs_period_us & cpu.cfs_quota_us:兩個文件配合起來設置 CPU 的使用上限,兩個文件的單位都是微秒(us)。
- cfs_period_us:用來配置時間周期長度
- 取值范圍為 1 毫秒(ms)到 1 秒(s)
- cfs_quota_us:用來配置當前 cgroup 在設置的周期長度內所能使用的 CPU 時間數
- 取值大于 1ms 即可
- 默認值為 -1,表示不受 cpu 時間的限制。
cpu.shares 用來設置 CPU 的相對值(比例),并且是針對所有的 CPU(內核),默認值是 1024。
假如系統中有兩個 cgroup,分別是 A 和 B,A 的 shares 值是 1024,B 的 shares 值是 512,那么 A 將獲得 1024/(1204+512)=66% 的 CPU 資源,而 B 將獲得 33% 的 CPU 資源。
shares 有兩個特點:
- 如果 A 不忙,沒有使用到 66% 的 CPU 時間,那么剩余的 CPU 時間將會被系統分配給 B,即 B 的 CPU 使用率可以超過 33%
- 如果添加了一個新的 cgroup C,且它的 shares 值是 1024,那么 A 的限額變成了 1024/(1204+512+1024)=40%,B 的變成了 20%
從上面兩個特點可以看出:
- 在閑的時候,shares 基本上不起作用,只有在 CPU 忙的時候起作用,這是一個優點。
- 由于 shares 是一個絕對值,需要和其它 cgroup 的值進行比較才能得到自己的相對限額,而在一個部署很多容器的機器上,cgroup 的數量是變化的,所以這個限額也是變化的,自己設置了一個高的值,但別人可能設置了一個更高的值,所以這個功能沒法精確的控制 CPU 使用率。
cpu.stat 包含了下面三項統計結果:
- nr_periods: 表示過去了多少個 cpu.cfs_period_us 里面配置的時間周期
- nr_throttled: 在上面的這些周期中,有多少次是受到了限制(即 cgroup 中的進程在指定的時間周期中用光了它的配額)
- throttled_time: cgroup 中的進程被限制使用 CPU 持續了多長時間(納秒)
原理
前面配置的參數都是 cfs_xxx,這里的 cfs 是 Completely Fair Scheduler 的縮寫。
CFS 是 Linux 內核中的調度器,它負責決定哪個進程在給定時間片內運行。CFS 使用 CFS 配額(cpu.cfs_quota_us)和 CFS 周期(cpu.cfs_period_us)來限制每個 cgroup 中的 CPU 使用。
CFS 的實現與 cgroups 協同工作,它負責追蹤每個 cgroup 中的進程消耗的 CPU 時間,并在每個調度周期結束時根據 cgroup 的 CPU 配額調整進程的運行時間。
如果一個 cgroup 中的進程在調度周期內超過了它的 CPU 配額,它將被調度器限制,從而實現了 CPU 的使用限制。
即:cgroups 中的 subsystem 負責提供配置,cfs 負責記錄進程使用的 cpu 時間,達到閾值后就從調度層面進行限制,避免該進程繼續使用 cpu。
演示
#繼續使用上面創建的子cgroup: test
#設置只能使用1個cpu的20%的時間
dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ sudo sh -c "echo 50000 > cpu.cfs_period_us"
dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ sudo sh -c "echo 10000 > cpu.cfs_quota_us"
#將當前bash加入到該cgroup
dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ echo $$
5456
dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ sudo sh -c "echo 5456 > cgroup.procs"
#在bash中啟動一個死循環來消耗cpu,正常情況下應該使用100%的cpu(即消耗一個內核)
dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ while :; do echo test > /dev/null; done
#--------------------------重新打開一個shell窗口----------------------
#通過top命令可以看到5456的CPU使用率為20%左右,說明被限制住了
#不過這時系統的%us+%sy在10%左右,那是因為我測試的機器上cpu是雙核的,
#所以系統整體的cpu使用率為10%左右
dev@ubuntu:~$ top
Tasks: 139 total, 2 running, 137 sleeping, 0 stopped, 0 zombie
%Cpu(s): 5.6 us, 6.2 sy, 0.0 ni, 88.2 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 499984 total, 15472 free, 81488 used, 403024 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 383332 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
5456 dev 20 0 22640 5472 3524 R 20.3 1.1 0:04.62 bash
#這時可以看到被限制的統計結果
dev@ubuntu:~$ cat /sys/fs/cgroup/cpu,cpuacct/test/cpu.stat
nr_periods 1436
nr_throttled 1304
throttled_time 51542291833
# cfs_period_us 值為 10W
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu/test# cat cpu.cfs_period_us
100000
# 往 cfs_quota_us 寫入 20000,即限制只能使用20%cpu
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu/test# echo 20000 > cpu.cfs_quota_us
# 新開一個窗口,運行一個死循環
$ while : ; do : ; done &
[1] 519
# top 看一下 cpu 占用率,果然是100%了
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
519 lixd 25 5 13444 2912 0 R 100.0 0.0 0:05.66 zsh
# 回到第一個shell窗口,限制當前進程的cpu使用率
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu/test# echo 519 >> cgroup.procs
# 再切回第二個窗口,發現519進程的cpu已經降到20%了,說明限制生效了
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
519 lixd 25 5 13444 2912 0 R 20.0 0.0 0:31.86 zsh
# 查看被限制的統計結果
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu/test# cat cpu.stat
nr_periods 2090
nr_throttled 2088
throttled_time 166752684900
小結
作用:cpu subsystem 用于限制 cgroups 下進程可以使用的 cpu 上限。
原理:cgroups 中的 subsystem 負責提供配置,cfs 負責記錄進程使用的 cpu 時間,達到閾值后就從調度層面進行限制,避免該進程繼續使用 cpu。
用法:
- 1)限制為具體值:用 cfs_period_us & cfs_quota_us 兩個配置可以嚴格限制進程 cpu 使用量。
- 2)按比例分配:用 shares 配置,可以使得多個 cgroups 之間按比例分配所有 cpu。
3. memory
memory subsystem 顧名思義,限制 cgroups 中進程的內存使用。
為什么需要內存控制
- 站在一個普通開發者的角度,如果能控制一個或者一組進程所能使用的內存數,那么就算代碼有 bug,內存泄漏也不會對系統造成影響,因為可以設置內存使用量的上限,當到達這個值之后可以將進程重啟。
- 站在一個系統管理者的角度,如果能限制每組進程所能使用的內存量,那么不管程序的質量如何,都能將它們對系統的影響降到最低,從而保證整個系統的穩定性。
內存控制能控制些什么?
- 限 制 cgroup 中所有進程所能使用的物理內存總量
- 限制 cgroup 中所有進程所能使用的物理內存+交換空間總量(CONFIG_MEMCG_SWAP): 一般在 server 上,不太會用到 swap 空間,所以不在這里介紹這部分內容。
- 限制 cgroup 中所有進程所能使用的內核內存總量及其它一些內核資源(CONFIG_MEMCG_KMEM): 限制內核內存有什么用呢?其實限制內核內存就是限制當前 cgroup 所能使用的內核資源,比如進程的內核棧空間,socket 所占用的內存空間等,通過限制內核內存,當內存吃緊時,可以阻止當前 cgroup 繼續創建進程以及向內核申請分配更多的內核資源。由于這塊功能被使用的較少,本篇中也不對它做介紹。
創建子 cgroup
在 /sys/fs/cgroup/memory 下創建一個子目錄就算是創建了一個子 cgroup
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory# cd /sys/fs/cgroup/memory
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory# mkdir test
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory# ls test/
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 notify_on_release
memory.kmem.tcp.failcnt memory.memsw.usage_in_bytes tasks
memory.kmem.tcp.limit_in_bytes memory.move_charge_at_immigrate
從上面 ls 的輸出可以看出,除了每個 cgroup 都有的那幾個文件外,和 memory 相關的文件還不少,這里先做個大概介紹(kernel 相關的文件除外),后面會詳細介紹每個文件的作用:
cgroup.event_control #用于eventfd的接口
memory.usage_in_bytes #顯示當前已用的內存
memory.limit_in_bytes #設置/顯示當前限制的內存額度
memory.failcnt #顯示內存使用量達到限制值的次數
memory.max_usage_in_bytes #歷史內存最大使用量
memory.soft_limit_in_bytes #設置/顯示當前限制的內存軟額度
memory.stat #顯示當前cgroup的內存使用情況
memory.use_hierarchy #設置/顯示是否將子cgroup的內存使用情況統計到當前cgroup里面
memory.force_empty #觸發系統立即盡可能的回收當前cgroup中可以回收的內存
memory.pressure_level #設置內存壓力的通知事件,配合cgroup.event_control一起使用
memory.swappiness #設置和顯示當前的swappiness
memory.move_charge_at_immigrate #設置當進程移動到其他cgroup中時,它所占用的內存是否也隨著移動過去
memory.oom_control #設置/顯示oom controls相關的配置
memory.numa_stat #顯示numa相關的內存
添加進程
也是往 cgroup 中添加進程只要將進程號寫入 cgroup.procs 就可以了。
#重新打開一個shell窗口,避免相互影響
root@DESKTOP-9K4GB6E:~# cd /sys/fs/cgroup/memory/test/
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo $$ >> cgroup.procs
#運行top命令,這樣這個cgroup消耗的內存會多點,便于觀察
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# top
# 后續操作不再在這個窗口進行,避免在這個bash中運行進程影響cgropu里面的進程數及相關統計
設置限額
設置限額很簡單,將閾值寫入 memory.limit_in_bytes 文件就可以了,例如:
- echo 1M > memory.limit_in_bytes:限制只能用 1M 內存
- echo -1 > memory.limit_in_bytes:-1 則是不限制
#回到第一個shell窗口
#開始設置之前,看看當前使用的內存數量,這里的單位是字節
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.usage_in_bytes
2379776
#設置1M的限額
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 1M > memory.limit_in_bytes
#設置完之后記得要查看一下這個文件,因為內核要考慮頁對齊, 所以生效的數量不一定完全等于設置的數量
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.usage_in_bytes
950272
#如果不再需要限制這個cgroup,寫-1到文件memory.limit_in_bytes即可
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo -1 > memory.limit_in_bytes
#這時可以看到limit被設置成了一個很大的數字
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.limit_in_bytes
9223372036854771712
如果設置的限額比當前已經使用的內存少呢?
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# free -h
total used free shared buff/cache available
Mem: 7.7Gi 253Mi 7.4Gi 0.0Ki 95Mi 7.3Gi
Swap: 2.0Gi 0.0Ki 2.0Gi
# 此時用了 1232K
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.usage_in_bytes
1232896
# 限制成500K
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 500k > memory.limit_in_bytes
# 再次查看發現現在只用了401K
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.usage_in_bytes
401408
# 發現swap多了1M,說明另外的數據被轉移到swap上了
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# free -h
total used free shared buff/cache available
Mem: 7.7Gi 254Mi 7.4Gi 0.0Ki 94Mi 7.3Gi
Swap: 2.0Gi 1.0Mi 2.0Gi
#這個時候再來看failcnt,發現有381次之多(隔幾秒再看這個文件,發現次數在增長)
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.failcnt
381
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.failcnt
385
#再看看memory.stat(這里只顯示部分內容),發現物理內存用了400K,
#但有很多pgmajfault以及pgpgin和pgpgout,說明發生了很多的swap in和swap out
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.stat
swap 946176 # 946K 差不多剛好是內存中少的量
pgpgin 30492
pgpgout 30443
pgfault 23859
pgmajfault 12507
從上面的結果可以看出,當物理內存不夠時,就會觸發 memory.failcnt 里面的數量加 1,但進程不會被 kill 掉,那是因為內核會嘗試將物理內存中的數據移動到 swap 空間中,從而讓內存分配成功。
如果設置的限額過小,就算 swap out 部分內存后還是不夠會怎么樣?
#--------------------------第一個shell窗口----------------------
# 限制到100k
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 100K > memory.limit_in_bytes
#--------------------------第二個shell窗口----------------------
# 嘗試執行 top 發現剛運行就被Kill了
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# top
Killed
從上面的這些測試可以看出,一旦設置了內存限制,將立即生效,并且當物理內存使用量達到 limit 的時候,memory.failcnt 的內容會加 1,但這時進程不一定就會
被 kill 掉,內核會盡量將物理內存中的數據移到 swap 空間上去,如果實在是沒辦法移動了(設置的 limit 過小,或者 swap 空間不足),默認情況下,就會 kill 掉 cgroup 里面繼續申請內存的進程。
行為控制
通過修改memory.oom_control文件,可以控制 subsystem 在物理內存達到上限時的行為。文件中包含以下 3 個參數:
-
oom_kill_disable:是否啟用 oom kill- 0:關閉
- 1:開啟
-
under_oom:表示當前是否已經進入 oom 狀態,也即是否有進程被暫停了。 -
oom_kill:oom 后是否執行 kill- 1:啟動,oom 后直接 kill 掉對應進程
- 2:關閉:當內核無法給進程分配足夠的內存時,將會暫停該進程直到有空余的內存之后再繼續運行。同時會更新 under_oom 狀態
- 注意:root cgroup 的 oom killer 是不能被禁用的
為了演示 OOM-killer 的功能,創建了下面這樣一個程序,用來向系統申請內存,它會每秒消耗 1M 的內存。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MB (1024 * 1024)
int main(int argc, char *argv[])
{
char *p;
int i = 0;
while(1) {
p = (char *)malloc(MB);
memset(p, 0, MB);
printf("%dM memory allocated\n", ++i);
sleep(1);
}
return 0;
}
保存上面的程序到文件~/mem-allocate.c,然后編譯并測試
#--------------------------第一個shell窗口----------------------
#編譯上面的文件
dev@dev:/sys/fs/cgroup/memory/test$ gcc ~/mem-allocate.c -o ~/mem-allocate
#設置內存限額為5M
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 5M > memory.limit_in_bytes"
#將當前bash加入到test中,這樣這個bash創建的所有進程都會自動加入到test中
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo $$ >> cgroup.procs"
#默認情況下,memory.oom_control的值為0,即默認啟用oom killer
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.oom_control
oom_kill_disable 0
under_oom 0
#為了避免受swap空間的影響,設置swappiness為0來禁止當前cgroup使用swap
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 > memory.swappiness"
#當分配第5M內存時,由于總內存量超過了5M,所以進程被kill了
dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
Killed
#設置oom_control為1,這樣內存達到限額的時候會暫停
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 1 >> memory.oom_control"
#跟預期的一樣,程序被暫停了
dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
#--------------------------第二個shell窗口----------------------
#再打開一個窗口
dev@dev:~$ cd /sys/fs/cgroup/memory/test/
#這時候可以看到memory.oom_control里面under_oom的值為1,表示當前已經oom了
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.oom_control
oom_kill_disable 1
under_oom 1
#修改test的額度為7M
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 7M > memory.limit_in_bytes"
#--------------------------第一個shell窗口----------------------
#再回到第一個窗口,會發現進程mem-allocate繼續執行了兩步,然后暫停在6M那里了
dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
5M memory allocated
6M memory allocated
# 創建上面的文件并編譯
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# vim ~/mem-allocate.c
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# gcc ~/mem-allocate.c -o ~/mem-allocate
# 限制5M的上限
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 5M > memory.limit_in_bytes
#將當前bash加入到test中,這樣這個bash創建的所有進程都會自動加入到test中
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo $$ >> cgroup.procs
#默認情況下,會啟用oom killer
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.oom_control
oom_kill_disable 0
under_oom 0
oom_kill 1
#為了避免受swap空間的影響,設置swappiness為0來禁止當前cgroup使用swap
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 0 > memory.swappiness
#當分配第5M內存時,由于總內存量超過了5M,所以進程被kill了
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
Killed
#設置oom_control為1,這樣內存達到限額的時候會暫停
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 1 >> memory.oom_control
#跟預期的一樣,程序被暫停了
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
#--------------------------第二個shell窗口----------------------
#再打開一個窗口
dev@dev:~$ cd /sys/fs/cgroup/memory/test/
#這時候可以看到memory.oom_control里面under_oom的值為1,表示當前已經oom了
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.oom_control
oom_kill_disable 1
under_oom 1
oom_kill 2
#修改test的額度為7M
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 7M > memory.limit_in_bytes
# 切換會第一個窗口,發送程序又跑了兩步,停在了6M
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
5M memory allocated
6M memory allocated
其他
進程遷移(migration)
當一個進程從一個 cgroup 移動到另一個 cgroup 時,默認情況下,該進程已經占用的內存還是統計在原來的 cgroup 里面,不會占用新 cgroup 的配額,但新分配的內存會統計到新的 cgroup 中(包括 swap out 到交換空間后再 swap in 到物理內存中的部分)。
我們可以通過設置 memory.move_charge_at_immigrate 讓進程所占用的內存隨著進程的遷移一起遷移到新的 cgroup 中。
enable: echo 1 > memory.move_charge_at_immigrate
disable:echo 0 > memory.move_charge_at_immigrate
注意: 就算設置為 1,但如果不是 thread group 的 leader,這個 task 占用的內存也不能被遷移過去。
換句話說,如果以線程為單位進行遷移,必須是進程的第一個線程,如果以進程為單位進行遷移,就沒有這個問題。
當 memory.move_charge_at_immigrate 被設置成 1 之后,進程占用的內存將會被統計到目的 cgroup 中,如果目的 cgroup 沒有足夠的內存,系統將嘗試回收目的 cgroup 的部分內存(和系統內存緊張時的機制一樣,刪除不常用的 file backed 的內存或者 swap out 到交換空間上,請參考Linux 內存管理),如果回收不成功,那么進程遷移將失敗。
注意:遷移內存占用數據是比較耗時的操作。
移除 cgroup
當 memory.move_charge_at_immigrate 為 0 時,就算當前 cgroup 中里面的進程都已經移動到其它 cgropu 中去了,由于進程已經占用的內存沒有被統計過去,當前 cgroup 有可能還占用很多內存,當移除該 cgroup 時,占用的內存需要統計到誰頭上呢?
答案是依賴 memory.use_hierarchy 的值,
- 如果該值為 0,將會統計到 root cgroup 里;
- 如果值為 1,將統計到它的父 cgroup 里面。
force_empty
當向 memory.force_empty 文件寫入 0 時(echo 0 > memory.force_empty),將會立即觸發系統盡可能的回收該 cgroup 占用的內存。該功能主要使用場景是移除 cgroup 前(cgroup 中沒有進程),先執行該命令,可以盡可能的回收該 cgropu 占用的內存,這樣遷移內存的占用數據到父 cgroup 或者 root cgroup 時會快些。
memory.swappiness
該文件的值默認和全局的 swappiness(/proc/sys/vm/swappiness)一樣,修改該文件只對當前 cgroup 生效,其功能和全局的 swappiness 一樣,請參考Linux 交換空間中關于 swappiness 的介紹。
注意:有一點和全局的 swappiness 不同,那就是如果這個文件被設置成 0,就算系統配置的有交換空間,當前 cgroup 也不會使用交換空間。
memory.use_hierarchy
該文件內容為 0 時,表示不使用繼承,即父子 cgroup 之間沒有關系;當該文件內容為 1 時,子 cgroup 所占用的內存會統計到所有祖先 cgroup 中。
如果該文件內容為 1,當一個 cgroup 內存吃緊時,會觸發系統回收它以及它所有子孫 cgroup 的內存。
注意: 當該 cgroup 下面有子 cgroup 或者父 cgroup 已經將該文件設置成了 1,那么當前 cgroup 中的該文件就不能被修改。
#當前cgroup和父cgroup里都是1
dev@dev:/sys/fs/cgroup/memory/test$ cat memory.use_hierarchy
1
dev@dev:/sys/fs/cgroup/memory/test$ cat ../memory.use_hierarchy
1
#由于父cgroup里面的值為1,所以修改當前cgroup的值失敗
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 > ./memory.use_hierarchy"
sh: echo: I/O error
#由于父cgroup里面有子cgroup(至少有當前cgroup這么一個子cgroup),
#修改父cgroup里面的值也失敗
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 > ../memory.use_hierarchy"
sh: echo: I/O error
memory.soft_limit_in_bytes
有了 hard limit(memory.limit_in_bytes),為什么還要 soft limit 呢?hard limit 是一個硬性標準,絕對不能超過這個值。
而 soft limit 可以被超越,既然能被超越,要這個配置還有啥用?先看看它的特點
- 1)當系統內存充裕時,soft limit 不起任何作用
- 2)當系統內存吃緊時,系統會盡量的將 cgroup 的內存限制在 soft limit 值之下(內核會盡量,但不 100% 保證)
從它的特點可以看出,它的作用主要發生在系統內存吃緊時,如果沒有 soft limit,那么所有的 cgroup 一起競爭內存資源,占用內存多的 cgroup 不會讓著內存占用少的 cgroup,這樣就會出現某些 cgroup 內存饑餓的情況。如果配置了 soft limit,那么當系統內存吃緊時,系統會讓超過 soft limit 的 cgroup 釋放出超過 soft limit 的那部分內存(有可能更多),這樣其它 cgroup 就有了更多的機會分配到內存。
從上面的分析看出,這其實是系統內存不足時的一種妥協機制,給次等重要的進程設置 soft limit,當系統內存吃緊時,把機會讓給其它重要的進程。
注意: 當系統內存吃緊且 cgroup 達到 soft limit 時,系統為了把當前 cgroup 的內存使用量控制在 soft limit 下,在收到當前 cgroup 新的內存分配請求時,就會觸發回收內存操作,所以一旦到達這個狀態,就會頻繁的觸發對當前 cgroup 的內存回收操作,會嚴重影響當前 cgroup 的性能。
memory.pressure_level
這個文件主要用來監控當前 cgroup 的內存壓力,當內存壓力大時(即已使用內存快達到設置的限額),在分配內存之前需要先回收部分內存,從而影響內存分配速度,影響性能,而通過監控當前 cgroup 的內存壓力,可以在有壓力的時候采取一定的行動來改善當前 cgroup 的性能,比如關閉當前 cgroup 中不重要的服務等。目前有三種壓力水平:
-
low
- 意味著系統在開始為當前 cgroup 分配內存之前,需要先回收內存中的數據了,這時候回收的是在磁盤上有對應文件的內存數據。
-
medium
- 意味著系統已經開始頻繁為當前 cgroup 使用交換空間了。
-
critical
- 快撐不住了,系統隨時有可能 kill 掉 cgroup 中的進程。
如何配置相關的監聽事件呢?和 memory.oom_control 類似,大概步驟如下:
- 利用函數 eventfd(2) 創建一個 event_fd
- 打開文件 memory.pressure_level,得到 pressure_level_fd
- 往 cgroup.event_control 中寫入這么一串:
<event_fd> <pressure_level_fd> <level> - 然后通過讀 event_fd 得到通知
注意: 多個 level 可能要創建多個 event_fd,好像沒有辦法共用一個
Memory thresholds
我們可以通過 cgroup 的事件通知機制來實現對內存的監控,當內存使用量穿過(變得高于或者低于)我們設置的值時,就會收到通知。使用方法和 memory.oom_control 類似,大概步驟如下:
- 利用函數 eventfd(2) 創建一個 event_fd
- 打開文件 memory.usage_in_bytes,得到 usage_in_bytes_fd
- 往 cgroup.event_control 中寫入這么一串:
<event_fd> <usage_in_bytes_fd> <threshold> - 然后通過讀 event_fd 得到通知
stat file
這個文件包含的統計項比較細,需要一些內核的內存管理知識才能看懂,這里就不介紹了(怕說錯)。詳細信息可以參考 Memory Resource Controller中的“5.2 stat file”。這里有幾個需要注意的地方:
- 里面 total 開頭的統計項包含了子 cgroup 的數據(前提條件是 memory.use_hierarchy 等于 1)。
- 里面的 'rss + file_mapped" 才約等于是我們常說的 RSS(ps aux 命令看到的 RSS)
- 文件(動態庫和可執行文件)及共享內存可以在多個進程之間共享,不過它們只會統計到他們的 owner cgroup 中的 file_mapped 去。(不確定是怎么定義 owner 的,但如果看到當前 cgroup 的 file_mapped 值很小,說明共享的數據沒有算到它頭上,而是其它的 cgroup)
小結
作用:限制 cgroups 中的進程占用的內存上限
用法:
- 1)
memory.limit_in_bytes配置進程可以使用的內存上限(hard limit),當超過該閾值時,一般是嘗試使用 swap,如果不行則直接 kill 掉。 - 2)
memory.soft_limit_in_bytes配置進程可以使用的內存上行(soft limit),當系統內存不足時,cgroups 會優先將使用量超過 soft limit 的進程進行內存回收,騰出內存。 - 3)
memory.oom_control參數配置內存使用量到達閾值時內核的處理行為,默認為 oom_kill。
原理:當進程使用內存超過memory.limit_in_bytes 之后,系統會根據 memory.oom_control 配置的行為進行處理,一般是嘗試使用 swap,如果不行則直接 kill 掉。
本節沒有介紹 swap 和 kernel 相關的內容,不過在實際使用過程中一定要留意 swap 空間,如果系統使用了交換空間,那么設置限額時一定要注意一點,那就是當 cgroup 的物理空間不夠時,內核會將不常用的內存 swap out 到交換空間上,從而導致一直不觸發 oom killer,而是不停的 swap out/in,導致 cgroup 中的進程運行速度很慢。
如果一定要用交換空間,最好的辦法是限制 swap+物理內存 的額度,雖然我們在這篇中沒有介紹這部分內容,但其使用方法和限制物理內存是一樣的,只是換做寫文件 memory.memsw.limit_in_bytes 罷了。
4. 小結
本文主要簡單介紹了 pid、cpu、memory 這三個 subsystem 的作用和基本使用,具體如下:
| subsystem | 功能 | 用法 | 原理 | 備注 |
|---|---|---|---|---|
| pid | 限制 cgroups 中進程使用的 pid 數 | 配置 subsystem 中的 pids.max 即可 | 當 cgroups 中的進程調用 fork 或者 clone 系統調用時會判斷,subsystem 中配置的 pids.max 和當前 pids.current 的值,來確定是否能夠創建新的進程(或線程) | linux 中的 pid 是有限的,通過該 subsystem 可以有效防止 fork 炸彈之類的惡意進程 |
| cpu | 限制 cgroups 中進程使用的 cpu 上限 | 1)限制為具體值:用 cfs_period_us & cfs_quota_us 兩個配置可以嚴格限制進程 cpu 使用量。 2)按比例分配:用 shares 配置,可以使得多個 cgroups 之間按比例分配所有 cpu。 | subsystem 負責提供配置,cfs 負責記錄進程使用的 cpu 時間,達到閾值后就從調度層面進行限制,避免該進程繼續使用 cpu。 | 一般使用 cfs_period_us & cfs_quota_us 方式限制具體值用得比較多。 |
| memory | 限制 cgroups 中進程使用的 memory 上限 | 1)memory.limit_in_bytes 配置進程可以使用的內存上限(hard limit),當超過該閾值時,一般是嘗試使用 swap,如果不行則直接 kill 掉。 2)memory.soft_limit_in_bytes 配置進程可以使用的內存上行(soft limit),當系統內存不足時,cgroups 會優先將使用量超過 soft limit 的進程進行內存回收,騰出內存。 3)memory.oom_control 參數配置內存使用量到達閾值時內核的處理行為,默認為 oom_kill。 |
當進程使用內存超過memory.limit_in_bytes 之后,系統會根據 memory.oom_control 配置的行為進行處理,一般是嘗試使用 swap,如果不行則直接 kill 掉。 |
如果系統使用了交換空間,那么設置限額時一定要注意一點,那就是當 cgroup 的物理空間不夠時,內核會將不常用的內存 swap out 到交換空間上,從而導致一直不觸發 oom killer,而是不停的 swap out/in,導致 cgroup 中的進程運行速度很慢。 |
如果你對云原生技術充滿好奇,想要深入了解更多相關的文章和資訊,歡迎關注微信公眾號。
搜索公眾號【探索云原生】即可訂閱
5. 參考
cgroups(7) — Linux manual page
美團技術團隊---Linux 資源管理之 cgroups 簡介
Red Hat---資源管理指南
總結
以上是生活随笔為你收集整理的深入剖析 Linux Cgroups 子系统:资源精细管理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 新一代通信协议 - Socket.D
- 下一篇: 通过腾讯网页快捷登录协议截取 QQ邮箱