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-02-10 15:19:51

    Git:代码冲突常见解决方法

    如果系统中有一些配置文件在服务器上做了配置修改,然后后续开发又新添加一些配置项的时候, 在发布这个配置文件的时候,会发生代码冲突:

  • 2017-02-10 15:24:14

    linux学习之——vim简明教程

    学习 vim 并且其会成为你最后一个使用的文本编辑器。没有比这个更好的文本编辑器了,非常地难学,但是却不可思议地好用。 我建议下面这四个步骤: 存活 感觉良好 觉得更好,更强,更快 使用VIM的超能力

  • 2017-02-10 16:22:13

    git历史记录查询

    查看提交历史:git log 查看提交历史并显示版本间的差异:git log -p 查看指定历史:git log xxx(sha1值) -p 查看提交历史(指定时间):

  • 2017-02-13 17:50:05

    cURL error 60: SSL certificate problem: unable to get local issuer certificate

    Drupal 8 version uses Guzzle Http Client internally, but under the hood it may use cURL or PHP internals. If you installed PHP cURL on your PHP server it typically uses cURL and you may see an exception with error Peer certificate cannot be authenticated with known CA certificates or error code CURLE_SSL_CACERT (60).