在我之前的文章中有粉丝提到内存不足,需要频繁清理系统缓存的问题,今天我们就来聊聊Page Cache相关的一系列问题。
怎么观测Page Cache?
在Linux上直接查看Page Cache的方式有很多,包括free 、/proc/vmstat 命令等,它们的内容其实是一致的,这些性能查询工具的数据来源都是/proc/meminfo,今天我们就用最常用的 free 命令的输出解释下。
我们观察free命令的输出,在结合/proc/meminfo的结果,你可以发现 buff/cache 包括下面这几项:
buff/cache = Buffers + Cached + SReclaimable。
从这个公式中,你能看到 free 命令中的 buff/cache 是由 Buffers、Cached 和 SReclaimable 这三项组成的,它强调的是内存的可回收性,也就是说,可以被回收的内存会统计在这一项。关于buff/cache的介绍,我在前面的文章中有详细讲过。这里的SReclaimable是指可以被回收的内核内存,包括 dentry 和 inode 等。
Page Cache有什么用?
- Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中(可以通过dd命令对磁盘和文件读写观测缓存效果)。
- 从写的角度来说,不仅可以优化磁盘和文件的写入,对应用程序也有好处,应用程序可以在数据真正落盘前,就返回去做其他工作。
- 从读的角度来说,既可以加速读取那些需要频繁访问的数据,也降低了频繁 I/O 对磁盘的压力。
Page Cache操作不当的危害
如果你的业务对Page Cache比较敏感,比如说你的业务数据对延迟很敏感,或者再具体一点,你的业务指标对TP99(99 分位)要求较高,这种场景下,如果对Page Cache操作不当会产生的问题。
手工误操作Page Cache导致业务性能下降
我们知道,对于Page Cache而言,是可以通过drop_cache来清掉的,很多人在看到系统中存在非常多的Page Cache时会习惯使用drop_cache来清理它们。
于是这样就引入了一个容易被我们忽略的问题:当我们执行 echo 2 来 drop slab 的时候,它也会把 Page Cache 给 drop 掉,业务性能产生了明显的下降。
- inode 是内存中对磁盘文件的索引,进程在查找或者读取文件时就是通过 inode 来进行操作的。
- 进程会通过inode来找到文件的地址空间(address_space),然后结合文件偏移(会转换成 page index)来找具体的Page, inode被清理需要去磁盘读取。
内核机制引起Page Cache被回收导致业务性能下降
在内存紧张的时候会触发内存回收,内存回收会尝试去回收 reclaimable(可以被回收的)内存,这部分内存既包含 Page Cache 又包含 reclaimable kernel memory(比如 slab),我们可以通过/proc/vmstat 来观察的内核回收的事件。
grep inodesteal /proc/vmstat
这个行为对应的事件是 inodesteal,就是上面这两个事件,其中:
- kswapd_inodesteal:是指在 kswapd 回收的过程中,因为回收 inode 而释放的 pagecache page 个数。
- pginodesteal 是指 kswapd 之外其他线程在回收过程中,因为回收 inode 而释放的 pagecache page 个数。
如何避免Page Cache 被回收而引起的性能问题?
从应用代码层面来优化
从应用程序代码层面来解决是相对比较彻底的方案,因为应用更清楚哪些 Page Cache 是重要的,哪些是不重要的,所以就可以明确地来对读写文件过程中产生的 Page Cache 区别对待。例如:
- 对于重要的数据,可以通过 mlock(2) 来保护它,防止被回收以及被 drop。
- 对于不重要的数据(比如日志),那可以通过 madvise(2) 告诉内核来立即释放这些 Page Cache。
从系统层面来调整
在有些情况下,对应用程序而言,修改源码是件比较麻烦的事,如果可以不修改源码来达到目的那就最好不过了。Linux 内核同样实现了这种不改应用程序的源码而从系统层面调整来保护重要数据的机制,这个机制就是 memory cgroup,它提供了几个内存水位控制线 memory.{min, low, high, max}。
- 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。