杨建旭
作者杨建旭2016-11-25 13:24
技术经理, 中国人民银行清算总中心

性能指标之资源指标 如何判断是否有内存泄漏及关注的指标

字数 6737阅读 9224评论 0赞 4

内存泄露为什么不容易判断

在性能问题里面内存有没有泄露、如果有泄露是哪里泄露了这两个问题是非常难判断和定位的。甚至有厂商对内存是否健康的评判就是:下次系统重启之前,应用能正常运行。换句话说,有内存泄露找不出来也没关系,定期重启一下服务器,而且在生产环境,的确有些企业是这么干的。

CPU如果利用率异常,可以查哪个进程中的哪个函数占用CPU多,相应的,内存也可以查哪个进程占内存多。为什么内存问题不像CPU问题那么容易定位?

内存泄露是指使用内存完成后没有释放,内存增长并不能分辨增长出来的内存是进程真正要用的,还是进程泄露出来的。而CPU的占用是瞬时的、确定的,不存在某个进程申请了CPU占着不用的情况。

在一个讨论组里,有人提问:对一个基于Java的Web系统进行压力测试,如果虚拟用户数从峰值下滑的同时,内存占用率却保持在峰值不变,是否能得出Java程序存在内存泄露的问题?

回答是否定的,原因如下:

首先,内存占用率指的是什么内存的占用率

在不同的OS上有不同的内存管理机制,比如AIX上,我们最关注是计算内存,但如果内存利用率指的是计算内存+非计算内存的话,即使内存占用率上升也说明不了太多问题。再比如Linux上,我们最关注是active内存,如果内存利用率指的是active+buffer+cache,即使内存占用率上升也说明不了太多问题。

第二,其他进程的干扰

操作系统上运行的进程千千万,内存不下降,可能是其他应用/系统进程对内存的使用,应具体分辨是哪个进程占据了内存。因此考察是否有内存泄露应关注的是指定进程有没有内存增长,这样比较容易排除干扰。不过,查看进程的Data Segment也只能查看这个进程使用的一部分内存,而这个进程使用的Shared Memory Segment则不在这个指标中,但同样需要关注(内存是分段的(Segment),每个段都是独立的,有各自的度量读数)

第三,进程池的原因

假如只关注计算内存(AIX),如果服务端的应用是一个100个进程的进程池,应用刚启动的时候没有客户端的连接进来,因此没有启动任何进程,随着客户连接的增多,100个进程统统启动,并常驻内存;再假如这些进程使用的内存是分配好不变的,那么内存占用率保持在峰值不变,是很正常的表现。

CICS里面也有进程常驻内存的概念。常驻内存后,进程不会掉下去,因此没有创建、销毁进程的开销。

第四,JVM内存管理的原因

如果不是Java程序,内存不下降甚至内存上升,也有上述的多种原因,何况这是Java程序,存在一个JVM内存管理机制的原因。

我们曾经遇到这样一个案例。对某Linux服务器上的某应用进行压力测试,在一周的测试过程中,发现内存不断增加。尽管服务器上每天定时清理内存(如下),但总趋势仍然是内存增长。

备注:drop_caches是清理无用的cache,对于dirty状态的是不清理的,直到dirty的内存被写入磁盘。但如果用sync操作把dirty的内存flush到磁盘中,后续的drop_caches将释放更多的内存。

后续我们就发现,这是JVM内存管理机制造成的内存泄露假象。该系统在测试过程中Java full GC(全量垃圾回收)没有被调起,老年代的内存没法被释放。虽然应用使用的内存并没有超过JVM设定的heap大小,但从Linux内存监控的指标上看,active内存是不断增加的。

为什么full GC没有被调起呢?这个场景下,老年代内存的增长会非常缓慢,几天内都不会达到触发full GC的标准,以致出现内存使用量不断增长不回收的现象。并且,这个Java应用是一个独立的Java程序,并没有运行在应用中间件上,因此没有中间件帮它做合理的GC策略,而应用本身也没有去调起full GC。

后经调整应用,主动调起full GC,内存增长问题得到解决。

第五,本应用其他逻辑的干扰

也许这个应用是个接收客户端数据报送并进行ETL处理的程序,服务端的应用在收到客户端的数据后,开始启动其他进程/线程/模块去做后续处理,后续处理需要分配内存。

继续最初的问题,回到那个基于Java的Web系统,如果虚拟用户数从峰值下滑到0,内存占用率却继续上扬,是否能得出Java程序存在内存泄露的问题?

回答仍然是否,可能的原因还是上面那几条。

总而言之,内存泄露是非常难判断的事,需要长时间的测试才能得到猜测性结论。

谁占用的内存多

首先找到哪个应用或哪个进程占用的内存多。

1、物理内存占用

Nmon sheet

根据经验,nmon的top sheet- Memory by command最直观,也最容易直接看出哪个进程消耗的内存资源多。

然后采用类似ps –ef| grep java这样的命令查看这个进程具体是什么内容。

另外,有不少命令也可以看哪个进程占用物理内存多,但讲真,经常用命令行去看,但经常看不出来什么结果。例如下图,每个进程消耗的物理内存似乎差距不大,虽然这个例子中服务器上跑oracle这样的系统软件,内存都是oracle占的,但即使不跑oracle,这些进程的内存占用往往也差距不大。

Svmon

列出消耗物理内存前十的进程
svmon -Pt10 | perl -e 'while(<>){print if($.==2||$&&&!$s++);$.=0 if(/^-+$/)}'

Svmon里面的inuse指的是这个进程对物理内存的消耗,包括计算内存+非计算内存。其实非计算内存,我们一般是不做过多关注的,及时占用的多,也没什么问题。

ps

ps aux | head -1 ; ps aux | sort -rn +4 | head -10
按照占用物理内存的百分比排序,列出前十个进程。

Nmon command

nmon --> t (top processes) --> 4 (order in process size)

2、Paging Space占用

用到Paging Space不一定说明这个进程占用内存多,很有很能是它被其他进程挤出来的。查出谁在用Paging Space,大概率是查出谁是受害者。

按照占用Paging Space的进程排序
svmon -P -O sortseg=pgsp

检查哪个进程引起的Paging到Paging Space(IBM script)。脚本发现po这个指标大于50的时候保存进程相关信息退出

Paging Space一旦为这个分页分配了磁盘空间,就不会因为这个分页换回物理内存而释放,因此经常可以看到Paging Space的利用率不为0,但此时物理内存占用也不多。Paging Space的利用率不为0只能说明历史上有物理内存不足的情况。

进一步关注指定进程是否有内存泄漏

在稳定性测试(也叫持久测试或疲劳测试)中,需要观察内存是否有泄露。然而使用内存的进程千千万,整个服务器的内存增长似乎也不能判断某个进程的内存有泄露。因此在稳定性测试过程中往往需要全程关注指定进程的内存消耗,比如运行3天、7天。

查看内存使用情况的命令有ps、sar、svmon、vmstat等等,但本文并不从工具使用的角度来介绍,而是从性能测试中关注指标的角度来介绍。如果采用其他命令查看内存,需注意,相似的名字在不同命令当中的含义是不一样的,一定要搞清楚这个字段的真正含义。

例1:Virtual这个词,有时候在内存里面指Paging Space(换页空间),有时指进程空间里面占用的所有分页(包括物理内存和Paging Space中的分页)。

例2:Nmon中的PgIn/PgOut、topas中的PageIn/PageOut是指对文件系统的换页,而vmstat中的pi/po是对Paging Space的换页,而topas P中进程的PAGE SPACE是指进程的Data Segment。

进程使用的数据段

1. 获取来源

ps gv 进程号:SIZE(单位为KB)
svmon –P 进程号:work process private、work shared library data、text data BSS heap、USLA heap、application stack、private load data等segment之和(单位为4KB或64KB)
topas P中进程的PAGE SPACE(单位为4KB)

2. 指标说明

内存泄露指进程自己申请分配、使用了内存但没有在使用完毕后释放,大量的泄露会导致物理内存用满,降低系统效率。

如何判断一个进程有没有内存泄露?AIX中使用ps gv命令观察特定进程的SIZE指标,如果SIZE经过长时间测试后,不断增长,则可能有内存泄露的嫌疑,这里说的是嫌疑,而不是一定。况且,查看进程的SIZE值也只能查看这个进程使用的一部分内存,而这个进程使用的Shared Memory Segment则不在这个指标中。如果是JAVA程序由于涉及到JVM的内存管理,问题就更难判断的,我们先放下JAVA程序不表,单说C的程序。

SIZE在ps命令当中的解释是The virtual size of the data section of the process (in 1KB units)。为什么看这个指标,则需要从进程空间开始说。

进程空间的内存可以分为三种类型:
1)数据段:Data Segment (Data + BSS + Heap)
2)栈:Stack
3)代码段:Code Segment

栈(Stack):包括返回地址、自动分配的变量,都是一会儿有一会儿没的,系统自动回收,不会造成内存泄露。

代码段:进程跑起来肯定要有代码,代码基本上可以说是固定大小的,不会造成内存泄露。当然如果代码太大,装不进内存,那就另当别论了。

剩下的数据段就是进程自己分配、使用的分页,数据段包括Data + BSS + Heap。
Data是已经初始化的全局和静态变量。
BSS是未初始化的全局和静态变量,比如static int i;。
Heap(堆)是进程中malloc, realloc等函数申请的,需要free等函数释放。

3. 举例

ps gv命令中SIZE就是该进程数据段的virtual size(1KB为单位),这些分页可能在物理内存中也可能在Paging Space中。

检查SIZE列在长期的测试过程中是否有明显的持续增长,如有,说明可能有内存泄露。

长期的抓取和后期的图形化处理,可以写脚本或代码来实现

其他命令也可以看到这个值,以下图为例“svmon –P 进程号”可以看work process private的virtual大小+work shared library data的virtual大小。如果有text data BSS heap、USLA heap、application stack、private load data等segment,还需把这些segment也加上。由于svmon中统计的segment较多,因此不推荐采用svmon统计Data Segment。

二者的单位是4K的分页。(169+85)4=2544=1016,与ps v得到的SIZE值相同。

解释一下为什么单位是4K,svmon的输出结果中,work process private和work shared library data的PSize(Page Size)类型是sm。而这台机器上命令svmon显示,只有s和m两个类型,分别对应4KB和64KB。而sm这个类型是什么呢?

AIX上面进程空间的虚拟内存分页默认的页大小是4K,但POWER5+以上的处理器支持4种页大小,分别是4KB(small),64KB(medium),16MB(large)和16GB(huge),POWER6处理器开始支持4K和64K的混合形式,即一个segment里面既有4K分页,也有64K分页,当需要大块内存、需要提升性能的时候用64K分页,当64K分页可能会浪费内存的时候则用4K分页。svmon命令需要告诉用户这个segment有多少内存,为了统计时的方便,就只用一个单位来度量,这个单位就是sm中的第一个字母s(small),对应的度量单位是4K,这个条记录后面的列中的数据都依据这个度量单位出具。

另外解释一下inuse和virtual。Svmon里面的inuse指的是这个进程对物理内存的消耗,包括计算内存+非计算内存。而virtual指的是进程空间里面的分页,这个分页也许在物理内存,也许在Paging Space。假如说一个进程使用的分页都在物理内存的话,inuse>=virtual,因为此时inuse里面有文件缓存,而virtual里面没有文件缓存,文件缓存是操作系统给缓存的,和进程空间没关系。

也可以Topas,敲击P,看指定进程的PAGE SPACE,也是254,254*4=1016,与ps v得到的SIZE值相同。

这里的PAGE SPACE的单位是4KB,比较好查,只要man topas就可以找到这一段:PAGE SPACE:The virtual working set size used by process (4 KB pages)

有人会问,为什么不从nmon里面取值?nmon的TOP Sheet里面的进程也有SIZE等内存的指标,但nmon的TOP Sheet中只列出占CPU比较多的N个进程,如果被监控的进程占CPU很低,就不会出现在TOP Sheet中。或者一开始被监控进程占CPU较多,后来由于它占用的CPU减少而从TOP sheet中移除了,那么我们并不知道这个进程是销毁了还是CPU利用率太低了。

进程使用的共享内存段

除了Data Segment可以造成内存泄露,如果进程分配共享内存,也可能造成内存泄露。

共享内存是某个进程分配,其他进程可以访问的内存段,共享内存会映射到每个进程的地址空间。

那么如何查看指定进程消耗的共享内存呢?
从进程空间的角度看,共享内存段在这里

首先,还是要介绍概念,共享内存在AIX上可能有两种内存段:shared memory segment和memory mapped segment。为什么有两种段呢?这是由于程序调用了不同的实现接口导致的(System V Shared Memory services (shmat) 和BSD Memory Mapped Services)。BSD Memory Mapped Services可以将文件直接映射到内存而省去中间的buffer,但它也可以用来创建共享内存,它创建出来的共享内存段就是memory mapped segment。

查看指定进程消耗的共享内存的具体方法如下

1. 获取来源

32位程序
svmon -P进程号| egrep "Vsid|shared memory|mmap maps"

64位程序
svmon -P进程号| egrep "Vsid|shmat/mmap"
查看virtual字段

2. 指标说明

如果查出来某个内存段是memory mapped segment类型的,这个segment里面是不是共享内存,需要用ipcs –mS辅助判断(因为memory mapped segment也可能是文件直接映射内存)。ipcs查到的都是共享内存,因此可以通过svmon –P中这个段的Vsid( virtual segment ID)在ipcs中查找有没有这个段号。

这里没有现成的例子,只是截个示意图

ipcs –mS看到的是所有共享内存段,而没有段的大小。如果看段的大小可以加-b选项。需注意,-b选项看到的是可分配的最大值,而不是已分配的。

SEGSZ的单位是Byte。-b列出共享内存段以及消息、信号量的最大值,也就是可分配的最大值,而不是已分配的(数据库程序除外)。-m列出活动的共享内存段。

参考:http://www-01.ibm.com/support/docview.wss?uid=isg3T1012013

下节预告

结合实际案例,介绍内存增长的误判。

性能指标之资源指标 内存基本概念以及与性能相关的基本配置
性能指标之资源指标 如何判断物理内存是否够用

微信公众号:性能测试与调优

如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!

4

添加新评论0 条评论

Ctrl+Enter 发表

本文隶属于专栏