第八章 文件系统性能观测

内存参数说明

min_free_kbytes = 总内存数量 × 1% ~ 3%。watermark_scale_factor 推荐值在 100 ~ 500 之间。

8.1 PageCache 观测

通过前面两节的学习,我们已经知道了物理内存分配并不一定很快就能完成。如果应用数据 + PageCache 占用过大,而且 kswapd 回收也来不及的时候,应用程序可能会在分配物理内存时陷入直接内存回收直接内存规整逻辑,进而导致巨大的耗时突增。

那么问题来了,当我们的应用程序在线上出现耗时增长的时候,我们该如何定位问题是不是因为直接内存回收、直接内存规整导致的呢?

造成直接内存回收的第一大因素就是可用内存紧张了。

我们可以通过查看服务器 /proc/meminfo 中的信息来推算可用内存是否充足。

cat /proc/meminfo 的输出中,MemAvailable 代表的是可用的内存。如果它很低了,那么你的应用程序的耗时突增大概率就是因为可用内存不足而导致了。

MemAvailable 大概约等于 MemFree(空闲内存)和 Cached(PageCache 消耗,内存紧张的时候可以回收利用)之和。Buffers 是设备缓存,一般比较小,我们可以忽略它。

另外还要注意你的应用程序是否配置了 NUMA 亲和性绑定。如果配置了的话,即使是整台物理机上存在较多的空闲内存,也会因为你应用绑定的 node 内存不足而出现直接内存回收。

可以使用 numastat -m 命令查看各个 NUMA 的可用内存余量详细数据。

其中 MemFree 是空闲的物理内存数量,Active(file) + Inactive(file) 是内存紧张时可以回收的 Page Cache 大小。

在我们传统的认知里,可用内存大约是等于 MemFree + Page Cache。但是即使 Page Cache 看起来占用比较多,仍然也可能触发直接内存回收延迟。

$ cat /proc/meminfo
MemTotal:       65688788 kB
MemFree:        24402436 kB
MemAvailable:   59627020 kB
Buffers:         2959520 kB
Cached:         34477880 kB
# numastat -m
                         Node 0          Node 1           Total
                --------------- --------------- ---------------
MemTotal                32025.26        32123.95        64149.21
MemFree                 11493.27        12329.07        23822.34
MemUsed                 20531.99        19794.88        40326.87
......
Active(file)             8040.43         7872.62        15913.05
Inactive(file)           9656.48        10428.46        20084.94

Page Cache 中的页面分为两类:

  • 干净页面:内存中没有修改,不需要更新到硬盘。
  • 脏页面:内存中做过修改,需要写入到磁盘后才可以删除。

对于干净页面回收处理过程比较简单,直接删除就行了。对于脏页面,需要磁盘 IO 完成后才能回收。如果磁盘 IO 打满的话,即使是 Page Cache 看起来很多,也会因为无法快速回收而导致出现直接内存回收延迟!

所以我们还需要观察内核中的脏页,以及磁盘 IO 的相关情况。在内核的伪文件 /proc/vmstat 中提供了很多相关信息。

/proc/vmstat 中包含的信息很多,我把和 Page Cache 回收相关的关键指标提取了出来:

  • pgscan_direct: 进程直接扫描的 Page 数量
  • pgsteal_direct: 进程直接回收的 Page 数量
  • pgscan_kswapd: kswapd 扫描的 Page 数量
  • pgsteal_kswapd: kswapd 回收的 Page 数量
  • compact_stall: 直接内存规整次数
  • compact_fail: 直接内存规整失败次数
  • compact_success: 直接内存规整成功次数
  • nr_dirty: 脏页个数
  • nr_writeback: 正在被回写的页面数量
  • pgpgout: 内存中多少页被写回磁盘

通过上面这些指标的变化趋势可以观测 Page Cache 回收情况。假如我们发现 pgscan_direct 值的变化比较频繁,那么大概可以推测应用的性能可能是因为 Page Cache 回收不过来而导致的性能问题了。

如果你嫌 /proc/vmstat 看起来不方便,还可以使用 sar 命令。

# cat /proc/vmstat
...
pgsteal_kswapd 82183721
pgsteal_direct 4838
pgscan_kswapd 88889658
pgscan_direct 4879
compact_stall 877
compact_fail 259
compact_success 618
nr_dirty 68
nr_writeback 0
pgpgout 2182776294
...
# sar -B 1

sar 的输出中,pgscank/s 代表的是每秒 kswapd 扫描的页面数量,pgscand/s 代表的是进程直接扫描的页面数量。

如果你已经知道了有直接内存回收在发生,想看下是具体是哪些进程在执行这些耗时的操作。那么还可以进一步使用内核中预置的 tracepoint 来观察。

  • mm_vmscan_direct_reclaim_xxx 直接回收开始/结束
  • mm_compaction_xxx 内存规整开始/结束
  • writeback_xxx 脏页回写开始/结束

关于 tracepoint 怎么用,可以参考《深入理解Linux进程与内存》第14.5节。

8.2 buffer 和 cache 的原理

只要接触过 Linux 开发的同学大概率就了解内存消耗中 buffer 和 cache 这两个概念。但是这两个概念到底啥区别,我一直是记了忘,忘了再记,一直都没能理解到很深。所以决定从源码视角来带大家一起挖一下这两个指标的具体含义。

8.2.1 /proc/meminfo 输出原理

我们在查看 /proc/meminfo 的时候,输出的前面几行一般如下图所示:

# cat /proc/meminfo
MemTotal:       65688788 kB
MemFree:        24270080 kB
MemAvailable:   40522772 kB
Buffers:         2959660 kB
Cached:         34592776 kB
SwapCached:            0 kB
Active:         17603432 kB
Inactive:       20741748 kB
Active(anon):    1255776 kB
Inactive(anon):   173096 kB
Active(file):   16347656 kB
Inactive(file): 20568652 kB

其中包含了 BuffersCached。这两个英文单词本身就是个近义词,所以单看字面意思根本理解不清楚。所以我翻出来了它们的源码。

当我们访问 /proc/meminfo 文件时,内核会执行到 meminfo_proc_show 函数。我们来看看它是如何获取并展示这两个字段的。

meminfo_proc_show 中执行的 si_meminfo(&i) 是在统计系统中 “Buffer” 的消耗,并会将值记录到 i.bufferram 的成员中。

cached 的计算有点小复杂,先是调用 global_node_page_state(NR_FILE_PAGES) 计算了所有的 NR_FILE_PAGES 类型的缓存。然后又在该值的基础上减去了 swapcache 大小,又减去了 “Buffer” 消耗。

// file:fs/proc/meminfo.c
static int meminfo_proc_show(struct seq_file *m, void *v)
{
  struct sysinfo i;
  // 统计buffers
  si_meminfo(&i);
  // 统计cached
  cached = global_node_page_state(NR_FILE_PAGES) -
      total_swapcache_pages() - i.bufferram;
  ...
  show_val_kb(m, "MemTotal:       ", i.totalram);
  ...
  show_val_kb(m, "Buffers:        ", i.bufferram);
  show_val_kb(m, "Cached:         ", cached);
}

8.2.2 Buffers 数据来源

我们再单独分一小节看一下 i.bufferram 中的数据是怎么来的。

si_meminfo 中调用 nr_blockdev_pages 函数来填充 bufferram 字段的值。

// file:mm/page_alloc.c
void si_meminfo(struct sysinfo *val)
{
  val->totalram = totalram_pages();
  ...
  val->bufferram = nr_blockdev_pages();
  ...
}

nr_blockdev_pages 中是遍历了 blockdev 文件系统下的 superblock 中的所有 inodes,将其中的 i_mapping->nrpages 值进行了累加。

// file:block/bdev.c
long nr_blockdev_pages(void)
{
  struct inode *inode;
  long ret = 0;
  list_for_each_entry(inode, &blockdev_superblock->s_inodes, i_sb_list)
    ret += inode->i_mapping->nrpages;
  return ret;
}

这里就需要介绍下 blockdev 文件系统了。这是内核中的一个特殊的文件系统,对用户层不可见。该文件系统的作用是用文件的方式管理块设备。使得可以通过标准的 VFS 函数来调用和操作块设备。

有了 blockdev 后,在每个块设备的 inode (bdev 伪文件系统的 inode) 下面会存在一个 address_space,其中将会放置缓存数据的 page 页数量。

一般来说,在这里缓存数据情况不多。只有以下情形:

  • 使用 dd 命令直接操作一个块设备
  • 或者是访问文件系统的元数据 inode

未格式化成文件系统之前是块设备,关于格式化的原理可以参见我之前的一篇文章《理解格式化原理》。

我们在服务器中上面这两种情形需要的内存缓存还是比较小的。所以,我们在 meminfo 看到的 Buffers 一般也比较小。

8.2.3 Cached 数据来源

我们日常真正大量使用的是我们对文件中数据进行读写时需要申请的内存缓存。而这些都是汇总到 meminfo 中的 cached 指标来输出的。前面我们看到,cached 值是通过如下过程计算出来的:

// file:fs/proc/meminfo.c
static int meminfo_proc_show(struct seq_file *m, void *v)
{
  // 统计cached
  cached = global_node_page_state(NR_FILE_PAGES) -
      total_swapcache_pages() - i.bufferram;
  ...
  show_val_kb(m, "Cached:         ", cached);
  ...
}

global_node_page_state 函数用于获取系统中不同类型页面的总数。如:

  • NR_ACTIVE_ANON:活动匿名页面
  • NR_INACTIVE_ANON:非活动匿名页面
  • NR_INACTIVE_FILE:活动文件页面
  • NR_ACTIVE_FILE:非活动文件页面
  • NR_FILE_PAGES:所有文件页面缓存(Page Cache)之和
  • NR_FILE_DIRTY:被应用程序修改过,但未刷新到磁盘的脏页

其在内部是通过 global_node_page_state_pages 函数访问的 vm_node_stat 全局变量。

// file:include/linux/vmstat.h
static inline
unsigned long global_node_page_state_pages(enum node_stat_item item)
{
  long x = atomic_long_read(&vm_node_stat[item]);
  return x;
}

vm_node_stat 是 Linux 内核中的一个全局数组,它用于统计记录每个 NUMA 节点相关的内存页面信息。其中的值可以通过访问 /proc/vmstat 来查看。

$ cat /proc/vmstat
nr_free_pages 5530352
nr_zone_inactive_anon 41081
nr_zone_active_anon 319777
nr_zone_inactive_file 5445886
nr_zone_active_file 4307255
......
nr_file_pages 9905924
......

我们要看的是 vm_node_stat[NR_FILE_PAGES] 中的值。而该字段中保存的是整个 Page Cache 的统计,既包括对文件内容的缓存统计,也包括对文件元数据 inode 的统计。


本章总结

我们在查看 meminfo 的输出的时候,或者用其它一些命令观测内存的消耗统计的时候,经常会看到 buffer 和 cache 这一对儿概念。

  • 在内核中,buffer 字段输出的是 blockdev 文件系统下所有 inodes 中的 i_mapping->nrpages 值的累加。在用 dd 命令操作裸的块设备,以及访问文件系统中的 inode 的时候会使用缓存,并更新这些值。
  • 在内核中,有一个全局变量 vm_node_stat,其中的 NR_FILE_PAGES 保存了所有文件页面缓存(Page Cache)使用的内存之和。内核在输出 cache 字段的时候,主要就是读取的该字段值,但是输出前减去了 buffer。

其实在我们日常工作中,绝大部分的 PageCache 都是用来做文件数据的读写缓存的。这部分输出是统计在 cache 中的。而 buffer 字段中包含的 inode 等缓存用量相对较少,不严谨的场景中可以将它忽略!

加入知识星球

有想继续加入知识星球的同学微信扫描下面的二维码即可加入。另外在公众号后台发送「星球优惠券」可以获取开发内功修炼读者的专属优惠券。

[Image 391 on Page 84]
(此处为二维码图片,请根据实际情况查看原图)