前几天遇到一个游戏业务TOP客户反馈的问题,大致背景是自建的数据库业务,购买的是阿里云裸金属服务器总是遇到内存不足的情况,每次重启过不了多久就会不断报内存页分配失败,而且无论怎么扩容内存业务改善都不大,内存问题我们遇到过很多类,今天就就着这个案例来说一说内存使用的场景分析
问题排查前我们先大致整理一下Linux内存使用分为几个大类,以及都如何去统计,以便与日后排查问题能有更好的方向性。
一. 查看总的内存使用情况
通常我们是这样看内存的剩余情况的:
[root@iZm5ed****sZ ~]# free -m total used free shared buffers cached Mem: 7856 2332 5524 0 7 2123 -/+ buffers/cache: 200 7655 Swap: 0 0 0
关于这部分信息的解读我们结合《Linux Performance and Tuning Guidelines》中的截图:
截图取自(page 47):https://lenovopress.com/redp4285.pdf
从上图结合我们的测试环境可以获取到的信息如下:
1、总内存7856M ,已用内存2332M
2、由于buffers + cached内存实际上也是可用内存,该内存也可以通过echo 3 > /proc/sys/vm/drop_caches 回收pagechae、dentries and inodes ,所以实际上已经使用的内存是200M, 剩余free内存 7655M 。
PS:buffers + cached 分别是什么不展开讨论了上面链接的文档中也有详细说明,free命令的统计方式也在改进,后面版本已经不单独统计-/+ buffers/cache行了,直接帮我们算好了。那么真正used部分消耗在哪里了呢?
二. RSS 部分消耗
RSS(resident set size) 也就是每个进程用了具体的多少页的内存,linux内存管理中进程的代码,库,堆和栈都会消耗内存,但是申请出来的内存,只要没真正touch过,是不算的,这里怎么理解呢,例如我调用malloc申请一段内存,此时只在虚拟内存段中分配了,实际这一段空间并没有映射真正的物理内存,只有当你尝试写这个内存页时系统才会通过缺页异常为这个虚拟内存页映射物理内存。进程实际占用的物理内存我们只需要统计下RSS就可以了,我们可以从/proc/PID/status 里获取到每个进程的rss,其实ps,top等工具也是从/proc下面统计的:
[root@iZm5****rsZ ~]# cat /proc/1686/status Name: assist_daemon State: S (sleeping) Tgid: 1686 Pid: 1686 PPid: 1 TracerPid: 0 Uid: 0 0 0 0 Gid: 0 0 0 0 Utrace: 0 FDSize: 256 Groups: VmPeak: 17968 kB VmSize: 17828 kB VmLck: 0 kB VmHWM: 2192 kB VmRSS: 2192 kB VmData: 15064 kB VmStk: 84 kB VmExe: 2628 kB VmLib: 0 kB VmPTE: 52 kB VmSwap: 0 kB Threads: 8
同时为了统计方便/proc/PID/statm 第二列就是RSS内存使用page页的多少,而在linux下默认使用的page页大小是4KB 。所以我上面计算求和后,最后乘以4就是最终的内存大小,我们写个脚本遍历一下:
[root@iZm****rsZ ~]# cat test.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`
三. slab内存消耗部分
slab分配器,是为了弥补buddy system分配器最小分配粒度是以物理页帧(page)为单位进行管理的缺陷,内核中有大量的数据结构只需要若干bytes的空间,倘若仍按页来分配,势必会造成大量的内存被浪费掉。那这部分怎么统计呢?我们可以先看下slabtop命令,可以理解为slab版本的top:
slab内存的消耗我们可以查看/proc/slabinfo文件,具脚本为:
[root@iZm5e****sZ ~]# echo `cat /proc/slabinfo |awk 'BEGIN{sum=0;}{sum=sum+$3*$4;}END{print sum/1024/1024}'` MB 88.8909 MB
四、PageTables内存消耗
通俗来讲,虚拟内存的管理的核心是解决如何在小的物理内存中运行更大程序的问题。
在Linux中,解决这个问题的关键是一个叫做page table[PT页面转换表]的结构。Linux把物理内存分为了固定统一大小的块,称为page[页],一般为4KB,并且每个页都有一个编号 [page frame number]。这样一个512M大小的内存将包括128K个页。这种方式称为paging,使得操作系统对内存的管理更方便。page table的作用就是将进程操作的地址[虚拟地址]转换成物理地址。
那么去哪里看PageTables占用多少呢:
[root@iZm5****rsZ ~]# echo `grep PageTables /proc/meminfo | awk '{print $2}'` KB 2872 KB
把上面几个脚本合计一下:
[root@ali-test ~]# cat test.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 [root@ali-test ~]# sh test.sh 310420KB, 5060KB, 93.2412MB rss+pagetable+slabinfo=400.2412MB total used free shared buff/cache available Mem: 3789 193 2146 0 1449 3336 Swap: 0 0 0
这里我们会发现一个问题,我们计算的总内存占用400.2412M ,free 统计的used内存193M,首先这里free计算的used啥也不是,是直接减出来的:total - free - buffers - cache(这里buffer和cache都有一部分是不可回收的所以减多了具体可以看/proc/meminfo)
但是即使这样依然有差距,相差的到底是哪一部分?一般我们认为是shared内存重复统计导致的,我们在计算RSS时由于RSS包含了共享内存部分(RSS = total_RSS+files+share_mm),所以可能存在部分share内存被重复累加了,以sshd为例可以看到很多so等共享库包含在内:
[root@ali-test ~]# pmap 1046 1046: /usr/sbin/sshd -D 0000565429212000 800K r-x-- sshd 00005654294d9000 16K r---- sshd 00005654294dd000 4K rw--- sshd 00005654294de000 36K rw--- [ anon ] 000056542b414000 132K rw--- [ anon ] 00007ff927ec6000 48K r-x-- libnss_files-2.17.so 00007ff927ed2000 2044K ----- libnss_files-2.17.so 00007ff9280d1000 4K r---- libnss_files-2.17.so 00007ff928b18000 4K r---- libkrb5support.so.0.1 00007ff928b19000 4K rw--- libkrb5support.so.0.1 00007ff928b1a000 8K r-x-- libfreebl3.so 00007ff928b1c000 2044K ----- libfreebl3.so 00007ff928d1b000 4K r---- libfreebl3.so 00007ff928d1c000 4K rw--- libfreebl3.so 00007ff928d1d000 232K r-x-- libnspr4.so 00007ff928d57000 2044K ----- libnspr4.so 00007ff928f56000 4K r---- libnspr4.so 00007ff928f57000 8K rw--- libnspr4.so 00007ff928f59000 8K rw--- [ anon ] 00007ff929d39000 4K rw--- [ anon ] . . . . . . 00007ff929d3a000 412K r-x-- libssl.so.1.0.2k 00007ff929da1000 2048K ----- libssl.so.1.0.2k 00007ff929fa1000 16K r---- libssl.so.1.0.2k 00007ff929fa5000 28K rw--- libssl.so.1.0.2k 00007ff929fac000 112K r-x-- libsasl2.so.3.0.0 00007ff929fc8000 2044K ----- libsasl2.so.3.0.0 00007ff92a1c7000 4K r---- libsasl2.so.3.0.0 00007ff92a1c8000 4K rw--- libsasl2.so.3.0.0 00007ff92a1c9000 92K r-x-- libpthread-2.17.so 00007ff92a1e0000 2044K ----- libpthread-2.17.so 00007ff92a3df000 4K r---- libpthread-2.17.so 00007ff92a3e0000 4K rw--- libpthread-2.17.so 00007ff92a3e1000 16K rw--- [ anon ] 00007ff92a3e5000 84K r-x-- libgcc_s-4.8.5-20150702.so.1 00007ff92a3fa000 2044K ----- libgcc_s-4.8.5-20150702.so.1 00007ff92a5f9000 4K r---- libgcc_s-4.8.5-20150702.so.1 00007ff92a5fa000 4K rw--- libgcc_s-4.8.5-20150702.so.1 00007ff92a5fb000 312K r-x-- libdw-0.176.so 00007ff92a649000 2048K ----- libdw-0.176.so 00007ff92a849000 8K r---- libdw-0.176.so 00007ff92a84b000 4K rw--- libdw-0.176.so 00007ff92a84c000 16K r-x-- libgpg-error.so.0.10.0 00007ff92a850000 2044K ----- libgpg-error.so.0.10.0 00007ff92aa4f000 4K r---- libgpg-error.so.0.10.0 00007ff92aa50000 4K rw--- libgpg-error.so.0.10.0 00007ff92aa51000 500K r-x-- libgcrypt.so.11.8.2 00007ff92aace000 2044K ----- libgcrypt.so.11.8.2 00007ff92accd000 4K r---- libgcrypt.so.11.8.2 00007ff92acce000 12K rw--- libgcrypt.so.11.8.2 00007ff92acd1000 4K rw--- [ anon ] 00007ff92acd2000 80K r-x-- liblz4.so.1.7.5 00007ff92ace6000 2044K ----- liblz4.so.1.7.5 00007ff92aee5000 4K r---- liblz4.so.1.7.5 00007ff92aee6000 4K rw--- liblz4.so.1.7.5 00007ff92aee7000 148K r-x-- liblzma.so.5.2.2 00007ff92be9c000 8K rw--- [ anon ] 00007ff92be9e000 1800K r-x-- libc-2.17.so 00007ff92c060000 2048K ----- libc-2.17.so 00007ff92c260000 16K r---- libc-2.17.so 00007ff92c264000 8K rw--- libc-2.17.so 00007ff92c266000 20K rw--- [ anon ] 00007ff92ebaa000 136K r-x-- ld-2.17.so 00007ff92edaa000 92K rw--- [ anon ] 00007ff92edca000 4K rw--- [ anon ] 00007ff92edcb000 4K r---- ld-2.17.so 00007ff92edcc000 4K rw--- ld-2.17.so 00007ff92edcd000 4K rw--- [ anon ] 00007fffd2868000 132K rw--- [ stack ] 00007fffd299b000 8K r-x-- [ anon ] ffffffffff600000 4K r-x-- [ anon ] total 112880K [root@ali-test ~]# cat /proc/1046/statm 28219 1091 833 200 0 190 0
上面有两个结论:
1. statm中第二段是RSS = 1091 pages ,第三段是share = 833 pages,RSS包含了share,但是和pmap中计算的total为什么差这么多呢,因为total里把每个so的所有内存都算上了,而进程真正占用的部分是遵从最小化原则的就是调用了的部分才会映射进来,所以实际以statm里的第三段为准。
2. 从用户态很难严格区分每个共享内存被几个进程占用,从而去抵消重复计算部分,所以共享内存存在计算误差。
结合上面的内容,我们介入后发现了几个特征,其一从meminfo看业务报错时其实free内存还有好几个G,其二系统日志中在不断打印页申请失败(page allocateion failure)如下:
从这里可以看出,客户环境有大量的4K free页,但是4K以上的页已经全为0了,很明显齐内存碎片化非常严重,此时申请大页内存基本都会失败,说明客户业务在频繁申请释放4K页打散了大页内存。同时可以看到pagetable快接近150G了,说明虚拟内存页表非常大明显是不正常或者说不健康的。
1. echo 1 >/proc/sys/vm/compact_memory (手动触发一次内存规整,会合并部分可移动内存页,是有损的因为要遍历内存所以一般线上环境不推荐)
2. /proc/sys/vm/extfrag_threshold 调整这个默认值,默认是500,可以尝试调整到200,表示系统在内存碎片的处理倾向于做memory compaction,迁移合并及时的话,就可以缓解碎片,这个最好是在开机后默认配置,不要等出问题后再配。
3. sync; echo 3 >/proc/sys/vm/drop_caches 主动回收cache,只是缓解一下不解决问题。
4. vm.zone_reclaim_mode , 我们线上的主机中该参数均为默认值 0, 不会触发 reclaim 操作而是直接返回 zone full ,所以改为1,但是如果做为文件服务器的话建议改为0,因为这种场景大部分内存应该都需要用于文件系统缓存来提高IO响应时间。
客户业务主要为自建oracle数据库,且没有启用hugepage,大内存部署oracle还是推荐使用大页,这是最大的一个问题:
1. 配置数据库使用大页,配置sga_max_size
2. 设置系统预留大页内存:vm.nr_hugepages=xxx(hugepage是2M,pages换算的总大小要大于数据库定义的sga_max_size)
3. 配置memlock数量要大于大页的数量 -1代表不限制:/etc/security/limits.conf
oracle soft memlock -1
oracle hard memlock -1