5

一、背景

近期在公司的某台linux虚拟机上,发现内存几乎消耗殆尽,但找不到其去向。
在调查过程中,重点分析了/proc/meminfo文件,对其内存占用进行了学习与分析。

特记录在此,与君分享。

二、环境

  • 虚拟机OS : CentOS Linux release 7.4.1708 (Core)
  • 虚拟机平台 : VMWare

三、问题描述

通过free -htop查看内存消耗,发现used已接近最大可用内存,但各进程常驻内存(RES)远比used要小。

先摆出结论:

在VMWare虚拟机平台上,宿主机可以通过一个叫Balloon driver(vmware_balloon module)的驱动程序模拟客户机linux内部的进程占用内存,被占用的内存实际上是被宿主机调度到其他客户机去了。
但这种驱动程序模拟的客户机进程在linux上的内存动态分配并没有被linux内核统计进来,于是造成了上述问题的现象。

3.1 top 结果

按内存消耗排序,取消耗大于0的部分

top - 16:46:45 up 8 days, 10:25,  1 user,  load average: 0.00, 0.01, 0.05
Tasks: 109 total,   1 running, 108 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.1 us,  0.0 sy,  0.0 ni, 99.9 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  7994080 total,   185776 free,  7625996 used,   182308 buff/cache
KiB Swap:  4157436 total,   294944 free,  3862492 used.   115964 avail Mem 

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND               DATA
  3725 root      20   0 9057140 1.734g   5020 S   0.3 22.7 367:48.86 java               8882680
  1087 mysql     20   0 2672240 233064   1076 S   0.0  2.9 102:33.71 mysqld             2596840
   496 root      20   0   36828   3512   3388 S   0.0  0.0   0:31.13 systemd-journal        356
 14564 root      20   0  145700   2424   1148 S   0.0  0.0   0:02.94 sshd                   924
     1 root      20   0  128164   2404    724 S   0.0  0.0   1:02.18 systemd              84628
 14713 root      20   0  157716   2204   1512 R   0.0  0.0   0:00.08 top                   1176
 14568 root      20   0  115524   1784   1272 S   0.0  0.0   0:00.59 bash                   632
   687 root      20   0  305408   1548   1168 S   0.0  0.0  13:59.34 vmtoolsd             75352
   676 root      20   0  216388   1240    872 S   0.0  0.0   1:56.69 rsyslogd            148768
   682 root      20   0  472296    908    160 S   0.0  0.0   1:06.73 NetworkManager      222852
   684 root      20   0   24336    752    444 S   0.0  0.0   0:22.19 systemd-logind         504
   690 polkitd   20   0  534132    560    220 S   0.0  0.0   0:07.34 polkitd             450080
   677 dbus      20   0   32772    460    128 S   0.0  0.0   0:08.34 dbus-daemon           8900
   688 root      20   0   21620    452    296 S   0.0  0.0   4:42.68 irqbalance             488
   698 root      20   0  126232    432    328 S   0.0  0.0   0:30.25 crond                 1312
   922 root      20   0  562392    412     28 S   0.0  0.0   4:52.69 tuned               304472
   924 root      20   0  105996    188     92 S   0.0  0.0   0:03.64 sshd                   760
   653 root      16  -4   55452     84      0 S   0.0  0.0   0:08.81 auditd                8664
   532 root      20   0   46684      4      4 S   0.0  0.0   0:02.81 systemd-udevd         1916
   705 root      20   0  110044      4      4 S   0.0  0.0   0:00.02 agetty                 344

3.2 top结果第四行内存总体使用情况

属性 大小(G) 说明
total 7.6 可分配内存总计值
used 7.26 已分配内存
free 0.17 未分配内存
buff/cache 0.17 buff与缓存
  • 各属性满足公式:total = used + free + buff/cache
  • used在该linux版本(centos7)上,已经反映实际分配的内存,不需要再去除buff/cache部分

3.3 top进程列表内存相关列统计

合计(G) 说明
VIRT 14.236 进程申请的虚拟内存大小,申请不意味着分配,该值与实际内存消耗关系不大。
RES 1.9747 进程常驻内存,包含进程间共享内存。
SHR 0.0171 进程间共享内存,该值是推算出来的,存在误差,意义不大。

3.4 问题来了

RES合计值比used少了5G多!这些内存哪去了?

理论上,各进程的RES合计值因为会重复计算共享内存,应该比used值略大。实际上这两个值也往往是接近的,不应该差这么多。

四、清查linux内存消耗

为了进一步检查linux中内存消耗的去向,需要对/proc/meminfo文件进行一次彻底的分析统计。

linux上各种内存查看工具如free,top实际上都是从/proc下面找linux内核的各种统计文件。

4.1 /proc/meminfo内容

MemTotal:        7994080 kB
MemFree:          125256 kB
MemAvailable:     932412 kB
Buffers:               8 kB
Cached:           993796 kB
SwapCached:          252 kB
Active:          1182220 kB
Inactive:        1213960 kB
Active(anon):     693796 kB
Inactive(anon):   717156 kB
Active(file):     488424 kB
Inactive(file):   496804 kB
Unevictable:           0 kB
Mlocked:               0 kB
SwapTotal:       4157436 kB
SwapFree:        4157172 kB
Dirty:                 8 kB
Writeback:             0 kB
AnonPages:       1402140 kB
Mapped:            41584 kB
Shmem:              8576 kB
Slab:             143220 kB
SReclaimable:      86720 kB
SUnreclaim:        56500 kB
KernelStack:        5360 kB
PageTables:         7184 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:     8154476 kB
Committed_AS:    2073776 kB
VmallocTotal:   34359738367 kB
VmallocUsed:      191584 kB
VmallocChunk:   34359310332 kB
HardwareCorrupted:     0 kB
AnonHugePages:   1284096 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
DirectMap4k:       89920 kB
DirectMap2M:     4104192 kB
DirectMap1G:     6291456 kB

4.2 meminfo内容分析

属性 大小(k) 说明 扩展说明
MemTotal: 7994080 可供linux内核分配的内存总量。 比物理内存总量少一点,因为主板/固件会保留一部分内存、linux内核自己也会占用一部分内存。
MemFree: 125256 表示系统尚未分配的内存。
MemAvailable: 932412 当前可用内存。 MemFree只是尚未分配的内存,并不是所有可用的内存。有些已经分配掉的内存是可以回收再分配的。比如cache/buffer、slab都有一部分是可以回收的,这部分可回收的内存加上MemFree才是系统可用的内存,即MemAvailable。同时要注意,MemAvailable是内核使用特定的算法估算出来的,并不精确。
Buffers: 8 块设备(block device)所占用的特殊file-backed pages,包括:直接读写块设备,以及文件系统元数据(metadata)比如superblock使用的缓存页。 Buffers内存页同时也在LRU list中,被统计在Active(file)或Inactive(file)之中。
Cached: 993796 所有file-backed pages 用户进程的内存页分为两种:file-backed pages(与文件对应的内存页),和anonymous pages(匿名页),比如进程的代码、映射的文件都是file-backed,而进程的堆、栈都是不与文件相对应的、就属于匿名页。file-backed pages在内存不足的时候可以直接写回对应的硬盘文件里,称为page-out,不需要用到交换区(swap);而anonymous pages在内存不足时就只能写到硬盘上的交换区(swap)里,称为swap-out。
SwapCached: 252 SwapCached包含的是被确定要swap-out,但是尚未写入交换区的匿名内存页。 SwapCached内存页会同时被统计在LRU或AnonPages或Shmem中,它本身并不占用额外的内存。
Active: 1182220 active包含active anon和active file LRU是一种内存页回收算法,Least Recently Used,最近最少使用。LRU认为,在最近时间段内被访问的数据在以后被再次访问的概率,要高于最近一直没被访问的页面。于是近期未被访问到的页面就成为了页面回收的第一选择。Linux kernel会记录每个页面的近期访问次数,然后设计了两种LRU list: active list 和 inactive list, 刚访问过的页面放进active list,长时间未访问过的页面放进inactive list,回收内存页时,直接找inactive list即可。另外,内核线程kswapd会周期性地把active list中符合条件的页面移到inactive list中。
Inactive: 1213960 inactive包含inactive anon和inactive file
Active(anon): 693796 活跃匿名页,anonymous pages(匿名页)。
Inactive(anon): 717156 非活跃匿名页
Active(file): 488424 活跃文件内存页
Inactive(file): 496804 非活跃文件内存页
Unevictable: 0 因为种种原因无法回收(page-out)或者交换到swap(swap-out)的内存页 Unevictable LRU list上是不能pageout/swapout的内存页,包括VM_LOCKED的内存页、SHM_LOCK的共享内存页(同时被统计在Mlocked中)、和ramfs。在unevictable list出现之前,这些内存页都在Active/Inactive lists上,vmscan每次都要扫过它们,但是又不能把它们pageout/swapout,这在大内存的系统上会严重影响性能,unevictable list的初衷就是避免这种情况的发生。
Mlocked: 0 被系统调用"mlock()"锁定到内存中的页面。Mlocked页面是不可收回的。 被锁定的内存因为不能pageout/swapout,会从Active/Inactive LRU list移到Unevictable LRU list上。Mlocked与以下统计项重叠:LRU Unevictable,AnonPages,Shmem,Mapped等。
SwapTotal: 4157436 swap空间总计
SwapFree: 4157172 当前剩余swap
Dirty: 8 需要写入磁盘的内存页的大小 Dirty并不包括系统中全部的dirty pages,需要再加上另外两项:NFS_Unstable 和 Writeback,NFS_Unstable是发给NFS server但尚未写入硬盘的缓存页,Writeback是正准备回写硬盘的缓存页。
Writeback: 0 正在被写回的内存页的大小
AnonPages: 1402140 Anonymous pages(匿名页)数量 + AnonHugePages(透明大页)数量 进程所占的内存页分为anonymous pages和file-backed pages,理论上,所有进程的PSS之和 = Mapped + AnonPages。PSS是Proportional Set Size,每个进程实际使用的物理内存(比例分配共享库占用的内存),可以在/proc/[1-9]*/smaps中查看。
Mapped: 41584 正被用户进程关联的file-backed pages Cached包含了所有file-backed pages,其中有些文件当前不在使用,但Cached仍然可能保留着它们的file-backed pages;而另一些文件正被用户进程关联,比如shared libraries、可执行程序的文件、mmap的文件等,这些文件的缓存页就称为mapped。
Shmem: 8576 Shmem统计的内容包括:1.shared memory;2.tmpfs和devtmpfs。所有tmpfs类型的文件系统占用的空间都计入共享内存,devtmpfs是/dev文件系统的类型,/dev/下所有的文件占用的空间也属于共享内存。可以用ls和du命令查看。如果文件在没有关闭的情况下被删除,空间仍然不会释放,shmem不会减小,可以用 lsof -a +L1 /<mount_point> 命令列出这样的文件。 shared memory被视为基于tmpfs文件系统的内存页,既然基于文件系统,就不算匿名页,所以不被计入/proc/meminfo中的AnonPages,而是被统计进了:CachedMapped(当shmem被attached时候)。然而它们背后并不存在真正的硬盘文件,一旦内存不足的时候,它们是需要交换区才能swap-out的,所以在LRU lists里,它们被放在Inactive(anon)Active(anon)unevictable (如果被locked的话)里。注意:/proc/meminfo中的 Shmem 统计的是已经分配的大小,而不是创建时申请的大小。
Slab: 143220 通过slab分配的内存,Slab=SReclaimable+SUnreclaim slab是linux内核的一种内存分配器。linux内核的动态内存分配有以下几种方式:1.alloc_pages/__get_free_page:以页为单位分配。2.vmalloc:以字节为单位分配虚拟地址连续的内存块。3.slab:对小对象进行分配,不用为每个小对象分配一个页,节省了空间;内核中一些小对象创建析构很频繁,Slab对这些小对象做缓存,可以重复利用一些相同的对象,减少内存分配次数。4.kmalloc:以slab为基础,以字节为单位分配物理地址连续的内存块。
SReclaimable: 86720 slab中可回收的部分。
SUnreclaim: 56500 slab中不可回收的部分。
KernelStack: 5360 给用户线程分配的内核栈消耗的内存页 每一个用户线程都会分配一个kernel stack(内核栈),内核栈虽然属于线程,但用户态的代码不能访问,只有通过系统调用(syscall)、自陷(trap)或异常(exception)进入内核态的时候才会用到,也就是说内核栈是给kernel code使用的。在x86系统上Linux的内核栈大小是固定的8K或16K。Kernel stack(内核栈)是常驻内存的,既不包括在LRU lists里,也不包括在进程的RSS/PSS内存里。RSS是Resident Set Size 实际使用物理内存(包含共享库占用的内存),可以在/proc/[1-9]*/smaps中查看。
PageTables: 7184 Page Table的消耗的内存页 Page Table的用途是翻译虚拟地址和物理地址,它是会动态变化的,要从MemTotal中消耗内存。
NFS_Unstable: 0 发给NFS server但尚未写入硬盘的缓存页
Bounce: 0 bounce buffering消耗的内存页 有些老设备只能访问低端内存,比如16M以下的内存,当应用程序发出一个I/O 请求,DMA的目的地址却是高端内存时(比如在16M以上),内核将在低端内存中分配一个临时buffer作为跳转,把位于高端内存的缓存数据复制到此处。
WritebackTmp: 0 正准备回写硬盘的缓存页
CommitLimit: 8154476 overcommit阈值,CommitLimit = (Physical RAM * vm.overcommit_ratio / 100) + Swap Linux是允许memory overcommit的,即承诺给进程的内存大小超过了实际可用的内存。commit(或overcommit)针对的是内存申请,内存申请不等于内存分配,内存只在实际用到的时候才分配。但可以申请的内存有个上限阈值,即CommitLimit,超出以后就不能再申请了。
Committed_AS: 2073776 所有进程已经申请的内存总大小
VmallocTotal: 34359738367 可分配的虚拟内存总计
VmallocUsed: 191584 已通过vmalloc分配的内存,不止包括了分配的物理内存,还统计了VM_IOREMAP、VM_MAP等操作的值 VM_IOREMAP是把IO地址映射到内核空间、并未消耗物理内存
VmallocChunk: 34359310332 通过vmalloc可分配的虚拟地址连续的最大内存
HardwareCorrupted: 0 因为内存的硬件故障而删除的内存页
AnonHugePages: 1284096 AnonHugePages统计的是Transparent HugePages (THP),THP与Hugepages不是一回事,区别很大。Hugepages在/proc/meminfo中是被独立统计的,与其它统计项不重叠,既不计入进程的RSS/PSS中,又不计入LRU Active/Inactive,也不会计入cache/buffer。如果进程使用了Hugepages,它的RSS/PSS不会增加。而AnonHugePages完全不同,它与/proc/meminfo的其他统计项有重叠,首先它被包含在AnonPages之中,而且在/proc/<pid>/smaps中也有单个进程的统计,与进程的RSS/PSS是有重叠的,如果用户进程用到了THP,进程的RSS/PSS也会相应增加,这与Hugepages是不同的。 Transparent Huge Pages 缩写 THP ,这个是 RHEL 6 开始引入的一个功能,在 Linux6 上透明大页是默认启用的。由于 Huge pages 很难手动管理,而且通常需要对代码进行重大的更改才能有效的使用,因此 RHEL 6 开始引入了 Transparent Huge Pages ( THP ), THP 是一个抽象层,能够自动创建、管理和使用传统大页。THP 为系统管理员和开发人员减少了很多使用传统大页的复杂性 , 因为 THP 的目标是改进性能 , 因此其它开发人员 ( 来自社区和红帽 ) 已在各种系统、配置、应用程序和负载中对 THP 进行了测试和优化。这样可让 THP 的默认设置改进大多数系统配置性能。但是 , 不建议对数据库工作负载使用 THP 。这两者最大的区别在于 : 标准大页管理是预分配的方式,而透明大页管理则是动态分配的方式。
HugePages_Total: 0 预分配的可使用的标准大页池的大小。HugePages在内核中独立管理,只要一经定义,无论是否被使用,都不再属于free memory。 Huge pages(标准大页) 是从 Linux Kernel 2.6 后被引入的,目的是通过使用大页内存来取代传统的 4kb 内存页面, 以适应越来越大的系统内存,让操作系统可以支持现代硬件架构的大页面容量功能。
HugePages_Free: 0 标准大页池中尚未分配的标准大页
HugePages_Rsvd: 0 用户程序预申请的标准大页,尚未真的分配走
HugePages_Surp: 0 标准大页池的盈余
Hugepagesize: 2048 标准大页大小,这里是2M
DirectMap4k: 89920 映射为4kB的内存数量 DirectMap所统计的不是关于内存的使用,而是一个反映TLB效率的指标。TLB(Translation Lookaside Buffer)是位于CPU上的缓存,用于将内存的虚拟地址翻译成物理地址,由于TLB的大小有限,不能缓存的地址就需要访问内存里的page table来进行翻译,速度慢很多。为了尽可能地将地址放进TLB缓存,新的CPU硬件支持比4k更大的页面从而达到减少地址数量的目的, 比如2MB,4MB,甚至1GB的内存页,视不同的硬件而定。所以DirectMap其实是一个反映TLB效率的指标。
DirectMap2M: 4104192 映射为2MB的内存数量
DirectMap1G: 6291456 映射为1GB的内存数量

4.3 根据meminfo统计内存占用

linux上的内存消耗总的来说有两部分,一部分是内核kernel进程,另一部分是用户进程。因此我们统计这两部分进程的内存消耗再相加即可。

  • 内核部分:Slab+ VmallocUsed + PageTables + KernelStack + HardwareCorrupted + Bounce + X
X是指linux内核没有统计进来的,动态内存分配中通过alloc_pages分配的内存。

http://linuxperf.com/?cat=7就指出了一个这样的例子:

在VMware guest上有一个常见问题,就是VMWare ESX宿主机会通过guest上的Balloon driver(vmware_balloon module)占用guest的内存,有时占用得太多会导致guest无内存可用,这时去检查guest的/proc/meminfo只看见MemFree很少、但看不出内存的去向,原因就是Balloon driver通过alloc_pages分配内存,没有在/proc/meminfo中留下统计值,所以很难追踪。

  • 用户进程部分:Active + Inactive + Unevictable + (HugePages_Total * Hugepagesize)Cached + AnonPages + Buffers + (HugePages_Total * Hugepagesize)

根据上述公式,除掉X部分,得出linux内核统计出来的已分配内存为:2.62G。

该值远小于使用freetop得到的used。
这里推测原因就是linux没有统计进来的alloc_pages分配的内存。
考虑到该linux确实是在VMWare平台上申请的虚拟机,因此我们推测是由于虚拟机平台内存不足,于是宿主机模拟该客户机内部进程消耗内存,实际上将内存调度到其他虚客户机去了。

五、结论

虚拟机管理员尚未最终确认,目前仅仅是推测

在VMWare虚拟机平台上,宿主机可以通过一个叫Balloon driver(vmware_balloon module)的驱动程序模拟客户机linux内部的进程占用内存,被占用的内存实际上是被宿主机调度到其他客户机去了。
但这种驱动程序模拟的客户机进程在客户机linux上是通过alloc_pages实现的内存动态分配,并没有被linux内核统计进来,于是造成了内存去向不明的现象。


下塘烧饼
97 声望17 粉丝

个人文章迁移到知乎了:[链接]