记一次线上docker容器内存占用过高不释放的问题解决过程
问题描述
在jupyter中执行hive语句将表查询结果保存到本地目录中,从prometheus中监控到内存一直飙升到最大内存限制35G,在容器内部通过top命令或者ps命令查看或者docker stats命令查看内存占用远没有那么多。考虑到模型部署时或者有用户反应遇到过OOM Killed情况,开始研究这种现象原因。
问题分析
命令查看内存数值不同是因为命令的输出内存含义不同,具体查看第一条和第二条;内存过高不释放问题查看第三条。
1. prometheus与docker stats命令内存数值含义
-
prometheus: prometheus的架构介绍就不在此说了,线上k8s集群的监控主要基于两个Exporter:
-
Node Exporter负责收集 host 硬件和操作系统数据。它将以pod方式运行在每个host 上,可使用kubectl get po -n monitoring查看
Export通过HTTP将收集的数据发送给prometheus server,可以通过prometheus的界面target查看指标接口
-
cAdvisor负责收集容器数据。它将在kubelet一起运行在所有 host 上。
cAdvisor是google开发的容器监控工具,可以通过github.com/cadvisor查看提供给prometheus的监控指标及含义 container_memory_cache: Total page cache memory
container_memory_usage_bytes: Current memory usage, including all memory regardless of when it was accessed container_memory_working_set_bytes: Current working set可知,查看的prometheus监控指标container_memory_usage_bytes中包含了cache
-
-
docker stats
从docker官方文档中可以看到以下描述:
On Linux, the Docker CLI reports memory usage by subtracting page cache usage from the total memory usage.
所以通过docker stats命令查得的内存大小实际为container_memory_usage_bytes - container_memory_cache
[root@TX-220-54-202 ~]# docker stats f29f61770a02 --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
f29f61770a02 k8s_notebook_jupyter-ningsheng-0kctiqfg_kubeflow_35a5db12-b7fa-11e9-9fa1-246e967d5d94_2 0.13% 619.9MiB / 8GiB 7.57% 0B / 0B 16.9MB / 520kB 313
[root@jupyter-ningsheng-0kctiqfg notebook]# cat /sys/fs/cgroup/memory/memory.limit_in_bytes
8589934592
[root@jupyter-ningsheng-0kctiqfg notebook]# cat /sys/fs/cgroup/memory/memory.usage_in_bytes
1710297088
[root@jupyter-ningsheng-0kctiqfg notebook]# cat /sys/fs/cgroup/memory/memory.stat
cache 1059909632 # page cache, including tmpfs (shmem), in bytes
rss 546828288 # anonymous and swap cache, not including tmpfs (shmem), in bytes
mapped_file 2973696 # size of memory-mapped mapped files, including tmpfs(shmem), in bytes
inactive_anon 0 # anonymous and swap cache on inactive LRU list, including tmpfs (shmem), in bytes
active_anon 547405824 # file-backed memory on active LRU list, in bytes
inactive_file 1048981504# file-backed memory on inactive LRU list, in bytes
active_file 12390400 # file-backed memory on active LRU list, in bytes
# active_file + inactive_file = cache - size of tmpfs
# active_anon + inactive_anon = anonymous memory + file cache for tmpfs + swap cache
具体memory.stat文件中其他指标的具体含义可查看resource_management_guide
-
结论
prometheus中取到的cAdvise数据container_memory_usage_bytes,即容器内部cat /sys/fs/cgroup/memory/memory.usage_in_bytes数值 prometheus中取到的cAdvise数据container_memory_cache,即容器内部cat /sys/fs/cgroup/memory/memory.stat数值
2. top或者ps进程内存占用数据含义
进程使用的内存包含如下:
A. 用户空间的匿名映射页:比如调用malloc分配的内存,以及使用MAP_ANONYMOUS的mmap;当系统内存不够时,内核可以将这部分内存交换出去。
B. 用户空间的文件映射页(map file): 比如指定文件的mmap. 当系统内存不够时内核可以回收这些页.
C. 用户空间的文件映射页(map tmfs): 比如shmem。当系统内存不够时,内核可以回收这些页.
D. page cache: 比如读取块设备文件的buffer cache以及读取文件系统的文件缓存
-
从 cat /proc/[pid]/status 文件中读取的信息
VmPeak: 4372 kB 内存使用峰值
VmSize: 4372 kB 进程虚拟地址空间大小
VmLck: 0 kB 进程锁住的物理内存大小,锁住的物理内存无法交换到硬盘
VmPin: 0 kB
VmHWM: 656 kB 进程所使用的物理内存的峰值
VmRSS: 656 kB 进程正在使用的物理内存大小
RssAnon: 84 kB
RssFile: 572 kB
RssShmem: 0 kB
-
top或者ps查看
[root@jupyter-ningsheng-0kctiqfg notebook]# ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 4372 656 ? Ss 8月14 0:01 tini -- start-singleuser.sh --ip=0.0.0.0 --port=8888 --allow-root
[root@jupyter-ningsheng-0kctiqfg notebook]# top
.........
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 4372 656 572 S 0.0 0.0 0:01.20 tini
参数解释:
- VSZ & VIRT
假如进程申请100m的内存,但实际只使用了10m,那么它会增长100m,而不是实际的使用量
进程占用的虚拟内存大小
- RES & RSS
如果申请100m的内存,实际使用10m,它只增长10m
VmRSS = RssAnon + RssFile + RssShmem
- 进程RSS与Cgroup RSS之间的差异 cgroup memory.stat 显示的RSS为RssAnon,比PS显示的进程的RSS少了shmem, mapped file
3. 内存过高不释放问题
- 现象: 针对用户代码分析得知,hive进行将查询的表结果保存到本地时(表数据百G)时,内存会一直增长直到限制。
- 实验模拟: 根据现象进行实验机器[8G mem]模拟,使用dd if=/dev/zero of=/export/test bs=1024M count=10 内存占用,发现cache立马增加且不会释放。但malloc之后会迅速释放掉占用的大量内存
[root@jupyter-ningsheng-0kctiqfg notebook]# cat /sys/fs/cgroup/memory/memory.stat |head -n 1
cache 2506752
[root@jupyter-ningsheng-0kctiqfg notebook]# dd if=/dev/zero of=/export/test bs=1024M count=10
10+0 records in
10+0 records out
10737418240 bytes (11 GB) copied, 31.7019 s, 339 MB/s
[root@jupyter-ningsheng-0kctiqfg notebook]# cat /sys/fs/cgroup/memory/memory.stat |head -n 1
cache 6566047744
[root@jupyter-ningsheng-0kctiqfg notebook]# ./mem-allocate
1G memory allocated
2G memory allocated
3G memory allocated
4G memory allocated
5G memory allocated
6G memory allocated
7G memory allocated
[root@jupyter-ningsheng-0kctiqfg notebook]# cat /sys/fs/cgroup/memory/memory.stat |head -n 1
cache 1810432
- 问题分析:
-
当对文件系统进行写入时,该写入不会立即提交到磁盘,这将是非常低效的。相反,写入页面缓存的内存区域中,并且周期性地以块的形式写入磁盘。
Linux上的’Flush’负责将脏页写入磁盘。它是一个定期唤醒的守护进程,用于确定是否需要写入磁盘,如果需要,则执行。
Flush在容器之外运行,因为容器没有自己的内核,不能使用自己的内核或刷新守护进程,需要等待物理机进行flush或者手动物理机执行echo 1 > /proc/sys/vm/drop_caches。
-
查看/proc/sys/vm/dirty_background_ratio 值为10%
vm.dirty_background_ratio是内存可以填充“脏数据”的百分比。这些“脏数据”在稍后是会写入磁盘的,pdflush/flush/kdmflush这些后台进程会稍后清理脏数据。举一个例子,host有512G内存,那么整个物理机cache判断有了51.2G才会开始清理它。
-
在分配这部分已经被 buffer/cache 占用的内存的时候,内核会先对其上面的脏数据进行写回操作,保证数据一致后才会清空并分给进程使用。如果此时你的进程是突然申请大量内存,而且你的业务是一直在产生很多脏数据(比如日志),并且系统没有及时写回的时候,此时系统给进程分配内存的效率会很慢,系统IO也会很高。
4. OOM问题
现象:
[k8s@TX-220-54-9 ~]$ kubectl get po --all-namespaces -o wide |grep OOM
model-deployment svc-03256fa3-960d-4c89-8020-741eae1851c6 0/1 OOMKilled 0 2d19h 10.244.2.179 tx-220-54-9.h.chinabank.com.cn <none> <none>
问题分析: dockerId: 2e8e56c5bf8a2e4a5c5ef824f1323d44f2194b9ffbb4336d579bd0ff435a2f98 查看54-9主机内核日志dmesg如下,
[7600330.400352] Memory cgroup stats for /kubepods.slice/kubepods-pod9df61518_c02a_11e9_9694_246e967d5d94.slice/docker-2e8e56c5bf8a2e4a5c5ef824f1323d44f2194b9ffbb4336d579bd0ff435a2f98.scope: cache:0KB rss :66947340KB rss_huge:11945984KB shmem:0KB mapped_file:0KB dirty:0KB writeback:2508KB swap:0KB inactive_anon:0KB active_anon:66949060KB inactive_file:0KB active_file:4KB unevictable:0KB
4397 [7600330.400359] Tasks state (memory values in pages):
4398 [7600330.400359] [ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name
4399 [7600330.400665] [ 26635] 0 26635 255 1 28672 0 -998 pause
4400 [7600330.400668] [ 26792] 0 26792 5522 2561 86016 0 -998 bash
4401 [7600330.400677] [ 26971] 0 26971 9650443 28076 1048576 0 -998 java
4402 [7600330.400679] [ 27038] 0 27038 5513 2523 77824 0 -998 bash
4403 [7600330.400680] [ 27044] 0 27044 5515 2554 81920 0 -998 bash
4404 [7600330.400683] [ 27136] 0 27136 21533 1321 217088 0 -998 su
4405 [7600330.400685] [ 27137] 1000 27137 4982 2516 81920 0 -998 bash
4406 [7600330.400687] [ 27220] 1000 27220 17824362 16715656 134701056 0 -998 python
4407 [7600330.400753] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=docker-2e8e56c5bf8a2e4a5c5ef824f1323d44f2194b9ffbb4336d579bd0ff435a2f98.scope,mems_allowed=0-1,oom_memcg=/kubepods.slice/kubepod s-pod9df61518_c02a_11e9_9694_246e967d5d94.slice,task_memcg=/kubepods.slice/kubepods-pod9df61518_c02a_11e9_9694_246e967d5d94.slice/docker-2e8e56c5bf8a2e4a5c5ef824f1323d44f2194b9ffbb4336d579bd0ff435a2f98.sc ope,task=python,pid=27220,uid=1000
4408 [7600330.400758] Memory cgroup out of memory: Kill process 27220 (python) score 0 or sacrifice child
可以看到用户进程申请的cache为0,而rss超过70多G,可知用户代码申请的内存空间确实很大,需要用户查看代码或者增大申请内存。
解决方案:
-
页面查得的prometheus内存监控项改为container_memory_working_set_bytes,或者缓存和工作内存全部展示,而不是显示useg_memory
-
用户OOM问题,
a.建议用户审查代码,运维查看下内核日志综合分析一下场景原因。
b.考虑关闭cgroup oom killer。内核无法给进程分配足够的内存时,将会暂停该进程直到有空余的内存之后再继续运行.