什么是压力测试
压力测试是一种基础的质量保证活动,它应当成为每一种有意义的软件测试结果的一部分。压力测试的主要思路很简单:不是在常规条件下手动或自动运行测试,而是在缩减的机器或系统资源下运行测试。要被缩减的资源主要包括:内存储器、可用的CPU、磁盘空间,以及网络带宽。 通过运行一个工具,可以缩减这些资源,我们称之为压力器(stressor)。
图1展示了一个运行中的压力工具,EatMem。通常来说,命名压力器以吃(EAT)作为前缀,后面紧跟要消耗的资源类型,所以,缩减内存、可用CPU和磁盘空间的压力器相应地命名为EatMem,EatCpu和EatDisk。 EatMem压力器是一个命令行程序,它接受一...全部
压力测试是一种基础的质量保证活动,它应当成为每一种有意义的软件测试结果的一部分。压力测试的主要思路很简单:不是在常规条件下手动或自动运行测试,而是在缩减的机器或系统资源下运行测试。要被缩减的资源主要包括:内存储器、可用的CPU、磁盘空间,以及网络带宽。
通过运行一个工具,可以缩减这些资源,我们称之为压力器(stressor)。
图1展示了一个运行中的压力工具,EatMem。通常来说,命名压力器以吃(EAT)作为前缀,后面紧跟要消耗的资源类型,所以,缩减内存、可用CPU和磁盘空间的压力器相应地命名为EatMem,EatCpu和EatDisk。
EatMem压力器是一个命令行程序,它接受一个简单的参数,指出需要运行多长时间,以分钟为单位。EatMem大约每三秒钟尝试随机地分配大量的可用内存。如图1所示,内存申请可能会失败。在那种情况下,EatMem持续重试直至成功。
当然,压力器工具自身并不执行任何实际的测试;它仅仅简单地为测试准备机器环境,如果不是这样,那么在正常测试环境下很难测试。
[img] [/img]
图1 运行中的EatMem压力器
整体程序结构
EatMem压力器工具,是用C#语言写的,整体结构如图2所示。
我通过对InteropServices命名空间增加using语句来开始程序,以便可以调用本机的Win32函数。特别地,我调用GlobalAlloc和GlobalFree函数来分配和释放内存,以及调用GlobalMemoryStatus来决定要分配多少内存。
我还使用一些Win32 API函数来控制控制台(console)文字的颜色。
GlobalAlloc和GlobalFree函数定义在kernel32。dll库中。通过使用DllImport属性,我从我的C#压力器程序中调用它们。
GlobalFree的原始C++签名如下:
HGLOBAL GlobalAlloc(UINT uFlags,
SIZE_T dwBytes);
HGLOBAL返回类型是众多Win32符号常量中的一个。
它本质上是一个指针,指向由GlobalAlloc函数分配内存的首地址,所以我把此类型转换成System。IntPtr类型。IntPtr是一个平台相关的类型,用于代表指针或句柄。在下一部分我将全面地解释GlobalAlloc和他的伙伴GlobalFree。
我使用GlobalMemoryStatus函数返回当前可用内存的数量,以便决定通过GlobalAlloc函数试图分配的内存数量。GlobalMemoryStatus的Win32签名是这样的:
void GlobalMemoryStatus(LPMEMORYSTATUS lpBuffer);
传递一个指向结构的指针/引用给GlobalMemoryStatus,它使用内存信息填充此结构。
结构定义如图2所示。结构中最重要的字段是dwAvailVirtual字段,它包含调用GlobalMemoryStatus函数后的可用虚拟内存的数量,以字节为单位。
EatMem工具主要由一个while循环组成,循环一直执行,直到当前的系统日期/时间超出了工具的启动时间加上由命令行指定的时间间隔(以分钟为单位)。
在主while循环内部,还有第二重for循环,for循环执行8次。第二重循环从功能上说并不是必要的;它在这儿是为了提供一个好的即时的显示进度输出的方式。
工具内部细节
现在让我们详细讨论EatMem工具的每一部分。
我将在这个专栏的下一部分讨论神奇的控制台输出,另外我还将讨论其他替代解决方案以及对基础代码的一些更改。图3展示了让EatMem启动的代码。
首先我声明了一个1024的常量,因为我使用千字节(kilobytes)做EatMem的显示,但是在计算上仍使用字节作为单位。
然后我初始化一个Random对象,以便能生成一些伪随机数。
我使用一项很原始的技术来获取EatMem的运行时间(以分钟为单位)。我的代码假定正好有一个命令行参数,而且刚好是-dN格式,N是一个数字。
如图,我得到了数字的值,因为args[0]代表一个形如“-d123”的串,Substring(2)是“123”这一部分。我使用标准的技术来判断停止时间:创建了代表时间间隔的TimeSpan对象,以分钟为单位,对EatMem的启动时间应用TimeSpan的加号重载操作符,得到了停止时间。
在进入主while循环前,我要做的最后一件事是,使用以下的代码行来显示一个简要的输出头信息。
// print output headers
WriteLine("Begin EatMem for " + duration + " minutes");
WriteLine("Consuming random 10%-70% of virtual memory");
WriteLine("Launch test harness while EatMem stressor runs");
WriteLine(" Available Available Current");
WriteLine(" Physical Virtual Memory");
WriteLine(" Time Memory Memory Allocation Pct。
");
WriteLine("---------------------------------------------");
值得注意的是,为了清晰起见,我对语句稍作了一些改变。你可以通过专栏上的“代码下载”获得准确的语句。
之后,在专栏中你将看到大量的可用附加信息。因为压力器工具试图修改它们自身运行机器的状态,压力器自身的所有输出在某种程序上来说是不必要的。最重要的需要监控的数据是给定时间的可用虚拟内存的数量。
一旦进入了主while循环和辅助for循环,我将花费的时间放在一个TimeSpan对象中,并计算一个随机的百分比,如下所示:
while (DateTime。UtcNow 0。70)
throw new Exception("Bad random pct");
在这儿,我硬编码EatMem,使用标准的映射技术,来生成一个0。
10到0。70之间的随机数。调用Random。NextDouble将生成一个[0。0, 1。0]范围内的数,它大于或等于0。0,严格小于1。0。如果我在这个间隔上乘上(0。70-0。10)=0。
60,这个间隔将映射到[0。0, 0。6]范围的一个随机数。如果再加上0。10,最终这个间隔将映射到[0。10, 0。70]。
下一步将计算出要吃掉的字节数:
// 计算要吃掉的字节数
GlobalMemoryStatus(ref memStatusBuffer);
uint numBytesToEatInBytes =
(uint)(randomPct * memStatusBuffer。
dwAvailVirtual);
uint numBytesToEatInKB =
numBytesToEatInBytes / basicUnit;
我通过C#互操作机制调用Win32 GlobalMemoryStatus方法,以获取当前可用虚拟内存的字节数。
因为原始C++签名请求一个指向结构的指针参数,所以我传给它一个指向MEMORYSTATUS结构的引用。换而言之,以托管代码的方式来表达,我传递了一个结构的引用,因为结构将会被改变。在内存状态信息被存入memStatusBuffer结构后,我可以把它拿出来使用。
我通过将可用字节数乘以上一步计算出的随机百分比,来确定要分配的字节数。为了显示,同样地,我把结果转换为千字节。
现在我要用GlobalAlloc函数来试图分配内存了(见图4)。GlobalAlloc函数接受两个参数。
第一个是一个标志,指定几种分配方式中的一种。
GMEM_FIXED = 0x0000分配固定内存。
GMEM_MOVEABLE = 0x0002分配可移动内存。
GMEM_ZEROINIT = 0x0040初始化内存内容,为0。
GPTR = 0x0040 Combines GMEM_FIXED and GMEM_ZEROINIT。
GHND = 0x0042 Combines GMEM_MOVEABLE and GMEM_ZEROINIT。
这儿,我使用GPTR=0x0040来分配固定内存,并将它初始化为0。
GlobalAlloc的第二个参数是要分配的字节数。如果分配成功,GlobalAlloc返回IntPtr。Zero(大致等于非托管代码的null)。
由于某些原因,分配可能会出现失败;实际上,在压力条件下,我希望有时分配出现失败。所以我并没有抛出异常,我只是打印一个警告信息并重试。GlobalAlloc函数是从堆上分配指定的字节数。
当处理内存分配时,有三个概念要分辨:物理内存、页面文件和虚拟内存。
程序执行指令时,这条指令必须在RAM中。RAM有一些固定大小;在最近的桌面机器上,512M是一个典型的值。如果程序比RAM小,整个程序将被装载到内存中。虚拟内存使得比RAM大的程序可以运行。必要时,操作系统使用页面文件来把代码换入和换出内存。
缺省的最初的页面文件的大小为RAM大小的1。5倍,如果需要的话,页面文件可以增加。虚拟内存的最大值并不相同,在32位Windows上,2GB是一个典型值。
在分配内存后,我需要获得更新后的内存状态,以便显示不同的诊断数据:
// 获取当前的内存状态
GlobalMemoryStatus(ref memStatusBuffer);
uint availablePhysicalInKB =
memStatusBuffer。
dwAvailPhys / (uint)basicUnit;
uint availableVirtualInKB =
memStatusBuffer。dwAvailVirtual / (uint)basicUnit;
numBytesToEatInBytes =
(uint)(randomPct * memStatusBuffer。
dwAvailVirtual);
numBytesToEatInKB = numBytesToEatInBytes / basicUnit;
我再次调用GlobalMemoryStatus函数。
这次我计算出可用的物理内存数,和虚拟内存一样,用千字节表示。然后,我计算出要吃掉的内存的字节数和千字节数,用于显示。
在EatMem中大部分的代码行用于输出显示。我以打印当前花费时间作为开始:
// 打印时间
string time = elapsedTime。
Hours。ToString("00") + ":" +
elapsedTime。Minutes。ToString("00") + ":" +
elapsedTime。
Seconds。ToString("00");
Console。Write("{0,8}", time);
接着,我打印出可用的物理内存和虚拟内存数量,以及要分配的字节数:
// 打印内存数
Console。
Write("{0,12} {1,12} {2,12}",
availablePhysicalInKB + " KB",
availableVirtualInKB + " KB",
numBytesToEatInKB + " KB");
注意,为了简单起见,我显示的已分配内存总数是基于当前的内存状态,而不是GlobalAlloc刚被调用时的内存状态。
接下来,显示被吃掉内存的百分比,以及充当状态条左边界的管道字符(|):
// 打印当前分配的百分数
Console。Write(" " + (int)(randomPct*100。0));
Console。
Write("|");
现在我使用一种古老的手段来显示状态条:
// 显示条
uint totalNumBars = 20;
uint eachBarIs = availableVirtualInKB / totalNumBars;
uint barsToPrint = numBytesToEatInKB / eachBarIs;
string bars = new string('#', (int)barsToPrint);
string spaces = new string('_', 20-(int)barsToPrint);
Console。
Write(bars);
Console。Write(spaces);
Console。WriteLine("|");
在这儿,我决定使用20个字符来显示状态条。假定已经有20个字符,通过将可用虚拟内存的总数除以20,可以计算出每个字符代表的宽度。
状态条由一些字符再加上一些空格组成。字符的个数是已分配内存的千字节数除以状态条中每个字符代表的宽度(eachBarIs)。空格的个数是20减去字符的个数。在前面的代码中我使用“#”字符。稍后在下一部分我将解释如何打印一个神奇的彩色条。
在所有显示工作完成以后,我将线程的执行暂停一会儿,以便测试系统去执行我假定此时正在运行的测试。(记住,一个压力器的整个思路是搭建一个机器环境,使得测试可以在缩减资源的条件下运行。)我强制暂停三秒种,并释放出已分配内存:
// 暂停,使得在测试压力下的应用程序获得执行时间
System。
Threading。Thread。Sleep(3000);
// 释放内存
IntPtr freeResult = GlobalFree(p);
if (freeResult != IntPtr。
Zero)
throw new Exception("GlobalFree() failed");
GlobalFree函数接受指向内存块的指针,该指针通过调用GlobalAlloc函数返回。
GlobalFree试图释放那块内存。如果释放成功,GlobalFree返回null/IntPtr。Zero。如果操作失败,GlobalFree返回输入参数。对压力器来说,如果GlobalFree失败,将遇到严重的问题,并需要抛出一个致命的异常。
EatMem工具在做完两件事后结束,在每8个辅助的for循环结尾显示一个信息条,并在主while循环结束后显示一个“完成”信息,主while循环在当前时间超出指定运行时间时终止执行。
很酷的控制台输出
写命令行测试工具时,适当地使用文字颜色可以使你的输出易于阅读和解释。
例如,EatMem使用绿色代表内存数,红色代表错误信息,以及使用白色的空格建立内存分配状态条。我的同事告诉我,我在这儿过分地在意颜色的使用。在Microsoft(R) 。NET Framework 1。
1环境下打印彩色的控制台文字的技巧是,使用Win32函数GetStdHandle以及SetConsoleTextAttribute。你可以声明如下:
[DllImport("kernel32。
dll")]
extern static IntPtr GetStdHandle (int nStdHandle);
[DllImport("kernel32。dll")]
extern static bool SetConsoleTextAttribute(
IntPtr hConsoleOutput, int wAttributes);
要设置文字颜色,首先调用GetStdHandle,它返回标准输出的句柄。
然后调用SetConsoleTextAttribute来指定文字颜色。这段代码将打印淡黄色的“Hello”,然后打印淡红色的“Bye”:
const int STD_OUTPUT_HANDLE = -11;
IntPtr hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hStdOut, 0x0006);
Console。
WriteLine("Hello");
SetConsoleTextAttribute(hStdOut, 0x0004);
Console。WriteLine("Bye");
值-11是一个魔数常量,代表标准输出。
常量0x0006和0x0004相应地代表淡黄和淡红。指定的文字颜色持续有效,直到再次调用SetConsoleTextAttribute。控制台输出时,颜色常量的“淡”版本对于文字来说太暗淡。你可以通过逻辑或0x0008来强化任意文字颜色常量。
因此,这段代码将以亮黄色打印“Howdy Dowdy”:
SetConsoleTextAttribute(hStdOut, 0x0006 | 0x0008);
Console。Write("Howdy Dowdy");
由于处理十六进制常量非常恼人,通常在你的工具中放置符号常量。
这儿是一些我常用于命令壳程序的常量:
const int BLACK = 0x0000;
const int BLUE = 0x0001 | 0x0008; // 亮蓝
const int GREEN = 0x0002 | 0x0008; // 亮绿
const int CYAN = 0x0003 | 0x0008; // etc。
const int RED = 0x0004 | 0x0008;
const int MAGENTA = 0x0005 | 0x0008;
const int YELLOW = 0x0006 | 0x0008;
const int WHITE = 0x0007 | 0x0008;
除了指定文字颜色以外,你还可以指定背景颜色。
这是通过在文字颜色上,或(OR)上代表背景颜色的另一常量。例如,这段代码将把文字上下文设置为亮黄色(0x0006|0x0008),背景色为淡红色(0x0040):
SetConsoleTextAttribute(hStdOut, 0x0006 | 0x0008 | 0x0040);
通常,我只喜好使用亮白、淡白(灰色)以及黑色作为我的背景色。
以下显示我用于EatMem中的常量:
const int BGWHITE = 0x0070 | 0x0080;
const int BGBLACK = 0x0000 | 0x0000;
const int BGGRAY = 0x0070;
使用这些颜色代码,你可以完成大量的颜色格式化工作。
这儿展示了我如何打印在EatMem中的内存状态条:
string bars = new string('_', (int)barsToPrint);
string spaces = new string('_', 20-(int)barsToPrint);
SetConsoleTextAttribute(hStdOut, BLACK | BGWHITE);
Console。
Write(bars);
SetConsoleTextAttribute(hStdOut, WHITE | BGBLACK);
Console。Write(spaces);
SetConsoleTextAttribute(hStdOut, GREEN | BGBLACK);
Console。
WriteLine("|");
通过在白色背景上打印黑色空格,可以生成一个白色的条。然后如果改换成在黑色背景上打印白色文字,将生成一个黑色的背景。
我在每个条下面使用下划线产生一条线。
。
NET Framework 2。0中的Console类大大地增强了对奇特输出的支持。如果你工作在。NET Framework 2。0环境中,相比P/Invoke机制而言,使用新的Console类是一个更好的方法。
Console类包括一系列的属性和方法:获取和设置屏幕缓存大小,控制台窗口大小,以及光标;改变控制台窗口的位置以及光标的位置;移动或清除屏幕缓冲区中的数据;改变前景和背景颜色;改变控制台标题栏上显示的文字;播放哔哔声。
工具的扩展
我展示的EatMem工具可以原封不动运行,但是它被特别地设计以便你可以很容易地修改。接下来让我们看看在EatMem初始版本基础上,你可能想扩展或增强的几种方式。
最可能修改的地方涉及内存分配技术。
EatMem使用Win32 GlobalAlloc和GlobalFree函数,但是这儿有几种内存分配函数供你使用。一种可能是使用Marshal。AllocHGlobal和Marshal。FreeHGlobal托管方法;他们分别是LocalAlloc和LocalFree的托管包装。
AllocHGlobal在某种程度上比GlobalFree更易于使用,但不如GlobalFree灵活。如果你想避免直接使用P/Invoke机制,解决方案是使用AllocHGlobal。
另一种替换GlobalAlloc和GlobalFree的方案是使用VirtualAlloc和VirtualFree Win32函数。
VirtualAlloc函数保留或提交一些位于调用者进程的虚拟地址空间的页面区域。VirtualAlloc的示例,参见图5。
你可能想到在基础EatMem版本上的一个增强是,防止多次内存分配的失败。
如代码所示,如果内存分配失败,EatMem简单地打印一个错误消息,并重试。这可能导致一系列不被注意到的分配失败。你可以很容易地增加一个计数器,跟踪连续分配失败的次数,如果计数器达到上限值,你可以抛出异常或动态调整内存分配的随机最大分配百分比(当前硬编码为70%)。
在这种增加上的一种可能的变化是跟踪内存分配失败的百分比,并相应地调整压力器参数。
如果你在。NET Framework 2。0下工作,而且并不想使用托管的AllocHGlobal,因为你需要Win32 GlobalAlloc的灵活性,你可以利用新的SafeHandles命名空间,它包括了对文件和操作系统句柄的功能性支持的类。
你。收起