0 引 言软件老化是指在长时间运行的系统中,残留的老化缺陷(aging related bugs,ARBs)被激活导致的性能下降或崩溃故障[1]。软件老化现象已经在许多系统中观测到,例如:Linux系统[2]、安卓操作系统[3]、Web服务[4]、边缘计算[5]和TensorFlow[6]等。ARBs主要包括内存泄漏、未释放的文件锁、存储问题、套接字异常、未释放的文件句柄和错误的积累[7]。其中内存泄漏是导致软件老化的重要原因之一。内存泄漏主要由应用程序内存管理使用中的软件故障引起的,它会导致长时间不断运行的系统中内存资源耗尽。Macêdo等[8]介绍了内存相关软件老化的机制。Andrzejak等[9]在C/C++中收集了分配的内存片段、大小和它们的生命周期,并使用机器学习分类器来检测C/C++代码中的内存泄漏。Ghanavati等[10]研究了Java中内存泄漏的机制,当应用程序不再使用对象但垃圾回收器(garbage collector, GC)无法将它们从工作内存中删除时,就会发生内存泄漏,从而导致应用程序消耗越来越多的资源。内存泄漏是研究最多的软件老化问题,因为它会影响系统可用性,并且在实际系统中经常出现,因此本文将侧重研究检测由内存泄漏引发的软件老化。现阶段软件老化最常用的两种检测方法是:基于趋势分析的软件老化检测方法和基于多版本比较的软件老化检测方法。基于趋势分析的老化检测方法是给待测软件施加一定的压力,并且收集对应时间序列的资源消耗,通过趋势分析可以得出该待测软件的资源消耗是否存在单调的趋势,如果存在单调递增的趋势,则认为该待测软件存在软件老化[11]。Zheng等[12]提出了一种基于Hodrick-Prescott的非线性趋势分析来检测软件老化。Machida等[13]研究了用Mann-Kendall趋势分析来检测软件老化。但是,基于趋势分析的软件老化检测方法并不能确保内存泄漏的存在,因为进程堆的大小可能会增加,这不仅是因为存在内存泄漏,还因为应用程序的内存使用模式[14]。因此,基于趋势分析的软件老化检测方法存在高误报的问题。为了解决基于趋势分析检测不准确的问题,Langner等[15]首次提出基于比较的方法(多版本检测方法),他们希望通过比较相关开发版本之间的运行时的资源消耗来发现软件老化问题。Langner等[16]和Ghanavati等[17]在开发过程中提出了一种自动检测内存泄漏的方法,比较了同一软件相关开发版本的内存分配行为。Matias等[14]研究了内存泄漏的机制,并将软件差分分析与统计技术相结合来检测内存泄漏。基于差分分析的多版本比较方法一个前提就是需要该软件的健壮版本作为基准版本,然后跟待测版本的资源消耗进行差分分析,做出判断。但是基于多版本比较的软件老化检测方法的局限性在于:1) 无法在没有先验知识(该软件之前的健壮版本)的情况下检测到软件老化;2) 即使有该软件的先前版本,但不能确保该软件的先前版本不存在软件老化问题。比如在文献[17]中,Thrift-0.4被视无泄漏软件。但在Apache错误报告中[18]显示它存在内存泄漏问题,该bug的描述为:所有测试都在同一个Java虚拟机中运行,似乎TestAsymcClientManager正在泄漏套接字。如果不能保证软件的基准版本不存在软件老化的前提,最后得出的检测结果是不鲁棒的。因此本文提出:在单版本上基于差分分析的负载相关方法对软件老化进行检测。这一想法是基于医学领域上,对于某些病人的检测是通过压力测试,正常人与病人在不同的压力下表现是不相同的。在不同压力下,病人的某些指标的差异会特别大,但正常人的某些指标差异则无明显差异[19]。因此,通过监控单个版本软件在不同负载下的资源消耗,存在老化问题的软件在高负载下资源消耗的增长速率比低负载下资源消耗的增长速率快,并且呈比例地增长,该比例接近高低负载的比值。本文的贡献在于:1) 降低了基于趋势分析的软件老化检测方法高误报的问题;2) 可对单版本软件老化进行检测,无需该软件先前的健壮版本作为基准版本。1 本文方法本文的方法流程如图1所示。使用负载生成器对单个版本软件施加不同的负载,以不同的时间间隔运行待测软件,时间间隔短的为高负载,时间间隔长的低负载。每隔一定的时间间隔,分别收集不同负载下软件的RSS(resident set size)使用情况。在不同的负载下,存在内存泄漏的软件与不存在内存泄漏的软件的行为不同:存在内存泄漏的软件在高负载下内存的积累比低负载下积累得多,所以在不同负载下资源的消耗差异很大;不存在内存泄漏的软件因为不存在内存的积累,所以在不同负载下资源消耗的差距会很小。用差异函数R=(RSShigh,10.14188/j.1671-8836.2022-0207.F001图1方法概述Fig.1Overview of our approachRSSlow)量化差异变化,high和low分别代表高负载和低负载,RSSorignal表示在最开始时收集的数据。RSShigh和RSSlow表示的是同一时间点不同负载下收集的数据。定义的差异函数R如下:R=RSShigh-RSSorignalRSSlow-RSSorignal (1)在同一时间点收集不同负载下软件的资源消耗,用当前时间点的资源消耗减去初始时间点的资源消耗,得到从开始时间点到当前时间点的内存积累。R表示的是在不同负载下软件内存积累的比值,是对同一软件的不同负载下的资源消耗做差分分析。如果不同负载下每个时间点计算的R值(即软件内存积累的比值)接近或高于高低负载的比值则认为该软件存在由内存泄漏引发的软件老化。文献[14]中,作者对软件的不同版本做差分分析,该方法的前提是需要将不存在软件老化的版本作为基准版本,使用基准版本跟待测版本的资源消耗进行比较。但是在现实中,大部分的待测软件都是新项目,并没有历史版本,或者不能保证历史版本不存在软件老化。本文的方法只需要软件的单个版本,对不同负载下软件的资源消耗进行差分分析,更适应于真实的场景。2 实验部分2.1 实验环境实验平台是由一台计算机(主机)托管两台虚拟机(VM1和VM2)组成的。其中,主机配置为:Intel(R) Core(TM) i5-7300HQ CPU @ 2.50 GHz 2.50 GHz, 8.00 GB RAM,1 TB磁盘,Windows10;VM1的配置为:1个CUP内核,2.0 GB RAM,50 GB磁盘,CentOS-7;VM2的配置同VM1。每个实验进行10次,以减少实验的偶然性误差。2.2 老化指标的选取特定于应用程序的指标常用来检测正在运行程序中的内存泄漏,这些度量指标有RSS、虚拟内存大小、堆大小和堆使用情况等。文献[14]将不同的指标进行比较认为RSS更适合检测软件老化。因此,本文将RSS作为老化指标,通过编写Shell脚本定时读取文件/proc/PID中status文件中的VmRSS字段内容来收集RSS使用情况。2.3 调查应用程序(Squid)Squid是一个Web的缓存代理,可以支持FTP、HTTP和DNS等协议的解析。它位于客户端和服务器之间,在不同的协议中发挥重要作用[20]。Squid的主要任务是当客户端访问页面时,会通过Squid获取这些数据,并且将这些数据缓存在Squid的cache中,当客户端下次访问该页面时,可以快速读取数据。它是一个比较成熟的软件项目,已经被很多互联网服务提供商和网站使用,比如Wikipedia(维基解密),它还被嵌入到许多网络设备产品中。因此,Squid的可靠性很重要[21]。本文的实验使用了两种不同版本的Squid,分别是存在泄漏的版本Squid-3.2.1和没有泄漏的健壮版本Squid-3.1.23。2.4 实验分析2.4.1 Java demo分析为了找到负载和资源消耗之间的关系,将内存泄漏注入到Java程序中。Java中的内存泄漏机制如图2所示。当一个对象被不正确地引用时会发生内存泄漏,例如:当引用的对象未被使用。因此,我们编写了两个Java程序,一个是遭受了内存泄漏,泄漏内存约为250 Kb,另一个是不存在内存泄漏。采用作者编写的Java Demo可以手动注入泄漏并且知道每次内存泄漏的大小,每运行一次该代码就会有250 Kb的内存没有得到回收,可以更清晰地分析出不同负载下资源消耗的增长比值跟高低负载比值之间的关系。10.14188/j.1671-8836.2022-0207.F002图2Java中的内存泄漏机制Fig.2The mechanism of memory leak in Java采用两种不同的负载。高负载:每6 s运行一次代码;低负载:每30 s运行一次代码。负载差异为5倍,Java程序一共运行2 h10 min,先运行程序10 min达到稳定状态,然后每个程序在不同的负载下执行2 h,每5 s收集一次程序的RSS使用情况,共收集1 440个数据。Java程序的RSS使用情况趋势如图3和图4所示。10.14188/j.1671-8836.2022-0207.F003图3不同负载下存在泄漏程序的RSS使用情况Fig.3The RSS usage of leaky procedure under different workload10.14188/j.1671-8836.2022-0207.F004图4不同负载下无泄漏程序的RSS使用情况Fig.4The RSS usage of no leaky procedure under different workload通过观察图3可知,存在内存泄漏的程序在不同的负载下RSS使用情况差异非常大,但在图4中,无内存泄漏程序在不同负载下RSS使用情况差异很小。为了量化不同负载下的差异,对单个软件的版本在不同负载下的RSS使用情况进行差分分析,计算每个时间点的R,并绘制出如图5所示的箱形图。10.14188/j.1671-8836.2022-0207.F005图5两个Java程序的差异R值Fig.5The difference R of the two Java procedure在图5中,箱形图中绿色的三角形代表的是平均值,箱子里的横线代表的是中位数,箱形外面的黑色圆圈代表的是异常值,存在内存泄漏的Java demo的R平均值接近5,而无内存泄漏的Java demo的R远离5。R越大表示高负载和低负载下程序内存积累的差距也越大。因此可以得出结论:如果软件存在内存泄漏,则差值R接近于或者高于高低负载的比值;软件不存在内存泄漏,则差值R远低于高低负载的比值。2.4.2 Squid分析监测Squid两个不同版本Squid-3.1.23(健壮)和Squid-3.2.1(泄漏)。在文献[21]中,作者发现当Squid-3.2.1在解析FTP协议时发生内存泄漏。本文在主机上构建了两台虚拟机VM1和VM2。在VM1中的Firxfox-52.2.0浏览器上设置Squid缓存代理,并编写Shell脚本作为负载生成器控制VM1中的负载。在VM2中,安装软件Squid并监控Squid进程的RSS使用情况。实验平台设置如图6所示。实验采取高负载和低负载两种不同负载。其中,高负载:每10 s对FTP url进行一次访问;低负载:每50 s对FTP url进行一次访问。10.14188/j.1671-8836.2022-0207.F006图6实验平台设置Fig.6Testbed setup使用FTP url是ftp://ftp.adobe.com,负载相差5倍,每次实验持续30 min,前5 min使运行的软件达到稳定的状态,每10 s收集一次数据,共收集了300个数据。实验结果如图7和图8所示。10.14188/j.1671-8836.2022-0207.F007图7Squid-3.2.1在不同负载下的RSS使用情况Fig.7The RSS usage of Squid-3.2.1 under different workload10.14188/j.1671-8836.2022-0207.F008图8Squid-3.1.23在不同负载下的RSS使用情况Fig.8The RSS usage of Squid-3.1.23 under different workload从图7和图8可以得出与Java demo实验相同的结果。如图7所示,Squid-3.2.1在不同负载下RSS的使用情况差距很大。在图8中,Squid-3.1.23在不同负载下的差异很小。对不同负载下单个软件版本的RSS使用情况进行差分分析,计算每个点的R并制作出如图9所示的箱形图。10.14188/j.1671-8836.2022-0207.F009图9Squid-3.2.1和Squid-3.1.23的差异值RFig.9The difference R of Squid-3.2.1 and Squid-3.1.23从图9中可以发现,在Squid-3.2.1中,R的平均值接近5,R=5表示高负载下软件的资源消耗是低负载下的5倍,内存在不断积累,存在软件老化。而在Squid-3.1.23中R的值大多分布在1左右,R=1表示软件在不同负载下,资源消耗是一样的,不存在软件老化。在商业软件Squid中,如果软件存在泄漏,则差值R接近于高低负载的比值。3 结 语本文提出了一种基于差分分析负载相关方法,用于软件开发过程的早期检测软件老化(内存泄漏相关缺陷),并将它应用于成熟的商业软件Squid。提出的方法侧重于负载和资源消耗之间的关系。实验表明,该方法可以有效地检测开发阶段的软件老化问题。与基于趋势检测的方法相比[13],该方法可以有效的检测到软件老化;与多版本检测方法相比[14],该方法可以在单个版本上检测到软件老化。提出的方法不需要先验知识,避免了对先前健壮版本的错误选择导致检测误报。本文所做的工作存在一定的局限性:1) 没有充分考虑负载和资源消耗之间的关系是否与其他条件有关;2) 只使用RSS作为老化指标,其他系统指标没有考虑;3) 仅讨论了该方法在Squid中的应用情形。在一些复杂的系统中,存在许多干扰因素,尚不清楚本文方法是否适用于其他软件。在未来的工作中,我们计划将提出的方法应用到更多的系统,考虑更多的老化指标而不局限于单个指标RSS,并考虑这些指标间的相关性。此外,还将考虑采用更全面的数学模型进行建模。