【转】容器内存监控,是看rss还是wss?

77次阅读
没有评论

文章来源:容器内存监控,是看 rss 还是 wss?– 简书

问题

cadvisor 提供了多个容器内存相关的指标,其中最重要的是以下几个:

(From https://help.aliyun.com/document_detail/413870.html

Pod 命令如何计算内存使用量

执行 kubectl top pod 命令得到的结果,并不是容器服务中 container_memory_usage_bytes 指标的内存使用量,而是指标 container_memory_working_set_bytes 的内存使用量,计算方式如下:

  • container_memory_usage_bytes = container_memory_rss + container_memory_cache + kernel memory
  • container_memory_working_set_bytes = container_memory_usage_bytes – total_inactive_file(未激活的匿名缓存页)
  • container_memory_working_set_bytes 是容器真实使用的内存量,也是资源限制 limit 时的重启判断依据

由此可见,k8s 注重 container_memory_working_set_bytes(下面简称 wss)。不少网上的容器告警规则范例也使用了 wss。那我们到底应该用 rss 还是 wss 作为容器告警的指标呢?

上周团队内对此产生了一些疑问,这里做了一些深入的分析和实验。

解读

  1. 关于 active_file 和 inactive_file

根据https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt

active_file 和 inactive_file 的定义为:

inactive_file   - # of bytes of file-backed memory on inactive LRU list.
active_file - # of bytes of file-backed memory on active LRU list.

Linux 系统会把进程占用后多余的内存用作 page cache,当访问文件后就加载到内存中,加速后面再次访问文件的速度。当系统需要更多常驻内存的时候,又会从 page cache 腾出空间。这种用途的内存叫做 file-backed memory(相对应与文件无关的叫 anonymous memory)。page cache 分为 inactive_file 和 active_file。第一次读写文件后的 cache,属于 inactive_file,多次访问这个文件之后,属于 active_file。inactive_file 的 cache 是会可以被操作系统直接回收使用的,active_file 不会直接回收,而是先变成 inactive_file。

可以用下面的实验进行验证。

实验一:

进入一个 kun 容器,此容器的内存 limit 是 375MB(不是 MiB)。

内核版本是 5.10

root@container-0:/# uname -a
Linux container-0 5.10.134-12.2.al8.x86_64 #1 SMP Thu Oct 27 10:07:15 CST 2022 x86_64 x86_64 x86_64 GNU/Linux

cat /sys/fs/cgroup/memory/memory.limit_in_bytes          
375390208
  1. 查看 /sys/fs/cgroup/memory/memory.stat 文件,完整内容如下:
root@container-0:/# cat /sys/fs/cgroup/memory/memory.stat             cache 3244032
rss 35553280
rss_huge 0
shmem 0
mapped_file 675840
dirty 675840
writeback 0
swap 0
pgpgin 390783690
pgpgout 390780736
pgfault 923765304
pgmajfault 0
inactive_anon 35590144
active_anon 0
inactive_file 540672
active_file 2838528
unevictable 0
hierarchical_memory_limit 375390208
hierarchical_memsw_limit 9223372036854771712
total_cache 3244032
total_rss 35553280
total_rss_huge 0
total_shmem 0
total_mapped_file 675840
total_dirty 675840
total_writeback 0
total_swap 0
total_pgpgin 390783690
total_pgpgout 390780736
total_pgfault 923765304
total_pgmajfault 0
total_inactive_anon 35590144
total_active_anon 0
total_inactive_file 540672
total_active_file 2838528
total_unevictable 0

我们关注的是:

total_cache 3244032

total_rss 35553280

total_inactive_file 540672

total_active_file 2838528

  1. 写入 1 个 100MB 的文件
root@container-0:/# dd if=/dev/zero of=test_100m bs=1M count=100 
100+0 records in
100+0 records out
104857600 bytes (105 MB, 100 MiB) copied, 0.708941 s, 148 MB/s

再次查看 /sys/fs/cgroup/memory/memory.stat:

total_cache 108269568

total_rss 35553280

total_inactive_file 105431040

total_active_file 2838528

cache 和 inactive_file 增加了 100MB,而 rss 和 active_file 没有变化

  1. 重复读这个文件两次
root@container-0:/# cat test_100m > /dev/null

再次查看 /sys/fs/cgroup/memory/memory.stat:

total_cache 108269568

total_rss 34607104

total_inactive_file 540672

total_active_file 107864064

cache 和 rss 没有变化,inactive_file 减少 100MB,active_file 增加了 100MB。

可见多次访问此文件后,内存 cache 从 inactive 变成了 active。

对应,容器的指标曲线:

  1. 重复访问多个 100MB 文件
root@container-0:/# cp test_100m{,.1}
root@container-0:/# cp test_100m{,.2}
root@container-0:/# cp test_100m{,.3}
root@container-0:/# cat test_100m.1 > /dev/null
root@container-0:/# cat test_100m.1 > /dev/null
root@container-0:/# time cat test_100m.2 > /dev/null
real    0m0.965s
user    0m0.000s
sys     0m0.092s
root@container-0:/# 
root@container-0:/# time cat test_100m.2 > /dev/null
real    0m0.091s
user    0m0.000s
sys     0m0.019s
root@container-0:/# time cat test_100m.3 > /dev/null
real    0m0.922s
user    0m0.001s
sys     0m0.095s
root@container-0:/# time cat test_100m.3 > /dev/null
real    0m0.110s
user    0m0.000s
sys     0m0.021s

第二次读文件的时候,耗时变短,说明已经使用了 cache。

查看 /sys/fs/cgroup/memory/memory.stat:

total_cache 342380544

total_rss 33660928

total_inactive_file 123699200

total_active_file 217837568

cache 已经使用了 300MB+,但 inactive 并不会全部转成 active(跟文件大小有关,如果是 10MB 的文件,active 会用的更多)。

查看 usage:

root@container-0:/# cat /sys/fs/cgroup/memory/memory.usage_in_bytes
374579200

从指标数据看,container_memory_working_set_bytes 是 251MB,跟 usage – total_inactive_file = 374-123 = 251MB 能对应上。

  1. 尝试清理 page cache
root@container-0:/# echo 1 > /proc/sys/vm/drop_caches
bash: /proc/sys/vm/drop_caches: Read-only file system

在 Linux 主机上,我们可以执行上面的命令来清理 cache,但在容器里执行报错。说明只能清理整个操作系统的 cache,无法只清理 cgroup 产生的 cache。

  1. 增加 RSS 内存使用

通过一个简单脚本,逐渐增加容器中的 RSS 内存使用。

从 18:12 开始,可以看到随着 rss 内存增加,wss 也逐渐增加,cache 逐渐减少。

观察 /sys/fs/cgroup/memory/memory.stat:

最初:

total_rss 102707200

total_inactive_file 135884800

total_active_file 135843840

=>

total_rss 205164544

total_inactive_file 85196800

total_active_file 85307392

=>

total_rss 300863488

total_inactive_file 37302272

total_active_file 37400576

=>

total_rss 374124544

total_inactive_file 581632

total_active_file 749568

可以看到:

  1. rss + active_file + inactive_file 不会超过 memory limit
  2. 并不是先把 inactive_file 先回收完才回收 active_file 部分的,而是随着 inactive_file 减少,部分 active_file 会变成 inactive_file,两者维持一定的比例。从上面曲线中 wss 的上升速度比 rss 上升慢也推导出 active_file 部分是在减少的。

直到最后 cache 已经回收完了,内存不够分配,容器发生了 OOMKilled。

  1. 理解 container_memory_working_set_bytes

working set 是 Linux 中的概念,但没有一个严格的定义,比如 解释 1 解释 2 解释 3 等。

k8s 文档 中,对 working set 的描述是:

结合 cadvisor 中 container_memory_working_set_bytes 指标的计算方法,wss 可以约等于 rss+active_file。

上面 highlight 的文字也解释了为什么 k8s 选择 wss 作为监控指标,即 active_file 部分的 cache 不是总能被回收。这里没有明确说,但在这份 kernel 文档 的 10.1 部分有提到 active_list 什么时候能迁移到 inactive_list,就是没有再被引用的情况下。一种场景是如果写大量数据到文件,已经写入内存 cache 但还没来得及回写完到磁盘,这时的 active_file 就是被引用的。

内核中 dirty_writeback_centisecs 的值默认值是 5 秒,一般不会导致大量写数据积压。但由于无法衡量 active_file 中有多少可以变为 inactive_file 从而被回收,因此 k8s 保险起见,选择了 active_file 都要被计算在内存使用量以内。

由此可见:

  1. container_memory_usage_bytes > container_memory_working_set_bytes > container_memory_rss
  2. 如果要申请的内存量 > limit – rss,内存分配必定失败 如果要申请的内存量 > limit – wss,又有可能成功,也有可能失败,取决于 active_file 能回收的部分够不够多

接下来,如果容器中的应用申请内存失败,会发生什么事情?

  1. 延伸:容器发生 OOM,到底指的是什么?

有很详细的文章介绍:

结合文章内容及我的理解,要点是:

  1. 在 k8s 上,同时有 Pod 驱逐和 OOM Killer 两套机制在工作
  2. Pod 驱逐在节点内存不足时按配置的策略驱逐个别容器到其他节点,是保护不会因为节点自身 OOM 整体挂掉。驱逐机制见 文档。这时容器退出的原因是 Evicted,不算是容器发生 OOM
  3. OOM Killer 则是 Linux 的机制,k8s 不直接控制,但:

    1. 通过 oom_score_adj 参数调整 OOM 的行为
    2. 监控容器是否发生了 OOM Kill 事件导致容器退出,如果有会标记退出的 reason 为 OOMKilled
  4. OOM Killer 运行在两个层面:

    1. 节点 OS 的 OOM Killer,当节点内存不足时杀进程,但不管是不是容器的进程,也是起到了保护节点的作用
    2. 容器内部的 OOM Killer,当容器内存分配不足时,杀容器内部的进程。如果是杀主进程就会导致容器退出。这时内存分配不足时,有可能 rss 很高,也 rss 不高但 wss 很高,即 cache 中有部分未及时回写磁盘导致内存无法回收。

实验二

  1. 让容器发生 OOMKilled 退出

通过简单脚本,每运行一次消耗 1MB 内存,直到 RSS 内存打满。即实验一中的第六步。

由于 RSS 打满时,主进程是内存使用量最高的,因此被 kill 掉,容器退出。

  1. 容器内部发生 OOM 但不退出

通过简单脚本,每运行一次消耗 100MB 内存,直到 RSS 内存打满。

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                                                                    
  54209 root      20   0  111940 110676   1376 S   0.0   0.1   0:00.10 tail                                                                                                       
  54226 root      20   0  111940 110676   1376 S   0.0   0.1   0:00.10 tail                                                                                                       
  54350 root      20   0  111128 109884   1384 S   0.0   0.1   0:00.09 tail

此时再执行一次脚本,发现有旧进程 54226 退出,多了一个新进程 54475。

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                                                                    
  54209 root      20   0  111940 110676   1376 S   0.0   0.1   0:00.10 tail                                                                                                       
  54475 root      20   0  110992 110008   1520 S   0.0   0.1   0:00.08 tail                                                                                                       
  54350 root      20   0  111128 109884   1384 S   0.0   0.1   0:00.09 tail

dmesg 中可以看到 oom 信息

root@container:/# dmesg | grep oom
[8306983.891401] tail invoked oom-killer: gfp_mask=0xcc0(GFP_KERNEL), order=0, oom_score_adj=999

从图上看,WSS 已经打满,但主进程没有受影响,容器还在正常运行。

  1. 想尝试节点内存不足导致的 OOM Kill 和 Pod 驱逐,没有环境

回顾问题

回到最初的问题,容器内存使用率告警,应该用 rss 还是 wss 指标。那要看我们是要对什么问题做预警。

预警容器 OOM

解读 3 可以看出,容器发生 OOM 有三种情况:

  1. cache 都已经被回收:容器的 rss 使用率和 wss 使用率都很高,没法再分配出内存。如果主进程是占用内存最多的进程,主进程被容器内 OOM Killer 杀掉
  2. cache 无法回收:容器的 rss 使用率不高,瞬间大量写数据无法及时回写磁盘导致 wss 使用率很高,再申请内存时无法分配。如果主进程是占用内存最多的进程,主进程被容器内 OOM Killer 杀掉
  3. 节点内存不足,某容器主进程的 rss 内存使用量最高,跟使用率无关。此时容器主进程被节点 OOM Killer 杀掉

对于第一种情况,预警容器 OOM 可以使用 wss 或 rss 使用率的规则。

第二种情况,由于通常是突发写数据造成,无论用 wss 或 rss 使用率都无法及时告警。

对于第三种情况,应该用节点内存使用率的告警,通知给集群维护者。对于容器 rss 使用率和使用量,告警给容器使用方没有意义(使用量无法定阈值,使用率很高但使用量低不会在节点层面被 kill)。

预警容器被驱逐

解读 3 可以看出,容器被被驱逐的必要条件,是节点内存不足,按照 策略 驱逐个别容器,跟容器 wss 使用率和 rss 使用率无关。

因此预警容器被驱逐,应该用节点内存使用率的告警,通知给集群维护者。对于容器 wss 使用率和使用量,告警给容器使用方没有意义(使用量无法定阈值,使用率很高不代表会优先被驱逐)。

这里还需要注意的是,驱逐机制是按节点的 wss 来判断是否达到了驱逐条件,见 文档

结论

从上面可以看出,使用 wss 指标做告警,并没有比 rss 更优。

然后,wss 指标存在几个问题:

  1. wss 很高,可能只是 active_file 内存占用多,需要的时候是可以被回收的。使用 wss 容易出现用户无需处理的误告
  2. rss 指标更符合大家的使用习惯,使用 wss 更难以对业务方解释。在传统的 Linux 主机监控,我们一般用 MemTotal-MemAvailable 内存大小代表主机的内存使用水位,并对此做告警。其中 MemAvailable 包含 page cache,见 文档,这也与 wss 的定义不同。

综上,没有必要使用 container_memory_working_set_bytes 作为容器内存使用率的告警,使用 container_memory_rss 即可。

无法解决的告警问题:

  1. 突发的大量内存申请,由于告警周期是是 1 分钟,可能在 OOM 前不会触发告警。
  2. 如果容器内存使用率的定义为 rss/limit,在节点允许超卖的情况下(limit > request),仍有可能出现容器内存使用率不高,但节点由于超卖过多、内存压力大导致 OOM 或者被驱逐的问题。这时可能不会触发告警。

参考资料

实际 Linux 的内存管理很复杂,概念也很多,一些资料:

Cgroup – Linux 内存资源管理

Chapter 10 Page Frame Reclamation(Page Cache 回收机制)

正文完
 
评论(没有评论)