docker-4.1 容器内存问题

4,136 阅读9分钟

记一次线上docker容器内存占用过高不释放的问题解决过程

问题描述

在jupyter中执行hive语句将表查询结果保存到本地目录中,从prometheus中监控到内存一直飙升到最大内存限制35G,在容器内部通过top命令或者ps命令查看或者docker stats命令查看内存占用远没有那么多。考虑到模型部署时或者有用户反应遇到过OOM Killed情况,开始研究这种现象原因。

问题分析

命令查看内存数值不同是因为命令的输出内存含义不同,具体查看第一条和第二条;内存过高不释放问题查看第三条。

1. prometheus与docker stats命令内存数值含义

  • prometheus: prometheus的架构介绍就不在此说了,线上k8s集群的监控主要基于两个Exporter:

    1. Node Exporter负责收集 host 硬件和操作系统数据。它将以pod方式运行在每个host 上,可使用kubectl get po -n monitoring查看

      Export通过HTTP将收集的数据发送给prometheus server,可以通过prometheus的界面target查看指标接口

    2. 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

参数解释:

  1. VSZ & VIRT

假如进程申请100m的内存,但实际只使用了10m,那么它会增长100m,而不是实际的使用量

进程占用的虚拟内存大小

  1. 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

  • 问题分析:
  1. 当对文件系统进行写入时,该写入不会立即提交到磁盘,这将是非常低效的。相反,写入页面缓存的内存区域中,并且周期性地以块的形式写入磁盘。

    Linux上的’Flush’负责将脏页写入磁盘。它是一个定期唤醒的守护进程,用于确定是否需要写入磁盘,如果需要,则执行。

    Flush在容器之外运行,因为容器没有自己的内核,不能使用自己的内核或刷新守护进程,需要等待物理机进行flush或者手动物理机执行echo 1 > /proc/sys/vm/drop_caches。

  2. 查看/proc/sys/vm/dirty_background_ratio 值为10%

    vm.dirty_background_ratio是内存可以填充“脏数据”的百分比。这些“脏数据”在稍后是会写入磁盘的,pdflush/flush/kdmflush这些后台进程会稍后清理脏数据。举一个例子,host有512G内存,那么整个物理机cache判断有了51.2G才会开始清理它。

  3. 在分配这部分已经被 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,可知用户代码申请的内存空间确实很大,需要用户查看代码或者增大申请内存。

解决方案:

  1. 页面查得的prometheus内存监控项改为container_memory_working_set_bytes,或者缓存和工作内存全部展示,而不是显示useg_memory

  2. 用户OOM问题,

    a.建议用户审查代码,运维查看下内核日志综合分析一下场景原因。

    b.考虑关闭cgroup oom killer。内核无法给进程分配足够的内存时,将会暂停该进程直到有空余的内存之后再继续运行.