容器实战高手课---09 Page Cache:为什么我的容器内存使用量总是在临界点

你好,我是程远。

上一讲,我们讲了Memory Cgroup是如何控制一个容器的内存的。我们已经知道了,如果容器使用的物理内存超过了Memory Cgroup里的memory.limit_in_bytes值,那么容器中的进程会被OOM Killer杀死。

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

这是怎么回事呢?今天这一讲我就来聊聊这个问题。

问题再现

我们可以用这里的代码做个容器镜像,然后用下面的这个脚本启动容器,并且设置容器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这两个值。

如下图所示,我们可以看到容器内存的上限值设置为104857600bytes(100MB),而这时整个容器的已使用内存显示为104767488bytes,这个值已经非常接近上限值了。

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

但是,如果这时候我们继续启动一个程序,让这个程序申请并使用50MB的物理内存,就会发现这个程序还是可以运行成功,这时候容器并没有发生OOM的情况。

这时我们再去查看参数memory.usage_in_bytes,就会发现它的值变成了103186432bytes,比之前还少了一些。那这是怎么回事呢?
在这里插入图片描述

知识详解:Linux系统有那些内存类型?

要解释刚才我们看到的容器里内存分配的现象,就需要先理解Linux操作系统里有哪几种内存的类型。

因为我们只有知道了内存的类型,才能明白每一种类型的内存,容器分别使用了多少。而且,对于不同类型的内存,一旦总内存增高到容器里内存最高限制的数值,相应的处理方式也不同。

Linux内存类型

Linux的各个模块都需要内存,比如内核需要分配内存给页表,内核栈,还有slab,也就是内核各种数据结构的Cache Pool;用户态进程里的堆内存和栈的内存,共享库的内存,还有文件读写的Page Cache。

在这一讲里,我们讨论的Memory Cgroup里都不会对内核的内存做限制(比如页表,slab等)。所以我们今天主要讨论与用户态相关的两个内存类型,RSS和Page Cache。

RSS

先看什么是RSS。RSS是Resident Set Size的缩写,简单来说它就是指进程真正申请到物理页面的内存大小。这是什么意思呢?

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

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

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

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

然后,我们运行top命令查看这个程序在运行了malloc()之后的内存,我们可以看到这个程序的虚拟地址空间(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文件。

Page Cache

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

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

还是用我们这一讲最开始的的例子:代码程序去读取100MB的文件,在读取文件前,系统中Page Cache的大小是388MB,读取后Page Cache的大小是506MB,增长了大约100MB左右,多出来的这100MB,正是我们读取文件的大小。

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

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

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

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

RSS & Page Cache in Memory Cgroup

学习了RSS和Page Cache的基本概念之后,我们下面来看不同类型的内存,特别是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 Cgroup中内存类型的统计方法,我们再回过头看这一讲开头的问题,为什么memory.usage_in_bytes与memory.limit_in_bytes的值只相差了90KB,我们在容器中还是可以申请出50MB的物理内存?

我想你应该已经知道答案了,容器里肯定有大于50MB的内存是Page Cache,因为作为Page Cache的内存在系统需要新申请物理内存的时候(作为RSS)是可以被释放的。

知道了这个答案,那么我们怎么来验证呢?验证的方法也挺简单的,在Memory Cgroup中有一个参数memory.stat,可以显示在当前控制组里各种内存类型的实际的开销。

那我们还是拿这一讲的容器例子,再跑一遍代码,这次要查看一下memory.stat里的数据。

第一步,我们还是用同样的脚本来启动容器,并且设置好容器的Memory Cgroup里的memory.limit_in_bytes值为100MB。

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

我们可以看到,容器启动后,cache,也就是Page Cache占的内存是99508224bytes,大概是99MB,而RSS占的内存只有1826816bytes,也就是1MB多一点。

这就意味着,在这个容器的Memory Cgroup里大部分的内存都被用作了Page Cache,而这部分内存是可以被回收的。
在这里插入图片描述
那么我们再执行一下我们的mem_alloc程序,申请50MB的物理内存。

我们可以再来查看一下memory.stat,这时候cache的内存值降到了46632960bytes,大概46MB,而rss的内存值到了54759424bytes,54MB左右吧。总的memory.usage_in_bytes值和之前相比,没有太多的变化。
在这里插入图片描述
从这里我们发现,Page Cache内存对我们判断容器实际内存使用率的影响,目前Page Cache完全就是Linux内核的一个自动的行为,只要读写磁盘文件,只要有空闲的内存,就会被用作Page Cache。

所以,判断容器真实的内存使用量,我们不能用Memory Cgroup里的memory.usage_in_bytes,而需要用memory.stat里的rss值。这个很像我们用free命令查看节点的可用内存,不能看”free”字段下的值,而要看除去Page Cache之后的”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的内存使用量。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/892321.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MybatisPlus分页Page插件

分页Page插件 首先,要在配置类中注册MyBatisPlus的核心插件,同时添加分页插件。设置分页查询的配置类,interceptor只有拦截作用,功能需要自己添加。这里我们添加上分页查询功能。 import com.baomidou.mybatisplus.annotation.DbType; impo…

Java中的数组

一、数组的创建及初始化 1、创建数组 int 表示数组中元素类型 int[] 表示数组的类型 array 表示数组名 2、数组初始化 数组初始化可分为动态初始化和静态初始化,动态初始化只初始化数组的大小,而静态初始化是直接给出数组中的具体元素 动态初始化&am…

dlib库实现人脸检测

摘要 本文将向您介绍如何使用dlib库在图片以及视频中实现人脸识别检测。通过简单的Python代码,我们将展示如何定位图片中的人脸并绘制边框。 引言 人脸识别技术在当今世界越来越普及,应用场景广泛,如安全监控、身份认证、图像处理等。dlib…

OpenCV高级图形用户界面(11)检查是否有键盘事件发生而不阻塞当前线程函数pollKey()的使用

操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 轮询已按下的键。 函数 pollKey 无等待地轮询键盘事件。它返回已按下的键的代码或如果没有键自上次调用以来被按下则返回 -1。若要等待按键被按…

考研C语言程序设计_语法相关(持续更新)

目录 一、语法题strlen转义字符内置数据类型字符串结束标志局部变量和全局变量名字冲突 局部优先switch语句中的关键字数组初始化是否正确注意define不是关键字C语言中不能用连等判断switch( )的括号里可以是什么类型?关于if关于switch关于while 二、程序阅读题有关static有关…

【重学 MySQL】六十九、揭秘级联约束,让你的数据库关系更智能、更强大!

【重学 MySQL】六十九、揭秘级联约束,让你的数据库关系更智能、更强大! 级联约束的定义级联约束的类型级联约束的应用场景级联约束的实现方式级联约束的注意事项 在MySQL数据库中,级联约束是维护数据完整性和一致性的重要机制。它允许在执行某…

Spring源码分析:bean加载流程

背景 在Spring中,Bean的加载和管理是其核心功能之一,包括配置元数据解析、Bean定义注册、实例化、属性填充、初始化、后置处理器处理、完成创建和销毁等步骤。 源码入口 AbstractBeanFactory#doGetBean 具体源码流程如下: bean加载流程&#…

万界星空科技:智能称重打标系统

万界星空科技的称重系统是其为制造业,特别是线缆、漆包线、食品等行业提供的重要解决方案之一。以下是对该系统的详细介绍: 一、系统概述 万界星空科技称重系统是集成在其MES(制造执行系统)中的一个功能模块,专门用于…

数据结构之旅(顺序表)

前言: Hello,各位小伙伴们我们在过去的60天里学完了C语言基本语法,由于小编在准备数学竞赛,最近没有给大家更新,并且没有及时回复大家的私信,小编在这里和大家说一声对不起!,小编这几天会及时给大家更新初阶数据结构的内容,然后我们来学习今天的内容吧! 一. 顺序表的概念和结…

2024.10.15 sql

刷题网站&#xff1a; 牛客网 select device_id as user_infos_example from user_profile where id < 2 select device_id, university from user_profile where university"北京大学" select device_id, gender, age, university from user_profile where ag…

Bellman-Ford

思路 外层遍历V-1次内层遍历所有边&#xff08;共E次&#xff09;&#xff0c;尝试更新起点的终点的dist值 原材料是backup&#xff08;前次遍历的结果&#xff09;维持住性质&#xff08;见下&#xff09; 优点 允许负环 允许负权边 有特殊性质 缺点 复杂度达到 例题 代码…

2、CSS笔记

文章目录 二、CSS基础CSS简介CSS语法规范CSS代码风格CSS选择器CSS基础选择器标签选择器类选择器--最常用id选择器通配符选择器 CSS复合选择器交集选择器--重要并集选择器--重要后代选择器--最常用子代选择器--重要兄弟选择器相邻兄弟选择器通用兄弟选择器 属性选择器伪类选择器…

Flutter url_launcher:打开网页、邮件、电话和短信的最佳实践

Flutter url_launcher&#xff1a;打开网页、邮件、电话和短信的最佳实践 视频 https://youtu.be/uGT43gZNkyc https://www.bilibili.com/video/BV1G42EYcE7K/ 前言 原文 如何在 Flutter 中使用 url_launcher 打开网页和发送短信 本文介绍了如何在 Flutter 中使用 url_launc…

【深度学习代码调试1】环境配置篇(上) -- 安装PyTorch(安利方法:移除所有国内源,使用默认源)

【深度学习代码调试1】环境配置篇 -- 安装TensorFlow和PyTorch 写在最前面1. 创建新的Conda环境2. 安装PyTorch及相关库&#xff08;可以直接跳到2.3安装方法&#xff09;2.1 检查CUDA版本2.2 解决安装过程中常见问题2.2.1 超时问题&#xff08;这个不是最终解决方案&#xff0…

【argparse】 菜鸟实用教程指南

文章目录 0. 引言1. argparse简介2. argparse的使用3. 实例操作4. 代码运行4.1 命令行执行4.1 IDE执行 5. 总结 0. 引言 在深度学习的过程中&#xff0c;我们常常需要操作和调参大量的参数。如果采用硬编码&#xff08;直接在代码中赋值&#xff09;的方式来设置这些参数&…

java项目之科研工作量管理系统的设计与实现源码(springboot+vue+mysql)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的科研工作量管理系统的设计与实现。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 科研工作…

【C语言】算术运算、关系运算、逻辑运算

算术运算&#xff1a;常见的数字运算&#xff0c;加减乘除等 关系运算&#xff1a;数值之间大小多少的关系 逻辑运算&#xff1a;逻辑与、或、非 #include <stdio.h> /* 功能&#xff1a;算术运算、关系运算、逻辑运算 时间&#xff1a;2024年10月 地点&#xff1a;贤者…

斯坦福 CS229 I 机器学习 I 构建大型语言模型 (LLMs)

1. Pretraining -> GPT3 1.1. Task & loss 1.1.1. 训练 LLMs 时的关键点 对于 LLMs 的训练来说&#xff0c;Architecture&#xff08;架构&#xff09;、Training algorithm/loss&#xff08;训练算法/损失函数&#xff09;、Data&#xff08;数据&#xff09;、Evalu…

Linux INPUT 子系统实验

按键、鼠标、键盘、触摸屏等都属于输入(input)设备&#xff0c;Linux 内核为此专门做了一个叫做 input子系统的框架来处理输入事件。输入设备本质上还是字符设备&#xff0c;只是在此基础上套上了 input 框架&#xff0c;用户只需要负责上报输入事件&#xff0c;比如按键值、坐…

Qt-系统文件相关介绍使用(61)

目录 描述 输⼊输出设备类 打开/读/写/关闭 使用 先初始化&#xff0c;创建出大致的样貌 输入框设置 绑定槽函数 保存文件 打开文件 提取文件属性 描述 在C/C Linux 中我们都接触过关于文件的操作&#xff0c;当然 Qt 也会有对应的文件操作的 ⽂件操作是应⽤程序必不…