本章主要演示以下 cgroups 下各個 subsystem 的作用。
根據(jù)難易程度,依次演示了 pids 、cpu 和 memory 3 個 subsystem 的使用。
注:本文所有操作在 Ubuntu20.04 下進行。
如果你對云原生技術(shù)充滿好奇,想要深入了解更多相關(guān)的文章和資訊,歡迎關(guān)注微信公眾號。
搜索公眾號【探索云原生】即可訂閱
1. pids
pids subsystem 功能是限制 cgroup 及其所有子孫 cgroup 里面能創(chuàng)建的總的 task 數(shù)量。
注意:這里的 task 指通過 fork 和 clone 函數(shù)創(chuàng)建的進程,由于 clone 函數(shù)也能創(chuàng)建線程(在 Linux 里面,線程是一種特殊的進程),所以這里的 task 也包含線程。
本文統(tǒng)一以進程來代表 task,即本文中的進程代表了進程和線程>
創(chuàng)建子 cgroup
創(chuàng)建子 cgroup,取名為 test
#進入目錄/sys/fs/cgroup/pids/并新建一個目錄,即創(chuàng)建了一個子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 中現(xiàn)有的總的進程數(shù)量
- pids.max: 當前 cgroup 及其所有子孫 cgroup 中所允許創(chuàng)建的總的最大進程數(shù)量
限制進程數(shù)
首先是將當前 bash 加入到 cgroup 中,并修改pids.max
的值,為了便于測試,這里就限制為 1:
#--------------------------第一個shell窗口----------------------
# 將當前bash進程加入到該cgroup
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/pids/test# echo $$ > cgroup.procs
#將pids.max設(shè)置為1,即當前cgroup只允許有一個進程
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/pids/test# echo 1 > pids.max
由于 bash 已經(jīng)占用了一個進程,所以此時 bash 中已經(jīng)無法創(chuàng)建新的進程了:
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
創(chuàng)建新進程失敗,于是命令運行失敗,說明限制生效。
打開另一個 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,已經(jīng)到 pids.max 的限制了。
當前 cgroup 和子 cgroup 之間的關(guān)系
當前 cgroup 中的 pids.current
和 pids.max
代表了當前 cgroup 及所有子孫 cgroup 的所有進程,所以子孫 cgroup 中的 pids.max 大小不能超過父 cgroup。
如果子 cgroup 中的 pids.max 設(shè)置的大于父 cgroup 里的值,會怎么樣?
答案是子 cgroup 中的進程不光受子 cgroup 限制,還要受其父 cgroup 的限制。
#繼續(xù)使用上面的兩個窗口
#--------------------------第二個shell窗口----------------------
#將pids.max設(shè)置成2
dev@dev:/sys/fs/cgroup/pids/test$ echo 2 > pids.max
#在test下面創(chuàng)建一個子cgroup
dev@dev:/sys/fs/cgroup/pids/test$ mkdir subtest
dev@dev:/sys/fs/cgroup/pids/test$ cd subtest/
#將subtest的pids.max設(shè)置為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里面的數(shù)據(jù)
#test里面的數(shù)據(jù)如下:
dev@dev:~$ cd /sys/fs/cgroup/pids/test
dev@dev:/sys/fs/cgroup/pids/test$ cat pids.max
2
#這里為2表示目前test和subtest里面總的進程數(shù)為2
dev@dev:/sys/fs/cgroup/pids/test$ cat pids.current
2
dev@dev:/sys/fs/cgroup/pids/test$ cat cgroup.procs
3083
#subtest里面的數(shù)據(jù)如下:
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已經(jīng)等于pids.max了,
#所以創(chuàng)建新進程失敗,于是命令運行失敗,說明限制生效
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已經(jīng)等于pids.max了,
#所以創(chuàng)建新進程失敗,于是命令運行失敗,說明子cgroup中的進程數(shù)不僅受自己的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,在下面兩種情況下,會出現(xiàn) pids.max < pids.current 的情況:
- 設(shè)置 pids.max 時,將其值設(shè)置的比 pids.current 小
- 將其他進程加入到當前 cgroup 有可能會導(dǎo)致 pids.current > pids.max
- 因為 pids.max 只會在當前 cgroup 中的進程 fork、clone 的時候生效,將其他進程加入到當前 cgroup 時,不會檢測 pids.max,所以可能觸發(fā)這種情況
小結(jié)
作用:pids subsystem 用于限制 cgroups 下能夠創(chuàng)建的 task(進程和線程)數(shù)。
原理:在調(diào)用 fork 和 clone 時對比 subsystem 中配置的 pids.max 和 pids.current 值來判斷當前是否能夠繼續(xù)創(chuàng)建 task。
用法:配置 pids.max 防止容器消耗完 pid。
2. cpu
在 cgroup 里面,跟 CPU 相關(guān)的子系統(tǒng)有 cpusets、cpuacct 和 cpu。
-
其中 cpuset 主要用于設(shè)置 CPU 的親和性,可以限制 cgroup 中的進程只能在指定的 CPU 上運行,或者不能在指定的 CPU 上運行,同時 cpuset 還能設(shè)置內(nèi)存的親和性。設(shè)置親和性一般只在比較特殊的情況才用得著,所以這里不做介紹。
-
cpuacct 包含當前 cgroup 所使用的 CPU 的統(tǒng)計信息,信息量較少,有興趣可以去看看它的文檔,這里不做介紹。
本節(jié)只介紹 cpu 子系統(tǒng),包括怎么限制 cgroup 的 CPU 使用上限及相對于其它 cgroup 的相對值。
創(chuàng)建子 cgroup
通用是創(chuàng)建子目錄即可。
#進入/sys/fs/cgroup/cpu并創(chuàng)建子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:兩個文件配合起來設(shè)置 CPU 的使用上限,兩個文件的單位都是微秒(us)。
- cfs_period_us:用來配置時間周期長度
- 取值范圍為 1 毫秒(ms)到 1 秒(s)
- cfs_quota_us:用來配置當前 cgroup 在設(shè)置的周期長度內(nèi)所能使用的 CPU 時間數(shù)
- 取值大于 1ms 即可
- 默認值為 -1,表示不受 cpu 時間的限制。
cpu.shares 用來設(shè)置 CPU 的相對值(比例),并且是針對所有的 CPU(內(nèi)核),默認值是 1024。
假如系統(tǒng)中有兩個 cgroup,分別是 A 和 B,A 的 shares 值是 1024,B 的 shares 值是 512,那么 A 將獲得 1024/(1204+512)=66% 的 CPU 資源,而 B 將獲得 33% 的 CPU 資源。
shares 有兩個特點:
- 如果 A 不忙,沒有使用到 66% 的 CPU 時間,那么剩余的 CPU 時間將會被系統(tǒng)分配給 B,即 B 的 CPU 使用率可以超過 33%
- 如果添加了一個新的 cgroup C,且它的 shares 值是 1024,那么 A 的限額變成了 1024/(1204+512+1024)=40%,B 的變成了 20%
從上面兩個特點可以看出:
- 在閑的時候,shares 基本上不起作用,只有在 CPU 忙的時候起作用,這是一個優(yōu)點。
- 由于 shares 是一個絕對值,需要和其它 cgroup 的值進行比較才能得到自己的相對限額,而在一個部署很多容器的機器上,cgroup 的數(shù)量是變化的,所以這個限額也是變化的,自己設(shè)置了一個高的值,但別人可能設(shè)置了一個更高的值,所以這個功能沒法精確的控制 CPU 使用率。
cpu.stat 包含了下面三項統(tǒng)計結(jié)果:
- nr_periods: 表示過去了多少個 cpu.cfs_period_us 里面配置的時間周期
- nr_throttled: 在上面的這些周期中,有多少次是受到了限制(即 cgroup 中的進程在指定的時間周期中用光了它的配額)
- throttled_time: cgroup 中的進程被限制使用 CPU 持續(xù)了多長時間(納秒)
原理
前面配置的參數(shù)都是 cfs_xxx
,這里的 cfs 是 Completely Fair Scheduler 的縮寫。
CFS 是 Linux 內(nèi)核中的調(diào)度器,它負責決定哪個進程在給定時間片內(nèi)運行。CFS 使用 CFS 配額(cpu.cfs_quota_us
)和 CFS 周期(cpu.cfs_period_us
)來限制每個 cgroup 中的 CPU 使用。
CFS 的實現(xiàn)與 cgroups 協(xié)同工作,它負責追蹤每個 cgroup 中的進程消耗的 CPU 時間,并在每個調(diào)度周期結(jié)束時根據(jù) cgroup 的 CPU 配額調(diào)整進程的運行時間。
如果一個 cgroup 中的進程在調(diào)度周期內(nèi)超過了它的 CPU 配額,它將被調(diào)度器限制,從而實現(xiàn)了 CPU 的使用限制。
即:cgroups 中的 subsystem 負責提供配置,cfs 負責記錄進程使用的 cpu 時間,達到閾值后就從調(diào)度層面進行限制,避免該進程繼續(xù)使用 cpu。
演示
#繼續(xù)使用上面創(chuàng)建的子cgroup: test
#設(shè)置只能使用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中啟動一個死循環(huán)來消耗cpu,正常情況下應(yīng)該使用100%的cpu(即消耗一個內(nèi)核)
dev@ubuntu:/sys/fs/cgroup/cpu,cpuacct/test$ while :; do echo test > /dev/null; done
#--------------------------重新打開一個shell窗口----------------------
#通過top命令可以看到5456的CPU使用率為20%左右,說明被限制住了
#不過這時系統(tǒng)的%us+%sy在10%左右,那是因為我測試的機器上cpu是雙核的,
#所以系統(tǒng)整體的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
#這時可以看到被限制的統(tǒng)計結(jié)果
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
# 新開一個窗口,運行一個死循環(huán)
$ 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
# 再切回第二個窗口,發(fā)現(xiàn)519進程的cpu已經(jīng)降到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
# 查看被限制的統(tǒng)計結(jié)果
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/cpu/test# cat cpu.stat
nr_periods 2090
nr_throttled 2088
throttled_time 166752684900
小結(jié)
作用:cpu subsystem 用于限制 cgroups 下進程可以使用的 cpu 上限。
原理:cgroups 中的 subsystem 負責提供配置,cfs 負責記錄進程使用的 cpu 時間,達到閾值后就從調(diào)度層面進行限制,避免該進程繼續(xù)使用 cpu。
用法:
- 1)限制為具體值:用 cfs_period_us & cfs_quota_us 兩個配置可以嚴格限制進程 cpu 使用量。
- 2)按比例分配:用 shares 配置,可以使得多個 cgroups 之間按比例分配所有 cpu。
3. memory
memory subsystem 顧名思義,限制 cgroups 中進程的內(nèi)存使用。
為什么需要內(nèi)存控制
- 站在一個普通開發(fā)者的角度,如果能控制一個或者一組進程所能使用的內(nèi)存數(shù),那么就算代碼有 bug,內(nèi)存泄漏也不會對系統(tǒng)造成影響,因為可以設(shè)置內(nèi)存使用量的上限,當?shù)竭_這個值之后可以將進程重啟。
- 站在一個系統(tǒng)管理者的角度,如果能限制每組進程所能使用的內(nèi)存量,那么不管程序的質(zhì)量如何,都能將它們對系統(tǒng)的影響降到最低,從而保證整個系統(tǒng)的穩(wěn)定性。
內(nèi)存控制能控制些什么?
- 限 制 cgroup 中所有進程所能使用的物理內(nèi)存總量
- 限制 cgroup 中所有進程所能使用的物理內(nèi)存+交換空間總量(CONFIG_MEMCG_SWAP): 一般在 server 上,不太會用到 swap 空間,所以不在這里介紹這部分內(nèi)容。
- 限制 cgroup 中所有進程所能使用的內(nèi)核內(nèi)存總量及其它一些內(nèi)核資源(CONFIG_MEMCG_KMEM): 限制內(nèi)核內(nèi)存有什么用呢?其實限制內(nèi)核內(nèi)存就是限制當前 cgroup 所能使用的內(nèi)核資源,比如進程的內(nèi)核棧空間,socket 所占用的內(nèi)存空間等,通過限制內(nèi)核內(nèi)存,當內(nèi)存吃緊時,可以阻止當前 cgroup 繼續(xù)創(chuàng)建進程以及向內(nèi)核申請分配更多的內(nèi)核資源。由于這塊功能被使用的較少,本篇中也不對它做介紹。
創(chuàng)建子 cgroup
在 /sys/fs/cgroup/memory 下創(chuàng)建一個子目錄就算是創(chuàng)建了一個子 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 相關(guān)的文件還不少,這里先做個大概介紹(kernel 相關(guān)的文件除外),后面會詳細介紹每個文件的作用:
cgroup.event_control #用于eventfd的接口
memory.usage_in_bytes #顯示當前已用的內(nèi)存
memory.limit_in_bytes #設(shè)置/顯示當前限制的內(nèi)存額度
memory.failcnt #顯示內(nèi)存使用量達到限制值的次數(shù)
memory.max_usage_in_bytes #歷史內(nèi)存最大使用量
memory.soft_limit_in_bytes #設(shè)置/顯示當前限制的內(nèi)存軟額度
memory.stat #顯示當前cgroup的內(nèi)存使用情況
memory.use_hierarchy #設(shè)置/顯示是否將子cgroup的內(nèi)存使用情況統(tǒng)計到當前cgroup里面
memory.force_empty #觸發(fā)系統(tǒng)立即盡可能的回收當前cgroup中可以回收的內(nèi)存
memory.pressure_level #設(shè)置內(nèi)存壓力的通知事件,配合cgroup.event_control一起使用
memory.swappiness #設(shè)置和顯示當前的swappiness
memory.move_charge_at_immigrate #設(shè)置當進程移動到其他cgroup中時,它所占用的內(nèi)存是否也隨著移動過去
memory.oom_control #設(shè)置/顯示oom controls相關(guān)的配置
memory.numa_stat #顯示numa相關(guān)的內(nèi)存
添加進程
也是往 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消耗的內(nèi)存會多點,便于觀察
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# top
# 后續(xù)操作不再在這個窗口進行,避免在這個bash中運行進程影響cgropu里面的進程數(shù)及相關(guān)統(tǒng)計
設(shè)置限額
設(shè)置限額很簡單,將閾值寫入 memory.limit_in_bytes 文件就可以了,例如:
- echo 1M > memory.limit_in_bytes:限制只能用 1M 內(nèi)存
- echo -1 > memory.limit_in_bytes:-1 則是不限制
#回到第一個shell窗口
#開始設(shè)置之前,看看當前使用的內(nèi)存數(shù)量,這里的單位是字節(jié)
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.usage_in_bytes
2379776
#設(shè)置1M的限額
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 1M > memory.limit_in_bytes
#設(shè)置完之后記得要查看一下這個文件,因為內(nèi)核要考慮頁對齊, 所以生效的數(shù)量不一定完全等于設(shè)置的數(shù)量
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被設(shè)置成了一個很大的數(shù)字
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.limit_in_bytes
9223372036854771712
如果設(shè)置的限額比當前已經(jīng)使用的內(nèi)存少呢?
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
# 再次查看發(fā)現(xiàn)現(xiàn)在只用了401K
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.usage_in_bytes
401408
# 發(fā)現(xiàn)swap多了1M,說明另外的數(shù)據(jù)被轉(zhuǎn)移到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,發(fā)現(xiàn)有381次之多(隔幾秒再看這個文件,發(fā)現(xiàn)次數(shù)在增長)
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(這里只顯示部分內(nèi)容),發(fā)現(xiàn)物理內(nèi)存用了400K,
#但有很多pgmajfault以及pgpgin和pgpgout,說明發(fā)生了很多的swap in和swap out
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# cat memory.stat
swap 946176 # 946K 差不多剛好是內(nèi)存中少的量
pgpgin 30492
pgpgout 30443
pgfault 23859
pgmajfault 12507
從上面的結(jié)果可以看出,當物理內(nèi)存不夠時,就會觸發(fā) memory.failcnt 里面的數(shù)量加 1,但進程不會被 kill 掉,那是因為內(nèi)核會嘗試將物理內(nèi)存中的數(shù)據(jù)移動到 swap 空間中,從而讓內(nèi)存分配成功。
如果設(shè)置的限額過小,就算 swap out 部分內(nèi)存后還是不夠會怎么樣?
#--------------------------第一個shell窗口----------------------
# 限制到100k
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 100K > memory.limit_in_bytes
#--------------------------第二個shell窗口----------------------
# 嘗試執(zhí)行 top 發(fā)現(xiàn)剛運行就被Kill了
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# top
Killed
從上面的這些測試可以看出,一旦設(shè)置了內(nèi)存限制,將立即生效,并且當物理內(nèi)存使用量達到 limit 的時候,memory.failcnt 的內(nèi)容會加 1,但這時進程不一定就會
被 kill 掉,內(nèi)核會盡量將物理內(nèi)存中的數(shù)據(jù)移到 swap 空間上去,如果實在是沒辦法移動了(設(shè)置的 limit 過小,或者 swap 空間不足),默認情況下,就會 kill 掉 cgroup 里面繼續(xù)申請內(nèi)存的進程。
行為控制
通過修改memory.oom_control
文件,可以控制 subsystem 在物理內(nèi)存達到上限時的行為。文件中包含以下 3 個參數(shù):
-
oom_kill_disable
:是否啟用 oom kill- 0:關(guān)閉
- 1:開啟
-
under_oom
:表示當前是否已經(jīng)進入 oom 狀態(tài),也即是否有進程被暫停了。 -
oom_kill
:oom 后是否執(zhí)行 kill- 1:啟動,oom 后直接 kill 掉對應(yīng)進程
- 2:關(guān)閉:當內(nèi)核無法給進程分配足夠的內(nèi)存時,將會暫停該進程直到有空余的內(nèi)存之后再繼續(xù)運行。同時會更新 under_oom 狀態(tài)
- 注意:root cgroup 的 oom killer 是不能被禁用的
為了演示 OOM-killer 的功能,創(chuàng)建了下面這樣一個程序,用來向系統(tǒng)申請內(nèi)存,它會每秒消耗 1M 的內(nèi)存。
#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
#設(shè)置內(nèi)存限額為5M
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 5M > memory.limit_in_bytes"
#將當前bash加入到test中,這樣這個bash創(chuàng)建的所有進程都會自動加入到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空間的影響,設(shè)置swappiness為0來禁止當前cgroup使用swap
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 0 > memory.swappiness"
#當分配第5M內(nèi)存時,由于總內(nèi)存量超過了5M,所以進程被kill了
dev@dev:/sys/fs/cgroup/memory/test$ ~/mem-allocate
1M memory allocated
2M memory allocated
3M memory allocated
4M memory allocated
Killed
#設(shè)置oom_control為1,這樣內(nèi)存達到限額的時候會暫停
dev@dev:/sys/fs/cgroup/memory/test$ sudo sh -c "echo 1 >> memory.oom_control"
#跟預(yù)期的一樣,程序被暫停了
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,表示當前已經(jīng)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窗口----------------------
#再回到第一個窗口,會發(fā)現(xiàn)進程mem-allocate繼續(xù)執(zhí)行了兩步,然后暫停在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
# 創(chuàng)建上面的文件并編譯
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創(chuàng)建的所有進程都會自動加入到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空間的影響,設(shè)置swappiness為0來禁止當前cgroup使用swap
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 0 > memory.swappiness
#當分配第5M內(nèi)存時,由于總內(nèi)存量超過了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
#設(shè)置oom_control為1,這樣內(nèi)存達到限額的時候會暫停
root@DESKTOP-9K4GB6E:/sys/fs/cgroup/memory/test# echo 1 >> memory.oom_control
#跟預(yù)期的一樣,程序被暫停了
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,表示當前已經(jīng)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
# 切換會第一個窗口,發(fā)送程序又跑了兩步,停在了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 時,默認情況下,該進程已經(jīng)占用的內(nèi)存還是統(tǒng)計在原來的 cgroup 里面,不會占用新 cgroup 的配額,但新分配的內(nèi)存會統(tǒng)計到新的 cgroup 中(包括 swap out 到交換空間后再 swap in 到物理內(nèi)存中的部分)。
我們可以通過設(shè)置 memory.move_charge_at_immigrate 讓進程所占用的內(nèi)存隨著進程的遷移一起遷移到新的 cgroup 中。
enable: echo 1 > memory.move_charge_at_immigrate
disable:echo 0 > memory.move_charge_at_immigrate
注意: 就算設(shè)置為 1,但如果不是 thread group 的 leader,這個 task 占用的內(nèi)存也不能被遷移過去。
換句話說,如果以線程為單位進行遷移,必須是進程的第一個線程,如果以進程為單位進行遷移,就沒有這個問題。
當 memory.move_charge_at_immigrate 被設(shè)置成 1 之后,進程占用的內(nèi)存將會被統(tǒng)計到目的 cgroup 中,如果目的 cgroup 沒有足夠的內(nèi)存,系統(tǒng)將嘗試回收目的 cgroup 的部分內(nèi)存(和系統(tǒng)內(nèi)存緊張時的機制一樣,刪除不常用的 file backed 的內(nèi)存或者 swap out 到交換空間上,請參考Linux 內(nèi)存管理),如果回收不成功,那么進程遷移將失敗。
注意:遷移內(nèi)存占用數(shù)據(jù)是比較耗時的操作。
移除 cgroup
當 memory.move_charge_at_immigrate 為 0 時,就算當前 cgroup 中里面的進程都已經(jīng)移動到其它 cgropu 中去了,由于進程已經(jīng)占用的內(nèi)存沒有被統(tǒng)計過去,當前 cgroup 有可能還占用很多內(nèi)存,當移除該 cgroup 時,占用的內(nèi)存需要統(tǒng)計到誰頭上呢?
答案是依賴 memory.use_hierarchy 的值,
- 如果該值為 0,將會統(tǒng)計到 root cgroup 里;
- 如果值為 1,將統(tǒng)計到它的父 cgroup 里面。
force_empty
當向 memory.force_empty 文件寫入 0 時(echo 0 > memory.force_empty),將會立即觸發(fā)系統(tǒng)盡可能的回收該 cgroup 占用的內(nèi)存。該功能主要使用場景是移除 cgroup 前(cgroup 中沒有進程),先執(zhí)行該命令,可以盡可能的回收該 cgropu 占用的內(nèi)存,這樣遷移內(nèi)存的占用數(shù)據(jù)到父 cgroup 或者 root cgroup 時會快些。
memory.swappiness
該文件的值默認和全局的 swappiness(/proc/sys/vm/swappiness)一樣,修改該文件只對當前 cgroup 生效,其功能和全局的 swappiness 一樣,請參考Linux 交換空間中關(guān)于 swappiness 的介紹。
注意:有一點和全局的 swappiness 不同,那就是如果這個文件被設(shè)置成 0,就算系統(tǒng)配置的有交換空間,當前 cgroup 也不會使用交換空間。
memory.use_hierarchy
該文件內(nèi)容為 0 時,表示不使用繼承,即父子 cgroup 之間沒有關(guān)系;當該文件內(nèi)容為 1 時,子 cgroup 所占用的內(nèi)存會統(tǒng)計到所有祖先 cgroup 中。
如果該文件內(nèi)容為 1,當一個 cgroup 內(nèi)存吃緊時,會觸發(fā)系統(tǒng)回收它以及它所有子孫 cgroup 的內(nèi)存。
注意: 當該 cgroup 下面有子 cgroup 或者父 cgroup 已經(jīng)將該文件設(shè)置成了 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)當系統(tǒng)內(nèi)存充裕時,soft limit 不起任何作用
- 2)當系統(tǒng)內(nèi)存吃緊時,系統(tǒng)會盡量的將 cgroup 的內(nèi)存限制在 soft limit 值之下(內(nèi)核會盡量,但不 100% 保證)
從它的特點可以看出,它的作用主要發(fā)生在系統(tǒng)內(nèi)存吃緊時,如果沒有 soft limit,那么所有的 cgroup 一起競爭內(nèi)存資源,占用內(nèi)存多的 cgroup 不會讓著內(nèi)存占用少的 cgroup,這樣就會出現(xiàn)某些 cgroup 內(nèi)存饑餓的情況。如果配置了 soft limit,那么當系統(tǒng)內(nèi)存吃緊時,系統(tǒng)會讓超過 soft limit 的 cgroup 釋放出超過 soft limit 的那部分內(nèi)存(有可能更多),這樣其它 cgroup 就有了更多的機會分配到內(nèi)存。
從上面的分析看出,這其實是系統(tǒng)內(nèi)存不足時的一種妥協(xié)機制,給次等重要的進程設(shè)置 soft limit,當系統(tǒng)內(nèi)存吃緊時,把機會讓給其它重要的進程。
注意: 當系統(tǒng)內(nèi)存吃緊且 cgroup 達到 soft limit 時,系統(tǒng)為了把當前 cgroup 的內(nèi)存使用量控制在 soft limit 下,在收到當前 cgroup 新的內(nèi)存分配請求時,就會觸發(fā)回收內(nèi)存操作,所以一旦到達這個狀態(tài),就會頻繁的觸發(fā)對當前 cgroup 的內(nèi)存回收操作,會嚴重影響當前 cgroup 的性能。
memory.pressure_level
這個文件主要用來監(jiān)控當前 cgroup 的內(nèi)存壓力,當內(nèi)存壓力大時(即已使用內(nèi)存快達到設(shè)置的限額),在分配內(nèi)存之前需要先回收部分內(nèi)存,從而影響內(nèi)存分配速度,影響性能,而通過監(jiān)控當前 cgroup 的內(nèi)存壓力,可以在有壓力的時候采取一定的行動來改善當前 cgroup 的性能,比如關(guān)閉當前 cgroup 中不重要的服務(wù)等。目前有三種壓力水平:
-
low
- 意味著系統(tǒng)在開始為當前 cgroup 分配內(nèi)存之前,需要先回收內(nèi)存中的數(shù)據(jù)了,這時候回收的是在磁盤上有對應(yīng)文件的內(nèi)存數(shù)據(jù)。
-
medium
- 意味著系統(tǒng)已經(jīng)開始頻繁為當前 cgroup 使用交換空間了。
-
critical
- 快撐不住了,系統(tǒng)隨時有可能 kill 掉 cgroup 中的進程。
如何配置相關(guān)的監(jiān)聽事件呢?和 memory.oom_control 類似,大概步驟如下:
- 利用函數(shù) eventfd(2) 創(chuàng)建一個 event_fd
- 打開文件 memory.pressure_level,得到 pressure_level_fd
- 往 cgroup.event_control 中寫入這么一串:
<event_fd> <pressure_level_fd> <level>
- 然后通過讀 event_fd 得到通知
注意: 多個 level 可能要創(chuàng)建多個 event_fd,好像沒有辦法共用一個
Memory thresholds
我們可以通過 cgroup 的事件通知機制來實現(xiàn)對內(nèi)存的監(jiān)控,當內(nèi)存使用量穿過(變得高于或者低于)我們設(shè)置的值時,就會收到通知。使用方法和 memory.oom_control 類似,大概步驟如下:
- 利用函數(shù) eventfd(2) 創(chuàng)建一個 event_fd
- 打開文件 memory.usage_in_bytes,得到 usage_in_bytes_fd
- 往 cgroup.event_control 中寫入這么一串:
<event_fd> <usage_in_bytes_fd> <threshold>
- 然后通過讀 event_fd 得到通知
stat file
這個文件包含的統(tǒng)計項比較細,需要一些內(nèi)核的內(nèi)存管理知識才能看懂,這里就不介紹了(怕說錯)。詳細信息可以參考 Memory Resource Controller中的“5.2 stat file”。這里有幾個需要注意的地方:
- 里面 total 開頭的統(tǒng)計項包含了子 cgroup 的數(shù)據(jù)(前提條件是 memory.use_hierarchy 等于 1)。
- 里面的 'rss + file_mapped" 才約等于是我們常說的 RSS(ps aux 命令看到的 RSS)
- 文件(動態(tài)庫和可執(zhí)行文件)及共享內(nèi)存可以在多個進程之間共享,不過它們只會統(tǒng)計到他們的 owner cgroup 中的 file_mapped 去。(不確定是怎么定義 owner 的,但如果看到當前 cgroup 的 file_mapped 值很小,說明共享的數(shù)據(jù)沒有算到它頭上,而是其它的 cgroup)
小結(jié)
作用:限制 cgroups 中的進程占用的內(nèi)存上限
用法:
- 1)
memory.limit_in_bytes
配置進程可以使用的內(nèi)存上限(hard limit),當超過該閾值時,一般是嘗試使用 swap,如果不行則直接 kill 掉。 - 2)
memory.soft_limit_in_bytes
配置進程可以使用的內(nèi)存上行(soft limit),當系統(tǒng)內(nèi)存不足時,cgroups 會優(yōu)先將使用量超過 soft limit 的進程進行內(nèi)存回收,騰出內(nèi)存。 - 3)
memory.oom_control
參數(shù)配置內(nèi)存使用量到達閾值時內(nèi)核的處理行為,默認為 oom_kill。
原理:當進程使用內(nèi)存超過memory.limit_in_bytes
之后,系統(tǒng)會根據(jù) memory.oom_control
配置的行為進行處理,一般是嘗試使用 swap,如果不行則直接 kill 掉。
本節(jié)沒有介紹 swap 和 kernel 相關(guān)的內(nèi)容,不過在實際使用過程中一定要留意 swap 空間,如果系統(tǒng)使用了交換空間,那么設(shè)置限額時一定要注意一點,那就是當 cgroup 的物理空間不夠時,內(nèi)核會將不常用的內(nèi)存 swap out 到交換空間上,從而導(dǎo)致一直不觸發(fā) oom killer,而是不停的 swap out/in,導(dǎo)致 cgroup 中的進程運行速度很慢。
如果一定要用交換空間,最好的辦法是限制 swap+物理內(nèi)存 的額度,雖然我們在這篇中沒有介紹這部分內(nèi)容,但其使用方法和限制物理內(nèi)存是一樣的,只是換做寫文件 memory.memsw.limit_in_bytes 罷了。
4. 小結(jié)
本文主要簡單介紹了 pid、cpu、memory 這三個 subsystem 的作用和基本使用,具體如下:
subsystem | 功能 | 用法 | 原理 | 備注 |
---|---|---|---|---|
pid | 限制 cgroups 中進程使用的 pid 數(shù) | 配置 subsystem 中的 pids.max 即可 | 當 cgroups 中的進程調(diào)用 fork 或者 clone 系統(tǒng)調(diào)用時會判斷,subsystem 中配置的 pids.max 和當前 pids.current 的值,來確定是否能夠創(chuàng)建新的進程(或線程) | linux 中的 pid 是有限的,通過該 subsystem 可以有效防止 fork 炸彈之類的惡意進程 |
cpu | 限制 cgroups 中進程使用的 cpu 上限 | 1)限制為具體值:用 cfs_period_us & cfs_quota_us 兩個配置可以嚴格限制進程 cpu 使用量。 2)按比例分配:用 shares 配置,可以使得多個 cgroups 之間按比例分配所有 cpu。 | subsystem 負責提供配置,cfs 負責記錄進程使用的 cpu 時間,達到閾值后就從調(diào)度層面進行限制,避免該進程繼續(xù)使用 cpu。 | 一般使用 cfs_period_us & cfs_quota_us 方式限制具體值用得比較多。 |
memory | 限制 cgroups 中進程使用的 memory 上限 | 1)memory.limit_in_bytes 配置進程可以使用的內(nèi)存上限(hard limit),當超過該閾值時,一般是嘗試使用 swap,如果不行則直接 kill 掉。 2)memory.soft_limit_in_bytes 配置進程可以使用的內(nèi)存上行(soft limit),當系統(tǒng)內(nèi)存不足時,cgroups 會優(yōu)先將使用量超過 soft limit 的進程進行內(nèi)存回收,騰出內(nèi)存。 3)memory.oom_control 參數(shù)配置內(nèi)存使用量到達閾值時內(nèi)核的處理行為,默認為 oom_kill。 |
當進程使用內(nèi)存超過memory.limit_in_bytes 之后,系統(tǒng)會根據(jù) memory.oom_control 配置的行為進行處理,一般是嘗試使用 swap,如果不行則直接 kill 掉。 |
如果系統(tǒng)使用了交換空間,那么設(shè)置限額時一定要注意一點,那就是當 cgroup 的物理空間不夠時,內(nèi)核會將不常用的內(nèi)存 swap out 到交換空間上,從而導(dǎo)致一直不觸發(fā) oom killer,而是不停的 swap out/in,導(dǎo)致 cgroup 中的進程運行速度很慢。 |
如果你對云原生技術(shù)充滿好奇,想要深入了解更多相關(guān)的文章和資訊,歡迎關(guān)注微信公眾號。
搜索公眾號【探索云原生】即可訂閱
5. 參考
cgroups(7) — Linux manual page
美團技術(shù)團隊---Linux 資源管理之 cgroups 簡介文章來源:http://www.zghlxwxcb.cn/news/detail-783717.html
Red Hat---資源管理指南文章來源地址http://www.zghlxwxcb.cn/news/detail-783717.html
到了這里,關(guān)于深入剖析 Linux Cgroups 子系統(tǒng):資源精細管理的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!