距离上一次更新该文章已经过了 633 天,文章所描述的內容可能已经发生变化,请留意。
转自: https://cloud.tencent.com/developer/article/1495508
Kubernetes 对内存资源的限制实际上是通过 cgroup 来控制的,cgroup 是容器的一组用来控制内核如何运行进程的相关属性集合。针对内存、CPU 和各种设备都有对应的 cgroup。cgroup 是具有层级的,这意味着每个 cgroup 拥有一个它可以继承属性的父亲,往上一直直到系统启动时创建的 root cgroup。关于其背后的原理可以参考:深入理解Kubernetes资源限制:内存。
今天我们将通过实验来探索容器在什么情况下会被 oom-killed。
1. 实验准备
首先你需要一个 Kubernetes 集群,然后通过 kubectl 创建一个 Pod,内存限制为 123Mi
。
1 2 3
| $ kubectl run --restart=Never --rm -it --image=ubuntu --limits='memory=123Mi' -- sh If you don't see a command prompt, try pressing enter. root@sh:/#
|
重新打开一个 shell 窗口,找出刚创建的 Pod 的 uid:
1 2
| $ kubectl get pods sh -o yaml | grep uid uid: bc001ffa-68fc-11e9-92d7-5ef9efd9374c
|
在运行该 Pod 的节点上找出其 cgroup 的内存设置:
1 2 3 4
| $ cd /sys/fs/cgroup/memory/kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c
$ cat memory.limit_in_bytes 128974848
|
其中 memory.limit_in_bytes
表示当前限制的内存额度。128974848
正好等于 123*1024*1024
。
如果你查看一下这个 Pod 的 cgroup 目录,就会发现 Pod 中的每个容器都会在该目录下创建一个子 cgroup 目录:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| $ ll /sys/fs/cgroup/memory/kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c
总用量 0 drwxr-xr-x 2 root root 0 4月 28 18:46 64ae20d221399e618bbf8c15f3b5ae5050062d497971d0af5346d5532fa5c585 drwxr-xr-x 2 root root 0 4月 28 18:25 a398d3c012bb37dd9fe5fef524842a8699de931bce3a4e3753a49ef1694b33ee -rw-r--r-- 1 root root 0 4月 28 18:46 cgroup.clone_children --w--w--w- 1 root root 0 4月 28 18:46 cgroup.event_control -rw-r--r-- 1 root root 0 4月 28 18:46 cgroup.procs -rw-r--r-- 1 root root 0 4月 28 18:46 memory.failcnt --w------- 1 root root 0 4月 28 18:46 memory.force_empty -rw-r--r-- 1 root root 0 4月 28 18:46 memory.kmem.failcnt -rw-r--r-- 1 root root 0 4月 28 18:24 memory.kmem.limit_in_bytes -rw-r--r-- 1 root root 0 4月 28 18:46 memory.kmem.max_usage_in_bytes -r--r--r-- 1 root root 0 4月 28 18:46 memory.kmem.slabinfo -rw-r--r-- 1 root root 0 4月 28 18:46 memory.kmem.tcp.failcnt -rw-r--r-- 1 root root 0 4月 28 18:46 memory.kmem.tcp.limit_in_bytes -rw-r--r-- 1 root root 0 4月 28 18:46 memory.kmem.tcp.max_usage_in_bytes -r--r--r-- 1 root root 0 4月 28 18:46 memory.kmem.tcp.usage_in_bytes -r--r--r-- 1 root root 0 4月 28 18:46 memory.kmem.usage_in_bytes -rw-r--r-- 1 root root 0 4月 28 18:24 memory.limit_in_bytes -rw-r--r-- 1 root root 0 4月 28 18:46 memory.max_usage_in_bytes -rw-r--r-- 1 root root 0 4月 28 18:46 memory.memsw.failcnt -rw-r--r-- 1 root root 0 4月 28 18:46 memory.memsw.limit_in_bytes -rw-r--r-- 1 root root 0 4月 28 18:46 memory.memsw.max_usage_in_bytes -r--r--r-- 1 root root 0 4月 28 18:46 memory.memsw.usage_in_bytes -rw-r--r-- 1 root root 0 4月 28 18:46 memory.move_charge_at_immigrate -r--r--r-- 1 root root 0 4月 28 18:46 memory.numa_stat -rw-r--r-- 1 root root 0 4月 28 18:46 memory.oom_control ---------- 1 root root 0 4月 28 18:46 memory.pressure_level -rw-r--r-- 1 root root 0 4月 28 18:46 memory.soft_limit_in_bytes -r--r--r-- 1 root root 0 4月 28 18:46 memory.stat -rw-r--r-- 1 root root 0 4月 28 18:46 memory.swappiness -r--r--r-- 1 root root 0 4月 28 18:46 memory.usage_in_bytes -rw-r--r-- 1 root root 0 4月 28 18:46 memory.use_hierarchy -rw-r--r-- 1 root root 0 4月 28 18:46 notify_on_release -rw-r--r-- 1 root root 0 4月 28 18:46 tasks
|
输出结果的前两个目录其实就是 pause 容器的 pause
进程和业务容器的 bash
进程创建的两个子 cgroup 目录。可以来证实一下,我的环境使用的容器运行时是 containerd,可以通过 crictl 工具来查看,如果你使用的是 docker,方法类似。
先在运行该容器的节点上找到该业务容器的 ID:
1 2 3
| $ crictl ps|grep "CONTAINER_RUNNING sh"
64ae20d221399 sha256:d131e0fa2585a7efbfb187f70d648aa50e251d9d3b7031edf4730ca6154e221e 17 hours ago CONTAINER_RUNNING sh 0
|
查看该容器的 pid:
1 2 3 4 5
| $ crictl inspect 64ae20d221399|grep pid
"pid": 32308, "pid": 1 "type": "pid"
|
查看该进程所属的 cgroup,即进程在 cgroup 树中的路径:
1 2 3 4 5
| $ cat /proc/32308/cgroup
... 4:memory:/kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c/64ae20d221399e618bbf8c15f3b5ae5050062d497971d0af5346d5532fa5c585 ...
|
进入该目录,查看内存限制:
1 2 3 4
| $ cd /sys/fs/cgroup/memory/kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c/64ae20d221399e618bbf8c15f3b5ae5050062d497971d0af5346d5532fa5c585
$ cat memory.limit_in_bytes 128974848
|
可以看到该 cgroup 的内存限制和父 cgroup 一样,而父 cgroup 其实就是 Pod 级别的 cgroup。
按照预想,一旦 Pod 消耗的内存资源超过这个限制,cgroup 就会杀死容器进程,我们来测试一下。
2. 压力测试
先在容器中安装压力测试工具:
1
| root@sh:/# apt update; apt install -y stress
|
在另一个一个 shell 窗口中执行 dmesg -Tw
命令查看系统的 Syslog。
回到第一个 shell 窗口进行压力测试,限制内存在 100M 以内:
1 2 3
| root@sh:/# stress --vm 1 --vm-bytes 100M & [1] 271 root@sh:/# stress: info: [271] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
|
执行第二次压力测试:
1 2 3 4 5
| root@sh:/# stress --vm 1 --vm-bytes 50M stress: info: [273] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd stress: FAIL: [271] (415) <-- worker 272 got signal 9 stress: WARN: [271] (417) now reaping child worker processes stress: FAIL: [271] (451) failed run completed in 7s
|
可以看到系统通过发送 signal 9(SIGKILL) 信号杀死了第一次压力测试的进程(进程 ID 为 271
)。
可以在另一个 shell 窗口中看到系统的 syslog 日志输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| [Sat Apr 27 22:56:09 2019] stress invoked oom-killer: gfp_mask=0x14000c0(GFP_KERNEL), nodemask=(null), order=0, oom_score_adj=939 [Sat Apr 27 22:56:09 2019] stress cpuset=a2ed67c63e828da3849bf9f506ae2b36b4dac5b402a57f2981c9bdc07b23e672 mems_allowed=0 [Sat Apr 27 22:56:09 2019] CPU: 0 PID: 32332 Comm: stress Not tainted 4.15.0-46-generic #49-Ubuntu [Sat Apr 27 22:56:09 2019] Hardware name: BHYVE, BIOS 1.00 03/14/2014 [Sat Apr 27 22:56:09 2019] Call Trace: [Sat Apr 27 22:56:09 2019] dump_stack+0x63/0x8b [Sat Apr 27 22:56:09 2019] dump_header+0x71/0x285 [Sat Apr 27 22:56:09 2019] oom_kill_process+0x220/0x440 [Sat Apr 27 22:56:09 2019] out_of_memory+0x2d1/0x4f0 [Sat Apr 27 22:56:09 2019] mem_cgroup_out_of_memory+0x4b/0x80 [Sat Apr 27 22:56:09 2019] mem_cgroup_oom_synchronize+0x2e8/0x320 [Sat Apr 27 22:56:09 2019] ? mem_cgroup_css_online+0x40/0x40 [Sat Apr 27 22:56:09 2019] pagefault_out_of_memory+0x36/0x7b [Sat Apr 27 22:56:09 2019] mm_fault_error+0x90/0x180 [Sat Apr 27 22:56:09 2019] __do_page_fault+0x4a5/0x4d0 [Sat Apr 27 22:56:09 2019] do_page_fault+0x2e/0xe0 [Sat Apr 27 22:56:09 2019] ? page_fault+0x2f/0x50 [Sat Apr 27 22:56:09 2019] page_fault+0x45/0x50 [Sat Apr 27 22:56:09 2019] RIP: 0033:0x558182259cf0 [Sat Apr 27 22:56:09 2019] RSP: 002b:00007fff01a47940 EFLAGS: 00010206 [Sat Apr 27 22:56:09 2019] RAX: 00007fdc18cdf010 RBX: 00007fdc1763a010 RCX: 00007fdc1763a010 [Sat Apr 27 22:56:09 2019] RDX: 00000000016a5000 RSI: 0000000003201000 RDI: 0000000000000000 [Sat Apr 27 22:56:09 2019] RBP: 0000000003200000 R08: 00000000ffffffff R09: 0000000000000000 [Sat Apr 27 22:56:09 2019] R10: 0000000000000022 R11: 0000000000000246 R12: ffffffffffffffff [Sat Apr 27 22:56:09 2019] R13: 0000000000000002 R14: fffffffffffff000 R15: 0000000000001000 [Sat Apr 27 22:56:09 2019] Task in /kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c/a2ed67c63e828da3849bf9f506ae2b36b4dac5b402a57f2981c9bdc07b23e672 killed as a result of limit of /kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c [Sat Apr 27 22:56:09 2019] memory: usage 125952kB, limit 125952kB, failcnt 3632 [Sat Apr 27 22:56:09 2019] memory+swap: usage 0kB, limit 9007199254740988kB, failcnt 0 [Sat Apr 27 22:56:09 2019] kmem: usage 2352kB, limit 9007199254740988kB, failcnt 0 [Sat Apr 27 22:56:09 2019] Memory cgroup stats for /kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c: cache:0KB rss:0KB rss_huge:0KB shmem:0KB mapped_file:0KB dirty:0KB writeback:0KB inactive_anon:0KB active_anon:0KB inactive_file:0KB active_file:0KB unevictable:0KB [Sat Apr 27 22:56:09 2019] Memory cgroup stats for /kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c/79fae7c2724ea1b19caa343fed8da3ea84bbe5eb370e5af8a6a94a090d9e4ac2: cache:0KB rss:48KB rss_huge:0KB shmem:0KB mapped_file:0KB dirty:0KB writeback:0KB inactive_anon:0KB active_anon:48KB inactive_file:0KB active_file:0KB unevictable:0KB [Sat Apr 27 22:56:09 2019] Memory cgroup stats for /kubepods/burstable/podbc001ffa-68fc-11e9-92d7-5ef9efd9374c/a2ed67c63e828da3849bf9f506ae2b36b4dac5b402a57f2981c9bdc07b23e672: cache:0KB rss:123552KB rss_huge:0KB shmem:0KB mapped_file:0KB dirty:0KB writeback:0KB inactive_anon:0KB active_anon:123548KB inactive_file:0KB active_file:0KB unevictable:0KB [Sat Apr 27 22:56:09 2019] [ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name [Sat Apr 27 22:56:09 2019] [25160] 0 25160 256 1 28672 0 -998 pause [Sat Apr 27 22:56:09 2019] [25218] 0 25218 4627 872 77824 0 939 bash [Sat Apr 27 22:56:09 2019] [32307] 0 32307 2060 275 57344 0 939 stress [Sat Apr 27 22:56:09 2019] [32308] 0 32308 27661 24953 253952 0 939 stress [Sat Apr 27 22:56:09 2019] [32331] 0 32331 2060 304 53248 0 939 stress [Sat Apr 27 22:56:09 2019] [32332] 0 32332 14861 5829 102400 0 939 stress [Sat Apr 27 22:56:09 2019] Memory cgroup out of memory: Kill process 32308 (stress) score 1718 or sacrifice child [Sat Apr 27 22:56:09 2019] Killed process 32308 (stress) total-vm:110644kB, anon-rss:99620kB, file-rss:192kB, shmem-rss:0kB [Sat Apr 27 22:56:09 2019] oom_reaper: reaped process 32308 (stress), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
|
从宿主机的视角来看,PID 为 32308
的进程被 oom-killed 了,我们需要重点关注最后一段日志输出:
对于刚刚创建的 Pod 而言,有好几个进程作为 OOM killer 的候选人,其中最重要的进程是 pause
,用来为业务容器创建共享的 network namespace,其 oom_score_adj
值为 -998
,可以确保不被杀死。oom_score_adj
值越低就越不容易被杀死。关于 Pod 的 QoS 与 OOM 值的对应关系,可以参考:Kubernetes 资源管理概述。
除了 pause
进程外,剩下的进程 oom_score_adj
值均为 939
,我们可以根据 Kubernetes 官方文档中公式来验证一下:
1
| min(max(2, 1000 - (1000 * memoryRequestBytes) / machineMemoryCapacityBytes), 999)
|
进程的 oom_score_adj
值可以通过以下命令来查看:
1 2 3
| $ cat /proc/32308/oom_score_adj
939
|
其中 memoryRequest
是 pod 申请的资源,memoryCapacity
是节点的内存总量。可以看到,申请的内存越多,oom 值越低,也就越不容易被杀死。
查看运行该 Pod 的节点内存总量:
1 2 3 4 5 6 7
| $ kubectl describe nodes k3s | grep Allocatable -A 5 Allocatable: cpu: 1 ephemeral-storage: 49255941901 hugepages-1Gi: 0 hugepages-2Mi: 0 memory: 2041888Ki
|
如果只设置了 limits,Kubernetes 会自动把 Pod 的 requests 设置成和 limits 一样。所以其他进程的 oom_score_adj
值为 1000–123*1024/2041888=938.32
,这个值已经很接近 syslog 中输出的 939 了。
OOM killer 会根据进程的内存使用情况来计算 oom_score
的值,并根据 oom_score_adj
的值来进行微调。
进程的 oom_score
值可以通过以下命令来查看:
1 2 3
| $ cat /proc/32308/oom_score
1718
|
因为业务容器内所有进程的 oom_score_adj
值都相同,所以谁的内存使用量最多,oom_score
值就越高,也就越容易被杀死。因为第一个 stress 进程使的内存使用量最多(100M),oom_score
值最高(值为 1718),所以被杀死。
3. 总结
Kubernetes 通过 cgroup 和 OOM killer 来限制 Pod 的内存资源,在实际使用中我们需要小心区分 OS 级别的 OOM 和 Pod 级别的 OOM。