转自:https://www.sunliaodong.cn/2021/03/11/Linux-PageCache%E8%AF%A6%E8%A7%A3/
应用程序要存储或访问数据时,只需读或者写”文件”的一维地址空间即可,而这个地址空间与存储设备上存储块之间的对应关系则由操作系统维护。说白了,文件就是基于内核态Page Cache的一层抽象。
相关场景
- 服务器的 load 飙高;
- 服务器的 I/O 吞吐飙高;
- 业务响应时延出现大的毛刺;
- 业务平均访问时延明显增加。
什么是page cache
page cache是内存管理的内存,属于内核不属于用户。
查看方式
- /proc/meminfo
- free命令
- vmstat命令
page cache指标说明
通过/proc/meminfo查看内存信息如下:
1 |
MemTotal: 2046920 kB |
通过计算发现:
Buffers + Cached + SwapCached = Active(file) + Inactive(file) + Shmem + SwapCached
在 Page Cache 中,Active(file)+Inactive(file) 是 File-backed page(与文件对应的内存页),平时用的 mmap() 内存映射方式和 buffered I/O 来消耗的内存就属于这部分,这部分在真实的生产环境上也最容易产生问题。
SwapCached说明(生产环境中不建议开启,防止IO引起性能抖动)
SwapCached 是在打开了 Swap 分区后,把 Inactive(anon)+Active(anon) 这两项里的匿名页给交换到磁盘(swap out),然后再读入到内存(swap in)后分配的内存。由于读入到内存后原来的 Swap File 还在,所以 SwapCached 也可以认为是 File-backed page,即属于 Page Cache。
Shmem
- Shmem 是指匿名共享映射这种方式分配的内存(free 命令中 shared 这一项)
- 进程使用mmap(MAP_ANON|MAP_SHARED)的方式申请内存
- tmpfs: 磁盘的速度是远远低于内存的,有些应用程序为了提升性能,会避免将一些无需持续化存储的数据写入到磁盘,而是把这部分临时数据写入到内存中,然后定期或者在不需要这部分数据时,清理掉这部分内容来释放出内存。在这种需求下,就产生了一种特殊的 Shmem:tmpfs
frem命令的说明
数据来源于/proc/meminfo
1 |
free -k |
通过源码可知:buff/cache = Buffers + Cached + SReclaimable
SReclaimable 是指可以被回收的内核内存,包括 dentry 和 inode。
缓存的具体含义
官方定义
1 |
Buffers %lu |
具体解释
- Buffers 是对原始磁盘块的临时存储,也就是用来缓存磁盘的数据,通常不会特别大(20MB 左右)。这样,内核就可以把分散的写集中起来,统一优化磁盘的写入,比如可以把多次小的写合并成单次大的写等等。
- Cached 是从磁盘读取文件的页缓存,也就是用来缓存从文件读取的数据。这样,下次访问这些文件数据时,就可以直接从内存中快速获取,而不需要再次访问缓慢的磁盘。
- SReclaimable 是 Slab 的一部分。Slab 包括两部分,其中的可回收部分,用 SReclaimable 记录;而不可回收部分,用 SUnreclaim 记录。
- Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中
PageCache数据结构
- 内存管理系统与Page Cache交互,负责维护每项 Page Cache 的分配和回收,同时在使用 memory map 方式访问时负责建立映射;
- VFS 与Page Cache交互,负责 Page Cache 与用户空间的数据交换,即文件读写;
- 具体文件系统则一般只与 Buffer Cache 交互,它们负责在外围存储设备和 Buffer Cache 之间交换数据。
- 一个Page Cache包含多个Buffer Cache,一个Buffer Cache与一个磁盘块一一对应;假定了 Page 的大小是 4K,则文件的每个4K的数据块最多只能对应一个 Page Cache 项,它通过一个是 radix tree来管理文件块和page cache的映射关系,Radix tree 是一种搜索树,Linux 内核利用这个数据结构来通过文件内偏移快速定位 Cache 项。
为什么使用page cache
减少 I/O,提升应用的 I/O 速度
- 具体文件系统:,如 ext2/ext3、jfs、ntfs 等,负责在文件 Cache和存储设备之间交换数据
- 虚拟文件系统VFS: 负责在应用程序和文件 Cache 之间通过 read/write 等接口交换数据
- 内存管理系统: 负责文件 Cache 的分配和回收
- 虚拟内存管理系统(VMM): 则允许应用程序和文件 Cache 之间通过 memory map的方式交换数据
- 在 Linux 系统中,文件 Cache 是内存管理系统、文件系统以及应用程序之间的一个联系枢纽。
page cache的产生
产生方式
- Buffered I/O(标准 I/O)如:read/write/sendfile等;
- Memory-Mapped I/O(存储映射 I/O)如:mmap;
- sendfile和mmap都是零拷贝的实现方案。
产生方式的区别
- 标准 I/O 是写的 (write(2)) 用户缓冲区 (Userpace Page 对应的内存),然后再将用户缓冲区里的数据拷贝到内核缓冲区 (Pagecache Page 对应的内存);如果是读的 (read(2)) 话则是先从内核缓冲区拷贝到用户缓冲区,再从用户缓冲区读数据,也就是 buffer 和文件内容不存在任何映射关系。
- 对于存储映射 I/O 而言,则是直接将 Pagecache Page 给映射到用户地址空间,用户直接读写 Pagecache Page 中内容。
常规文件读写
FileChannel#read,FileChannel#write,共涉及四次上下文切换(内核态和用户态的切换,包括read调用,read返回,write调用,write返回)和四次数据拷贝。
脏页
1 |
cat /proc/vmstat | egrep "dirty|writeback" |
nr_dirty 表示当前系统中积压了多少脏页,nr_writeback 则表示有多少脏页正在回写到磁盘中,他们两个的单位都是 Page(4KB)。
mmap
- 文件(page cache)直接映射到用户虚拟地址空间,内核态和用户态共享一片page cache,避免了一次数据拷贝
- 建立mmap之后,并不会立马加载数据到内存,只有真正使用数据时,才会引发缺页异常并加载数据到内存
memory map具体步骤如下
- 应用程序调用mmap(图中1),先到内核中
- 后调用do_mmap_pgoff(图中2),该函数从应用程序的地址空间中分配一段区域作为映射的内存地址,并使用一个VMA(vm_area_struct)结构代表该区域,
- 之后就返回到应用程序(图中3)
- 当应用程序访问mmap所返回的地址指针时(图中4),由于虚实映射尚未建立,会触发缺页中断(图中5)。之后系统会调用缺页中断处理函数(图中6),在缺页中断处理函数中,内核通过相应区域的VMA结构判断出该区域属于文件映射,于是调用具体文件系统的接口读入相应的Page Cache项(图中7、8、9),并填写相应的虚实映射表。
- 经过这些步骤之后,应用程序就可以正常访问相应的内存区域了。
sendfile
- 使用sendfile的方式避免了用户空间与内核空间的交互,复制次数减少到三次,内核态与用户态切换减少到两次。
- 在 Linux 内核 2.4 及后期版本中,针对套接字缓冲区描述符做了相应调整,DMA自带了收集功能,对于用户方面,用法还是一样。内部只把包含数据位置和长度信息的描述符追加到套接字缓冲区,DMA 引擎直接把数据从内核缓冲区传到协议引擎,从而消除了最后一次 CPU参与的拷贝动作。
顺序读写
文件预读
文件的预读机制,它是一种将磁盘块预读到page cache的机制,执行步骤如下:
- 对于每个文件的第一个读请求,系统读入所请求的页面并读入紧随其后的少数几个页面(不少于一个页面,通常是三个页面),这时的预读称为同步预读。
- 对于第二次读请求,如果所读页面不在Cache中,即不在前次预读的group中,则表明文件访问不是顺序访问,系统继续采用同步预读;如果所读页面在Cache中,则表明前次预读命中,操作系统把预读group扩大一倍,并让底层文件系统读入group中剩下尚不在Cache中的文件数据块,这时的预读称为异步预读。
- 无论第二次读请求是否命中,系统都要更新当前预读group的大小。
- 系统中定义了一个window,它包括前一次预读的group和本次预读的group。任何接下来的读请求都会处于两种情况之一:
- 第一种情况是所请求的页面处于预读window中,这时继续进行异步预读并更新相应的window和group;
- 第二种情况是所请求的页面处于预读window之外,这时系统就要进行同步预读并重置相应的window和group。
图中group指一次读入page cached的集合;window包括前一次预读的group和本次预读的group;浅灰色代表要用户想要查找的page cache,深灰色代表命中的page。
顺序读写高效的原因
以顺序读为例,当用户发起一个 fileChannel.read(4kb) 之后,实际发生了两件事
- 操作系统从磁盘加载了 16kb 进入 PageCache,这被称为预读
- 操作通从 PageCache 拷贝 4kb 进入用户内存
- 当用户继续访问接下来的 [4kb,16kb] 的磁盘内容时,便是直接从 PageCache 去访问了
page cache的消亡
page cache的回收主要是针对free 命令中的 buff/cache 中的这些就是“活着”的 Page Cache。回收的过程如下图所示:
回收的方式主要是两种:直接回收和后台回收,具体的回收行为,可以使用以下命令查看:
1 |
sar -B 1 |
- pgscank/s : kswapd(后台回收线程) 每秒扫描的 page 个数。
- pgscand/s: Application 在内存申请过程中每秒直接扫描的 page 个数。
- pgsteal/s: 扫描的 page 中每秒被回收的个数。
- %vmeff: pgsteal/(pgscank+pgscand), 回收效率,越接近 100 说明系统越安全,越接近 0 说明系统内存压力越大。
sar -B与/proc/vmstat比对
sar -B | /proc/vmstat |
---|---|
pgscank | pgscan_kswapd |
pgscand | pgscan_direct |
pgsteal | pgsteal_kswapd+pgsteal_direct |
其他
DMA(Direct Memory Access,直接存储器访问)
- DMA的出现就是为了解决批量数据的输入/输出问题。DMA是指外部设备不通过CPU而直接与系统内存交换数据的接口技术
- DMA控制器需要具备的功能:
- 能向CPU发出系统保持信号,提出总线接管请求
- 当CPU同意接管请求之后,对总线的控制交给DMA
- 能对存储器寻址及能修改地址指针,实现对内存的读写
- 能决定本次DMA传送的字节数,判断DMA传送是否借宿
- 发送DMA结束信号,使CPU恢复正常工作状态
堆外内存
堆内存与堆外内存的关系
堆内内存 | 堆外内存 | |
---|---|---|
底层实现 | 数组,JVM内存 | unsafe.allocateMemory(size)返回直接内存 |
分配大小限制 | -Xms-Xmx, 数组大小,当前JVM free memory大于1.5G时,ByteBuffer.allocate(900M)会报错 |
-XX:MaxDirectMemorySize参数从JVM层面限制,同时受到机器虚拟内存的限制 |
垃圾回收 | 当前DirectByteBuffer不再被使用时,会触发内部cleaner的钩子 保险起见,可以考虑手动回收 ((DirectBuffer)buffer).cleaner().clean() |
|
内存复制 | 堆内内存---堆外内存---pageCache | 堆外内存--pageCache |
最佳实践
- 当需要申请大块的内存时,堆内内存会受到限制,只能分配堆外内存。
- 堆外内存适用于生命周期中等或较长的对象。(如果是生命周期较短的对象,在 YGC 的时候就被回收了,就不存在大内存且生命周期较长的对象在 FGC 对应用造成的性能影响)。
- 堆内内存刷盘的过程中,还需要复制一份到堆外内存,这部分内容可以在 FileChannel 的实现源码中看到细节
- 使用 HeapByteBuffer 读写都会经过 DirectByteBuffer,写入数据的流转方式其实是:HeapByteBuffer -> DirectByteBuffer -> PageCache -> Disk,读取数据的流转方式正好相反。
- 使用 HeapByteBuffer 读写会申请一块跟线程绑定的 DirectByteBuffer。这意味着,线程越多,临时 DirectByteBuffer 就越会占用越多的空间。
- 堆外内存就是把内存对象分配在Java虚拟机堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。
- 内存回收流程
PageCache内存回收
回收过程
在内存紧张的时候会触发内存回收,内存回收会尝试去回收reclaimable(可被回收)的内存。包括PageCache以及reclaimable kernel memory(比如slab)。
避免PageCache回收出现的性能问题
memory cgroup protection
- memory.max:memory cgroup 内的进程最多能够分配的内存,如果不设置的话,就默认不做内存大小的限制
- memory.high:当 memory cgroup 内进程的内存使用量超过了该值后就会立即被回收掉,目的是为了尽快的回收掉不活跃的 Page Cache。
- memory.low:用来保护重要数据的,当 memory cgroup 内进程的内存使用量低于了该值后,在内存紧张触发回收后就会先去回收不属于该 memory cgroup 的 Page Cache,等到其他的 Page Cache 都被回收掉后再来回收这些 Page Cache。
- memory.min:用来保护重要数据的,只不过与 memoy.low 有所不同的是,当 memory cgroup 内进程的内存使用量低于该值后,即使其他不在该 memory cgroup 内的 Page Cache 都被回收完了也不会去回收这些 Page Cache。
- 总结:如果你想要保护你的 Page Cache 不被回收,你就可以考虑将你的业务进程放在一个 memory cgroup 中,然后设置 memory.{min,low} 来进行保护;与之相反,如果你想要尽快释放你的 Page Cache,那你可以考虑设置 memory.high 来及时的释放掉不活跃的 Page Cache。
出现load过高的原因
直接内存回收引起
内存回收过程
后台回收原理:
通过调整参数vm.min_free_kbytes来提高后台进程回收频率。
1 |
cat /proc/sys/vm/min_free_kbytes |
通过调整内存水位,在一定程度上保障了应用的内存申请,但是同时也带来了一定的内存浪费,因为系统始终要保障有这么多的 free 内存,这就压缩了 Page Cache 的空间。调整的效果你可以通过 /proc/zoneinfo 来观察
系统中脏页积压过多
内存申请过程
解决方法
设置配置:/proc/vmstat
1 |
vm.dirty_background_bytes = 0 |
系统numa策略配置不当
内存泄漏
OOM KILL逻辑
可以调整oom_score_adj来防止进程被杀掉(不建议配置)
如何观察内核内存泄漏
- 如果 /proc/meminfo 中内核内存(比如 VmallocUsed 和 SUnreclaim)太大,那很有可能发生了内核内存泄漏
- 周期性地观察 VmallocUsed 和 SUnreclaim 的变化,如果它们持续增长而不下降,也可能是发生了内核内存泄漏
- 通过 /proc/vmallocinfo 来看到该模块的内存使用情况
- kmemleak 内核内存分析工具
排查思路
/proc/meminfo | 含义以及排查思路 |
---|---|
Active(anon) | 在active anon lru的page,与下一项相互转换 |
Inactive(anon) | 在inactive anonlru的page,可以交换到swap分区,(active anno也是)但是不能回收 程序使用malloc()或mmap()匿名方式申请并且写后的内存。如果过大,排除思路: 1.使用top找出内存消耗最大的进程 2.使用pmap分析该进程 3.如果没有任何进程内存开销大,则重点排除tmpfs |
Unevictable | 在系统内存紧张时不能被回收,主要组成: 1.ram disk或ramfs消耗的内存 2.以SHM_LOCK方式申请的Shmem 3.使用mlock()序列函数来管理的内存 |
Mlocked | 属于Unevictable的一种,重点排除mlock()方式包含的内存 |
AnonPages | AnonPages!=Active(anon)+Inactive(anon) 因为shmem(包括tmpfs)虽然属于active(anon)或Inactive(anon),但是他们有自己的内存文件,所以不属于AnonPages active anon和Inactive anon表示不可回收但是可以被交换到swap分区的内存 AnonPages没有对应文件的内存 排除malloc()方式申请的内存或mmap(PROT_WRITE,MAP_ANON|MAP_PRIVATE)方式申请的内存 |
Mapped | 使用mmap(2)申请,没有被unmap的内存;unmap包含主动调用unmap(2)以及内核内存回收时的unmap 排查mmap()申请的内存 |
Shmem | 共享内存,特别注意tmpfs,排查思路 1. 使用top找出shr最大的进程 2.使用pmap分析该进程 3.如果没有任何进程消耗shr内存,则重点排查tmpfs |
Slab | 分为可被回收(SReclaimable)和不可以被回收(SUnreclaim),其中不可被回收的slab如果发生泄漏, 比如kmalloc申请的内存没有释放,排查思路 1.使用slaptop分析slab最大的数据 2.排查驱动程序以kmalloc()方式申请的内存 |
VmallocUsed | 通过vmalloc分配的内核内存,可以使用/proc/vmallocinfo,来判断哪些驱动程序以vmalloc方式申请的内存较多 可以尝试卸载驱动,释放内存 |
- 应用程序可以通过 malloc() 和 free() 在用户态申请和释放内存,与之对应,可以通过 kmalloc()/kfree() 以及 vmalloc()/vfree() 在内核态申请和释放内存
- vmalloc 申请的内存会体现在 VmallocUsed 这一项中,即已使用的 Vmalloc 区大小;而 kmalloc 申请的内存则是体现在 Slab 这一项中,它又分为两部分,其中 SReclaimable 是指在内存紧张的时候可以被回收的内存,而 SUnreclaim 则是不可以被回收只能主动释放的内存。
其他
清理缓存buffer/cache
运行sync将dirty的内容写回硬盘
通过修改proc系统的drop_caches清理free的cache
1 |
echo 3 > /proc/sys/vm/drop_caches |
可以通过/proc/vmstat
文件判断是否执行过drop_caches:
1 |
[root@instance-gctg007a ~]# cat /proc/vmstat | grep drop |
可以调用crond定时任务:每10分钟执行一次
1 |
*/10 * * * * sync;echo 3 > /proc/sys/vm/drop_caches; |
重要配置参数
/proc/sys/vm/dirty_ratio(同步刷盘)
这个参数控制文件系统的文件系统写缓冲区的大小,单位是百分比,表示系统内存的百分比,表示当写缓冲使用到系统内存多少的时候,开始向磁盘写出数据。增大之会使用更多系统内存用于磁盘写缓冲,也可以极大提高系统的写性能。但是,当你需要持续、恒定的写入场合时,应该降低其数值,一般启动上缺省是 10。设1加速程序速度
/proc/sys/vm/dirty_background_ratio(异步刷盘)
这个参数控制文件系统的pdflush进程,在何时刷新磁盘。单位是百分比,表示系统内存的百分比,意思是当写缓冲使用到系统内存多少的时 候,pdflush开始向磁盘写出数据。增大之会使用更多系统内存用于磁盘写缓冲,也可以极大提高系统的写性能。但是,当你需要持续、恒定的写入场合时, 应该降低其数值,一般启动上缺省是 5
/proc/sys/vm/dirty_writeback_centisecs
这个参数控制内核的脏数据刷新进程pdflush的运行间隔。单位是 1/100 秒。缺省数值是500,也就是 5 秒。如果你的系统是持续地写入动作,那么实际上还是降低这个数值比较好,这样可以把尖峰的写操作削平成多次写操
/proc/sys/vm/dirty_expire_centisecs
这个参数声明Linux内核写缓冲区里面的数据多“旧”了之后,pdflush进程就开始考虑写到磁盘中去。单位是 1/100秒。缺省是 30000,也就是 30 秒的数据就算旧了,将会刷新磁盘。对于特别重载的写操作来说,这个值适当缩小也是好的,但也不能缩小太多,因为缩小太多也会导致IO提高太快。建议设置为 1500,也就是15秒算旧。
/proc/sys/vm/drop_caches
释放已经使用的cache
/proc/sys/vm/page_cluster
该文件表示在写一次到swap区的时候写入的页面数量,0表示1页,1表示2页,2表示4页。
/proc/sys/vm/swapiness
该文件表示系统进行交换行为的程度,数值(0-100)越高,越可能发生磁盘交换。
/proc/sys/vm/vfs_cache_pressure
该文件表示内核回收用于directory和inode cache内存的倾向
参考
- proc帮助手册
- 常用性能分析工具
- NIO进阶篇:Page Cache、零拷贝、顺序读写、堆外内存
- 面试官:RocketMQ 如何基于mmap+page cache实现磁盘文件的高性能读写?
- 文件系统缓存dirty_ratio与dirty_background_ratio两个参数区别
- PageCache系列之五 统一缓存之PageCache