【IT168 技术文章】
TUXEDO作为一种成熟的事务处理中间件,一般用于大型系统的业务处理。在这样的系统中,一般客户端请求的并发数很大,而且对实时性要求很高,需要在规定的时间内处理完一个事务,并返回结果给用户。而且对系统的稳定性要求也很高,一般都要求7x24运作。为满足以上要求,除了系统各部分有良好的设计、编码和测试外,还需要在上线前对系统的处理能力、极限容量等做一个测试和评估,以便获得关于系统的更真实的性能情况。这种测试是从外部来观察系统的整体情况,比起某个部分的性能评比更有实际的意义,而且使得项目开发人员和以后的运行维护人员对系统的整体性能有一个具体的认识,便于调整和日后的维护。
接下来笔者以自己参与开发的一个大型电信项目为依托,结合自己设计和实现的TUXEDO服务器压力测试工具来分析一下压力测试中的一些方法和过程,以及要注意的问题。
压力测试必须有一定数量的并发客户端。为了测试,准备大量的PC,并在每一个上装一个client程序是不现实的,而且难以做到真正的并发,也不利于客户端数目不断变动的大量的测试,因此我们采用了软件模拟客户端的方法。对TUXEDO服务器而言,每一个客户端就是一个和它通信的进程,所以需要多少客户端简单的说就是开多少调用服务的进程,这个在OS的支持下是很容易实现的。这里有一个问题需要讨论,一般会想到用一个现成的压力测试软件来做。实际中发现,这种方式是有问题的。一般压力测试工具的方法是截获一个client到server的调用数据包,分析其中的数据,然后将一些数据进行参数化,例如一些ID等,然后生成一个可以产生大量并发的同类数据包的脚本,运行该脚本就可以进行压力测试,当然其中包括了很多度量。对于某些应用,例如新开户等,这种方法可以工作得很好,但是对于那些对数据真实性要求很高的服务,这种方法就难以实现。例如笔者做的压力测试中涉及的主要TUXEDO服务——用户帐单的查询和销帐处理。由于用户的电话号码或者帐号等通常是不连续的,中间有很多的空洞,而且销帐的服务是依赖于查询的返回结果的,如果不是数据库中一条真实的可以做销帐处理的帐单是不能成功的完成销帐流程的。这对构造数据带来很大的难度,而且那种构造出来的数据和实际的情况会有很大的差距,因为那可能被数据库的缓冲等进行优化,不能反映真实的性能,而最好的办法当然是用实际数据库中的真实数据。当然,这种真实也可以有一定的扩充,下面会涉及到关于压力测试数据的准备问题。通过上面的讨论,我们知道在一些实际的系统中,让了解系统应用的人自己动手来做压力测试很多时候是一个更好的选择。不必担心这个工作的复杂性,下面我们就相关的问题开始详细的分析。
1.模拟出指定数目的客户端。
开多进程需要OS的支持,下面以UNIX为例给出了一个实现的代码。需要特别注意的是后面注释有*的那一行代码,该行的意义是在子进程(pid=0,fork对父进程和子进程有不同返回,参考[1])中不再执行该循环。在UNIX中,子进程从fork()的下一句开始执行。如果没有上面那一句,新开出的子进程发现满足for循环的条件将继续执行循环,开出新的它自己的子进程,这样会产生复杂的进程树,可以计算那样得出的子进程的数目为。为更好的控制客户端的数量和保持进程相互关系的简明,我们通过上面那一句使得进程的关系只有两层,而且进程数目就等于P_NUM。
for ( i = 0; i < P_NUM; i++ ) //P_NUM为要开的进程数,也就是模拟客户端的数目
{
if ( (pid = fork()) == 0 ) break; //*
if ( pid < 0 ) exit(0);
}
if ( pid == 0 ) //子进程代码
{
child_process();
}
if ( pid ) //父进程代码
{
…
}
通过上面的代码,我们对客户端的产生有了清楚的认识,在进程创建后,父、子进程分道扬镳,开始各自的压力测试之旅。
2.系统真实数据的获取和传递
上面我们讨论了真实数据对压力测试结果可信程度的重要意义,现在的问题是如何设计一个获取数据库中数据并传递给调用服务的客户端的数据流程。这个前提是我们要对被测的服务比较熟悉,了解它的大致处理流程和涉及的数据。
以帐单的销帐为例,它需要销帐方式、操作员工号、涉及的每条帐单的部分信息,这些信息可以由用户的输入和帐单查询服务来得到。用户的输入我们可以在测试程序中设定,帐单的信息我们可以在之前调用查询服务来获取,因为销帐总是在帐单查询之后进行的,所以这个流程也是符合实际情况的。接下来的问题是帐单的查询也是需要有效的输入参数的,我们如何获取这些数据呢?答案自然还是从数据库中来。在真实系统中,帐单查询是前台接收到用户请求,根据用户的电话号码等来查询,在这里我们可以一次从数据库中取出一批有帐单的用户的信息(电话号码等)来作为查询的输入条件。因为这个取编号的操作不是真实系统应有的开销,所以我们可以在模拟客户端开始运行之前完成。
至此,我们对数据的流程有了一个大概的认识。下面就是具体实现的问题了。编号是一次性取出的,而且根据测试需要其数量是可以预先知道的,我们可以将其保存在一个大的数组中,后面我们可以看到用这种数据结构的好处。
获得了最基本的输入,我们就可以开始压力测试程序内部的数据流的设计了。
3.父、子进程的工作及数据通信方式
为保证并发,每个client进程都应该是独立的,而且自身运行必须维持一段时间,这样才能在TUXEDO服务器一端产生大量稳定的连接,进而产生调用,形成压力。每个client进程的工作是得到一个编号,调用帐单查询的服务获取对应用户的帐单,然后整理返回结果,加入操作员信息,再调用销帐服务,获得返回结果。每个client重复上面的过程,重复的次数作为一个参数n,这个是可以配置的,P_NUM * n 就是需要准备的编号的数量,这两个参数需要结合测试的要求来选择,通过改变他们可以进行多次不同的测试。
父进程的工作是获取编号到数组中,然后产生P_NUM个模拟的客户端,接下来它要给这些client分发数据,使得每个client可以持续运行。上面提到的编号数组相当于一个粮食仓库,父进程要给子进程稳定的粮食供应,使其不致于因为“饥饿”而导致停顿。分析这里的数据,还有一个特点就是每个数据都是一次性的,不能再次使用。父进程通过数组的下标可以很容易的保证数据的不重复,这也体现了数组在这种情况下的简洁高效。在父进程输送数据给子进程的过程中,我们考虑后采用了队列来实现,因为要保证每个子进程获得的数据都是唯一的,而且每个子进程最好能从一个已知的地点获取数据,这样便于子进程数目的伸缩,而队列恰好能满足这些要求。由于队列的访问是有控制的,大量的子进程去取可能会造成等待,但是实际测试中我们发现这种影响是很小的,在客户端数目为1000左右时仍不明显,因而我们可以认为队列是高效的。当然这里的问题也可能有更好的解决的办法,队列相对而言比较简单和清晰。
压力测试程序必须返回相应的结果,例如每个调用所耗的时间等等,这里不详细说明,可以根据实际的需要安排相关的代码来实现。
| 第1页: 模拟出指定数目的客户端 | 第2页: 测试系统的示意图 |