Web性能测试:工具之Siege详解

2017-09-13 13:49:21

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

  • 2017-07-11 21:54:14

    MYSQL5.7版本sql_mode=only_full_group_by问题

    一旦开启 only_full_group_by ,感觉,group by 将变成和 distinct 一样,只能获取受到其影响的字段信息,无法和其他未受其影响的字段共存,这样,group by 的功能将变得十分狭窄了

  • 2017-07-14 13:51:58

    NodeJS连接MySQL出现Cannot enqueue Handshake after invoking quit.

    原因在于node连接上mysql后如果因网络原因丢失连接或者用户手工关闭连接后,原有的连接挂掉,需要重新连接;如下代码,每次访问结束都关闭,每次开始访问前重连接下,代码中没有监听连接的fatal错误,copy需谨慎

  • 2017-07-14 13:53:02

    nodejs解决mysql和连接池(pool)自动断开问题

    最近在做一个个人项目,数据库尝试使用了mongodb、sqlite和mysql。分享一下关于mysql的连接池用法。项目部署于appfog,项目中我使用连接池链接数据库,本地测试一切正常。上线以后,经过几次请求两个数据接口总是报503。一直不明就里,今天经过一番排查终于顺利解决了。

  • 2017-07-15 16:13:26

    设置MySQL里的wait_timeout

    如果你没有修改过MySQL的配置,缺省情况下,wait_timeout的初始值是28800。

  • 2017-07-16 20:13:14

    nodejs,express 自制错误日志

    对于同步执行的代码,以上的处理已经足够简单。然而,当异步程序在执行时抛出异常的情况,Express 就无能为力。原因在于当你的程序开始执行回调函数时,它原来的栈信息已经丢失。

  • 2017-07-16 20:17:56

    NodeJS处理Express中异步错误

    本文主要阐述如何在 Express 中使用错误处理中间件(error-handling middleware)来高效处理异步错误。在 Github 上有对应 代码实例 可供参考。