当我们在讨论可用内存,我们在讨论什么?

事情的起因呢,是我们收到了一条内存报警,提示某台机器的可用内存不足,可用内存剩下不到14G。原本只是一个很简单的问题,但是呢,这次却发现了一些不一样的点。
登录机器后,free -m命令执行的结果如下:

]# free -m
              total        used        free      shared  buff/cache   available
Mem:         385445      363181        1917           4       20346       21147
Swap:             0           0           0

问题来了,报警里的14G内存,是从哪来的?好像没有一个数字和这个14G接近?

需要说明的是,我们内部的监控系统,提供了一个计算值freemem来表示机器的可用内存,这个值的计算方法是freemem = $MemFree + $Buffers + $Cached,其中$MemFree$Buffers$Cached是在/proc/meminfo中取到的值,看一下这个文件的内容:

]# cat /proc/meminfo
MemTotal:       394696352 kB
MemFree:         1964616 kB
MemAvailable:   21656108 kB
Buffers:          993900 kB
Cached:         11332776 kB
SwapCached:            0 kB
Active:         370488716 kB
Inactive:        4698796 kB
Active(anon):   362864228 kB
Inactive(anon):     1588 kB
Active(file):    7624488 kB
Inactive(file):  4697208 kB
Unevictable:           0 kB
Mlocked:               0 kB
SwapTotal:             0 kB
SwapFree:              0 kB
Dirty:              1964 kB
Writeback:             0 kB
AnonPages:      362861084 kB
Mapped:           249620 kB
Shmem:              4980 kB
Slab:           10263200 kB
SReclaimable:    8507928 kB
SUnreclaim:      1755272 kB
KernelStack:       38064 kB
PageTables:       740048 kB
// 省略....

那么监控计算出来freemem值为:(1964616+993900+11332776)/1024/1024 => 13.6G,说明报警系统是没有问题的,那为什么这个值和free命令里的值差距很大呢?对比free的输出,发现free内存和meminfo中数字差不多,但是buff/cache的和加起来比meminfo里大了不少,难不成多加了其他的内存?话不多说,找找free命令的源码看看吧。简单看了一下源码,找到最终计算这些数字的代码在:proc/sysinfo.c

void meminfo(void){
  // 省略...
  static const mem_table_struct mem_table[] = {
  {"Active",       &kb_active},       // important
  {"Active(file)", &kb_active_file},
  {"AnonPages",    &kb_anon_pages},
  {"Bounce",       &kb_bounce},
  {"Buffers",      &kb_main_buffers}, // important
  {"Cached",       &kb_page_cache},  // important
  // 省略...
  {"SReclaimable", &kb_slab_reclaimable}, // "slab reclaimable" (dentry and inode structures)
  {"SUnreclaim",   &kb_slab_unreclaimable},
  {"Shmem",        &kb_main_shared},  // kernel 2.6.32 and later
  {"Slab",         &kb_slab},         // kB version of vmstat nr_slab
  {"SwapCached",   &kb_swap_cached},
  // 省略...
  kb_main_cached = kb_page_cache + kb_slab_reclaimable;
  // 省略...
}

可以看到,free里的cache,是经过计算的,计算方法是$Cached + $SReclaimable,如果按这个计算方法,我们的监控系统里就少加了一个$SReclaimable值,看下meminfo里的这个值SReclaimable: 8507928 kB,大概8.5G,加上之前的13.6G,算下来和free的结果一致了。

再看看SReclaimable代表的是什么呢?内核文档里这样描述:

KReclaimable: Kernel allocations that the kernel will attempt to reclaim
under memory pressure. Includes SReclaimable (below), and other
direct allocations with a shrinker.
Slab: in-kernel data structures cache
SReclaimable: Part of Slab, that might be reclaimed, such as caches
SUnreclaim: Part of Slab, that cannot be reclaimed on memory pressure

Slab是内核的小数据cache,然后SReclaimable是这部分数据里可用被回收的,和Cache是一样的逻辑。那被free命令当作是Cache处理确实也没问题。

整个问题到这里,是不是就结束了呢?是不是只要我们的监控系统里计算freemem的时候多加一个SReclaimable的值就行了呢?

回到最初的问题,我们想要是什么?是希望能监控机器得可用资源,所以,这个内存可用资源应该尽可能靠近这台机器实际可以被使用得内存资源。一般情况下,$MemFree + $Buffers + $Cached + $SReclaimable应该是能基本反映机器的可用内存的,毕竟一般情况下通过echo 3 > /proc/sys/vm/drop_caches可用让系统把Buffers,Cached内存释放掉,经过测试,这个操作也会同步把SReclaimable内存也释放了。

但是上面忽略了一点,就是有些内存在执行echo 3 > /proc/sys/vm/drop_caches时是不会释放的。一个典型例子:

]# free -m -w
              total        used        free      shared     buffers       cache   available
Mem:          32010         332       31535           8           9         133       31374
Swap:             0           0           0
]# mkdir /tmp/tmpfs
]# mount -t tmpfs -o size=16g tmpfs /tmp/tmpfs/
]# dd if=/dev/zero of=/tmp/tmpfs/test bs=1G count=15
15+0 records in
15+0 records out
16106127360 bytes (16 GB) copied, 7.56507 s, 2.1 GB/s
]# free -m -w
              total        used        free      shared     buffers       cache   available
Mem:          32010         332       16124       15368           9       15543       15988
Swap:             0           0           0
]# echo 3 > /proc/sys/vm/drop_caches
]# free -m -w
              total        used        free      shared     buffers       cache   available
Mem:          32010         329       16206       15368           5       15468       16030
Swap:             0           0           0

可以看到,如果挂载的tmpfs中有数据,那么这个tmpfs实际所占用的空间是被计算在Cached内存里了,但是在drop_caches的时候,这部分内存是不会被释放的。那么,调整一下算法$MemFree + $Buffers + $Cached + $SReclaimable - $Shmem,这样相对之前来说,会更加准确一些。

然而事情还没完,如果注意下free命令的输出:

]# free -m
              total        used        free      shared  buff/cache   available
Mem:         385445      363181        1917           4       20346       21147
Swap:             0           0           0

发现有个available列,这个列代表什么?看名字好像和我们需要的值很像,这里直接参考man free的说明:

available
Estimation of how much memory is available for starting new applications, without swapping. Unlike the data provided by the cache or free fields, this field takes into account page cache
and also that not all reclaimable memory slabs will be reclaimed due to items being in use (MemAvailable in /proc/meminfo, available on kernels 3.14, emulated on kernels 2.6.27+, otherwise
the same as free)

看这个描述,这个字段代表在不进行swap的情况下,新的应用程序能使用的内存数量。这个和我们的需求非常的接近。数字来源在3.14及以上内核里是/proc/meminfo里的MemAvailable字段,在大于2.6.27的内核里是模拟出来的,其他情况保持和free内存一致。两个问题,内核是怎么计算MemAvailable的?free命令是怎么模拟的?

先看看内核的吧,这个Commit引入了这个字段:

for_each_zone(zone)
	wmark_low += zone->watermark[WMARK_LOW];

/*
 * Estimate the amount of memory available for userspace allocations,
 * without causing swapping.
 *
 * Free memory cannot be taken below the low watermark, before the
 * system starts swapping.
 */
available = i.freeram - wmark_low;

/*
 * Not all the page cache can be freed, otherwise the system will
 * start swapping. Assume at least half of the page cache, or the
 * low watermark worth of cache, needs to stay.
 */
pagecache = pages[LRU_ACTIVE_FILE] + pages[LRU_INACTIVE_FILE];
pagecache -= min(pagecache / 2, wmark_low);
available += pagecache;

/*
 * Part of the reclaimable swap consists of items that are in use,
 * and cannot be freed. Cap this estimate at the low watermark.
 */
available += global_page_state(NR_SLAB_RECLAIMABLE) -
	     min(global_page_state(NR_SLAB_RECLAIMABLE) / 2, wmark_low);

if (available < 0)
	available = 0;

大体逻辑,把freeram,pagecache,RECLAIMABLE这些都算上,除了这些内存中有部分低水位不让用的,都算在available里了。

再看看free命令里是怎么模拟的,代码在proc/sysinfo.c

  /* zero? might need fallback for 2.6.27 <= kernel <? 3.14 */
  if (!kb_main_available) {
#ifdef __linux__
    if (linux_version_code < LINUX_VERSION(2, 6, 27))
      kb_main_available = kb_main_free;
    else {
      FILE_TO_BUF(VM_MIN_FREE_FILE, vm_min_free_fd);
      kb_min_free = (unsigned long) strtoull(buf,&tail,10);

      watermark_low = kb_min_free * 5 / 4; /* should be equal to sum of all 'low' fields in /proc/zoneinfo */

      mem_available = (signed long)kb_main_free - watermark_low
      + kb_inactive_file + kb_active_file - MIN((kb_inactive_file + kb_active_file) / 2, watermark_low)
      + kb_slab_reclaimable - MIN(kb_slab_reclaimable / 2, watermark_low);

      if (mem_available < 0) mem_available = 0;
      kb_main_available = (unsigned long)mem_available;
    }
#else
      kb_main_available = kb_main_free;
#endif /* linux */

逻辑差不多,多了一些判断内核版本的代码,也是找到最低水位,然后一堆加加减减。

最后需要注意的是,虽然是说这个MemAvailable是在3.14才被加到内核里的,但是由于红帽的backport,我们现在的CentOS7默认使用的3.10内核也是支持这个字段的。总体来说,针对标题的这个需求,参考free命令的算法,应该能比较好的跨平台和系统完成可用内存的估算工作。