PS:Siege是一款开源的压力测试工具,设计用于评估WEB应用在压力下的承受能力。可以根据配置对一个WEB站点进行多用户的并发访问,记录每个用户所有请求过程的相应时间,并在一定数量的并发访问下重复进行。siege可以从您选择的预置列表中请求随机的URL。所以siege可用于仿真用户请求负载,而ab则不能。但不要使用siege来执行最高性能基准调校测试,这方面ab就准确很多。
Siege官网:http://www.joedog.org/
Siege下载:wget http://www.joedog.org/pub/siege/siege-latest.tar.gz
解压并安装:
# tar -zxvf siege-latest.tar.gz
# cd siege-2.72/
# ./configure
# make
# make install
参数详解:
-C,或–config 在屏幕上打印显示出当前的配置,配置是包括在他的配置文件$HOME/.siegerc中,可以编辑里面的参数,这样每次siege 都会按照它运行.
-v 运行时能看到详细的运行信息
-c n,或–concurrent=n 模拟有n个用户在同时访问,n不要设得太大,因为越大,siege 消耗本地机器的资源越多
-i,–internet 随机访问urls.txt中的url列表项,以此模拟真实的访问情况(随机性),当urls.txt存在是有效
-d n,–delay=n hit每个url之间的延迟,在0-n之间
-r n,–reps=n 重复运行测试n次,不能与 -t同时存在
-t n,–time=n 持续运行siege ‘n’秒(如10S),分钟(10M),小时(10H)
-l 运行结束,将统计数据保存到日志文件中siege .log,一般位于/usr/local/var/siege .log中,也可在.siegerc中自定义
-R SIEGERC,–rc=SIEGERC 指定用特定的siege 配置文件来运行,默认的为$HOME/.siegerc
-f FILE, –file=FILE 指定用特定的urls文件运行siege ,默认为urls.txt,位于siege 安装目录下的etc/urls.txt
-u URL,–url=URL 测试指定的一个URL,对它进行”siege “,此选项会忽略有关urls文件的设定
urls.txt文件:是很多行待测试URL的列表以换行符断开,格式为:
[protocol://]host.domain.com[:port][path/to/file]
用法举例:
siege -c 300 -r 100 -f url.txt
说明:-c是并发量,-r是重复次数。url.txt就是一个文本文件,每行都是一个url,它会从里面随机访问的。
url.txt内容:
http://192.168.80.166/01.jpg
http://192.168.80.166/02.jpg
http://192.168.80.166/03.jpg
http://192.168.80.166/04.jpg
http://192.168.80.166/05.jpg
http://192.168.80.166/06.jpg
结果说明:
** SIEGE 2.72
** Preparing 300 concurrent users for battle.
The server is now under siege.. done.Transactions: 30000 hits //完成30000次处理
Availability: 100.00 % //100.00 % 成功率
Elapsed time: 68.59 secs //总共使用时间
Data transferred: 817.76 MB //共数据传输 817.76 MB
Response time: 0.04 secs //响应时间,显示网络连接的速度
Transaction rate: 437.38 trans/sec //平均每秒完成 437.38 次处理
Throughput: 11.92 MB/sec //平均每秒传送数据
Concurrency: 17.53 //实际最高并发连接数
Successful transactions: 30000 //成功处理次数
Failed transactions: 0 //失败处理次数
Longest transaction: 3.12 //每次传输所花最长时间
Shortest transaction: 0.00 //每次传输所花最短时间1,发送post请求时,url格式为:http://www.xxxx.com/ POST p1=v1&p2=v2
2,如果url中含有空格和中文,要先进行url编码,否则siege发送的请求url不准确添加
siege -C 可以查看相关的配置参数,可以自行修改,比如是否显示log,超时时间
在英语中,“Siege”意为围攻、包围。同时Siege也是一款使用纯C语言编写的开源WEB压测工具,适合在GNU/Linux上运行,并且具有较强的可移植性。之所以说它是多线程编程的最佳实例,主要原因是Siege的实现原理中大量运用了多线程的各种概念。Siege代码中用到了互斥锁、条件变量、线程池、线程信号等很多经典多线程操作,因此对于学习多线程编程也大有裨益。最近花了一些时间学习到了Siege的源代码,本文将介绍一下Siege压测工具的内部原理,主要供系统测试同学、以及学习多线程编程的同学们参考。
一、工具背景
Siege是一名叫做Jeff Fulmer的伙计发起的开源项目,他的主页是:http://www.joedog.org/ 。从页面上看,Jeff Fulmer自从1999年起便开始“serving the Internets”,也算是一名老程序员了。Siege可谓是作者最杰出的作品。这款压测工具的名称“围攻”也比较生动形象展示了工具用途,即“围攻web服务器”。
Siege使用多线程实现,支持随机访问多个URL,可以通过控制并发数、总请求数(or压测时间)来实现对web服务的压测。Siege支持http,https,ftp三种请求方式,支持GET和POST方法,压测方式为同步压测,全部源代码总共13000行。功能还是非常全面的,很适合web开发在服务器开发完成后进行自测时使用。
二、工具使用
该工具主要在Linux环境下使用,下载链接为:http://download.joedog.org/siege/ 。安装方式和正常的linux环境软件安装步骤大致相同,先解压缩,再 config->make->make install。
$ tar –xzvf siege-3.0.8.tar.gz
$ cd siege-3.0.8
$ ./config
$ make
$ make install
在安装中需要注意的是make和make install可能会要求管理员权限,所以可能需要在make 和make install前面加上sudo。
使用方法如下:
siege [options] 或者 siege [options] URL其中options可选项有:
复制代码
-V --version 打印版本信息
-h --help 打印帮助信息
-v --verbose 在测试过程中输出更多的通知信息
-C --config 打印当前的配置信息(siege有一个名为.siegerc的配置文件)
-q --quite 此选项会覆盖掉--verbose,是安静模式,在测试中减少信息输出
-g --get 显示http头信息,适用于debug
-c --concurrent 最为常用的参数,每次测试必设置,并发数量,例 -c10代表10个并发
-i --internet 随机点击URL,在同时测试多个URL时可以使用,模拟用户随机访问的情形
-b --benchmark 每个请求之间没有延时,也是很常用的设置
-t --time 非常常用的参数,设置测试的时间,默认以分钟为单位,其他单位要自己设置,例如 -t10s,测试持续10秒
-r --reps 非常常用的参数,指定了测试几个回合结束,本参数和-t都可用来设置测试结束条件。
-f --file 指定一个存放URL链接的文件。siege支持随机访问多个url,因此这些url链接在文件中提供,较为常用。
-l --log 指定log文件,如果没有指定的话siege也有默认文件保存位置,文件名siege.log
-d --delay 指定时间延迟,在每个请求发出后,再随机延迟一段时间再发下一个
-H --header 指定http请求头部的一些内容
-A --user-agent 指定http请求中user-agent字段内容
-T --content-type 指定http请求中的content-type字段内容
复制代码
上面列了一大坨参数,其实还没有列全,有一些更少用的没有列出来。实际上,如果只是简单使用的话,大部分都不需要搞清楚。上文中有几个常用的功能选项已经注明(-b, -c, -t, -r, -f),掌握这几个基本就够用了。我们先来简单使用一下,有一个更清楚的认识。
复制代码
horstxu@horstxu-Lenovo-G400:~/Downloads/siege-3.0.8$ siege http://www.[某个网站].com -c10 -t5s -b
** SIEGE 3.0.8
** Preparing 10 concurrent users for battle.
The server is now under siege...
HTTP/1.1 200 0.14 secs: 1917 bytes ==> GET /
HTTP/1.1 200 0.15 secs: 1917 bytes ==> GET /
……………………
HTTP/1.1 200 0.16 secs: 1917 bytes ==> GET /
Lifting the server siege... done.
Transactions: 325 hits
Availability: 100.00 %
Elapsed time: 4.89 secs
Data transferred: 0.59 MB
Response time: 0.15 secs
Transaction rate: 66.46 trans/sec
Throughput: 0.12 MB/sec
Concurrency: 9.85
Successful transactions: 325
Failed transactions: 0
Longest transaction: 0.21
Shortest transaction: 0.11
复制代码
上面省略号省略了一些冗余的输出,并且我们屏蔽网站域名免得打广告。在上面的测试中,我们设置了10个并发用户,测试5秒时间,并且每个请求之间没有时延,也就是收到回复后马上发出下一个。测试的结果是,4.89秒内完成了325次请求,共传输0.59MB的数据,平均响应时间0.15秒,平均每秒66.46次请求,拓扑量0.12MB每秒,并发数平均9.85。统计的数据还算比较全面。
三、原理介绍
先简单画一下程序的流程图,如下图所示
如果并发用户数为n,那么就会相应创建n个压测线程,每个线程模拟1个用户。除了压测线程之外,主函数会额外生成2个线程,我们暂且称之为计时线程和控制线程。计时线程用于等待一开始我们设定的压测时间,到时间后通过线程信号通知控制线程。随后控制线程通过改变与压测线程共享的压测停止标志位,并发送终止信号来实现压测线程的停止。每个压测线程都会从结构体CREW中读取压测任务,这些压测任务由主函数添加。每个线程的测试数据均会输出到client结构体数组中,最后由主函数统一收集结果,并打印在屏幕上。
这一过程当中涉及的线程操作有条件变量,用于等待CREW中有压测任务到来,另外在计时线程中也用到了条件变量进行计时操作;互斥锁,用于改变CREW结构体成员的值时加锁保护数据;线程信号,用于线程间的相互通知;信号屏蔽字,用于将到来的异步信号用同步的方法去处理。源码中一大堆以pthread开头的函数操作,如果不清楚细节的话可以翻阅一下《UNIX环境高级编程》这本编程圣经来查阅一下。接下来我们进行更详细一些的代码分析。
四、源码分析
4.1 CREW与client两个结构体
CREW是用来统一管理所有压测线程的结构体,它在主函数中被声明,因此可以被所有线程共享。对其中成员变量的改动也需要加锁后进行。CREW结构体如下:
复制代码
struct CREW_T //用于管理所有压测线程的结构体
{
int size; //目标并发数目,即压测线程个数
int maxsize; //最大并发数目,即压测线程个数
int cursize; //目前的可用并发数,压测中时这个数字随压测线程实时变化
int total; //实际启动的并发数
WORK *head; //压测任务链表头部
WORK *tail; //压测任务链表尾部
BOOLEAN block; //当已经达到最大并发时,则不准再添加新的压测线程
BOOLEAN closed; //压测线程是否已经关闭
BOOLEAN shutdown; //压测线程是否应该停止了
pthread_t *threads; //长度为size的数组,存储线程号
pthread_mutex_t lock; //修改本结构体都要先加锁
pthread_cond_t not_empty; //用于表示cursize不为0的条件
pthread_cond_t not_full; //用于表示cursize不等于maxsize的条件
pthread_cond_t empty; //用于表示cursize等于0的条件
};
复制代码
每个压测线程都会维护属于自己的一份client,他们共同构成一个长度为n的数组。该结构体用于存储属于压测线程的相关信息,例如请求的响应时间,请求次数,数据流量等。这些统计信息最终将会反映给主进程做汇总输出。
复制代码
typedef struct
{
int id; //client编号,对于n个线程编号分别从0至n-1
unsigned long hits; //共完成几次transaction,每完成一次请求加1
unsigned long bytes; //收到的数据总量
unsigned int code; //返回码是小于400的,或者等于401,等于407,则该计数加1
unsigned int fail; //失败计数,只要返回码大于等于400,且不是401也不是407,则该计数加1
unsigned int ok200; //返回码是200的数量,200为成功请求
ARRAY urls; //要访问的URL列表
struct {
DCHLG *wchlg;
DCRED *wcred;
int www;
DCHLG *pchlg;
DCRED *pcred;
int proxy;
struct {
int www;
int proxy;
} bids;
struct {
TYPE www;
TYPE proxy;
} type;
} auth; //本结构体用于设置代理服务器信息以及鉴权信息
int status; //连接状态信息,包括未连接,正在连接,待读取等
float time; //统计请求花费的总时长
unsigned int rand_r_SEED; //随机数种子,用于随机访问URL的场景
float himark; //最慢一次请求花费的时间
float lomark; //最快一次请求花费的时间
} CLIENT;
复制代码
写到这里,其实本程序代码为什么有13000行之多已经可以看到原因了。作者对于很多模块都进行了封装,比如C语言没有的BOOLEAN类型,数组操作ARRAY类型,压测任务链表操作WORK类型,已经与C++中的class有些类似。我们可以举个简单的例子,比如WORK类型是这么定义的:
typedef struct work
{
void (*routine)();
void *arg;
struct work *next;
} WORK;
这里面的routine是一个函数指针,而arg是要传给前面函数的参数。整个压测任务由一个单向链表来存储在CREW中。程序中这样的例子还有很多,就不再赘述。接下来我们关注一下计时线程、控制线程、压测线程的核心代码。
4.2 计时线程
计时线程在到达一定时间之后,会向控制线程发送SIGTERM信号,通知控制线程停止压测。该函数并不算复杂,下面是核心代码,我们略去了一些不必要的代码,只展示出了最重要的部分:
复制代码
void siege_timer(pthread_t handler) //handler是控制线程的id
{
int err;
time_t now;
struct timespec timeout;
pthread_mutex_t timer_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t timer_cond = PTHREAD_COND_INITIALIZER; //专门用来计时的条件变量
if (time(&now) < 0) {
NOTIFY(FATAL, "unable to set the siege timer!");
}
timeout.tv_sec=now + my.secs; //设置超时时间,my.secs就是我们设置的压测时间,以秒为单位
timeout.tv_nsec=0;
pthread_mutex_lock(&timer_mutex);
for (;;) {
err = pthread_cond_timedwait( &timer_cond, &timer_mutex, &timeout);//使用条件变量进行计时操作
if (err == ETIMEDOUT) {
pthread_kill(handler, SIGTERM); //向handler线程发送sigterm信号
break;
} else {
continue;
}
}
pthread_mutex_unlock(&timer_mutex);
return;
}
复制代码
这段代码还是比较容易理解的,条件变量在到时之前根本不会被激活,基本上是因为计时到了而返回,这也是pthread_cond_timedwait的作用。为了使用条件变量,外面又包了一层互斥锁timer_mutex,虽然根本不会有其他线程来抢这把锁。到时间后,通过pthread_kill来向其他线程发送信号。
4.3 控制线程
控制线程其实只做一件事情,即等待计时线程发送终止信号,收到信号后调用相关函数取消正在执行的压测线程。这次同样略去一些代码,只看最核心的控制线程部分。相关代码如下:
复制代码
void sig_handler(CREW crew)
{
int gotsig = 0;
sigset_t sigs;
sigemptyset(&sigs);
sigaddset(&sigs, SIGHUP);
sigaddset(&sigs, SIGINT);
sigaddset(&sigs, SIGTERM);
sigprocmask(SIG_BLOCK, &sigs, NULL); //设置信号屏蔽字,在sigwait之前必须先屏蔽信号
sigwait(&sigs, &gotsig);//阻塞等待线程信号,用于响应计时线程pthread_kill发来的信号
fprintf(stderr, "\nLifting the server siege...");
crew_cancel(crew); //取消CREW中的所有任务,即让压测线程停止下来
pthread_usleep_np(501125); //人为使线程睡眠一小会,上面英文为原作者的注释
pthread_exit(NULL);
}
复制代码
4.4 压测线程
计时和控制线程还是比较容易理解的,代码结构也相对较为简单,接下来就瞧一下最为繁琐的压测线程。主函数将会通过for循环来创建n个压测线程,每个线程执行如下函数(同样略去了非关键代码):
复制代码
private void *crew_thread(void *crew)//压测线程,共有size个,取决于命令行-c后面的数字
{
WORK *workptr; //压测函数结构体的指针,真正的压测逻辑都在这里的函数中实现
CREW this = (CREW)crew; //这里的结构体CREW正是前文4.1节中提到的CREW,用于管理所有压测线程
while(TRUE){//这里是死循环,压测一直在循环执行中,除非调用pthread_exit退出
pthread_mutex_lock(&(this->lock));
while((this->cursize == 0) && (!this->shutdown)){//如果目前可用并发数cursize是空的,则等待
pthread_cond_wait(&(this->not_empty), &(this->lock)); //一开始创建的size个压测线程都会卡在这里
}
if(this->shutdown == TRUE){ //线程停止,则释放锁,退出,这里是唯一可以停止压测的地方
pthread_mutex_unlock(&(this->lock));
pthread_exit(NULL);
}
workptr = this->head; //取出第一个节点上的压测程序
this->cursize--; //可用并发数减一
%2