Linux内存全指标拆解与生产案例分享
本文从最基础的free、meminfo,到内核参数vm.min_free_kbytes、vm.swappiness,再到oom、内存黑洞以及核内外的泄露,再结合真正的生产案例,把你从一个遇到内存问题时无从下手的人,变为一个可拿着“证据”直接找开发自信“贴脸开大”的靓仔。
此文读完可能不能让你直接摇身变为内存大佬,但大概率会受益不浅。它结合了我与大佬们之间的探讨、经验和生产问题的实践,利用业余时间整理,总结的过程让我对内存也有了新的认识,全文大概1.5w字,分享出来让大家一起学习,抛砖引玉,也欢迎各位经验更丰富的老运维来补充和纠正知识点。
需要说明,以下所有实操、案例均基于银河麒麟服务器操作系统v10。如果你也对Linux感兴趣,可以点赞、关注,一起交流。
内存使用情况分析
Linux系统中有多种内存监视工具可以用来查看系统内存使用信息,常见的如top、ps、free、vmstat、sar、slabtop、pmap、smem,这些命令大多通过读取meminfo、slabinfo或是/proc/下的maps、mem、smaps等文件来收集数据,通过这些命令我们可以监视系统整体或者是各个进程的内存使用状况。
其中最常用的free命令是通过读取并整理/proc/meminfo文件中的数据并向我们展示,本节就以此命令入手来了解分析一下Linux的内存使用情况。
free命令
free命令相信每个与Linux打交道的朋友都用过,但对其命令的输出又了解多少呢?
# free -m total used free shared buff/cache availableMem: 127312 37644 60089 4196 29579 84436Swap: 4095 0 4095
- total:可使用物理内存的总量,该值从
/proc/meminfo的MemTotal和SwapTotal的值中获取,但需注意的是,其值不等同主机上内存条的容量,因为系统从加电到引导完成,firmware/BIOS要保留一些内存,再加上为kernel本身预留的内存,最后剩下可供kernel支配的内存才是MemTotal。 - used:已使用的物理内存、交换内存。used=total-free-buffers-cache,排除了buffers/cache,并非我们通常意义上认为的系统已使用内存量,所以判断已使用内存时,不能简单查看used值,used值不高时,如果buffers/cache占用了许多内存,最终可使用的内存量也是很低的。而Swap的used=SwapTotal-SwapFree。
- free:未使用的物理内存、交换内存。该值从
/proc/meminfo的MemFree和SwapFree获取,该值不能等同于系统当前可用内存,因为buffers/cache中的内存缓冲区(块设备所占内存页)、文件缓存页(普通文件所占内存页)、slab分配的SReclaimable都是可以进行回收再利用的。当我们一次性分配超出当前free值时,若缓存有足够可释放空间,将进行释放以供用户进程使用。 - buffers:内存缓冲区所占内存,等同
/proc/meminfo的Buffers。该值表示块设备所占用的缓存页,当直接进行读写块设备时(如dd命令)作用于磁盘数据的缓存,或是文件系统的元数据(例如超级块、inode所使用的缓存页)。 - cache:文件缓存页所占内存,等同
/proc/meminfo中Cached和SReclaimable的合计值,其表示普通文件数据所占用的缓存页,当我们直接读写一个文件时便会产生相应的缓存页,用户进程所创造的文件缓存页及slab分配器分配的可回收内存都被统计于此。cache和buffers的区别在于前者是针对文件系统(xfs/ext4等)上的文件进行读写产生的缓存,一个基于块设备(/dev/xxx)操作产生的缓存。 - shard:共享内存,tmpfs文件系统所占内存。等同
/proc/meminfo中的Shmem,该项统计了System V shard、POSIX shard以及mmap共享匿名内存映射。(其中System V shared和mmap共享匿名内存映射,这部分由内核管理,用户不可见;而"Filesystem"为"tmpfs"或者"devtmpfs"的就是POSIX shared,其中"/run","/sys/fs/cgroups"是系统目录,而"/dev/shm"是可由用户使用的,我们可以直接在/dev/shm目录下进行查看用户进程申请的获内存文件。) - available:一个估算值,估算当前真正可供用户进程使用的内存值,等同
/proc/meminfo中的MemAvailable。有些应用程序会根据系统的可用内存大小自动调整内存申请多少,这时free并不使用,系统中有些内存虽然被使用但是可以回收的,比如cache、buffer、slab都有一部分可回收,所以这部分可回收的再加上free才是系统可用的内存,即MemAvailable。
内存的不同口径
Linux系统为了解决进程空间隔离、内存效率低下、程序定位调试和编译运行等问题,采用了虚拟内存管理(Virtual Memory Management)这一机制。而为了详细描述一个进程的具体物理内存占用,又分出了很多概念,如RSS、PSS、USS等。
不同的内存检测工具有不同的统计口径,它们有些会明显标注,有些则会省略,因此在使用工具分析问题前,需要先将这些概念搞明白。
VSS
Linux的虚拟内存管理机制让每个进程感觉自己拥有一个巨大的连续的内存可以使用,进程在其空间中进行理论内存分配就是其虚拟内存占用,这些分配的内存只有到实际使用到时才会落实到物理内存上,在各类内存监视器中虚拟内存通常以VSS(VIRT、VSZ)来表示,而其他的内存统计口径如RSS、PSS、USS都是表示实际的物理内存占用。
例如top命令中的VIRT字段,其就代表对应进程“所需要”的虚拟内存,并不是实际的使用量。
RSS
RSS表示实际使用物理内存(包含共享库占用的内存),即单个进程实际占用内存的大小,RSS不太准确的地方在于它包括该进程所使用共享库的全部内存,但一个共享库可能被多个进程使用,实际共享库只会被装入内存一次。
PSS
PSS在RSS的基础上,将共享库的内存按使用的进程个数平均分为多份。假如有N个进程使用libxml2.so这个共享库,这个库加载了200K的代码在内存中,那么每个进程的PSS值中就包含(200/N)的共享库内存,理论上把系统所有进程的PSS加起来大致等同于所有进程占用内存的总大小。
USS
USS是进程独自占用的物理内存(不包含共享库占用内存),即单个进程私有的内存大小、该进程独占的内存部分。USS揭示了运行一个特定进程的真实内存增量大小。如果进程终止,USS就是实际被返还给系统的内存大小。
系统中如何查看进程的RSS/PSS/USS内存大小?
可以使用smem命令查看所有进程的对应内存大小:
| | |
|---|
| 查看所有进程的 RSS/PSS/USS(默认排序) | smem -k | -k |
| smem -k -s pss | -s pss |
| smem -k -s uss -r | -r |
| smem -k -P nginx | -P |
| smem -k -p 1234 | -p |
| smem -k -t -H | -t |
meminfo内容介绍
free、vmstat等命令大多是读取的meminfo文件,其输出信息往往有限,如果要获取更详细的系统内存整体使用情况,还是要详看/proc/meminfo文件。
对一些名词的解释:
- 文件页:内核缓存的磁盘数据(Buffer)和内核缓存的文件数据(Cache)都叫文件页
- 脏页:表示被应用程序修改但尚未同步到磁盘的内存数据页。当进程修改文件数据时,Linux内核不会立即写入磁盘,而是先将数据缓存在页高速缓存(Page Cache) 中,并将该内存页标记为
PG_dirty标志位,表示其内容与磁盘不一致 - 匿名页:这部分内存没有实际的载体,如堆、栈数据、共享内存等,不像文件缓存有硬盘文件这样一个载体
- pageout(页换出):通用概念,指物理内存中的「任意内存页」(含文件页、匿名页)被移出物理内存,文件页(如程序代码页、日志缓存页)换出是回写到对应磁盘文件(而非 Swap),匿名页(如进程堆 / 栈数据)换出是写到 Swap
- swapout(交换换出):侧重匿名页换出到 Swap,可理解为物理内存页移到Swap后,释放屋里内存空间
LRU部分
LRU是Kernel的页面回收算法使用的数据结构,Page cache和所有用户进程的内存(kernel stack和huge pages除外)都在LRU lists上:
- Active(anon):表示最近被访问过的匿名页
- Inactive(anon):表示长时间未被访问过的匿名页
- Active(file):表示最近被访问过的文件缓存页
- Inactive(file):表示长时间未被访问过的文件缓存页
- Unevictable:表示不能pageout/swapout的内存页,包括VM_LOCKED的内存页、SHM_LOCK的共享内存页和ramfs
Mlocked
该项统计的是被mlock()系统调用锁定的内存大小。被锁定的内存因为不能pageout/swapout,会从Active/Inactive LRU list移到Unevictable LRU list上。也就是说,当”Mlocked”增加时,”Unevictable”也同步增加,而”Active”或”Inactive"同时减小;当”Mlocked”减小的时候,”Unevictable”也同步减小,而”Active”或”Inactive”同时增加。
Dirty pages部分
Dirty pages包含NFS_Unstable和Writeback这两部分,系统中全部的dirty pages=(Dirty+NFS_Unstable+Writeback)
- NFS_Unstable:表示发给NFS server但尚未写入硬盘的缓存页
- Dirty:除了上述情况外,已经被修改但未进行回写的缓存页,也就是脏页
AnonPages
匿名页,其值并不等于Active(anon)+Inactive(anon),原因在于Shmem部分被计入LRU Active/Inactive(anon),但未记入AnonPages。
Mapped
Page cache中(“Cached”)包含了文件的缓存页,其中有些文件当前已不在使用,pagecache仍然可能保留着它们的缓存页面,而另一些文件正被用户进程关联,比如shared libraries、可执行程序的文件、mmap的文件等,这些文件的缓存页就称为mapped。
SLAB部分
Slab内存包含:dentry、inode、task、socket、kmalloc 分配的内核结构体、驱动内核对象等;
不包含:用户内存、文件 PageCache、HugePage、vmalloc 大块内存。
内核通过slab分配的内存被统计在以下三个值中:
- SReclaimable:slab可回收的部分。通常为文件元数据缓存。调用kmem_getpages()时加上SLAB_RECLAIM_ACCOUNT标记,表明是可回收的,计入SReclaimable,否则计入SUnreclaim
- SUnreclaim:slab中不可回收的部分,通常为进程、网络、驱动基础结构。
在较新的内核版本中我们还能看到一个KReclaimable统计值。KReclaimable为Sreclaimable+NRKERNEL_MISC_RECLAIMABLE,意为部分内核态可被回收的内存,NRKERNELMISCRECLAIMABLE可查看zoneinfo获取。
KernelStack
每个用户线程都会分配一个kernel stack(内核栈),内核栈虽然属于线程,但用户态的代码不能访问,只有通过系统调用(syscall)、自陷(trap)或异常(exception)进入内核态的时候才会用到,也就是说内核栈是给kernel code使用的。x86系统上Linux的内核栈大小是固定的8K或16K。
Kernel stack(内核栈)是常驻内存的,既不包括在LRU lists里,也不包括在进程的RSS/PSS内存里,所以我们认为它是kernel消耗的内存。
PageTables
Page Table用于将内存的虚拟地址翻译成物理地址,随着内存地址分配的越来越多,Page Table会增大。
Bounce
有些老设备只能访问低端内存,比如16M以下的内存,当应用程序发出一个IO请求,DMA的目的地址却是高端内存时(比如在16M以上),内核将在低端内存中分配一个临时buffer作为跳转,把位于高端的缓存数据复制到此处。这种额外的数据拷贝被称为“bounce buffering”,会降低IO性能。大量分配的bounce buffers也会占用额外的内存。
WritebackTmp
表示FUSE(用户空间文件系统)用于写回磁盘的缓冲区大小。
CommitLimit
允许进程「承诺申请」的最大内存总和(即系统级内存分配上限),是判断是否过度过量分配的核心阈值。
CommitLimit就是overcommit(内存过量分配)的阈值,申请的内存总数超过CommitLimit的话就算是overcommit(内存过量分配)。
其具体的值是通过内核参数vm.overcommit_ratio和vm.overcommit_kbytes间接设置的,公式如下:
CommitLimit=( Physical RAM * vm.overcommit_ratio/100 ) + Swap
vm.overcommit_ratio是内核参数,缺省值是50,表示物理内存的50%。
Committed_AS
表示所有进程已经申请的内存总大小,注意是申请的,不是已经分配的,如果Committed_AS超过了CommitLimit就表示发生了overcommit,超出越多表示overcommit越严重
Vmalloc部分
VmallocTotal:内核初始化时分配给vmalloc的虚拟内存区域,64位系统默认为32TB。
VmallocUsed:统计vmalloc区域使用了的虚拟内存大小
VmallocChunk:vmalloc区域最大的free连续区块大小
目前大多服务器系统上查看VmallocUsed和VmallocChunk一般为0,这是因为在v4.4.rc1版本中社区认为计算vmalloc统计信息的代价十分昂贵,直接删除了相关的计算并将其默认值设为0,这一修改直到v5.3.rc1版本才被删除,现在VmallocUsed统计的不再是虚拟内存占用情况,而是实际消耗的物理内存大小。
服务器系统现在用统计VmallocUsed可使用cat /proc/vmallocinfo grep-v "ioremap" | awk ’{total=total+$2};END{print total/1024}
HardwareCorrupted
系统检测到内存的硬件故障时,会把有问题的页面删除掉,不再使用,该项统计了删除掉的内存页的总大小。
AnonHugePages部分
AnonHugePages:用户态透明大页中的匿名页。进程使用透明大页时,在RSS/PSS中会统计,这点和标准大页不同
ShmemHugePages:用于共享内存或tmpfs的透明大页,Linux 4.8加入内核
ShmemPmdMapped:用户态共享内存映射的透明大页,Linux 4.8加入内核
FileHugePages:与AnonHugePages对应,用户态透明大页中的文件页
FilePmdMapped:用户态文件页映射的透明大页
需要注意的是Transparent HugePages(THP)与HugePages是两种东西,它们一个是打开内核选项后随应用申请自适应分配,一个则是需要提前进行初始化并设置可分配使用的大页数量。
HugePages部分
HugePages_Total:内存池中标准大页总数
HugePages_Free:内存池中标准大页空闲数,包括承诺分配但未最终分配的页框
HugePages_Rsvd:内存池承诺分配的大页数,所以内存池中真正空闲的页框数应该是HugePages_Free-HugePages_Rsvd
HugePages_Surp:内存池超额分配的大页数,通过nr_overcommit_hugepages设置最大允许超额分配数量
Hugepagesize:默认大页的大小
Hugetlb:大页内存池页框总大小
kylinos v10 arm版pagesize为64K时,目前默认支持512MB、2048KB和16GB三个大小的大页,可提前在cmdline中配置大页的大小和数量,但在使用大页内存时,最好将THP关闭,否则可能导致系统重启等问题:
DirectMap部分
DirectMap4k:表示虚拟内存直接映射4k物理页的大小
DirectMap2M:表示虚拟内存直接映射2M物理页的大小
DirectMap1G:表示虚拟内存直接映射1G物理页的大小
小结
至此基本所有meminfo输出都介绍完了,需要提醒的是,上文中的所有输出都是基于5.15内核,不同内核版本的输出选项和计算方式,可能会存在差异。
同时可以整理下所有的meminfo选项,分别归类出内核、用户进程所使用内存统计:
kernel内存的统计方式比较明确,即Slab + VmallocUsed + PageTables + KernelStack + HardwareCorrupted + Bounce + ghost;用户进程的内存有几种口径:
- 基于LRU进行统计:
(Active + Inactive + Unevictable) + (HugePages_Total * Hugepagesize) - 基于Page Cache进行统计
(Cached + AnonPages + BUffers) + (HUgePages_Total * Hugepagesize)
内存相关内核参数说明
有关Linux内存基础的free命令、meminfo文件等相关内容上文已经介绍,因此基础的Linux内存相关名词、参数在本文中不再过多赘述。
本节主要想说明Linux内存相关的两个重要的内核参数vm.min_free_kbytes和vm.swappiness,以及其相关影响,由此来介绍Linux基础的内存回收机制及swap交换空间的使用说明。
vm.min_free_kbytes介绍及影响
vm.min_free_kbytes用于控制系统可用内存的最小空闲大小。它指定了内核应该保留的空闲内存页面的数量(以KB为单位),以便能够及时满足系统的需要。
当系统的空闲内存低于vm.min_free_kbytes衍生的内存回收水位阈值时,内核会开始执行页回收策略,例如清除页面缓存或进行内存换页操作,以保持足够的空闲内存可供重要的内核任务使用。
适当设置vm.min_free_kbytes的值可以帮助提高系统的性能和稳定性,特别是在高内存使用场景下。通常推荐设置vm.min_free_kbytes的值为总内存的0.5%-1%左右。
银河麒麟服务器操作系统V10 SP1/2/3 系列的服务器系统都未默认设置vm.min_free_kbytes的数值,而是交由Linux内核进行初始化,后续我们就初始化开始说起。
vm.min_free_kbytes初始化
对于银河麒麟服务器4.19.90内核的系统,vm.min_free_kbytes的proc节点为/proc/sys/vm/min_free_kbytes,内核启动过程中对它有一次初始化,启动后若设置开启了THP(若未在grub中设置关闭THP默认开启),还将进行二次初始化。
cat /proc/cmdline:查看内核启动引导参数
初次初始化
vm.min_free_kbytes首次初始化函数抽象后的计算公式为:
min_free_kbytes=sqrt(lowem_kbytes*16)
lowem_kbytes:除去高端内存外可以申请到的空闲内存大小(通常即为系统总的可用内存大小)
sqrt():开方函数
初次初始化min_free_kbytes的大小范围限制是[128,65536],但当我们查看该值的实际大小时却常常发现其大于了65536,这就涉及到min_free_kbytes二次初始化过程。
二次初始化(THP)
在现代Linux系统中,THP(即透明大页)是一种常见的内存管理机制。THP通过自动透明方式工作,应用程序无需任何修改即可受益于大页面的使用。该机制下应用程序访问内存的方式不会改变,大页面的分配和管理由内核自动完成。可以通过查看/sys/kernel/mm/transparent_hugepage/enabled来检查THP状态:
# cat /sys/kernel/mm/transparent_hugepage/enabled always [madvise] never
always:表示全局启用透明大页功能
madvise:表示仅对显式标记 MADV_HUGEPAGE的内存区域启用透明大页,应用程序需通过 madvise()系统调用主动申请大页优化
never:完全关闭透明大页功能,系统仅使用标准 4KB 内存页
[]:方括号 []标注实际生效项
vm.min_free_kbytes二次初始化函数抽象后的计算公式为:
min_free_kbytes=min(pageblock_nr_pages*nr_zones*(2+MIGRATE_PCPTYPES*MIGRATE_PCPTYPES),nr_free_buffer_pages()/20)<<=(PAGE_SHIFT-10)
nr_zones:node的所有内存分区数量,例如DMA/NORMAL等分区,查看方式:cat /proc/pagetypeinfo
pageblock_order:(MX_ORDER-1),order表示最大的连续内存页,查看方式:cat /proc/pagetypeinfo
pageblock_nr_pages:pageblock大小 = (1UL << pageblock_order)
MIGRATE_PCPTYPES :用来表示每个CPU页帧高速缓存的数据结构中的链表的迁移类型数目,是内核中的一个枚举常量,值为3
nr_free_buffer_pages():表示返回除去高端内存外可以申请到空闲的page数量,nr_free_buffer_pages()/20含义代表总空闲内存的5%
PG_SHIFT:根据内存页大小取值不同,内存页为4K时PAGE_SHIFT=12,为64K时PAGE_SHIFT=16,查看内存页大小方式:getconf PAGESIZE
min():min函数限制min_free_kbytes的极限值不能超过总内存的5%
举个栗子
我手里有一台kunpeng-920的ARM机器,128c256g的配置。
查看PAGESIZE和MemTotal:
# cat /proc/meminfo MemTotal: 266400960 kB# getconf PAGESIZE65536
vm.min_free_kbytes初次初始化:
min_free_kbytes=sqrt(lowem_kbytes*16)=sqrt(266400960*16)=65287
因为65287在[128,65536]区间内,所以初次初始化后的vm.min_free_kbytes为65287。
若开启了THP,查看内存page:
# cat /proc/pagetypeinfoPage block order: 13Pages per block: 8192Number of blocks type Unmovable Movable Reclaimable HighAtomic CMA Isolate Node 0, zone DMA32 0 2 0 0 0 0 Node 0, zone Normal 4 119 1 0 0 0 Number of blocks type Unmovable Movable Reclaimable HighAtomic CMA Isolate Node 1, zone Normal 2 125 1 0 0 0 Number of blocks type Unmovable Movable Reclaimable HighAtomic CMA Isolate Node 2, zone Normal 2 125 1 0 0 0 Number of blocks type Unmovable Movable Reclaimable HighAtomic CMA Isolate Node 3, zone Normal 2 125 1 0 0 0
由上可知,MX_ORDER=13;一共有4个Node,5个Zone。
二次初始化计算过程:
min_free_kbytes=min(pageblock_nr_pages*nr_zones*(2+MIGRATE_PCPTYPES*MIGRATE_PCPTYPES),nr_free_buffer_pages()/20)<<=(PAGE_SHIFT-10)=min(2^12*5*(2+3*3),266400960/20)<<=(16-10)=min(4096*5*11*2^6, 13320048)=min(225280*64,13320048)=min(14417920,13320048)=13320048
总内存的5%为13320048,因为14417920>13320048,所以最终二次初始化完的vm.min_free_kbytes为13320048。
THP和HugePages的区别
这个时候可能就会有疑问,那THP和HugePages有什么区别和联系吗?主要对比以下几点:
| | |
|---|
| 分配时机 | | |
| 内存归属 | 独立内存池,从 MemTotal 扣除,普通进程不能使用 | |
| 使用方式 | 应用必须显式适配:1. 挂载 hugetlbfs2.mmap 指定 MAP_HUGETLB程序需要改造 | 完全透明,应用零修改,普通 malloc/mmap 自动生效 |
| 大页尺寸 | 根据硬件和内核而定,可同时配置多种尺寸,如512MB和16GB | 一般固定2MB,不支持 1GB/32MB 等其他大页 |
| 内存占用特性 | 一旦预留,无论是否被程序使用,内存永久占用、不可回收给系统 | |
| 适用业务场景 | 数据库、中间件、高性能计算、虚拟化,长期稳定占用大块内存 | 通用业务、Java 进程、普通业务服务,内存波动大、无法提前预估内存 |
| 内核开关位置 | /sys/kernel/mm/hugepages/ 每个尺寸独立池 | /sys/kernel/mm/transparent_hugepage/ 全局总开关 |
| 优缺点 | 优点:稳定、无运行时分配抖动、无回收延迟缺点:内存刚性占用,预估不准会浪费大量内存 | 优点:灵活、不用提前预留、适配所有程序缺点:内存碎片高时分配失败,回收 / 合并会产生 CPU 抖动 |
vm.min_free_kbytes与Linux内存回收机制的渊源
Linux的内存回收方式大体可分为三种:
vm.min_free_kbytes衍生了不同的内存回收水位,默认的内存回收水位计算方式如下:
- min水位:watermark[min] = vm.min_free_kbytes
- low水位:watermark[low] = vm.min_free_kbytes[min] * 5 / 4
- high水位:watermark[high] = vm.min_free_kbytes[min] * 3 / 2
当系统剩余内存低于不同内存回收阈值时,内核会开始执行不同的内存页回收策略:
- 当系统剩余空闲内存大于high水位时,表示此时系统内存足够,不会出发内存回收
- 当系统剩余空闲内存小于low水位时,表示此时存在内存压力,会触发kswapd内存回收,主要是对文件页和匿名页的回收,直到pages_high为止
- 当系统剩余空闲内存小于min水位时,表示此时用户内存耗尽,会触发直接内存direct reclaim回收,主要也是对文件和匿名页进行回收,期间进程会被阻塞
- 若是直接内存回收等方式还是无法释放出应用所需申请的连续内存时,就将触发oom,通过强制杀死其他占用大量内存的进行来释放内存。当然也不是说谁占用内存高就杀死谁,其是有一套打分机制的,具体后文详细描述
文件页和匿名页的回收都是基于LRU(最近最少使用)算法,Linux内核中维护着active和inactive两条链表,通过PG_active位状态判断是否活跃,通过PG_referenced位判断是否访问过,通过这两个页面标识符决定如何在两个链表之间移动页面。当内存不足执行回收时,内核会优先回收inactive链表尾部页面。
文件页回收:
如果这个文件页是干净的(clean),则直接释放内存,不影响系统性能。但如果是脏页(dirty),则需要先写入磁盘,再释放内存,这个过程就会发生IO操作,因此会影响系统性能。
匿名页回收:
因为这部分内存可能还会使用到,因此不能直接释放。如果开启了swap机制,则会先把内存写入swap中,等到需要的时候再从中读取。这个过程也会发生IO操作,因此会影响系统性能。
对于内存回收,应该根据需求设置合适的vm.min_free_kbytes参数,从而调整内存回收时机和频率,减少oom机制的触发,提高系统的性能和稳定性。
vm.swappiness介绍及影响
vm.swappiness用于控制换出运行时内存的相对权重,参数值大小对如何使用swap分区有很大联系。值越大,表示越积极使用swap分区,越小表示越积极使用物理内存。
但是在实际系统内存回收时,也不是完全按照swappiness进行回收权重比例划分,存在很多特殊情况,这也是为什么在vm.swappiness设置极小时,也会出现swap空间被使用的情况。
swappiness参数的具体作用及影响
纠正误区
首先要纠正一个误区,将vm.swappiness设置为1时,不是说当内存使用还剩1%时才会使用swap,这是一种错误的理解,实际上该参数是一个系统触发内存回收时设置anon和file比例的参数。
不管是kswapd异步回收还是direct reclaim阻塞式回收,决定回收页面数量的一个核心函数都是get_scan_count,有相关代码能力的朋友可以自行查看其逻辑。
当vm.swappiness=100时,匿名页和文件页将有同样的回收优先级,swappiness越小,回收时扫描匿名页的优先级就越低,而匿名页是没有实际的磁盘载体的,当触发其回收时,只能将内存写入至swap中,所以得出了swappiness越小,swap使用可能性就越低,反之swappiness越大,swap使用可能性就越高的结论。
若将swappiness设置为0,可以完全避免使用swap吗?
通过继续查看get_scan_count函数会发现,其针对系统各种内存情况定义了不同的scan_balance(回收扫描策略)。
当系统没有设置swap或者swap内存不足时,此时scan_balance=SCAN_FILE,表示仅回收文件缓存页
当在非全局回收情况下,并且swappiness等于0,则scan_balance=SCAN_FILE,也只扫描文件页,这个的非全局回收指的是cgroup管理的对应内存控制组,为了便于更好的管理各种大型应用(如数据库),通常都会将相应进程添加到一个对应的cgroup组中,例如容器环境下的内存limit,此时只会触发该组内的内存回收
当系统濒临OOM情况下,除非没有设置swappiness,否则将执行scan_balance=SCAN_EQUAL,系统将忽略swappiness的值,采取平均回收文件页和匿名页的策略
当系统的文件页几乎已经回收完了,则会scan_balance=SCAN_ANON强制回收匿名页
如果非活跃的文件缓存页足够,则会scan_balance = SCAN_FILE仅回收文件页 ,不会回收任何的匿名页
如果前边几个条件都没命中,则会scan_balance = SCAN_FRACT,进行全局回收,按照swappiness和回收效率动态计算文件页 / 匿名页的扫描页数,并针对“在线 / 离线 memcg” 做差异化的取整处理
通过第3点和第4点可以判断出,swappiness=0时,并不是完全避免使用swap,这也是通常我们把swappiness设置的很小时,但还是使用了swap的原因。
生产案例
问题现象
某用户现场部署了多台pg数据库实例,发现每台都占用了大量swap内存,使用top、ps等命令查看是pcsd进程占用了近4G的swap,重启pcsd服务后会释放,修改vm.swappiness内核参数后仍然会出现问题。
问题处理
- 通过上文我们已知,在swappiness不为0时,在进行内存回收时,会存在部分内存被交换至swap;且存在集中特殊情况,会直接忽略该参数,直接进行特定页面扫描策略进行回收至swap
- 首先要关注vm.min_free_kbytes大小及系统内存回收的触发情况
- 通过sosreport文件查看其系统vm.min_free_kbytes约为1.7G,也就是该watermark[low]约为2.1G
- 查看系统sar日志,或通过其他监控手段,查看内存使用情况,以及swap内存的使用增量
- 现场环境发现内存free值很接近watermark[low]值,这表明系统很有可能已经触发过kswapd或者direct内存回收,可以通过
sar -B 查看分页回收统计,主要观察pgscank/s( kswapd每秒扫描的内存页数量)、pgscand/s(direct reclaim直接回收数量)、pgsteal/s(每秒真正回收成功、释放出的内存页)字段,若值不为0,则表示已进行回收内存页,就有可能导致swap的增加 - 综上所述,此环境swap分区的原因就是剩余内存频繁降至watermark[low]水位之下,触发了kswapd内存回收,导致不活跃的内存被交易至swap分区
建议
- 合理配置vm.min_free_kbytes值,减少触发内存回收的可能,可以调小至0.5%-1%
- 调小vm.swappiness值,降低回收匿名页的概率,这里要注意,当使用多层级cgroup组时,要检查各个cgroup子目录下的vm.swappiness参数是否调小,若未同步变小,可单独设置其值:
find /sys/fs/cgroup/ -name '*swappiness'将查找到的所有文件的值设置为0
内存OOM问题分析与说明
OOM是out of memory的简写,虽然Linux Kernel有很多内存管理的技巧(从cache中回收、swap out等)来满足各种应用空间的vm内存需求,但当系统配置不合理、内存耗尽、无法分配的状况时,为了保证系统的稳定运行,内核会根据一定的算法规则,找出最应该优先被杀死的进程(理论上是占用内存最多的进程)进行杀死,从而释放内存空间,让系统得以稳定不会宕机,这个机制就是OOM killer。
OOM相关参数了解
panic_on_oom
此参数控制在kernel遇到oom时,是否触发panic。
- 值为2:内存不足时,强制触发kernel panic,内核崩溃
- 其他值:表示要区分具体情况,对于某些情况可以panic,有些情况只启动oom killer
- 约束为CONSTRAINT_NONE:在没有任何约束下发生了oom,表明 确实是内存不够用了,直接panic
- 约束为CONSTRAINT_CPUSET:表明当前内存为NUMA(非一致内存),此时会将一组cpuset和memory绑定,当出现CONSTRAINT_CPUSET的oom时,可能是当前memory node的内存不足了,整个系统的内存还是充足的
- 约束为CONSTRAINT_MEMORY_POLICY:memory policy是NUMA系统用于控制分配各个memory node资源的策略模块。在这种条件下出现了oom,可能是memory policy约束导致的,而不是系统内存不足
- CONSTRAINT_MEMCG:这是Cgroup中的memory control cgroup内存组控制策略。在该约束下的oom也不一定是 系统内存不足
overcommit_memory
此参数可以控制进程对虚拟内存过量使用的应对策略。
memory overcommit的意思是操作系统承诺给进程的内存大小超过了实际可用的内存。一个保守的操作系统不会允许memory overcommit,有多少就分配多少,再申请就没有了,这其实有些浪费内存,因为进程使用到的内存往往要比申请的少。
- 0:启发式overcommit,表示内核将检查是否有足够的可用内存供应用进程使用,如果足够则内存申请允许,否则,内存申请失败,并将错误返回至应用进程
- 1:永远允许overcommit,而不管当前内存状态如何,主要用于科学计算
- 2:永远禁止overcommit,系统不允许提交超过swap+overcommit_ratio % x totalram_pages值的内存
oom_kill_allocating_task
此参数用来触发oom时先杀掉哪种进程。
当然,一些系统进程如init或者用户设置了oom_score_adj的进程不是说杀就杀掉的
oom_dump_tasks
此参数用来记录触发oom时是否启动dump_tasks。dump_tasks可以记录进程标识信息、该进程使用的虚拟内存总量、物理内存、进程的页表信息等。
- 0:关闭dump_tasks。在大型系统中,可能存在上千进程,逐一打印使用内存信息可能会造成性能问题
- 非0:有三种情况会打印系统中所有task的内存状况
oom_adj、oom_score_adj和oom_score
此三个参数是用来控制进程打分的(分数越高,就越优先被杀死)。其关联性比较密切,都和具体的进程有关,位置都在/proc/进程pid/目录下:
# ls /proc/1049631/oom_oom_adj oom_score oom_score_adj # cat /proc/1049631/oom_adj 0# cat /proc/1049631/oom_score22# cat /proc/1049631/oom_score_adj 0
详细的oom_score计算过程可在源码oom_kill.c中查看。
内核会对进程打分(oom_score),主要包括两部分,系统打分和用户打分。
系统打分:
根据进程的物理内存消耗量(进程自身的空间、swap空间、页缓存空间)进行评估。
用户打分:
用户打分就是根据/proc/进程pid/oom_score_adj的值,其取值范围是-1000~1000。如果adj的值为-1000则表示禁止oom killer杀死该进程,其取0时表示用户不调整oom_score,负值表示要在实际打分值上减去一个折扣,正值表示要惩罚该task,也就是增加该进程的oom_score。
另外oom_adj是一个旧的接口参数,其功能类似oom_score_adj,取值范围-17~15。为了兼容,目前扔保留此参数,当操作此参数时,会换算为oom_score_adj。
OOM killer运行机制
当物理内存和交换空间都被用完时,如果还有进程来申请内存,内核无法通过内存回收、内存规整等手段释放出对应的空闲内存,将触发oom killer,其主要行为如下:
- 检查文件
/proc/sys/vm/panic_on_oom,其值若为2,那么系统一定会触发panic;如果为1,那么系统有可能触发panic;如果为0,或者上一步没有触发panic,那么内核继续检查文件/proc/sys/vm/oom_kill_allocating_task - 如果
/proc/sys/vm/oom_kill_allocating_task为1,那么内核将kill掉当前申请内存的进程,如果为0,内核将检查每个进程的分数,分数最高进程将被kill掉 - 进程被kill掉后,如果
/proc/sys/vm/oom_dump_tasks 为1,且系统中设置了core文件大小,将会由/proc/sys/kernel/core_pattern参数中指定的服务或路径生成coredump文件;如果其值为0,则不产生
生产案例
当系统发生oom时无外乎三种情况:
一是内存确实不足,该现象可能是由于客户部署的任务负载和系统内存不匹配,或是min_free_kbytes设置过低遇到了突发的内存使用激增的情况;
二是系统内存碎片化严重,此时系统剩余内存可能还高于内存回收的high水位,不会触发内存回收,但是由于剩下的内存都为零散的低阶内存,无法满足程序的高阶连续内存的分配需求而导致oom;
三是例如内核对Zone内存统计异常或内存硬件问题带来的频繁触发oom,这属于异常情况。
案例1
问题现象
系统还有可用内存,但应用还是会触发oom。
分析步骤
- 通过系统及内核日志,查看oom-killer后续的mem-info信息,发现在Node 0 Normal区域,free内存还远大于该区域内存回收的high值
- message下继续查看各阶连续内存的分布,发现该区域剩余内存均为order=0,1,2阶的低阶连续内存,而应用申请的是order=3的内存
- 正常逻辑下在连续内存不够时,系统应该转至kswap/direct内存回收,或是zone内存规整,但其却直接跳转到了oom,这是为何
- 未触发kswap/direct内存回收很容易解释,是因为Normal区域内存free还大于内存回收的high值,未触发zone内存规整的原因经分析发现,PAGE_ALLOC_COSTLY_ORDER的设定为3,Linux认为在合理的内存压力下,order小于等于3时内存自然合并满足内存分配需求,而此次申请的内存正好order=3,导致未触发内存规整,最终oom
业务分析
对于为何内存的碎片化会如此严重,结合用户的业务场景,也进行了分析。
- 通过监控内存的使用和回收情况发现,每天的凌晨4-5点都会触发内存回收,pgscank/s、pgscand/s急速上升,说明这段时间系统在频繁的触发内存回收;并且伴有大量的磁盘读操作,用户态回收的file cache被磁盘读所产生升的inode cache等slab内存占用
- 经过和客户沟通,每天凌晨系统业务都会进行安全扫描,随着系统不断的申请和释放内存页面,导致伙伴系统分配的物理内存页帧号越发随机,从而导致内存被隔断的概率更高,碎片化的程度越高
案例2
问题现象
在k8s的环境下部署了nginx容器,当容器内存占用过高时,在欧拉系统下会杀死nginx主进程,导致nginx容器重启,但在kylinos v10下会将nginx的子进程杀死,导致主进程未停止,故容器未重启。
分析步骤
- 对比了两个系统的具体内核版本,欧拉为5.10,麒麟v10为4.19,发现内核版本差异较大
- 查看oom-kill日志发现,两个系统在oom时的打印日志都不同,麒麟v10有杀死其子进程的提示,而欧拉没有
- 由于双方系统内核都做过定制化,故前往GitHub查看两个版本代码中oom_kill.c的区别
- 最终排查发现,在v5.0到v5.1-rc1的版本更新中,其机制发生较大的变化,主要移除了oom_kill优先杀死子进程而不是父进程的启发式算法
- 原因是oom kill机制一直是先通过oom_score_adj评分标准判断出最糟糕的进程触发oom,之前的启发式算法设计原因是假设子进程进行的工作要小于父进程,杀死子进程对整体工作影响较小,但当父进程占据内存很多,而子进程不多时,oom将会不断循环挑选杀死那些不是主要内存消耗的子进程,这添加了很多不必要的工作。
- 后续维护者应该是认为没有必要重新检查、挑选子进程杀死,而是直接杀死父进程
内存黑洞问题分析
Linux系统的内存追踪一直是个难题,很多人试着把能想到的各种内存消耗都加在一起,kernel、text、kernel modules、buffer、cache、slab、page table、process、RSS...等等,却总是和物理内存大小对不上,这是因为Linux kernel没有滴水不漏的统计所有的内存分配,我们知道kernel的动态内存分配通过以下几种接口:
- alloc_pages/__get_free_page:以页为单位分配
- vmalloc:以字节为单位分配虚拟地址连续的内存块
- slab allocator:kmalloc以字节为单位分配物理地址连续的内存块,它是以slab为基础的,使用slab层的general caches -- 大小为2^n,名称是kmalloc-32、kmalloc-64等
通过slab层分配的内存会被精确统计,可以参见/proc/meminfo中的slab/SReclaimable/SUnreclaim;通过vmalloc分配的内存也有统计,参见/proc/meminfo中的VmallocUsed和/proc/vmallocinfo,而通过alloc_pages分配的内存不会自动统计,除非调用alloc_pages的内核模块或驱动程序主动进行统计,否则只能看到free memory减少了,但是却在/proc/meminfo中看不到具体消耗,这部分就是我们常说的内存黑洞。
几种常见的内存黑洞原因
vmware balloon驱动内存占用
vmware guest上有一个常见问题,就是vmware ESX宿主机会通过guest上的Balloon driver占用guest内存,有时占用太多会导致guest无内存可用,这时去检查guest的/proc/meminfo只看到memfree很少,但看不出内存消耗到了哪里,原因就是Balloon driver通过alloc_pages分配内存,没有在/proc/meminfo中留下统计值,所以很难追踪。
此种情况下,一般modprobe -r卸载驱动后,系统的内存就能得以释放。
socket收发包队列积压
除去Balloon驱动占用内存外,也可以观察socket收发包堆积情况,可使用netstat -anop来查看目前所有的连接,主要关注Recv-Q和Send-Q队列,如果积压了太多的数据包,也是要占用统计之外的内存的。
pf_packet socket Recv-Q积压数据包
还有一些进程会启用packet socket收发二层协议,通过netstat无法观察到,只能使用ss协议。同样使用ss -ap查看当前socket 收发包队列情况,主要关注Recv-Q和Send-Q队列。
对于内存黑洞除了自己手动排查,GitHub上也有自动排查脚本,可以自己尝试一下,注意使用时需设置相关参数:https://github.com/xqjcool1/mem-leak-diagnose
其他
另外还有几点,不属于内存黑洞,但是常常被忽略的部分,需要注意:
HugePage配置
对于大页,我们可以通过meminfo中的HugePages_Total、HugePages_Free、Hugepagesize等参数直接看出当前划分大页的总量、空闲量、以及单个大页的pagesize。如果想要查看哪些进程使用了大页,可查看进程的/proc/PID/smaps中的anon_hugepage项,可以进行全局搜索:find /proc/*/smaps |xargs grep -ril "anon_hugepage"。
/tmp内存占用
/tmp下的文件也是直接占用内存的,通常可以直接进行统计。
VmallocUsed内存占用
4.19.90及以下版本的内核需要注意,/proc/meminfo中可能没有统计VmallocUsed,需要手动统计再代入计算。
内存泄漏问题分析
Linux下内存泄露是指应用程序或进程在运行时,分配的内存空间没有被正确释放和回收,从而导致系统中可用内存逐渐减少,最终导致系统性能下降、运行变慢,甚至系统崩溃,下面我们从核内、核外两方面介绍内存泄露问题的分析方法
核外(用户态)内存泄露分析
核外内存泄露大多现象比较明显,可以大概按照以下几个步骤来排查:
确认占用内存过大,或异常的进程,可以通过ps、top或其他监控手段
查看此进程的内存明细,可以通过/proc/PID/smaps来排查,其会列出进程所有虚拟内存区域(VMA),包括代码段、数据段、堆、栈、mmap匿名区、共享库映射等,并标明了每个区域虚拟内存占用大小、RSS、私有/共享属性等细粒度统计。
操作逻辑:先保存内存增长前、后的 smaps 文件,用diff对比找出增长的内存区域:
cat /proc/21216/smaps > beforeMem.txtcat /proc/21216/smaps > afterMem.txtdiff beforeMem.txt afterMem.txt > diff.txt
找到内存增长部分后,使用gdb将内存dump出来:
# 0x7f69cd8ba000 0x7f69cd8dd000为该段内存在进程虚拟地址空间中的范围(十六进制),# 也就是smaps的每个区域的首行,首列gdb -q -p PID --batch --ex "dump memory ./outputfile.dump 0x7f69cd8ba000 0x7f69cd8dd000"
随后使用 strings或hexdump查看并分析内容
strings outputfile.dump
若是这些数据中无有效信息,则要考虑使用valgrind等内存泄露工具做进一步分析。下面分享一个实际的案例来加强印象。
生产案例
收到用户反馈,操作系统启动运行一个月后,java进程占用内存约5.5G,但该java进程启动时配置的参数JVM堆最大为512M,需要分析是什么原因导致的内存异常。
- 对于此问题,我们首先通过pmap -xp <异常进程pid>来查看其内存的分布情况,发现其堆内内存占用并不多,主要是堆外存在大量的64/128M的内存块
- 对此我们对比查看smaps信息,找到这些内存块地址,并选取三个内存块进行gdb将其导出
- 使用strings分析导出文件,发现其内容都是重复的OWSTTaskTerminator字符串
- 反馈研发,协助分析,最终发现为java jdk的GC逻辑bug导致的内存泄露,此问题也在jdk官网发布,并进行了修复
核内(内核态)内存泄露分析
对于核内内存泄露的现场,目前较为多见的都为slab内存高占用情况。对此可以查看当前内存的使用情况,从中确定高占用的slab类型,其中slab在meminfo中又分为SReclaimable和SUnreclaim,前者为可回收内存,一般无需在意,在系统内存不足时可被回收释放,后边为不可回收缓存,过大时可能是内核及应用的缺陷导致的,这是需要重点关注的问题。
对于SUnreclaim占比过大时,可以使用slabtop -s c查看slab中占比较高的类型,最后再使用指令cat /sys/kernel/slab/kmalloc-4k/reclaim_account查看该类slab是否是不可回收类型。
若确定某类slab高占比且为不可回收类型,接下来一般会使用perf record工具来查看具体是哪个进程在申请slab,以及申请的堆栈信息。相关的排查思路可参考以下几步:
查看meminfo中的Slab部分grep -A2 Slab /proc/meminfo,主要关注SUnreclaim
slabtop -s c或cat /proc/slabinfo查看具体哪类slab存在高占用情况,确认下一步排查方向
确认具体类型后,可以通过网络以此类型为关键字来搜索,看是否已有同类的故障上报,或者上游kernel的changelog是否为已知问题
在知道对应的slab泄露类型且非已知内核故障后,对于常规的slab泄露排查,首先要查看内核源码中对应slab类型的申请、释放的函数调用,确认其申请、释放是否为成对出现,有无bug可能。
其次可以根据查到的申请、释放函数名,使用perf list|grep '函数名'来观察该函数是否在perf中存在对应的追踪点可以进行数据收集。通过工具分析是哪些进程申请了对应的slab但没有释放
使用perf收集slab的申请一般分为三种情况,第一种是要排查的slab类型对应的申请函数很明确,比如kmalloc-xxx类型的slab,其申请函数固定为kmem:kmalloc kmem:kmalloc_node kmem:kmem_cache_alloc kmem:kmem_cache_alloc_node,我们使用通用的perf采集命令即可进行其类型的slab内存申请记录,比如以下命令:
perf record -a -g -e kmem:kmalloc --filter 'bytes_alloc == 128' -e kmem:kmalloc_node --filter 'bytes_alloc == 128' -e kmem:kmem_cache_alloc --filter 'bytes_alloc == 128' -e kmem:kmem_cache_alloc_node --filter 'bytes_alloc == 128' -o perf_kmalloc_128.data sleep 60 2 > /dev/null
对于不同种类的kmalloc-xxx,只需要在perf中修改过滤器参数 --filter 'bytes_alloc == xxx'即可。
第二种是从slab类型中无法直接确定其申请函数时,需要在内核源码中查询该类型slab的申请流程,确定perf是否存在该流程的追踪点,这里又分为两种情况:
一是该slab申请内存的流程函数唯一时,仅需追踪此函数即可;
二是该slab申请内存函数不唯一,这时就要使用/sys/kernel/slab/<函数名>/slab_size命令,查看该slab的大小,追加滤器参数进行追踪,如其大小为1120,申请内存的函数为通用的kmem:kmem_cache_alloc的nfs_inode_cache,就可以这样抓取:
perf record -a -g -e kmem:kmem_cache_alloc --filter 'bytes_alloc == 1120'
第三种是特殊情况,由于linux系统的slab存在共用机制,对外暴露的某类slab类型实际是某一共同大小的内存对象的链接。在某些用户使用第三方内核模块存在自建slab类型时,可能出现表面上看到slab泄露类型与实际不符的情况,无法使用perf直接进行排查;
这时我们需要根据slabtop -s c查到主要占用内存的slab类型,到/sys/kernel/slab/目录中,使用ll /sys/kernel/slab/ | grep <slab类型名>查找其对应的实际共同大小的内存对象链接,如:
ll /sys/kernel/slab/ | grep ecryptfs_headerslrwxrwxrwx 1 root root 0 7月 30 17:27 ecryptfs_headers -> :0004096/
之后使用其对象链接来搜索查看,是否存在不同于正常环境的异常第三方模块共用slab:
ll /sys/kernel/slab/ | grep :0004096drwxr-xr-x 3 root root 0 7月 30 17:28 :0004096/lrwxrwxrwx 1 root root 0 7月 30 17:28 biovec-max -> :0004096/lrwxrwxrwx 1 root root 0 7月 30 17:28 ecryptfs_headers -> :0004096/lrwxrwxrwx 1 root root 0 7月 30 17:28 ecryptfs_xattr_cache -> :0004096/lrwxrwxrwx 1 root root 0 7月 30 17:28 irq_remap_cache -> :0004096/lrwxrwxrwx 1 root root 0 7月 30 17:28 sgpool-128 -> :0004096/
最后再根据查找到的第三方模块共用slab,反馈相应的第三方进行排查。
生产案例1
问题现象
收到用户反馈,4台物理机出现内存持续上涨的现象,根据观察每天增长7G左右,最终将物理内存耗尽,影响业务运行。
问题处理
对于此问题,我们先根据上文的排查步骤,定位到是哪种类型的slab内存增长导致的,此案例定位为kmalloc-128类型
收集kmalloc-128类型的申请信息:
perf record -a -g -e kmem:kmalloc --filter 'bytes_alloc == 128' -e kmem:kmalloc_node --filter 'bytes_alloc == 128' -e kmem:kmem_cache_alloc --filter 'bytes_alloc == 128' -e kmem:kmem_cache_alloc_node --filter 'bytes_alloc == 128' -o perf_kmalloc_128.data sleep 60 2 > /dev/null
执行perf命令收集kmalloc-128申请信息,抓取时间1分钟
收集完成后,执行perf script -i perf_kmalloc_128.data > perf_kmalloc_128.log解析出的日志就是各个进程申请kmalloc-128的情况与堆栈了
可以将其进行排序:grep -i 'bytes_alloc=128' perf_kmalloc_128.log | awk '{print $1}' | sort | uniq -c | sort -nr | head
排查发现,申请频率最高的进程是perf和ps进程,但用户反馈他们日常并不会执行perf和ps命令,也无定时任务
再次查看perf_kmalloc_128.log发现,perf基本上都是同一个pid进程申请的,并不是多个进程同时申请
对此再进一步查看进程堆栈,发现其申请内存的动作涉及了很多安全机制模块
进一步查看内核版本和cmdline,发现手动开启了安全模块,且查看changelog发现此内核版本确实存在某安全模块内存泄漏的问题,故升级内核解决
生产案例2
问题现象
用户反馈系统内存占满,但并未运行应用,仅挂载了几个NFS,不过NFS中的文件较多,几个加起来有上亿的文件,查看内存占满的原因。
问题处理
排查发现slab内存占用较高,slabtop查看nfs_inode_cache和dentry等占用约3G
首先要理解nfs_inode_cache,它是一个slab缓存,专门用于分配和回收NFS文件系统中inode对象。由于inode对象在文件系统中频繁的被创建和摧毁,故使用slab缓存,可以显著提高内存分配和回收的效率,减少内存碎片化,降低内存管理开销。
通常来说,当客户端访问NFS服务器上的文件时,内核会将文件的inode信息缓存到nfs_inode_cache中,通过缓存这些文件的元数据,可以避免系统重复向NFS服务器请求相同信息,从而降低网络延迟。
因此在高NFS IO下或NFS有海量文件时,nfs_inode_cache会动态增加以容纳更多的inode信息。虽然会占用较多内存,但其目的是为了加速应用运行,且nfs_inode_cache缓存属于可回收slab,在系统内存不足时通常可快速回收
简单介绍过nfs_inode_cache后,下一步就要找到具体是哪些进程访问并产生了如此巨大的nfs_inode_cache
查看内核源码发现,nfs_inode_cache并未采取特殊函数进行缓存分配,而是采用了通用的kmem_cache_alloc缓存申请接口进行分配。因为无法直接通过perf list中对应的跟踪点进行数据抓取
通过cat /sys/kernel/slab/nfs_inode_cache/slab_size查看其slab_size数值是唯一的1120,因此可以通过采用通用的kmem_cache_alloc追踪点,加上filter过滤器的方式精准抓取进程申请nfs_inode_cache的数据:
perf record -a -g -e kmem:kmem_cache_alloc --filter 'bytes_alloc == 1120' -o perf_kmalloc_1120.data sleep 60 2 > /dev/null
再使用perf script解析,结果显示当前环境都是prodchangecolle进程在申请nfs_inode_cache,再联系对应的服务方,来确认是否在频繁访问、查找NFS存储的现象
结语
能看到这里,说明你真的对Linux的内存机制比较感兴趣,如果你恰巧对网卡丢包、ping延迟等网络问题也想深入探讨,欢迎看我的历史发文。
后续我打算针对系统I/0、ebpf性能分析、内核驱动编译等话题继续写总结性文章,形成一个Linux深入分析的高质量系列文章,欢迎关注我,给我也给自己一些动力!