自学内容网 自学内容网

Linux中的页缓存机制

Linux下的Page Cache页缓存机制

工作中你是否经常遇到诸如:服务器的 load 飙高、 服务器的 I/O 吞吐飙高、 业务响应时延出现大的毛刺、 业务平均访问时延明显增加等问题?

这些问题,很可能是由于 Page Cache 管理不到位引起的,因为 Page Cache 管理不当除 了会增加系统 I/O 吞吐外,还会引起业务性能抖动。如果想从根本上解决这些问题,那就必须清楚什么是 Page Cache,为什么需要 Page Cache,Page Cache 的产生和回收是什么 样的?下面我就一一回答上述问题。

1. 什么是Page Cache

Page Cache,又称pcache,中文名称为页高速缓冲存储器,简称页高缓。它是将磁盘上的数据加载到内存中,当系统需要访问这些数据时,可以直接从内存中读取,而不必每次都去读取磁盘。这种方式显著减少了磁盘I/O操作,从而提高了系统性能。

Page Cache所占用的内存位于操作系统的内核空间中,通常与其他缓存层级(如CPU缓存、磁盘缓存)分开管理,这样的分层结构有助于提供不同层级的读取性能。

image-20250118170551072

2. Page Cache的工作原理

  1. 数据读取:当用户进程对文件发起读写请求时,内核首先检查该文件的内容是否已被加载到内存中的Page Cache中。如果数据不在缓存中(即发生了缓存未命中),内核会从磁盘读取数据,并将其存储在Page Cache中,以备后续使用。
  2. 数据写入:写操作首先会修改内存中的页面数据,而不是立刻写入磁盘。这允许Linux内核延迟磁盘写操作,合并多个写入请求,提高磁盘I/O效率。Page Cache的写入是以页为单位进行的,通常大小为4KB。这意味着,即使应用程序只修改了一个字节,操作系统也会将整个页标记为脏数据并进行写回。操作系统会以异步方式将脏数据定期或在适当的时机,通过写回机制将数据从Page Cache写入到磁盘中。
  3. 缓存同步:为了确保数据的一致性,Linux内核提供了同步写和异步写两种方式。同步写通过系统调用如fsync()强制将Page Cache中的数据写回磁盘;而异步写则是默认情况下,内核会在合适的时机(例如磁盘空闲时或通过后台线程)将数据从Page Cache写回磁盘。

3. 直观感受它的存在

在 Linux 上直接查看 Page Cache 的方式有很多,包括 /proc/meminfo、free 、/proc/vmstat 命令等,它们的内容其实是一致的。 我们先查看 /proc/meminfo 文件:

$ cat /proc/meminfo
...
Buffers:    1224 kB        
Cached:     111472 kB      
SwapCached:     36364 kB    
Active:         6224232 kB 
Inactive:       979432 kB  
Active(anon):   6173036 kB  
Inactive(anon): 927932 kB
Active(file):   51196 kB
Inactive(file): 51500 kB   
...
Shmem:          10000 kB   
...
SReclaimable:   43532 kB

根据上面的数据,Page Cache = Buffers + Cached + SwapCached = Active(file) + Inactive(file) + Shmem + SwapCache

其中,Active(file)+Inactive(file) 这2项是与文件对应的内存页,是你平时用 mmap() 内存映射方式和 buffered I/O 来消耗的那部分内存,也是在生产环境上也最容易产生问题的部分。SwapCached 是在打开了 Swap 分区后,把 Inactive(anon)+Active(anon) 这两项里 的匿名页给交换到磁盘(swap out),然后再读入到内存(swap in)后分配的内存。由 于读入到内存后原来的 Swap File 还在,所以 SwapCached 也可以认为是 File-backed page,即属于 Page Cache。我们用一张图直观地看一下:

image-20250118172942160注意,SwapCached 只在 Swap 分区打开的情况下才会有,而我建议你在生产环境中关闭 Swap 分区,因为 Swap 过程产生的 I/O 会很容易引起性能抖动。

Shmem 是指匿名共享映射这种方式分配的内存 (free 命令中 shared 这一项),比如 tmpfs(临时文件系统),这部分在真实的生产环境中产生的问题比较少,不是我们今天的重点内容。

当然, free 命令也可以查看系统中有多少 Page Cache,但它也是通过解析 /proc/meminfo文件得出这些统计数据的。

$ free -k
total     used     free   shared   buff/cache   availabl
Mem:7926580 7277960  49239210000 15622843068
Swap:8224764 380748  7844016

可以发现,buff/cache 包括了/proc/meminfo的这几项: buff/cache = Buffers + Cached + SReclaimable,它强调的是内存的可回收性,也就是说,可以被回收的内存会统计在这一项。其中 SReclaimable 是指可以被回收的内核内存,包括 dentry 和 inode 等。

掌握了 Page Cache 具体由哪些部分构成之后,在它引发一些问题时,你就能够知道需要 去观察什么。比如说,应用本身消耗内存(RSS)不多的情况下,整个系统的内存使用率还 是很高,那不妨去排查下是不是 Shmem(共享内存) 消耗了太多内存导致的。

4. 为什么需要Page Cache?

有的人可能会说,内核的 Page Cache 这么复杂,我不要不可以么? 答案是可以,但你会付出更大代价。如果不用内核管理的 Page Cache,那有两种方法处理: 一是应用程序维护自己的 Cache 做更加细粒度的控制,比如 MySQL,但它的实现非常复杂,成本挺高,不如Page Cache 实惠。 二是直接使用 Direct I/O 来绕过 Page Cache,但这种方法真的可取吗?

通过第一张图可以看到,Buffered IO 和Memory mapped IO会先把数据写入到 Page Cache,这样做可以减少 I/O 次数,进而提升读写效率。我们可以做个实验来比较在使用Page Cache前后2种情况下,读写文件的时间。

首先,我 们来生成一个 1G 大小的新文件,然后把 Page Cache 清空,确保文件内容不在内存中, 以此来比较第一次读文件和第二次读文件耗时的差异。

  1. 生成1个1G大小的文件:dd if=/dev/zero of=/home/leon/test bs=4096 ((count=1024*256))

  2. 清空 Page Cache:sync && echo 3 >/proc/sys/vm/drop_caches

  3. 第一次读取文件的耗时如下:

     $ time cat /home/leon/test &> /dev/null
      real  0m5.733s
      user  0m0.003s
      sys  0m0.213s
    
  4. 再次读取文件的耗时如下:

     $ time cat /home/leon/test &> /dev/null
      real  0m0.132s
      user  0m0.001s
      sys  0m0.130s
    

可以看到,第二次读取文件的耗时远小于第一次的耗时,这是因为 第一次是从磁盘来读取的内容,磁盘 I/O 是比较耗时的,而第二次读取的时候由于文件内容已经在第一次读取时被读到内存了,所以是直接从内存读取的数据,内存相比磁盘速度 是快很多的。这就是 Page Cache 存在的意义:减少 I/O,提升应用的 I/O 速度。所以,如果你不想为了很细致地管理内存而增加应用程序的复杂度,那你还是乖乖使用内 核管理的 Page Cache 吧,它是 ROI(投入产出比) 相对较高的一个方案。

5. Page Cache 的建立

Page Cache 的产生有两种不同的方式:Buffered I/O(标准 I/O)和Memory-Mapped I/O(存储映射 I/O)。

image-20250118182602347

标准 I/O :是写 (write(2)) 用户缓冲区 (Userpace Page 对应的内存),然后再将用户缓冲区里的数据拷贝到内核缓冲区 (Pagecache Page 对应的内存);如果是读的 (read(2)) 话,则是先从内核缓冲区拷贝到用户缓冲区,再从用户缓冲区读数据。 buffer 和文件内容不存在任何映射关系。

存储映射 I/O :直接将 Pagecache 映射到用户地址空间,用户直接读写 Pagecache 中的内容。

所以,存储映射 I/O 要比标准 I/O 效率高一些,毕竟少了“用户空间到内核空间互相拷贝”的过程。这也是很多应用开发者发现,为什么使用内存映射 I/O 比标准 I/O 方式性能要好一些的主要原因。

下面,用标准 I/O 为例演示一下 Page Cache 是如何创建的,请看示例脚本:

#!/bin/sh

#这是我们用来解析的文件
MEM_FILE="/proc/meminfo"

#这是在该脚本中将要生成的一个新文件
NEW_FILE="/home/yafang/dd.write.out"

#我们用来解析的Page Cache的具体项
active=0
inactive=0
pagecache=0
IFS=' '

#从/proc/meminfo中读取File Page Cache的大小
function get_filecache_size()
{
items=0
while read line
do
if [[ "$line" =~ "Active:" ]]; then
read -ra ADDR <<<"$line"
active=${ADDR[1]}
let "items=$items+1"
elif [[ "$line" =~ "Inactive:" ]]; then
read -ra ADDR <<<"$line"
inactive=${ADDR[1]}
let "items=$items+1"
fi

if [ $items -eq 2 ]; then
break;
fi
done < $MEM_FILE
}

#读取File Page Cache的初始大小
get_filecache_size
let filecache="$active + $inactive"

#写一个新文件,该文件的大小为1048576 KB
dd if=/dev/zero of=$NEW_FILE bs=1024 count=1048576 &> /dev/null

#文件写完后,再次读取File Page Cache的大小
get_filecache_size

#两次的差异可以近似为该新文件内容对应的File Page Cache
#之所以用近似是因为在运行的过程中也可能会有其他Page Cache产生
let size_increased="$active + $inactive - $filecache"


#输出结果
echo "File size 1048576KB, File Cache increased" $size inc

最终的测试结果是这样的:

File size 1048576KB, File Cache increased 1048648KB

可以看到,在创建一个文件的过程中,代码中 /proc/meminfo 里的
Active(file) 和 Inactive(file) 这两项会随着文件内容的增加而增加,它们增加的大小跟文件大小是一致的。这个过程看似简单,但是它涉及的内核机制还是很多的,换句话说,可能引起问题的地方还是很多的,我们用一张图简单描述下这个过程

image-20250118185011037

  • 过程可以描述为:

    1. 首先,往用户缓冲区 buffer(这是 Userspace Page) 写入数据
    2. 然后,buffer 中的数据拷贝到内核缓冲区(这是 Pagecache Page),如果内核缓冲区中还没有这个 Page,就会发生 Page Fault 会去分配一个 Page,拷贝结束后该 PagecachePage 是一个 Dirty Page(脏页)
    3. 然后,该 Dirty Page 中的内容会同步到磁盘,同步到磁盘后,该 Pagecache Page 变为 Clean Page 并且继续存在系统中。
  • 可以通过查看/proc/vmstst文件获取当前系统中的脏页大小

    $ cat /proc/vmstat | egrep "dirty|writeback"
    nr_dirty 40
    nr_writeback 2
    
    • nr_dirty 表示当前系统中积压了多少脏页,nr_writeback 则表示有多少脏页正在回写到磁盘中,他们两个的单位都是 Page(4KB),DirtyPages 如果积压得过多,在某些情况下也会容易引发问题。

6. PageCache的回收

服务器运行久了后,系统中 free 的内存会越来越少,用 free 命令来查看,大部分都会是 used 内存或者 buff/cache 内存,比如说下面这台生产环境中服务器的内存使用情况:

$ free -g
total used free shared buff/cache available
Mem: 125   41   6      0      7982
Swap: 0    0    0

free 命令中的 buff/cache 中的这些就是在用的 Page Cache,那它们什么时候会被回收呢?我们来看一张图:

image-20250118190159123

可以看到,应用在申请内存的时候,即使没有 free 内存,只要还有足够可回收的 PageCache,就可以通过回收 Page Cache 的方式来申请到内存,回收的方式主要是两种:直接回收和后台回收。可以通过sar命令来观察Page Cache 这2种回收方式:

$ sar -B 1
02:14:01 PM pgpgin/s pgpgout/s  fault/s   majflt/s  pgfree/s pgscank/s  pgscan
02:14:01 PM 0.14   841.53   106745.40   0.00     41936.13       0.00      0
02:15:01 PM 5.84   840.97   86713.56    0.00 43612.15   717.81    0
02:16:01 PM 95.02   816.53   100707.84   0.13 46525.81   3557.90   0
02:17:01 PM 10.56   901.38   122726.31   0.27 54936.13   8791.40   0
02:18:01 PM 108.14   306.69   96519.75    1.15 67410.50   14315.98 31
02:19:01 PM 5.97   489.67   88026.03    0.18 48526.07   1061.53   0

pgscank/s : kswapd(后台回收线程) 每秒扫描的 page 个数。
pgscand/s: Application 在内存申请过程中每秒直接扫描的 page 个数。

pgsteal/s: 扫描的 page 中每秒被回收的个数。
%vmeff: pgsteal/(pgscank+pgscand), 回收效率,越接近 100 说明系统越安全,越接近 0 说明系统内存压力越大。

这几个指标也是通过解析 /proc/vmstat 里面的数据来得出的,对应关系如下:

sar -B/proc/vmstat
pgscankpgscan_kswapd
pgscandpgscan_direct
pgstealpgscan_kswapd + pgscan_direct

本文,我强调几个重点:

  1. Page Cache 是在应用程序读写文件的过程中产生的,所以在读写文件之前你需要留意是否还有足够的内存来分配 Page Cache;
  2. Page Cache 中的脏页很容易引起问题,你要重点注意这一块;
  3. 在系统可用内存不足的时候就会回收 Page Cache 来释放出来内存,我建议你可以通过
    sar 或者 /proc/vmstat 来观察这个行为从而更好的判断问题是否跟回收有关

scan_direct |

本文,我强调几个重点:

  1. Page Cache 是在应用程序读写文件的过程中产生的,所以在读写文件之前你需要留意是否还有足够的内存来分配 Page Cache;
  2. Page Cache 中的脏页很容易引起问题,你要重点注意这一块;
  3. 在系统可用内存不足的时候就会回收 Page Cache 来释放出来内存,我建议你可以通过
    sar 或者 /proc/vmstat 来观察这个行为从而更好的判断问题是否跟回收有关

总的来说,Page Cache 的生命周期对于应用程序而言是相对比较透明的,即它的分配与回收都是由操作系统来进行管理的。正是因为这种“透明”的特征,所以应用程序才会难以控制 Page Cache,Page Cache 才会容易引发那么多问题。在接下来的文章里,我们就来看看究竟会引发什么样子的问题,以及你正确的分析思路是什么样子的。


原文地址:https://blog.csdn.net/liangzc1124/article/details/145234302

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!