FastCGI、ModPerl、PHP、普通CGI、SSI性能实测对比

/ns/wz/soft/data/20040225014342.htm


作者:NetDemon
email:netdemon@20cn.com
http://www.20cn.net/
本文版权属20CN网络安全小组所有,允许转载,但需保持文章的完整性并注明出处


现在的Web技术五花八门,不断有新的技术出现,如果您想制作一个能承受巨大访问量的互动网站(比如论坛等),那种方式最适合你呢?本文试图以比较实际的测试手法,对比一下各种技术的真实效果,测试包括了各种语言编写的CGI程序在不同的web环境下的效率,主要针对unix上的情况对windows的IIS,因为实际条件的限制,没法在同一个硬件条件下测试,因此没法完成.

阅读文章之前,我希望您先搞清楚一个概念,不要把CGI和perl语言联系在一起。

CGI英文全称是 Common Gateway Interface,通常翻译为共同网关接口,是HTTP服务器与机器上的其他程序进行通信的一个接口。这个“其他程序”可以使用任何计算机语言来编写,它通过CGI这个接口从HTTP服务器取得输入,然后把运行的结果又通过CGI这个接口交给HTTP服务器,而HTTP服务器把这个结果送给浏览器。

perl是一种解释形的计算机语言,具有强大且方便的文本处理能力,被誉为计算机的“瑞士军刀”,它被除了Windows之外的几乎所有的其他服务器操作系统所默认安装。BSD UINX 上有些系统命令就是用perl编写。如whereis which adduser catman等。因为HTTP动态页面主要就是文本的处理所以perl也就非常的适合来编写这个“其他程序”。这也就造成了一个很好笑的事实,上google搜索CGI,出来的国内网站基本都是在谈perl,在很多人眼里,CGI成了一种语言,成了perl的代名词。

CGI的出现让WEB从静态变为为动态,随着Web的越来越普及,很多的网站的都需要有动态的页面,以便与浏览者互交。CGI方式的缺点也越来越突出。因为HTTP要生成一个动态页面,系统就必须启动一个新的进程以运行CGI程序,不断地fork是一项很消耗时间和资源的工作。如果能够让HTTP服务器本身就支持一种语言,用这个语言来编写动态页面的话,这就至少不需要fork。因此就出现一种叫做动态网页设计语言的东西,如php,asp,jsp。这些不是真正的语言,他们都只是专门用于web的,脱离了web环境,他们就什么都干不了(php正在努力改变这种现状)。

而SSI(服务端包含)是一种早期的较为简单及粗糙的动态方式,能够实现的功能很少,基本上现在很少使用,但如果只是简单的处理,这也未尝不是一个好的方法。他是Http服务器本身所支持的"语言"。

modperl简单点说就是类似php的方式,让apache http服务器本身能够支持perl语言,除了能够不需要每一个请求就启动perl解释器来解释perl之外,还能够让你可以用perl来写apache的模块,让你自己的程序成为http服务器本身的功能,以及用perl语言来设置httpd.conf等,详细情况请见http://perl.apache.org/

至于fastcgi,那又是另外的一种方式,他让这个CGI程序,以类似系统的守护进程(NT服务)那样的运行,而不是每次请求都是启动一个新的进程,从理论上讲,这是目前最为有效的提高效率的方式。而且和CGI一样,不局限于用什么语言来写。详细资料:http://www.fastcgi.com/

无论用哪种方式,都不会比静态html快,我这里之所以把html的测试也包括主要是作为对比。

我在写这篇文章之前,也有其他的爱好者对这些方面做了一些测试并写有文章,但我觉得有些测试其实不大现实,比如,有些测试的静态文件是<html><body>hello World!</body></html>,而cgi是print "<html><body>hello World!</body></html>"; ,然后用并发1000个以上的连接来测试,但是现实我们怎么可能是这样的呢?如果只是把一个常量print出来,我们何苦用cgi?现实中我们的cgi一般总是需要对传入的HTTP变量作适当的处理,然后读取一个或多个文件或是读取数据库,继续处理,然后再输出。而实际的并发连接也通常不可能达到500或1000的数量。

因此我的测试尽量模拟比较现实的web行为,作为静态html参考的是20CN主页的"关于我们"这一页,http://www.20cn.net/aboutus.html,如果你简单的浏览一下20CN的各个页面,你可以发现,各个页面都有类似的地方,上方的logo,banner,主栏目连接部分是相同的,而下方的版权说明部分也是相同的,只是中间的主要页面主体部分不相同,因此我们可以认为这个站的各页面有3部分构成,如果用动态web的话,通常我们就是把相同的部分保存一个文件,每一页都输出这个内容,这样如果修改了这文件,整个网站都更改,不需要每页逐一修改,这也是动态web的最基本应用了(很遗憾,20CN因为早年的现实原因,实际并不是这样的实现的)。所以我就把这个about.html拆解为header.html fooder.html textbody.html三个文件,对应上、下、中。

而测试程序是这样,首先假设有一个输入,把这个输入变量处理分析过滤一通,然后读入这三个文件,把textbody这一部分也处理一通,然后再print出来。因为实际的处理应该还不止这么简单,因此测序程序我尽量使用非常没效率的处理方法作拟补。然后利用apahce自带的程序工具进行测试,并发连接数量方面我只使用50个,所谓并发(同时)连接50是指当apache把一个页面的数据在给浏览器1发送的这段时间内,他还同时还在给浏览器2 - 50发送,这样的情况相当于大概1000以上的人同时在线的网站,才有可能出现这样的情况。

用于测试的几种程序都是一样的工作流程:
打开3个文件-〉过滤输入-〉生成一个12位的随机字符-〉在文件2中查找这个字符-〉把文件2中所有的2、0、C、N的字符去掉-〉输出结果。(本来还有更加复杂的处理,但是用C实现起来很麻烦,就只能如此了),而shtml因为没办法做这样的处理,他只是简单的include这3个文件而已,aboutus.shtml的内容如下:
<!--#include virtual="/header.html" -->
<!--#include virtual="/textbody.html" -->
<!--#include virtual="/fooder.html" -->

好了,说了这么多,我看你也不耐烦了,下面就是你所关心的最重要的测试结果:

1.html静态页面KeepAlive
# ab -c50 -n3000 -k http://www.20cn.net/aboutus.html

Keep-Alive requests: 2985
Total transferred: 25224674 bytes
HTML transferred: 24266107 bytes
Requests per second: 1346.50 [#/sec] (mean)
Time per request: 37.13 [ms] (mean)
Time per request: 0.74 [ms] (mean, across all concurrent requests)
Transfer rate: 11321.67 [Kbytes/sec] received

2.html静态页面没有KeepAlive
# ab -c50 -n3000 http://www.20cn.net/aboutus.html

Requests per second: 916.03 [#/sec] (mean)
Time per request: 54.58 [ms] (mean)
Time per request: 1.09 [ms] (mean, across all concurrent requests)
Transfer rate: 7675.88 [Kbytes/sec] received

因为其他的方式都不能使用KeepAlive,而且ab的结果中最有意义的就是Requests per second(每秒完成多少次请求),我们取 Requests per second 916.03 作为下面测试的比较基准

3.SSI
# ab -c50 -n3000 http://www.20cn.org/aboutus.shtml
Requests per second: 507.10 [#/sec] (mean)

4.Perl写的普通CGI
# ab -c50 -n3000 http://www.20cn.org/cgi-bin/bench.pl
Requests per second: 8.00 [#/sec] (mean)

5.C写的普通CGI
# ab -c50 -n3000 http://www.20cn.org/cgi-bin/bench.cgi
Requests per second: 29.26 [#/sec] (mean)

6.Mod-Perl
# ab -c50 -n3000 http://www.20cn.org/mod-perl/bench.pl
Requests per second: 200.03 [#/sec] (mean)

7.PHP
# ab -c50 -n3000 http://www.20cn.com/bench.php
Requests per second: 180.14 [#/sec] (mean)

8.Perl写的FastCGI
# ab -c50 -n3000 http://www.20cn.org/cgi-bin/bench.fpl
Requests per second: 262.05 [#/sec] (mean)

9.C写的FastCGI
# ab -c50 -n3000 http://www.20cn.org/cgi-bin/bench.fcgi
Requests per second: 503.44 [#/sec] (mean)


结果非常的明显,用C的fastcgi达到了和shtml几乎一样的速度,考虑到shtml几乎不需要处理什么,我们可以认为C的Fastcgi比服务端包含还快。而perl写的FastCGI比C的性能差了近一半。但依然比其他任何方式都快。Mod-perl和php的原理差不多,只是用的语言不一样,所以他们的测试结果也是差不多。结果中最为突出的就是perl的普通CGI方式,他一定最低,这是测试之前就明白的道理。但低到这样的每秒只有8次请求的程度,就大跌眼镜了,只有C的普通CGI的4分之1。任何用perl写的以普通CGI方式运行的论坛阿什么的,说什么高速、海量都是完全吹牛的,本质就决定了它的不可能,完成3000次请求历时6分多钟,让我都没有耐心等待测试结果,本来准备进行的写入的测试也不想做了,留着等下回有耐心的时候再做吧。主要是测试带有写入,以及操作数据库的各种方式的差别。

测试环境:
硬件:PIII733 CPU 512M内存 IDE硬盘
OS:FreeBSD4.9-stable
软件:apache1.3.17+mod_fastcgi-2.2.12+mod_perl-1.27+mod_php4-4.3.4

Perl测试程序:
#!/usr/bin/perl

use 5.004;
use strict;
use Fcntl qw(:flock);
use CGI qw(:cgi);
use FCGI;#普通CGI 和 mod_perl 注释掉此行
$| = 1;

#读入2个固定文件,也是fastcgi的init部分
my @header = readfile("../data/header.html");
my @fooder = readfile("../data/fooder.html");

while(FCGI::accept() >= 0) { #进入fastcgi循环,普通CGI 和 mod_perl 注释掉此行

#取得一个url变量,只是为了让CGI.pm运行
#过滤非数字和英文字母
my $param = param('param');
$param =~ s/\W//g;

#现实中我们通常根据用户的输入请求打开一个文件
#但测试的话我们把这个变量改为固定的一个文件
$param = "../data/textbody.html";
my @textbody = readfile($param);

#把textbody拷贝到另外一个数组进行处理
#我们不要处理的结果,只为了让程序作点什么而已
my @tmp = @textbody;
chomp(@textbody);
foreach(@tmp){
my $myrand = myrand();
if(/^$myrand/){
$_ .= $_;
}
$_ =~ s/[2][0][C][N]//g;
}

#然后把textbody写到一个随机名字的文件
#这是带有写入的测试,在只读的测试中,我们不要这一步
#直接跳到 my @out = (@header,@textbody,@fooder);
my $filename = myrand();
if(writefile("/tmp/$filename",\@textbody)){
#成功就把继续把这个文件再读进来一次,和原来的文件合并输出
#并把文件删掉
my @rebody = readfile("/tmp/$filename");
chomp(@rebody);
my @out = (@header,@rebody,@fooder);
print "Content-type:text/html;charset=gb2312\n\n";
print "@out";
unlink("/tmp/$filename");
}else{
#不成功输出错误信息
error("WRITE_FILE_ERROR");
exit;
}

}# 结束Fastcgi循环,普通CGI 和 mod_perl 注释掉此行

######### 子程序 #######
#读参数1指定的文件名,返回文件句柄
sub readfile {
unless(open(FH,"<$_[0]")) {error("READ");exit;}
unless(flock(FH,LOCK_SH)) {error("R_LOCK");exit;}
return <FH>;
close(FH);
}

#把参数2引用的数组内容逐行写入参数1指定的文件名,成功返回真
sub writefile {
my $file = shift;
my $data = shift;
unless(open(FH,">$file")) {return 0;}
unless(flock(FH,LOCK_EX)) {return 0;}
foreach my $line(@$data){
print FH "$line\n";
}
close(FH);
return 1;
}

#显示出错信息
sub error {
print "Content-type:text/html;charset=gb2312\n\n";
print "<html><body><p align=\"center\">$_[0]</body></html>";
}

#生成12位随机字符
sub myrand {
my @salt_chars=('a'..'z');
my $salt;
for (my $i=0;$i<12;$i++) {
$salt .= $salt_chars[rand(26)];
}
return $salt;
}

C测试程序:
#include <fcgi_stdio.h> //非Fastcgi为<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

#define MAX_FILE_SIZE 1024*512

void MakeRandStr (char* str);
int ReadFile(char *filename,char *buf);
void Filter(char *buf, char *param);

int main(void){

char header[MAX_FILE_SIZE] = {0};
char fooder[MAX_FILE_SIZE] = {0};
char body[MAX_FILE_SIZE] = {0};
char html[MAX_FILE_SIZE * 3] = {0};
char tmp[MAX_FILE_SIZE * 3] = {0};
char randstr[16],param[512],buf[512];
int i,j,len;
char *query_string,*p;

//读入2个固定文件,也是fastcgi的init部分
ReadFile("../data/header.html",header);
ReadFile("../data/fooder.html",fooder);

while(FCGI_Accept() >= 0){ //fastcgi开始
//取得url变量,并对它过滤
query_string = getenv("QUERY_STRING");
strncpy(buf,query_string,512);
Filter(buf,param);

//然后打开一个我们需要显示的文件
ReadFile("../data/textbody.html",body);
MakeRandStr (randstr); //生成随机字符串

//把textbody拷贝到另外一个地方进行处理
strcpy(html,body);
//查找字符
p=html;
len = strlen(p) - strlen(randstr);
for(i=0;i<len;i++){
if(!strncmp(p,test,strlen(randstr))){
strcat(html,"1");
}
p++;
}
p=strtok(html,"20CN"); //去掉20CN
strcat(tmp,p);
while((p=strtok(NULL,"20CN"))){
strcat(tmp,p);
}
//处理完毕,把结果输出,我们做这些处理只是为了让程序消耗点资源
//为了最终的输出和作对比的html大小保持一致,我们还是输出原来的
sprintf(html,"%s\n%s\n%s\n",header,body,fooder);
printf("Content-type:text/html;charset=gb2312\n\n");
printf("%s\n",html);
}
}

/********* 子程序 *************/

//读文件
int ReadFile(char *filename,char *buf)
{
int fd,len = 0;
struct stat st;
fd = open(filename,O_RDONLY);
if(fd == -1)
return 0;
else
flock(fd,LOCK_SH);
fstat(fd,&st);
if(st.st_size < MAX_FILE_SIZE)
len=read(fd,buf,st.st_size);
else
len=read(fd,buf,MAX_FILE_SIZE);
return len;
}
//产生12位随机字符
void MakeRandStr (char* str)
{
int i, ch = (int)time(0);
srand(ch);
for(i=0;i<12;i++){
ch = rand()%26+ 97;
str[i] = ch;
}
str[i] = '\0';
}
//对非数字和英文字母的字符串进行过滤
void Filter(char *buf, char *param)
{
int i,j;
for(i=0;i<512;i++){
if(!isalnum(buf[i]))
buf[i] = ' ';
}
for(i=0,j=0;i<512;i++){
if(!isspace(buf[i])){
param[j] = buf[i];
j++;
}
}
}


php测试程序:
<?php

/* 过滤 URL GET 方式传入变量 param */
$param = preg_replace("/\W/", '', $_GET['param']);

/* 打开三个文档 */
$File = array('../data/header.html', '../data/fooder.html', '../data/textbody.html');
if(($fp1 = fopen($File[0], 'r')) === false) exit(1);
if(($fp2 = fopen($File[1], 'r')) === false) exit(1);
if(($fp3 = fopen($File[2], 'r')) === false) exit(1);

/* 读入 tmp */
$tmp1 = fread($fp1, filesize($File[0]));
$tmp2 = fread($fp2, filesize($File[1]));
$tmp3 = fread($fp3, filesize($File[2]));
$tmp = $tmp3;

fclose(fp1);
fclose(fp2);
fclose(fp3);

/* 取得 12字符随机字串 */
for($i = 0; $i < 12; $i++) {
srand((double)microtime() * 1000000);
$ran[$i] = rand() % 26 + 97;
}

/* 查找字串 */
$find = strspn($tmp, $ran);

/* 过滤 2,0,C,N */
$fliter = preg_replace("/[2|0|C|N]/", '', $tmp);

echo "$tmp1$tmp2$tmp3";
?>



===============================================
本文版权属20CN网络安全小组及其作者所有,如有转载,请保持文章完整性并注明出处
文章类型:原创 提交:NetDemon 核查:NetDemon