写该篇文章的用意不在于怎么解决某个问题,而是希望表达出因这次线上问题而引发出解决问题的思路。

问题背景

公司内部的一个license服务器,部署了一套apache+mod_wsgi+python服务,该服务用户量很小,但是内存使用量却很大,其中有一个比较奇怪的现象,就是我通过top命令看到的进程使用内存很小,但是free命令看到的使用内存确很大,下面我会先引出问题然后说一下我的解题思路

解题一

首先free命令查看使用和空闲内存,如下:

$ free -m
             total       used       free     shared    buffers     cached
Mem:          1877       1736        141          0          6         23
-/+ buffers/cache:       1706        170
Swap:            0          0          0

可能你已经发现内存使用达到了1736m(这不是重点),这里说一下我们的系统使用用户就只有几个人,甚至并发都没有,正常情况不可能使用1个多G的内存。
接下来我们top +M命令看一下进程用量:

top - 09:45:01 up 1 day, 23:59,  2 users,  load average: 0.00, 0.00, 0.00
Tasks: 112 total,   2 running, 110 sleeping,   0 stopped,   0 zombie
Cpu(s):  1.8%us,  1.5%sy,  0.0%ni, 96.7%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:   1922620k total,  1778904k used,   143716k free,     6900k buffers
Swap:        0k total,        0k used,        0k free,    23564k cached

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                                                               
 2639 apache    20   0 1722m 265m 3588 S  0.0 14.1   2:20.72 httpd                                                                                                                                 
 6246 apache    20   0 1530m  56m 3576 S  0.0  3.0   2:40.17 httpd                                                                                                                                 
 2564 apache    20   0 1530m  42m 3616 S  0.3  2.3   1:35.79 httpd                                                                                                                                 
 1641 mysql     20   0  818m  31m 2448 S  0.0  1.7   8:54.25 mysqld                                                                                                                                
 8895 apache    20   0  881m  29m 6496 S  0.0  1.6   0:25.66 httpd                                                                                                                                 
27060 root      20   0 28824 3052 2240 S  0.3  0.2   0:03.14 sshd                                                                                                                                  
21110 root      20   0 28716 2836 2176 S  0.0  0.1   0:00.12 sshd                                                                                                                                  
27460 root      20   0 28444 2692 2176 S  0.0  0.1   0:00.04 sshd                                                                                                                                  
 1179 root      20   0  165m 2004 1132 S  0.0  0.1   2:30.19 vmtoolsd                                                                                                                              
27062 root      20   0  105m 1996 1536 S  0.0  0.1   0:00.08 bash                                                                                                                                  
27075 root      20   0  105m 1984 1544 S  0.0  0.1   0:00.00 bash                                                                                                                                  
 1312 root      20   0 88788 1956  488 S  0.0  0.1   0:16.80 httpd                                                                                                                                 
27468 root      20   0 27732 1776 1352 S  0.0  0.1   0:00.00 sftp-server                                                                                                                           
21133 root      20   0 27732 1708 1236 S  0.0  0.1   0:00.00 sftp-server                                                                                                                           
21127 root      20   0 27732 1684 1236 S  0.0  0.1   0:00.02 sftp-server                                                                                                                           
21114 root      20   0 27732 1636 1236 S  0.0  0.1   0:00.02 sftp-server

有心人已经发现了问题,what append? 我的内存哪里去了,通过top命令观察进程使用的内存量最多也就400多m,但是free明明使用总量1736m。
注意: 有经验的人可能会说linux系统会使用buffers/cache来提高处理性能而且这部分内存是可以释放的,很多人都知道大名鼎鼎的《Linux ate my linux》网站这里附上网址:https://www.linuxatemyram.com/,这篇网站介绍的也大都是buffers、cache占用系统内存以提高性能,但是这里请注意看我的free命令结果,buffers为6m,cache为23m,used和top进程相差还是太大了。

太奇怪了,谁吃了我的内存!!!

解题二

上面我们已经知道,既然不是buffers/cache的问题,那内存哪里去了?会不会是free命令或者top命令的问题呢,既然free命令可以统计内存那她是怎么统计的呢。top又是怎么统计每个进程的内存呢,我们先来分析top命令,其实top命令是读取的/proc/$PID/statm文件,/proc/$PID/statm文件用linux官方的描述为:Process memory status information详细信息请参考连接https://www.kernel.org/doc/html/latest/filesystems/proc.html,
/proc/$PID/statm文件内容如下:

$ cat /proc/1449/statm 
2762 258 75 36 0 195 0

该文件包含了7个字段,每个字段的含义可参考下表:

Field

Content

size

total program size (pages)

(same as VmSize in status)

resident

size of memory portions (pages)

(same as VmRSS in status)

shared

number of pages that are shared

(i.e. backed by a file, same as RssFile+RssShmem in status)

trs

number of pages that are ‘code’

(not including libs; broken, includes data segment)

lrs

number of pages of library

(always 0 on 2.6)

drs

number of pages of data/stack

(including libs; broken, includes library text)

dt

number of dirty pages

(always 0 on 2.6)

上面的七个字段我们要特别留意其中的第二个字段也就是resident, 该字段在linux中有一个专业的术语叫RSS(Resident Set Size)翻译为中文就是:驻留集大小,其实使用过ps命令的同学应该注意到过,ps命令会输出进程的两个字段:RSS,VSZ,如下第五和第六列:

$ ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0  19232  1504 ?        Ss   Jul28   0:01 /sbin/init
root         2  0.0  0.0      0     0 ?        S    Jul28   0:00 [kthreadd]
root         3  0.0  0.0      0     0 ?        S    Jul28   0:00 [migration/0]

上面ps命令结果中的RSS字段就是对应的statm文件的第二个字段,细心的同学可能又发现了问题,不对啊?为什么statm文件的结果和ps命令的结果不一样的,这是因为statm文件的字段单位是page,我们知道linux是使用内存页来分配和寻址的,所以这里我们应该把statm结果的resident字段乘以内存页的大小(默认为4kb,也可以使用getconf PAGESIZE命令查看)。
关于RSS和VSZ的详细介绍这里不做过多解释,可以参考我的这篇文章,里面解释的很清楚:linux ps命令VSZ和RSS内存使用的区别

这里我们姑且认为RSS也就是/proc/1449/statm 文件的第二个字段就是我们程序使用的实际物理内存(其实也差不多了),也是top命令读取进程内存的大小,看到这里我们应该可以想到那我是不是只要统计/proc目录下所有进程的RSS内存信息不就可以知道所有进程占用的内存了吗?答案是肯定的,我写了下面的脚本用来统计系统所有进程的内存和,可以用作参考:

$ cat RSS.sh
#!/bin/bash                                                                                                               
for PROC in `ls  /proc/|grep "^[0-9]"`
do
  if [ -f /proc/$PROC/statm ]; then
      TEP=`cat /proc/$PROC/statm | awk '{print ($2)}'`
      RSS=`expr $RSS + $TEP`
  fi
done
RSS=`expr $RSS \* 4` # 注意这里要乘以4(内存页大小)
echo $RSS"KB"

那么上面我们已经可以统计出来系统中进程所占用的总内存了,但是这还不够,其实我们还可以使用一个工具来统计系统所占用的内存:nmon,该工具可以很直观的统计出系统内存被什么给占用,如下:

$ nmon

MySQL使用jemalloc管理内存_字段


上图圈起来的部分,我们可以看到还有两个东西占用了内存:Slab和PageTables,这两个又是什么东西?由于篇幅原因我这里只做一个简单的介绍,

简单的说内核为了高性能每个需要重复使用的对象都会有个池,这个slab池会cache大量常用的对象,所以会消耗大量的内存。运行命令:

$ slabtop
 Active / Total Objects (% used)    : 90854 / 97978 (92.7%)
 Active / Total Slabs (% used)      : 5073 / 5073 (100.0%)
 Active / Total Caches (% used)     : 97 / 181 (53.6%)
 Active / Total Size (% used)       : 24085.29K / 25296.83K (95.2%)
 Minimum / Average / Maximum Object : 0.02K / 0.26K / 4096.00K

  OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME                   
 18256  18124  99%    0.03K    163      112       652K size-32
  8964   8950  99%    0.14K    332       27      1328K sysfs_dir_cache
  8319   8010  96%    0.06K    141       59       564K size-64
  8200   7510  91%    0.19K    410       20      1640K dentry
  7632   7573  99%    0.07K    144       53       576K selinux_inode_security
  4580   4577  99%    0.19K    229       20       916K size-192
  4399   4286  97%    0.07K     83       53       332K Acpi-Operand
  4275   3675  85%    0.20K    225       19       900K vm_area_struct
  4116   4101  99%    0.58K    686        6      2744K inode_cache
  3619   2877  79%    0.05K     47       77       188K anon_vma_chain
  3312   3224  97%    0.04K     36       92       144K Acpi-Namespace
  2516   1152  45%    0.10K     68       37       272K buffer_head
  2212   1767  79%    0.55K    316        7      1264K radix_tree_node
  1932   1669  86%    0.04K     21       92        84K anon_vma
  1560   1328  85%    0.19K     78       20       312K filp
  1500   1460  97%    0.12K     50       30       200K size-128
  1020    992  97%    1.00K    255        4      1020K size-1024
   996    887  89%    0.62K    166        6       664K proc_inode_cache
   920    904  98%    0.04K     10       92        40K dm_io
   864    860  99%    0.02K      6      144        24K dm_target_io
   768    757  98%    0.50K     96        8       384K size-512
   640    634  99%    0.78K    128        5       512K shmem_inode_cache
   588    557  94%    1.00K    147        4       588K ext4_inode_cache
   468    456  97%    2.00K    234        2       936K size-2048
   440    375  85%    0.19K     22       20        88K cred_jar
   390    281  72%    0.12K     13       30        52K pid
   360    324  90%    0.25K     24       15        96K skbuff_head_cache
   340    283  83%    0.11K     10       34        40K task_delay_info
   261    256  98%    2.59K     87        3       696K task_struct
   240    118  49%    0.08K      5       48        20K blkdev_ioc
   238    233  97%    0.53K     34        7       136K idr_layer_cache
   234    234 100%    4.00K    234        1       936K size-4096
   225    214  95%    0.81K     25        9       200K task_xstate

从图我们可以看出各种对象的大小和数目,遗憾的是没有告诉我们slab消耗了多少内存。
我们自己来算下好了:

$ echo `cat /proc/slabinfo |awk 'BEGIN{sum=0;}{sum=sum+$3*$4;}END{print sum/1024/1024}'` MB
31.6606 MB

好吧,把每个对象的数目*大小,再累加,我们就得到了总的内存消耗量:31M

那么PageTables呢?
简单来说是linux用于记录和管理虚拟内存地址的,这是一个硬性开销,具体可自行搜索了解
好吧,知道是干嘛的啦,管理这些物理页面的硬开销,那么具体是多少呢?如下命令:

$ echo `grep PageTables /proc/meminfo | awk '{print $2}'` KB
58052 KB

通过上面的分析,我对linux系统内存的消耗做了一下总结:

  1. 进程消耗。
  2. slab消耗
    3.pagetable消耗。
    我们把三种消耗汇总下和free出的结果比对,我汇总成了一下脚本,用来统计三种内存消耗并和free命令做了对比:
$ cat cm.sh
#!/bin/bash
for PROC in `ls /proc/|grep "^[0-9]"`
do
  if [ -f /proc/$PROC/statm ]; then
      TEP=`cat /proc/$PROC/statm | awk '{print ($2)}'`
      RSS=`expr $RSS + $TEP`
  fi
done
RSS=`expr $RSS \* 4`
PageTable=`grep PageTables /proc/meminfo | awk '{print $2}'`
SlabInfo=`cat /proc/slabinfo |awk 'BEGIN{sum=0;}{sum=sum+$3*$4;}END{print sum/1024/1024}'`

echo $RSS"KB", $PageTable"KB", $SlabInfo"MB"
printf "rss+pagetable+slabinfo=%sMB\n" `echo $RSS/1024 + $PageTable/1024 + $SlabInfo|bc`
free -m

我们运行一下,看下结果:

$ ./cm.sh 
357528KB, 4664KB, 28.5003MB
rss+pagetable+slabinfo=381.5003MB
             total       used       free     shared    buffers     cached
Mem:          3833       2991        841          0         20        102
-/+ buffers/cache:       2868        964
Swap:         4031         13       4018

上面的结果不需要太关注我的内存总大小,因为我后来加了内存,但是现象还是一样的现象。
free结果说使用了2868m, 我们的cm脚本中的rss+pagetable+slabinfo=381.5003MB,有没有被吓到,rss+slab+pagetable一共才用了381m,但是free统计却使用了高达2868m那我另外的内存被用在哪里了?

解题三

其实通过上面的分析我们已经可以知道cm.sh脚本统计的内存应该是没有问题了,那我们应该想到另外一个问题:那是不是free命令统计的有问题呢?free命令中的内存信息从哪里来的呢?其实free命令是读取的/proc/meminfo文件,关于/etc/meminfo文件中的字段含义可以查看这个链接:https://www.howtouselinux.com/post/linux-memory-metrics-proc-meminfo 那么我们来对比下cm.sh脚本的结果和meminfo中内存信息:

$ ./cm.sh 
1367988KB, 6880KB, 27.7792MB
rss+pagetable+slabinfo=1368.7792MB
             total       used       free     shared    buffers     cached
Mem:          3833       2694       1138          0          1         56
-/+ buffers/cache:       2637       1195
Swap:         4031         33       3998

$ cat /proc/meminfo 
MemTotal:        3925040 kB
MemFree:         1166624 kB
Buffers:            1708 kB
Cached:            57548 kB
SwapCached:         2936 kB
···

上面我截取了meminfo文件中的关键信息,其他信息省略了,通过对比我们发现rss+pagetable_slabinfo=1368m,而free命令的used为2637,且free命令取meminfo文件内存数据也没有错,既然free命令没有错,统计的rss+pagetable+slabinfo内存也没有错,那meminfo命令中的MemFree为什么这么少呢?这个时候我们就想到了是不是meminfo文件中的MemFree统计有问题?
其实这里有引出了另外一个问题,linux对内存分配的统计是怎么样的,也就是linux会把所有分配出去的内存进行统计吗?答案是否定的,其实linux kernal动态分配的内存就有一部分没有计入/proc/meminfo文件中。
以下部分截取自另一篇文章:
我们知道,Kernel的动态内存分配通过以下几种接口:

alloc_pages/__get_free_page: 以页为单位分配
vmalloc: 以字节为单位分配虚拟地址连续的内存块
slab allocator
kmalloc: 以字节为单位分配物理地址连续的内存块,它是以slab为基础的,使用slab层的general caches — 大小为2^n,名称是kmalloc-32、kmalloc-64等(在老kernel上的名称是size-32、size-64等)。
通过slab层分配的内存会被精确统计,可以参见/proc/meminfo中的slab/SReclaimable/SUnreclaim;
通过vmalloc分配的内存也有统计,参见/proc/meminfo中的VmallocUsed 和 /proc/vmallocinfo(下节中还有详述);

而通过alloc_pages分配的内存不会自动统计,除非调用alloc_pages的内核模块或驱动程序主动进行统计,否则我们只能看到free memory减少了,但从/proc/meminfo中看不出它们具体用到哪里去了。比如在VMware guest上有一个常见问题,就是VMWare ESX宿主机会通过guest上的Balloon driver(vmware_balloon module)占用guest的内存,有时占用得太多会导致guest无内存可用,这时去检查guest的/proc/meminfo只看见MemFree很少、但看不出内存的去向,原因就是Balloon driver通过alloc_pages分配内存,没有在/proc/meminfo中留下统计值,所以很难追踪。

总结

通过上面的分析,我们知道MemFree不知去向的一个原因就是Balloon driver通过alloc_pages抢走了内存,然而使用balloon driver的最大可能就是vmware esx服务,后来联系了公司的私有云维护人员协助调研发现,公司确实使用了vmware esx服务,而且其中一台host内存已经接近瓶颈,把我们使用的机器切到了别的host上问题解决🤦♂️🤦♂️🤦♂️,是不是感觉整个人都傻了。这里提供两个文章可以供大家了解一下vmware esx服务的内存共享机制:https://blog.51cto.com/qq694157416/1345680

最终的结果很让人意外,但本篇文章希望提供一个内存分析的解题思路,授人以鱼不如授人以渔!!!