09 | Page Cache:为什么我的容器内存使用量总是在临界点?

本文仅作为学习记录,非商业用途,侵删,如需转载需作者同意。

上一节我们知道,如果容器使用的物理内存超过了 memory.limit_in_bytes,容器中的进程会被 OOM Killer 杀死。

不过在有一些容器使用场景中,容器应用有很多文件读写,你会发现整个容器的内存使用量已经接近 Memory Cgroup 上限值了,但是在容器中再申请内存,还是可以申请出来,并没有发生OOM。

一、问题再现

测试使用的代码:https://github.com/chengyli/training/tree/main/memory/page_cache

启动一个容器,并且给容器的Memory Cgroup 里的内存上限设置为100MB(104857600bytes)


#!/bin/bash

docker stop page_cache;docker rm page_cache

if [ ! -f ./test.file ]
then
  dd if=/dev/zero of=./test.file bs=4096 count=30000
  echo "Please run start_container.sh again "
  exit 0
fi
echo 3 > /proc/sys/vm/drop_caches
sleep 10

docker run -d --init --name page_cache -v $(pwd):/mnt registry/page_cache_test:v1
CONTAINER_ID=$(sudo docker ps --format "{{.ID}}\t{{.Names}}" | grep -i page_cache | awk '{print $1}')

echo $CONTAINER_ID
CGROUP_CONTAINER_PATH=$(find /sys/fs/cgroup/memory/ -name "*$CONTAINER_ID*")
echo 104857600 > $CGROUP_CONTAINER_PATH/memory.limit_in_bytes
cat $CGROUP_CONTAINER_PATH/memory.limit_in_bytes

查看运行中的容器的Memory Cgroup 中的 memory.limit_in_bytes 和 memory.usage_in_bytes 这两个值。

以下为作者测试的结果图:
在这里插入图片描述

我们把容器内存上限值和已使用的内存数值做个减法,104857600–104767488= 90112bytes,只差大概 90KB 左右的大小。

但是,如果这时候我们继续启动一个程序,让这个程序申请并使用 50MB 的物理内存,就会发现这个程序还是可以运行成功,这时候容器并没有发生 OOM 的情况。这时我们再去查看参数 memory.usage_in_bytes,就会发现它的值变成了 103186432bytes,比之前还少了一些。那这是怎么回事呢

在这里插入图片描述

二、知识详解:Linux系统有哪些内存类型

要解决上面的问题,需要理解Linux 系统中有哪几种操作类型,不同类型的内存在容器内存增高到最高限制的时候,处理的方式也不同。

2.1、Linux内存类型

Linux各个模块都需要内存:

  • 内核:需要分配内存给页表,内核栈,slab,也就是内核各种数据结构的 cache pool ;
  • 用户态:堆内存,栈内存,共享库内存,还有文件读写的 page cache

Memory Cgroup 不会对内核的内存做限制(比如页表,slab)
今天主要讨论的是用户态相关的两个内存类型:RSS,Page Cache

2.2、RSS

RSS :Resident Set Size 缩写(常驻内存),指进程真正申请到物理页面的内存大小。

应用程序申请内存时,比如调用 malloc() 来申请100MB 的内存大小。
malloc() 返回成功了,这时系统只是把100MB 的虚拟地址空间分配给了进程,但是并没有把实际的物理内存页面分配给进程。

当进程对这块内存地址开始做真正读写操作的时候,系统才会把实际需要的物理内存分配给进程。
而这个过程中,进程真正得到的物理内存就是 RSS

下面为作者测试验证的过程:

比如下面的这段代码,我们先用 malloc 申请 100MB 的内存。


    p = malloc(100 * MB);
            if (p == NULL)
                    return 0;

然后top 查看内存占用情况。
看到mem_alloc 程序的虚拟机地址空间(VIRT) 已经有了 106728KB(~100MB)
但是实际的物理内存 RSS (top命令中的是RES 就是 Resident 的简写,和RSS 是一个意思) 在这里只有 688KB

在这里插入图片描述

接着我们在程序里等待 30 秒之后,我们再对这块申请的空间里写入 20MB 的数据。


            sleep(30);
            memset(p, 0x00, 20 * MB)

当我们用 memset() 函数对这块地址空间写入20MB 的数据之后,再用top 查看,这个时候可以看到 虚拟地址空间 (VIRT)还是 106728,不过物理内存 RSS (RES) 的值变成了 21432(大小约为20MB),这里的单位都是KB

在这里插入图片描述

通过上面的实验,验证 RSS 就是进程里真正获得的物理内存的大小。

对于进程来说,RSS 内存包含了进程的代码段内存,栈内存,堆内存,共享库的内存,这些内存是进程运行必须的,刚才我们通过 malloc/memset 得到的内存,就是属于堆内存。

具体的每一部分的RSS 内存的大小,可以通过 /proc/[pid]/smaps文件查看。

在这里插入图片描述

2.3、Page Cache

每个进程除了各自独立分配到的 RSS 内存外。
如果进程对磁盘上的文件做了读写操作,Linux还会分配内存,把磁盘上读写到的页面存放在内存中,这部分内存就是 Page Cache

Page Cache 的主要作用是提高磁盘文件的读写性能,因为系统调用 read() write()的缺省行为都会把读过或者写过的页面存放在 Page Cache 里。

例如:
代码程序去读取 100MB的文件,在读取文件前,系统中的 Page Cache 的大小是388MB,读取后 Page Cache 的大小是506MB,增长了约100MB大小,正是我们读取文件的大小。

在这里插入图片描述
在Linux 系统里只要有空闲的内存,系统就会自动的把读写过的磁盘文件页面放入到Page Cache里,那么这些内存都被 Page Cache 占用了,一旦进程需要用到更多的物理内存,执行 malloc() 调用做申请时,就会发现剩余的物理内存不够了怎么办。

内存页面回收机制(page frame reclaim):Linux内存管理中的一种机制,会根据系统里空闲物理内存是否低于某个阈值(wartermark),来决定是否启动内存的回收。

内存回收的算法,根据不同类型的内存以及内存的最近最少使用原则,就是 LRU(Least Recently Used)算法决定哪些内存页面先释放,因为Page Cache的内存页面只是起到了cache的作用,自然会被优先释放的。

Page Cache:为了提高文件磁盘读写性能,而利用空闲内存的机制。同时内存管理中的页面回收机制,又能保证Cache 所占用的页面可以及时释放,这样一来就不会影响程序对内存的真正需求了。

2.4、RSS & Page Cache in Memory Cgroup

下面看下 RSS和 Page Cache 如何影响 Memory Cgroup工作的。

Linux 内核代码中,从mem_cgroup_charge_statistics() 函数里看到,Memory Cgroup 只统计了 RSS和Page Cache 这两部分内存。

RSS :当前Memory Cgroup 控制组里所有进程的RSS的总和;
Page Cache:控制组的进程读写磁盘文件后,被放入到Page Cache 里的物理内存。
在这里插入图片描述

Memory Cgroup 控制组里的 RSS 内存和 Page Cache 内存的和,正好是memory.usage_in_bytes 的值。

当控制组里的进程需要申请新的物理内存,而且memory.usage_in_bytes 里的值超过控制组里的内存上限 memory.limit_in_bytes,这时我们前面说的Linux的内存回收(page frame reclaim)就会被调用起来

控制组里的 Page Cache 的内存会根据心申请的内存大小释放一部分,这样我们还是能成功申请到新的物理内存,整个控制组里总的内存开销 memory.usage_in_bytes 还是不会超过上限值 memory.limit_in_bytes

三、解决问题

问题:
为什么 memory.usage_in_bytes 与 memory.limit_in_bytes 的值只相差了 90KB,我们在容器中可以申请到 50MB的内存。

容器里肯定有大于 50MB的内存是 Page Cache ,因为作为Page Cache 的内存在系统需要新申请物理内存的时候(作为RSS)是可以被释放的。

在Memory Cgroup 中有一个参数 memory.stat 可以显示在当前控制组里各种内存类型的实际的开销。

1、
用同样的脚本来启动容器,并设置好容器里的 Memory Cgroup 里的 memory.limit_in_bytes 的值为100MB

https://github.com/chengyli/training/blob/main/memory/page_cache/start_container.sh

启动容器后,这次我们不仅要看 memory.usage_in_bytes 还要看一下 memory.stat ,虽然 memory.stat 里的参数不少,我们目前只需要关注 cache 和 rss 这两个值。

容器启动后,cache值也就是 Page Cache 占的内存是9508224bytes,大概是 99MB,而 RSS 占的内存只有 1826816bytes,也就是 1MB 多一点。

这就意味着,在这个容器的 Memory Cgroup里大部分的内存都被用作了 Page Cache,而这部分内存是可以被回收的。

在这里插入图片描述

2、
再执行命令,申请50MB的内存

https://github.com/chengyli/training/blob/main/memory/page_cache/mem-alloc/mem_alloc.c

我们可以再来查看一下 memory.stat,这时候 cache 的内存值降到了 46632960bytes,大概 46MB,而 rss 的内存值到了 54759424bytes,54MB 左右吧。总的 memory.usage_in_bytes 值和之前相比,没有太多的变化。

在这里插入图片描述

结论:
Page Cache 完全就是Linux内核的一个自动行为,只要读写磁盘文件,只要有空闲的内存,就会被用作 Page Cache。

判断容器真实的内存使用量,不能用 Memory Cgroup里的 memory.usage_in_bytes 。而需要用 memory.stat 里的rss值。
很像我们用free 命令查看节点的可用内存,不能看free 字段下的值,而要看除去Page Cacge 之后的 available 字段下的值。

四、重点总结

Memory Cgroup 在统计每个控制组的内存使用时包含了两部分:RSS和 Page Cache

RSS:每个进程实际占用的物理内存,包括了进程的代码段内存,进程运行时需要的堆和栈的内存,这部分内存是进程运行所必须的。

Page Cache:进程在运行中读写磁盘后,作为cache而继续保留在内存中的,它的目的是为了提高文件磁盘的读写性能。

当节点内存紧张或者 Memory Cgroup 控制组的内存达到上限时,Linux 会对内存做回收操作。这个时候 Page Cache 的内存页面会被释放,这样空出来的内存就可以分配给新的内存申请。

因为在频繁磁盘访问的容器中,因为Page Cache的这种cache特性,我们往往会看到它的内存使用率一直接近容器内存的限制值(memory.limit_in_bytes)。

这个时候不用担心它的内存不够,判断容器的内存状态的时候,可以忽略Page Cache 部分,考虑RSS的内存使用量

五、评论

1、 很重要!!

Memory Cgroup 应该包含了对内核内存的限制,老师给出的例子比较简单,基本没有使用slab,可以试下在容器中打开海量小文件,内核内存inode,dentry等会被计算在内。

内存使用量计算公式,(memory.kmem.usage_in_bytes 表示该memorycgroup内核内存使用量):
memory.usage_in_bytes = memory.stat[rss] + memory.stat[cache] + memory.kmem.usage_in_bytes

Memory Cgroup OOM 不是根据memory.usage_in_bytes 判定的。
而是依据 working set (使用量 减去非活跃 file-backed 内存):
working set = memory.usage_in_bytes - total_inactive_file

2、
问题:
问一个操作系统相关的问题。根据我的理解,操作系统为了性能会在刷盘前将内容放在page cache中(如果可以申请的话),后续合适的时间刷盘。如果是这样的话,在一定条件下,可能还没刷盘,这个内存就需要释放给rss使用。这时必然就会先刷盘。这样会导致 系统 malloc 的停顿,对吗?如果是这样的话,另外一个问题就是 linux 是如何保证 磁盘的数据的 crash safe 的呢?

回答:
是的,在系统申请物理内存的时候,如果不够,会因为释放page cache 而增加延时,对于在 page cache 中的dirty page,有 kernel thread会根据 dirty_* 相关的参数在后台不断的写入磁盘,不过如果发生断电,那么在内存中还没写入的 dirty page 还是丢失的。

3、
问题:page_cache是不是会被很多进程共享呢,比如同一个文件需要被多个进程读写,这样的话,page_cache会不会无法被释放呢?

另外,老师能不能讲解下,这里面的page_cache和free中的cache、buffer、shared还有buffer cache的区别呢?

回答:
即使page cache 对应的文件被多个进程打开,在需要memory的时候还是可以释放page cache的。 进程打开的是文件,page cache 只是cache。

free 里的cache/buffer 就是page cache,早期Linux 文件相关的cache 内存分 buffer cache和page cache ,现在统一成page cache了。 shared内存一般是tmpfs 内存文件系统用到的内存。

4、
问题:
请问老师:这边结合了课程内容以及评论中的一些补充想在确认一下以下几个问题:
1 Memory Cgroup OOM 的依据是working set吗?还是说rss,working set都会进行判断
2 这边看到有评论的大佬给了对于memory.usage_in_bytes 以及working_set ,但是对这两个间的关系有一些疑惑,想请问一下老师是否可以理解为working_set = memory.stat[rss] + memory.kmem.usage_in_bytes+常用的page cache 这样?

回答:
OOM 判断还是根据:
新申请的内存+memory.usage_in_bytes - reclaim memory > memory.limit_in_bytes 来判断的

working_set 应该是cAdvsior 里的一个概念,可以看一下这段代码。它的定义是 memory.usage_in_bytes - inactive_file_memory ,不过在 memory reclaim(回收) 的时候,可以reclaim(回收) 的memory 是大于 inactive_file memory的。

https://github.com/google/cadvisor/blob/master/container/libcontainer/handler.go#L870

5、
问题:
请问文中提到rss包括共享内存的大小,那pss呢,pss和rss的区别不是 是否包括共享内存的大小码?

回答:
pss 对共享内存的计算做了一个处理,比如100个页面是两个进程共享的,那么每个进程记录50个页面在自己进程的pss里。

6
问题:
老师,如果新程序申请的内存大小是大于之前进程的page cache内存大小的;是不是就会发生oom?

回答:
这个要看之前page cache 总共使用了多少,如果新进程最后实际使用到的内存,比如RSS,和之前进程的RSS 相加大于容器的内存限制,那么就会发生 OOM。

六、补充知识

启动一个容器,看看在容器中,cat 一个大文件前后,memory.stat中的rss和cache分别是什么情况、

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值