回射服务器负载测试
负载发生器设计思路
(1)在客户端程序设计时,利用循环不断地向服务器发起Connect请求,服务器端通过Accept监听客户端连接请求。连接成功后,利用fcntl函数置已连接套接字为非阻塞模式。
代码:
for (i = 0; i < COUNT; i++) {
if(connect(sockfd[i], (SA*) &servaddr, sizeof(servaddr)) < 0)
printf ("no.%d connect error\n", i+1);
int flag = fcntl(sockfd[i], F_GETFL, 0);
fcntl(sockfd[i], F_SETFL, flag | O_NONBLOCK);
}
(2)创建epoll句柄并为套接字注册epoll监听事件。事件触发方式为ET,即Edge Triggered,在这种工作方式下,当描述符从未就绪变为就绪时,内核通过epoll将这个变化通知给进程。这里先注册EPOLLOUT事件,稍后在循环回射请求中再注册EPOLLIN事件。
代码:
for (i = 0; i < COUNT; i++)
{
event[i]. data. fd = sockfd[i];
event[i]. events = EPOLLOUT | EPOLLET;
epoll_ctl (epfd, EPOLL_CTL_ADD, sockfd[i], &event[i]);
}
(3)在监听套接字可读(EPOLLIN)和可写(EPOLLOUT)的状态下对服务器发起循环回射请求。注:可以将回射的内容放入内存中以增加程序效率。
代码:
char sendline[BUFSIZE]= "cli: echo", recvline[BUFSIZE];
n = epoll_wait (epfd, events, COUNT, -1);
for (i = 0; i < n; i++) {
if(events[i]. events & EPOLLOUT) {
snprintf (buffer, BUFSIZE, "%s", sendline);
k = write(events[i].data. fd, buffer, strlen(buffer));
if (k < 0) {
err_sys ("write socket error");
}
else if(k> 0) {
m++;
events[i].data. fd = sockfd[i];
events[i]. events = EPOLLIN;
epoll_ctl (epfd, EPOLL_CTL_MOD, sockfd[i], &events[i]);
}
} else if(events[i]. events & EPOLLIN) {//read socket
if(read(events[i].data. fd, recvline, BUFSIZE) > 0)
{
m++;
printf ("no.%d %s\n", m - COUNT + 1, recvline);
} //if
} //else if
} //for
性能测试(多线程、多进程、线程池、select和epoll版)
测试机操作系统为CentOS 7(64位),CPU为i5 4核 2.7GHz,内存为4G,硬盘为20G。
多线程服务器
报文长度为8
服务器为每一个客户端连接创建一个线程,每个线程单独处理客户端的回射请求。对于不同报文段长度,由于程序是先进行连接,再产生回射请求,因此连接时间理论上相同。
连接个数,回射次数 | 连接耗时(秒) | 回射耗时(秒) | 连接速率(秒) | 回射速率(秒) |
---|---|---|---|---|
5000连接,2500回射 | 8.09 | 0.0874 | 618.04 | 28604 |
10000连接,5000回射 | 19.21 | 0.1077 | 520.56 | 46425 |
20000连接,10000回射 | 43.44 | 0.2585 | 460.38 | 38669 |
40000连接,20000回射 | 78.85 | 0.6739 | 507.29 | 29677 |
50000连接,25000回射 | 104.13 | 0.7485 | 480.16 | 33400 |
平均数据 | 517.28 | 35355 |
连接数为5000,回射次数为2500时:
已经建立的可通信连接数为5000,设置回射次数为2500。在回射过程中CPU的系统平均使用率为8.7%。这种情况下客户-服务器每秒可以建立618次连接,服务器每秒可以处理28604次客户端回射请求。
连接数为20000,回射次数为10000时:
已经建立的可通信连接数为20000,设置回射次数为10000。在回射过程中CPU的系统平均使用率为20%。这种情况下客户-服务器每秒可以建立460次连接,服务器每秒可以处理38669次客户端回射请求。
连接数为50000,回射次数为25000时:
在25000次回射的情况下,CPU的第四个核负载达到了75.3%,CPU的平均负载峰值达到了52.6%。此时每秒能建立480次服务器-客户端连接,服务器每秒能处理33400次回射请求。
报文长度为50
连接个数,回射次数 | 回射耗时(秒) | 回射速率(秒) |
---|---|---|
5000连接,2500回射 | 0.0992 | 25252 |
20000连接,10000回射 | 0.3633 | 27548 |
50000连接,25000回射 | 1.2512 | 19980 |
平均数据 |
在增加了报文段长度后,对回射速率有明显的影响,尤其在大量回射请求时,报文段长度的增加使得回射速率大幅度降低。
连接数为5000,回射次数为2500时:
在报文段长度为50时,CPU的平均使用率达到4.6%,相比报文段为8时的8.7%有所降低(应该归结为在报文段为8时系统其他因素的影响使得吞吐量低于报文段为50的情况)。此时每秒服务器可以处理的回射请求为25252个。
连接数为20000,回射次数为10000时:
系统CPU使用率达到了39.7,仍然是CPU的第四个核的负载最高。此时每秒服务器能处理的回射请求约为19980个,相比报文段为8时降低了近50%。
连接数为50000,回射次数为25000时:
系统CPU使用率达到了39.7,仍然是CPU的第四个核的负载最高。此时每秒服务器能处理的回射请求约为19980个,相比报文段为8时降低了近50%。
多进程服务器
当一个连接建立时,服务器立即调用fork,然后由子进程完成客户端的回射请求,父进程则等待另一个连接。
报文长度为8
连接个数,回射次数 | 连接耗时(秒) | 回射耗时(秒) | 连接速率(秒) | 回射速率(秒) |
---|---|---|---|---|
5000连接,2500回射 | 15.09 | 0.0692 | 331.29 | 35816 |
20000连接,10000回射 | 62.60 | 0.1004 | 319.44 | 51413 |
50000连接,25000回射 | 238.57 | 108.80 | 209.57 | 229.7 |
平均数据 | 286.76 | 无效 |
由表可得,多进程版服务器在回射速率方面相比多线程服务器有所提高,但在建立连接方面面性能却大幅度降低,因为创建进程的开销在负载量高时远大于创建线程的开销,而且线程上下文切换的开销比进程上下文切换的开销小,所以多进程服务器性能在建立连接方面是弱于多线程服务器的。在进行25000次回射过程中能够明显感觉到系统的卡顿,甚至无法进行其他操作。
连接数为5000,回射次数为2500时:
在负载量较小时,CPU使用率与多线程服务器差不多,但进行2500次回射的时间却小于多线程版服务器。在这种情况下服务器每秒能处理的回射请求为35816。
连接数为20000,回射次数为10000时:
在负载量不太大时,CPU使用率几乎与前文的情况相同,但这次的10000次回射请求只耗费了0.19秒,是目前回射速率最高的情况,此时服务器每秒可以处理51413个回射请求。
连接数为50000,回射次数为25000时:
这张图能代表整个回射请求发生期间的服务器负载情况,服务器一直处理满负荷状态,此时处理机资源几乎全部用于进行回射请求,导致系统效率大大降低。25000次回射耗时108秒,服务器每秒只处理了229个回射请求,这意味着服务器在超高负载的情况下几乎处于崩溃的情况。
线程池服务器
报文长度为8
服务器通过预先创建一定数量的工作线程并限制其数量,等到客户端发起连接请求时,直接从线程池分配一个线程用于客户端请求。
连接个数,回射次数 | 连接耗时(秒) | 回射耗时(秒) | 连接速率(秒) | 回射速率(秒) |
---|---|---|---|---|
5000连接,2500回射 | 0.1373 | 0.3049 | 36416 | 8199 |
20000连接,10000回射 | 0.5926 | 1.2678 | 33749 | 7887 |
平均数据 | 35082 | 8043 |
从表观察得,线程池的引入使得客户端与服务器的建立连接的速度大大提升,这是因为不像之前的服务器,客户端发起连接请求时服务器端会为其申请资源,例如线程、进程等,这样可以减少建立连接的开销,代价是对于线程的调度和工作任务的分配使得回射请求的处理速度降低。
连接数为5000,回射次数为2500时:
在2500次回射情况下CPU使用率非常低,但也由于管理线程要对工作任务进行分配,对工作线程进行调度,使得服务器每秒仅能处理8199次回射请求。
连接数为20000,回射次数为10000时:
此时回射耗时1.26秒,服务器每秒仅能处理7887次回射请求。
总结:线程池版的服务器在建立连接方面体现出了高效性。
Select服务器
由于select监听的描述符的最大数目为1024,因此能够发起的连接也只有2048次。
报文长度为8
连接个数,回射次数 | 连接耗时(秒) | 回射耗时(秒) | 连接速率(秒) | 回射速率(秒) |
---|---|---|---|---|
2048连接,1024回射 | 3.069 | 0.0215 | 667 | 47627 |
报文长度为50
连接个数,回射次数 | 连接耗时(秒) | 回射耗时(秒) | 连接速率(秒) | 回射速率(秒) |
---|---|---|---|---|
2048连接,1024回射 | 2.0723 | 0.0550 | 988 | 18618 |
报文长度为10000
连接个数,回射次数 | 连接耗时(秒) | 回射耗时(秒) | 连接速率(秒) | 回射速率(秒) |
---|---|---|---|---|
2048连接,1024回射 | 3.044 | 0.0410 | 672 | 24975 |
Epoll服务器
Epoll服务器在建立连接和回射请求方面都有较高的效率。
报文长度为8B
连接数 | 连接耗时(秒) | 回射耗时(秒) | 连接速率(秒) | 回射速率(秒) | Network(KB/s) |
---|---|---|---|---|---|
20 | 0.0016 | 0.0006 | 12500 | 33333 | 3.7 |
400 | 0.0248 | 0.0084 | 16129 | 47619 | 74 |
1000 | 1.0635 | 0.0207 | 940 | 48309 | 175.2 |
2000 | 4.1096 | 0.0593 | 486 | 33726 | 360.7 |
10000 | 54.7141 | 0.2239 | 182 | 44662 | 1997.6 |
报文长度为100B
连接数 | 连接耗时(秒) | 回射耗时(秒) | 连接速率(秒) | 回射速率(秒) | Network(KB/s) |
---|---|---|---|---|---|
20 | 0.0018 | 0.0005 | 11111 | 40000 | 5.5 |
400 | 0.0244 | 0.0433 | 16393 | 9237 | 110 |
1000 | 1.0413 | 0.0537 | 960 | 18621 | 258.8 |
2000 | 1.1209 | 0.0733 | 1784 | 27285 | 534.2 |
10000 | 26.6783 | 0.6007 | 374 | 16647 | 2735 |
报文长度为1KB
连接数 | 连接耗时(秒) | 回射耗时(秒) | 连接速率(秒) | 回射速率(秒) | Network(KB/s) |
---|---|---|---|---|---|
20 | 0.0008 | 0.0213 | 25000 | 983 | 23.5 |
400 | 0.0288 | 0.2020 | 13888 | 1980 | 470 |
1000 | 1.0638 | 0.2946 | 940 | 3384 | 1101.6 |
2000 | 3.1058 | 0.6445 | 643 | 3103 | 2278.7 |
10000 | 17.6518 | 3.2813 | 566 | 3047 | 9723.6 |
总结:Epoll服务器在建立连接和回射请求方面都有较高的效率。
压力发生器完整代码(客户端程序)
有不足之处还望慷慨之处
/*************************************************************************
> File Name: lastcli.c
> Author:
> Mail:
> Created Time: 2017年08月04日 星期五 10时21分03秒
************************************************************************/
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#define COUNT 400 //客户端-服务器连接个数,也是服务器回射次数
#define BLOCK 8 //报文内容大小(8,100,1024,10240...)
#define BUFSIZE 102401
#define RET_ERR -1
#define RET_OK 0
typedef struct PacketHeader{
int length;
char *data;
} header; //之后用作变长报文的处理
char *StrHandle (char *str, int num){
str = (char *)malloc(num * sizeof(char));
FILE *fp = fopen("/home/allah/Desktop/web01.c", "r"); //回射的内容,自定义路径即可
if(num == 8)
fgets(str, 8, fp);
else if(num == 100)
fgets(str, 100, fp);
else if(num == 1024)
fgets(str, 1024, fp);
else if(num == 10240)
fgets(str, 10240, fp);
else if(num == 102400)
fgets(str, 102400, fp);
return str;
}
void SetNonBlock(int fd){//设置套接字为非阻塞模式
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}
int CreateConn(int sockfd[], int num, struct sockaddr_in sa){//创建连接
int i;
for(i = 0; i < num; i++) {
if( (sockfd[i] = socket(AF_INET, SOCK_STREAM, 0)) < 0)
err_quit("socket error");
if((connect(sockfd[i], (struct sockaddr_in*)&sa, sizeof(sa))) < 0)
err_quit("connect error");
SetNonBlock(sockfd[i]);
}
return 1;
}
struct sockaddr_in InitServ(struct sockaddr_in *sa, char *addr){//初始化服务器套接字地址结构
struct sockaddr_in servaddr = *sa;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(9877);
if(inet_pton(AF_INET, addr, &servaddr.sin_addr) < 0)
err_quit("inet_pton error");
return servaddr;
}
void EpollMultiAdd(struct epoll_event event[], int sk[], int operation, int num, int epfd){
//添加EPOLL监听事件
int i;
for(i = 0; i < num; i++)
{
event[i].data.fd = sk[i];
event[i].events = EPOLLOUT;
epoll_ctl(epfd, operation, sk[i], &event[i]);
}
}
void EpollMod(struct epoll_event event, int epfd, int fd, int operation){ //修改epoll监听事件
event.data.fd = fd;
event.events = operation;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &event);
}
int main(int argc, char** argv){
int iRet = RET_OK; // return 0
int i;
int sk[COUNT];
if(argc != 2)
err_sys("Usage cli <IPaddress>");
struct sockaddr_in sa;
sa = InitServ(&sa, argv[1]);
int j= 0;
//timeval结构用于测算程序运行时间
struct timeval tpstart1, tpend1;
float timeuse1;
gettimeofday(&tpstart1, NULL);
//create socket connection to serv
CreateConn(sk, COUNT, sa);
gettimeofday(&tpend1, NULL);
timeuse1 = 1000000 * (tpend1.tv_sec - tpstart1.tv_sec) + tpend1. tv_usec - tpstart1.tv_usec;
timeuse1 /= 1000000;
printf("conn time %lf s\n", timeuse1);
int efd;
efd = epoll_create(COUNT * 2);
if(efd == -1)
{
perror("epoll_create error!");
exit(1);
}
struct epoll_event event[COUNT];
struct epoll_event events[COUNT];
//add epoll event
EpollMultiAdd(event, sk, EPOLL_CTL_ADD, COUNT, efd);
getchar();
int n, m = 0, k = 0;
struct timeval tpstart, tpend;
float timeuse;
gettimeofday(&tpstart, NULL);
while(1){
char buffer[BUFSIZE] = {0}, recvline[BUFSIZE];
char *sendline = (char *)malloc(BLOCK * sizeof(char));
char temp[BLOCK];
sendline = StrHandle(temp, BLOCK);//set up ready-to-send content
if(m == COUNT)//用回射次数来控制整体循环次数
break;
n = epoll_wait(efd, events, COUNT , -1);
for(i = 0; i < n ; i++){
if(events[i].events & EPOLLOUT ){
printf("%s\n", sendline);
snprintf(buffer, BUFSIZE, "%s", sendline);
k = write(events[i].data.fd, buffer, strlen(buffer));//写入套接字
if(k < 0) {
err_sys("write socket error");
}
else if(k> 0){
EpollMod(events[i], efd, sk[i], EPOLLIN); //epoll由之前的监听套接字可写事件转到监听套接字可读事件
}
}else if(events[i].events & EPOLLIN){//read socket
if(read(events[i].data.fd, recvline, BUFSIZE) > 0)
{
m++;
printf("no.%drecv %s\n", m, recvline);
}
}
}
}
gettimeofday(&tpend, NULL);
timeuse = 1000000 * (tpend.tv_sec - tpstart.tv_sec) + tpend.tv_usec - tpstart.tv_usec;
timeuse /= 1000000;
printf("echo time %lfs\n", timeuse);
for(i = 0 ; i < COUNT; i++)
close(sk[i]) ; //关闭套接字
close(efd);
return RET_OK;
}