3 小試牛刀,編寫性能測試工具
作為一名后端研發(fā)人員,必須具備系統(tǒng)性能評估和分析能力,因為只有對系統(tǒng)總體性能了如指掌,才能知道系統(tǒng)什么時候需要擴容,系統(tǒng)哪里有性能瓶頸需要優(yōu)化。
本章將介紹如何宏觀的評估系統(tǒng)的總體性能,并重點介紹如何編寫性能測試工具對系統(tǒng)性能做一個實測,畢竟理論歸理論,理論上性能指標(biāo)還是需要靠實際壓測來檢驗。
3.1 系統(tǒng)的總體性能簡述
通常我們使用QPS、平均響應(yīng)時間、并發(fā)數(shù),這三個指標(biāo)來衡量一個系統(tǒng)的總體性能。
- QPS:表示系統(tǒng)每秒請求數(shù)。
- 平均響應(yīng)時間:表示系統(tǒng)平均單個請求耗時,單位為秒。
- 并發(fā)數(shù):表示系統(tǒng)能同時處理的請求數(shù)。
系統(tǒng)能支持的最大QPS是由平均響應(yīng)時間和并發(fā)數(shù)決定的,即QPS=并發(fā)數(shù)/平均響應(yīng)時間。純理論的解說可能不太好理解,我們這邊舉一個例子,比如我們?nèi)ャy行營業(yè)廳辦存款業(yè)務(wù),這時有10個柜臺可以辦理存款業(yè)務(wù),每個人辦理存款業(yè)務(wù)平均需要5分鐘,那么這個營業(yè)廳每小時可以處理多少的存款業(yè)務(wù)呢?這里10個柜臺可以辦理業(yè)務(wù)也就是說“并發(fā)數(shù)”為10,我們單位時間設(shè)定為小時,那么辦理存款業(yè)務(wù)平均需要5分鐘也就是說“平均響應(yīng)時間”為13分之1小時,那么這個營業(yè)廳每小時可以處理的存款業(yè)務(wù)數(shù)也就是QPS=10/(1/13),即130。
由上面的公式“QPS=并發(fā)數(shù)/平均響應(yīng)時間”,我們可以看出要提高系統(tǒng)的QPS,可以從兩個方面著手,一方面是提高并發(fā)數(shù),一方面是降低平均響應(yīng)時間。
提高并發(fā)數(shù)的方法
新增更多的服務(wù)器,采用多核服務(wù)器,單服務(wù)器上運行多進程提供服務(wù),進程采用更高效性的IO模型。降低平均響應(yīng)時間的方法
響應(yīng)時間通常由:網(wǎng)絡(luò)通訊時間(網(wǎng)絡(luò)io時間)+計算處理時間(cpu時間)+磁盤讀寫時間(磁盤io時間)構(gòu)成的??梢酝ㄟ^使用更大的網(wǎng)絡(luò)帶寬,更好的網(wǎng)卡來降低網(wǎng)絡(luò)通訊時間,可以使用更強大的cpu或者優(yōu)化算法來減少計算處理時間,可以使用SSD硬盤來替代普通硬盤來降低單次讀寫磁盤的io時間,可以在業(yè)務(wù)層和數(shù)據(jù)庫持久化層之間添加一個緩存層來減少磁盤io次數(shù)。
3.2 性能測試工具
我們的性能測試工具是命令行工具,命名為“benchMark”,它用于測試一個系統(tǒng)提供的網(wǎng)絡(luò)服務(wù)的總體性能,它具備命令參數(shù)功能、支持多客戶端并發(fā)、支持基于連接的壓測、支持基于特定業(yè)務(wù)請求的壓測等的功能。
3.2.1 命令參數(shù)功能
和所有Linux的命令一樣,我們的工具也支持命令參數(shù),在Linux下有一個getopt_long函數(shù)用于支持對命令行參數(shù)的解析,通過這個函數(shù)我們能輕易是實現(xiàn)對長短參數(shù)的解析和獲取。getopt_long函數(shù)所在的頭文件和函數(shù)原型如下:
#include <getopt.h>
int getopt_long(int argc, char * const argv[], const char *optstring, const struct option *longopts, int *longindex);
作為C/C++入口函數(shù),main的標(biāo)準(zhǔn)函數(shù)原型為“int main(int argc, char * argv[])”,命令行參數(shù)也正是通過main函數(shù)的argc、argv這兩個參數(shù)傳遞進來的。
- argc與argv參數(shù)
getopt_long函數(shù)開頭需要傳入的兩個參數(shù)就是main函數(shù)的argc和argv參數(shù),argc表示要解析參數(shù)的個數(shù),argv是具體要解析的參數(shù)列表。
- optstring參數(shù)
optstring為選項聲明串,其中一個字符代表一個選項,如果字符后面跟上一個英文冒號表明這個選項必須有一個選項參數(shù),這個選項參數(shù)可以和選項在同一個命令行參數(shù)中,即“-oarg”,也可以在下一個命令行參數(shù)中,即“-o arg”,這個選項參數(shù)的值可以從全局變量optarg中獲取到;如果字符后面跟上兩個冒號表明這個選項有一個可選的選項參數(shù),此時可選的選項參數(shù)要和選項在同一個命令行參數(shù)中,比如我們設(shè)置o為有可選的選項參數(shù)的選項,這時要設(shè)置可選的選項參數(shù),可選的選項參數(shù)必須和選項在同一個參數(shù)中,即“-oarg”,其中arg為o選項的可選的選項參數(shù),它們之間不能有空格,同樣的這個可選的選項參數(shù)可以在全局變量optarg中獲取,沒有設(shè)置可選的選項參數(shù)時,optarg的值為0。
- longopts參數(shù)
optstring中包含的只有短選項,當(dāng)要支持長選項的時候就需要使用longopts參數(shù)了,longopts參數(shù)是一個指向struct option數(shù)組的指針,在struct option數(shù)組中設(shè)置了長選項的相關(guān)信息。長選項在設(shè)置選項參數(shù)時和短選項有稍微的不同,短選項為“-oarg”或者“-o arg”,而長選項為“--arg=param”或者“--arg param”,短選項設(shè)置可選參數(shù)時只能使用“-oarg”的形式,長選項設(shè)置可選參數(shù)時只能使用“--arg=param”的形式。下面我們看一下struct option中各個字段的含義。
struct option
{
const char *name;
int has_arg;
int *flag;
int val;
};
name字段:表示長選項的名稱。
has_arg字段:表示長選項參數(shù)設(shè)置,如果設(shè)置成no_argument(或者0)表示長選項沒有參數(shù),如果設(shè)置成required_argument(或者1)表示長選項必須有一個參數(shù),如果設(shè)置成optional_argument(或者3)表示長選項有一個可選項參數(shù),長選項的可選參數(shù)必須使用“--arg=param”來設(shè)置。
flag字段:通常為NULL,如果flag為NULL,則當(dāng)解析到name設(shè)置的長選項時getopt_long返回最后一個val字段設(shè)置的值,通過這個方式可以使短選項和長選項具備相同的功能;如果flag不為NULL,則當(dāng)解析到name設(shè)置的長選項時getopt_long返回0,flag指向的變量的值將被設(shè)置成val設(shè)置的值,長選項沒解析到,則flag指向的變量的值不會被改變。
val字段:作為匹配到長選項時getopt_long的返回值(flag為NULL),或者作為設(shè)置flag指向的變量的值(flag不為NULL)。
- longindex參數(shù)
可以設(shè)置為NULL,不為NULL時用于返回匹配的長選項在長選項數(shù)組中的索引值。
3.2.2 多客戶端并發(fā)
我們使用fork的方式來模擬多客戶端并發(fā)的場景,子進程和父進程通過pipe函數(shù)創(chuàng)建的匿名管道來進行通信,父進程對測試結(jié)果進行匯總統(tǒng)計,在所有子進程結(jié)束后在父進程中打印測試的匯總結(jié)果。
3.2.3 基于連接還是基于請求
我們的測試工具支持基于連接(創(chuàng)建網(wǎng)絡(luò)連接成功后就馬上關(guān)閉網(wǎng)絡(luò)連接)的并發(fā)壓測;也支持基于請求(創(chuàng)建網(wǎng)絡(luò)連接成功后還會發(fā)起業(yè)務(wù)請求,并在接收完應(yīng)答數(shù)據(jù)后才關(guān)閉網(wǎng)絡(luò)連接)的并發(fā)壓測,可以針對不同的業(yè)務(wù)設(shè)置不同請求數(shù)據(jù)和應(yīng)答數(shù)據(jù)不同的校驗邏輯。
3.2.4 性能工具的局限性
我們的性能工具只能運行在單服務(wù)器上,故并發(fā)壓測能力受限于單服務(wù)器的并發(fā)能力,如果是測試外網(wǎng)服務(wù)還受限于服務(wù)器的外網(wǎng)帶寬,外網(wǎng)帶寬一旦跑滿并發(fā)數(shù)就難再有所提高。
3.3 數(shù)據(jù)交互過程
性能測試工具和網(wǎng)絡(luò)服務(wù)系統(tǒng)的數(shù)據(jù)交互過程如下圖:

3.4 類關(guān)系圖
benchMark主要由5個類和1個結(jié)構(gòu)體構(gòu)成。其中結(jié)構(gòu)體BenchMarkInfo用于保存測試相關(guān)的輸入?yún)?shù);BenchMarkFactory是一個簡單的工廠類,它用于生成具體的壓測類BenchMarkConn(基于連接)和BenchMarkHttp(基于http協(xié)議),BenchMarkBase是壓測基類;RobustIo類是對網(wǎng)絡(luò)io的封裝,它自帶讀緩沖區(qū)。這5個類和1個結(jié)構(gòu)體的關(guān)系如下類關(guān)系圖:

3.5 http的請求與應(yīng)答
在BenchMarkHttp類中涉及到了http協(xié)議,在BenchMarkHttp中我們壓測的是獲取web站點根目錄接口的性能,發(fā)起的是GET請求,url為“/”。http應(yīng)答解析方面我們使用開源的C語言的http解析api,它沒有其他特別庫的依賴,支持流式的解析,在流式解析過程中采用回調(diào)的方式來通知http協(xié)議相關(guān)的字段信息。在第9章“網(wǎng)絡(luò)通信與并發(fā)”中我們將自己實現(xiàn)一個簡單的http協(xié)議解析類,并對http協(xié)議做詳細的講解,加深大家對http協(xié)議的理解。
3.5.1 發(fā)起http請求
http請求報文由三部分組成,它們分別為:請求行(request_line)、請求頭集合(headers)、請求體(body);請求行和請求頭都是以“\r\n”為結(jié)尾,請求頭集合則以一個 空行(“\r\n”)作為請求頭集合結(jié)束的標(biāo)志,其中body為可選部分,我們http壓測使用的GET請求就沒有body部分。接下來看一下具體的請求數(shù)據(jù)生成函數(shù)。
void BenchMarkHttp::getRequestData(string & data, BenchMarkInfo & info)
{
//http GET請求
data = "GET / HTTP/1.1\r\n" //請求行,表示發(fā)起GET請求,url為"/",使用http 1.1協(xié)議
"User-Agent: benchMark v1.0\r\n" //User-Agent請求頭,表示當(dāng)前客戶端代理信息
"Host: " + info.host +"\r\n" //Host請求頭,表示請求的服務(wù)器域名
"Accept: */*\r\n" //Accept請求頭,表示接受任意格式的應(yīng)答數(shù)據(jù)
"\r\n"; //一個空行表示請求頭集合的結(jié)束
}
3.5.3 解析http應(yīng)答
我們使用的C語言的http解析api托管在github上,項目鏈接為:https://github.com/nodejs/http-parser 。
主要分為以下幾個步驟來使用開源的http解析api,下面是我們在BenchMarkHttp使用http解析api的相關(guān)代碼:
static int http_rsp_complete(http_parser * parser)
{
//取出之前在解析變量中設(shè)置的私有數(shù)據(jù),它為bool指針,
//它指向dealHttpReq函數(shù)中的finish變量
bool * pFinish = (bool *)parser->data;
//設(shè)置finish的值為true,表示已經(jīng)完成了一個http應(yīng)答的解析。
*pFinish = true;
return 0;
}
bool BenchMarkHttp::dealHttpReq(int sock, BenchMarkInfo & info, int33_t & bytes)
{
size_t ret = 0;
string data;
RobustIo rio; //封裝的io類變量
bool finish = false; //表示應(yīng)答是否解析完畢,它的指針會被傳遞給http解析變量
http_parser parser; //聲明http解析api的解析變量parser
http_parser_settings settings; //聲明http解析api的解析設(shè)置變量settings
http_parser_init(&parser, HTTP_RESPONSE); //初始化http解析api的解析變量parser
http_parser_settings_init(&settings); //初始化http解析api的解析設(shè)置變量settings
settings.on_message_complete = http_rsp_complete; //設(shè)置http應(yīng)答解析完成回調(diào)函數(shù)
parser.data = (void *)(&finish); //在解析變量parser中設(shè)置我們的私有數(shù)據(jù),在回調(diào)函數(shù)中會使用
getRequestData(data, info); //獲取http GET請求的發(fā)送數(shù)據(jù)
//使用封裝好的網(wǎng)絡(luò)io類發(fā)送http GET請求
ret = rio.rioWrite(sock, (void *)data.c_str(), data.size());
//發(fā)送失敗則返回false
if (ret != data.size())
{
return false;
}
//統(tǒng)計發(fā)送字節(jié)數(shù)
bytes += data.size();
char c;
int status;
//初始化io讀緩存區(qū)大小
rio.rioInit(1034);
//只要還沒解析完應(yīng)答則一直從網(wǎng)絡(luò)中獲取數(shù)據(jù),并解析
while (!finish)
{
//從網(wǎng)絡(luò)中讀取一個字節(jié),rioRead是自帶緩沖區(qū)的
//故不會頻繁的調(diào)用系統(tǒng)read函數(shù),帶來性能影響
if (rio.rioRead(sock, &c, 1) != 1)
{
break;
}
//統(tǒng)計接收字節(jié)數(shù)
++bytes;
//調(diào)用http解析核心api對流式數(shù)據(jù)進行解析,每次解析從網(wǎng)絡(luò)流中獲取的一個字節(jié)
//這個api函數(shù)返回解析成功的字節(jié)數(shù),如果返回值和傳入的要解析的字節(jié)數(shù)不一致
//則表明解析過程中發(fā)送錯誤,則返回false
if (http_parser_execute(&parser, &settings, &c, 1) != 1)
{
return false;
}
}
//獲取http應(yīng)答的狀態(tài)碼
status = parser.status_code;
if (3 == (status / 100)) //狀態(tài)碼為3xx的都表示請求成功,故返回true?
{
return true;
}
else
{
return false;
}
}
從上面代碼我們可以看到http解析api使用的變量為:parser和settings,它們的類型分別為http_parser和http_parser_settings,在使用它們之前我們需要分別使用http_parser_init和http_parser_settings_init進行初始化,在settings中設(shè)置了解析完畢的回調(diào)函數(shù)http_rsp_complete,在http_rsp_complete函數(shù)中我們將dealHttpReq中的finish變量的值設(shè)置為true表明http應(yīng)答已經(jīng)解析完畢,dealHttpReq就能退出http解析循環(huán)。在dealHttpReq函數(shù)中我們使用http_parser_execute這個http解析核心api來進行流式解析。
3.6 完整代碼
3.6.1 benchMark.cpp
#include <getopt.h>
#include <stdlib.h>
#include <iostream>
#include "benchMarkBase.h"
#include "benchMarkCommon.h"
#include "benchMarkFactory.h"
using namespace std;
void printVersion()
{
cout << "benchMark version: 1.0 , auth: rookie" << endl;
}
/*
輸出benchMark的使用方法
*/
void benchMarkUsage()
{
cout << "Usage: -h host -p port [option]" << endl;
cout << endl;
//常規(guī)選項
cout << "general options:" << endl;
cout << " --help print usage" << endl;
cout << " -v,--version print version info" << endl;
cout << endl;
//連接選項
cout << "connection options:" << endl;
cout << " -h,--host server host to connect to" << endl;
cout << " -p,--port server port" << endl;
cout << endl;
//并發(fā)選項
cout << "concurrent options:" << endl;
cout << " -c,--clients number of concurrent clients, default 4, max is 100" << endl;
cout << endl;
//交互選項
cout << "interaction options:" << endl;
cout << " -r,--request request type, support http(2) and connection(1), default conection" << endl;
cout << " -t,--times benchMark test time, unit seconds, default 60 sec" << endl;
cout << endl;
}
int dealArgv(int argc, char * argv[], BenchMarkInfo & info)
{
int opt = 0;
//短選項
const char shortOpts[] = "?vh:p:c:r:t:";
//長選項
const struct option longOpts[] =
{
{"help", no_argument, NULL, '?'},
{"version", no_argument, NULL, 'v'},
{"host", required_argument, NULL, 'h'},
{"port", required_argument, NULL, 'p'},
{"clients", required_argument, NULL, 'c'},
{"request", required_argument, NULL, 'r'},
{"times", required_argument, NULL, 't'},
{NULL, 0, NULL, 0} //長選項數(shù)組必須以一個空的設(shè)置為結(jié)束元素
};
//一直解析參數(shù)直到參數(shù)解析完畢
while ((opt = getopt_long(argc, argv, shortOpts, longOpts, NULL)) != -1)
{
switch (opt)
{
case 'v':
printVersion();
exit(0);
break;
case 'h':
info.host = optarg;
break;
case 'p':
info.port = atoi(optarg);
break;
case 'c':
info.clients = atoi(optarg);
break;
case 'r':
info.requestType = atoi(optarg);
break;
case 't':
info.times = atoi(optarg);
break;
case ':':
case '?':
benchMarkUsage();
return -1;
break;
}
}
return 0;
}
void benchMarkInfoInit(BenchMarkInfo & info)
{
info.host = "";
info.port = -1;
info.clients = 4; //默認并發(fā)4個客戶端
info.requestType = CONN; //默認是基于連接的壓測
info.times = 60; //默認壓測60秒
}
string getRequestTypeStr(int32_t requestType)
{
if (CONN == requestType)
{
return string("CONN");
}
else
{
return string("HTTP");
}
}
int checkArgv(BenchMarkInfo & info)
{
if ("" == info.host)
{
cout << "host is empty" << endl;
return -1;
}
if (info.port <= 0)
{
cout << "port parameter is invalid" << endl;
return -1;
}
if (info.clients <= 0)
{
cout << "number of clients is invalid" << endl;
return -1;
}
if (info.clients > 100)
{
cout << "max number of clients is 100" << endl;
return -1;
}
if (info.requestType != CONN && info.requestType != HTTP)
{
cout << "requestType only support 1(connection) and 2(http)" << endl;
return -1;
}
if (info.times <= 0)
{
cout << "times is invalid" << endl;
return -1;
}
cout << "benchMark running. "<< endl;
cout << "\thost[" << info.host << "], port[" << info.port
<< "], clients[" << info.clients << "], time["
<< info.times << "], requestType["
<< getRequestTypeStr(info.requestType) << "]" << endl << endl;
return 0;
}
int main(int argc, char * argv[])
{
int ret = 0;
BenchMarkInfo info;
benchMarkInfoInit(info); //初始化性能測試信息
ret = dealArgv(argc, argv, info); //解析輸入?yún)?shù)
if (ret != 0)
{
return -1;
}
ret = checkArgv(info); //校驗參數(shù)值是否合法
if (ret != 0)
{
benchMarkUsage();
return -1;
}
//使用BenchMark工廠類生成具體的BenchMark類
BenchMarkBase * pBase = BenchMarkFactory::getBenchMark(info.requestType);
//運行run進行壓測
pBase->run(info);
//釋放空間
delete pBase;
return 0;
}
3.6.2 BenchMarkBase類
- benchMarkBase.h
//表示頭文件只被編譯一次,相對于使用#ifndef ... #define ... #endif更方便
#pragma once
#include "benchMarkCommon.h"
class BenchMarkBase
{
public:
void run(BenchMarkInfo & info);
protected:
virtual void childDeal(int writeFd, BenchMarkInfo & info) = 0;
void parentDeal(int readFd, BenchMarkInfo & info);
private:
//nothing.
};
- benchMarkBase.cpp
#include "robustIo.h"
#include "benchMarkBase.h"
#include <errno.h>
#include <string.h>
#include <iostream>
using namespace std;
void BenchMarkBase::parentDeal(int readFd, BenchMarkInfo & info)
{
RobustIo rio; //封裝的io類變量
rio.rioInit(1024); //初始化讀緩沖區(qū)
int32_t childReportData[3]; //子進程的報告數(shù)據(jù)為3個int32_t變量
int32_t success = 0;
int32_t failed = 0;
int32_t bytes = 0;
while (true)
{
//讀取一個子進程的報告
if (rio.rioRead(readFd, childReportData, 12) != 12)
{
break; //讀失敗或者報告全部讀完
}
//統(tǒng)計成功次數(shù),成功次數(shù)放在第一個int32_t
success += childReportData[0];
//統(tǒng)計失敗次數(shù),失敗次數(shù)放在第二個int32_t
failed += childReportData[1];
//統(tǒng)計上下行流量,上下行流量放在第三個int32_t
bytes += childReportData[2];
}
//輸出壓測報告
cout << "benchMark report:" << endl;
if (CONN == info.requestType)
{
cout << "\tspeed=" << (success + failed) / info.times << " conn/sec. "
<< "success=" << success <<", failed=" << failed << endl;
}
else
{
cout << "\tspeed=" << (success + failed) / info.times << " pages/sec, "
<< (bytes / (info.times * 1024)) << " kbytes/sec. "
<< "success=" << success <<", failed=" << failed << endl;
}
}
/*
核心壓測函數(shù)
*/
void BenchMarkBase::run(BenchMarkInfo & info)
{
int fd[2];
int ret = 0;
int sockfd = 0;
pid_t childPid = 0;
RobustIo rio;
//先校驗在指定的host和port上是否開放了服務(wù)。
//創(chuàng)建tcp連接失敗說明沒開放服務(wù),直接返回終止壓測
sockfd = rio.newSocket((char *)info.host.c_str(), info.port);
if (sockfd < 0)
{
cout << "connect " << info.host << ":" << info.port
<< " failed. abort benchMark" << endl;
return;
}
close(sockfd);
ret = pipe(fd); //創(chuàng)建匿名管道用于父子進程間通信
if (ret != 0)
{
cout << "call pipe() failed! error message = " << strerror(errno) << endl;
return;
}
//調(diào)用fork創(chuàng)建指定的子進程數(shù)
for (int32_t i = 0; i < info.clients; ++i)
{
childPid = fork();
if (childPid <= 0) //從子進程中返回,或者父進程調(diào)用fork失敗
{
break;
}
}
if (0 == childPid) //從子進程中返回
{
close(fd[0]); //關(guān)閉匿名管道讀端
childDeal(fd[1], info); //在子進程中進行壓測,并傳入匿名管道寫fd
return; //壓測完子進程返回,并在返回main函數(shù)后結(jié)束進程運行
}
//在父進程中返回(調(diào)用fork失敗,或者調(diào)用fork全部成功)
if (childPid < 0) //調(diào)用fork失敗的話,打印一下調(diào)用失敗原因
{
cout << "fork childs failed. error message = " << strerror(errno) << endl;
}
//父進程關(guān)閉匿名管道的寫端,這里必須關(guān)閉寫端,
//否則在父進程接收子進程的壓測報告數(shù)據(jù)時,無法讀到文件結(jié)束標(biāo)志,
//阻塞在read調(diào)用,導(dǎo)致父進程無法退出。
close(fd[1]);
//在父進程中接收子進程報告,匯總統(tǒng)計后輸出最后的壓測報告
parentDeal(fd[0], info);
}
3.6.3 BenchMarkConn類
- benchMarkConn.h
#pragma once
#include "benchMarkBase.h"
class BenchMarkConn : public BenchMarkBase
{
public:
//nothing.
protected:
void childDeal(int writeFd, BenchMarkInfo & info);
public:
//nothing.
};
- benchMarkConn.cpp
#include "robustIo.h"
#include "benchMarkConn.h"
#include <errno.h>
#include <string.h>
void BenchMarkConn::childDeal(int writeFd, BenchMarkInfo & info)
{
RobustIo rio;
//子進程壓測報告數(shù)據(jù),第一個int32_t是成功次數(shù),
//第二個int32_t是失敗次數(shù),第三個int32_t是上下行流量統(tǒng)計
int32_t statData[3];
int32_t beginTime = time(NULL);
memset(statData, 0x0, sizeof(statData)); //初始化統(tǒng)計數(shù)據(jù)為0
while (time(NULL) <= beginTime + info.times)
{
int sock = rio.newSocket((char *)info.host.c_str(), info.port);
if (sock < 0)
{
if (errno != EINTR)
{
++statData[1]; //連接失敗數(shù)統(tǒng)計在statData[1]
}
}
else
{
++statData[0]; //連接成功數(shù)統(tǒng)計在statData[0]
}
close(sock);
}
//向匿名管道寫入壓測報告數(shù)據(jù)
rio.rioWrite(writeFd, statData, sizeof(statData));
}
3.6.4 BenchMarkHttp類
- benchMarkHttp.h
#pragma once
#include "benchMarkBase.h"
class BenchMarkHttp : public BenchMarkBase
{
public:
//nothing.
protected:
bool dealHttpReq(int sock, BenchMarkInfo & info, int32_t & bytes);
void getRequestData(string & data, BenchMarkInfo & info);
void childDeal(int writeFd, BenchMarkInfo & info);
private:
//nothing.
};
- benchMarkHttp.cpp
#include "robustIo.h"
#include "http_parser.h"
#include "benchMarkHttp.h"
#include <iostream>
using namespace std;
void BenchMarkHttp::getRequestData(string & data, BenchMarkInfo & info)
{
//http GET請求
data = "GET / HTTP/1.1\r\n" //請求行,表示發(fā)起GET請求,url為"/",使用http 1.1協(xié)議
"User-Agent: benchMark v1.0\r\n" //User-Agent請求頭,表示當(dāng)前客戶端代理信息
"Host: " + info.host +"\r\n" //Host請求頭,表示請求的服務(wù)器域名
"Accept: */*\r\n" //Accept請求頭,表示接受任意格式的應(yīng)答數(shù)據(jù)
"\r\n"; //一個空行表示請求頭集合的結(jié)束
}
static int http_rsp_complete(http_parser * parser)
{
//取出之前在解析變量中設(shè)置的私有數(shù)據(jù),它為bool指針,
//它指向dealHttpReq函數(shù)中的finish變量
bool * pFinish = (bool *)parser->data;
//設(shè)置finish的值為true,表示已經(jīng)完成了一個http應(yīng)答的解析。
*pFinish = true;
return 0;
}
bool BenchMarkHttp::dealHttpReq(int sock, BenchMarkInfo & info, int32_t & bytes)
{
size_t ret = 0;
string data;
RobustIo rio; //封裝的io類變量
bool finish = false; //表示應(yīng)答是否解析完畢,它的指針會被傳遞給http解析變量
http_parser parser; //聲明http解析api的解析變量parser
http_parser_settings settings; //聲明http解析api的解析設(shè)置變量settings
http_parser_init(&parser, HTTP_RESPONSE); //初始化http解析api的解析變量parser
http_parser_settings_init(&settings); //初始化http解析api的解析設(shè)置變量settings
settings.on_message_complete = http_rsp_complete; //設(shè)置http應(yīng)答解析完成回調(diào)函數(shù)
parser.data = (void *)(&finish); //在解析變量parser中設(shè)置我們的私有數(shù)據(jù),在回調(diào)函數(shù)中會使用
getRequestData(data, info); //獲取http GET請求的發(fā)送數(shù)據(jù)
//使用封裝好的網(wǎng)絡(luò)io類發(fā)送http GET請求
ret = rio.rioWrite(sock, (void *)data.c_str(), data.size());
//發(fā)送失敗則返回false
if (ret != data.size())
{
return false;
}
//統(tǒng)計發(fā)送字節(jié)數(shù)
bytes += data.size();
char c;
int status;
//初始化io讀緩存區(qū)大小
rio.rioInit(1024);
//只要還沒解析完應(yīng)答則一直從網(wǎng)絡(luò)中獲取數(shù)據(jù),并解析
while (!finish)
{
//從網(wǎng)絡(luò)中讀取一個字節(jié),rioRead是自帶緩沖區(qū)的
//故不會頻繁的調(diào)用系統(tǒng)read函數(shù),帶來性能影響
if (rio.rioRead(sock, &c, 1) != 1)
{
break;
}
//統(tǒng)計接收字節(jié)數(shù)
++bytes;
//調(diào)用http解析核心api對流式數(shù)據(jù)進行解析,每次解析從網(wǎng)絡(luò)流中獲取的一個字節(jié)
//這個api函數(shù)返回解析成功的字節(jié)數(shù),如果返回值和傳入的要解析的字節(jié)數(shù)不一致
//則表明解析過程中發(fā)送錯誤,則返回false
if (http_parser_execute(&parser, &settings, &c, 1) != 1)
{
return false;
}
}
//獲取http應(yīng)答的狀態(tài)碼
status = parser.status_code;
if (2 == (status / 100)) //狀態(tài)碼為2xx的都表示請求成功,故返回true?
{
return true;
}
else
{
return false;
}
}
void BenchMarkHttp::childDeal(int writeFd, BenchMarkInfo & info)
{
int sock = 0;
RobustIo rio;
//子進程壓測報告數(shù)據(jù),第一個int32_t是成功次數(shù),
//第二個int32_t是失敗次數(shù),第三個int32_t是上下行流量統(tǒng)計
int32_t statData[3];
int32_t beginTime = time(NULL);
memset(statData, 0x0, sizeof(statData)); //初始化統(tǒng)計數(shù)據(jù)為0
while (time(NULL) <= beginTime + info.times)
{
sock = rio.newSocket((char *)info.host.c_str(), info.port);
if (sock < 0)
{
++statData[1]; //發(fā)起連接失敗統(tǒng)計在statData[1]
}
else
{
//發(fā)起http請求,并統(tǒng)計上下行流量
if (dealHttpReq(sock, info, statData[2]))
{
++statData[0]; //http請求成功統(tǒng)計在statData[0]
}
else
{
++statData[1]; //http請求失敗統(tǒng)計在statData[1]
}
}
close(sock);
}
//向匿名管道寫入壓測報告數(shù)據(jù)
rio.rioWrite(writeFd, statData, sizeof(statData));
}
3.6.5 BenchMarkFactory類
- benchMarkFactory.h
#pragma once
#include <stdint.h>
#include "benchMarkBase.h"
#include "benchMarkConn.h"
#include "benchMarkHttp.h"
#include "benchMarkCommon.h"
class BenchMarkFactory
{
public:
static BenchMarkBase * getBenchMark(int32_t requestType);
protected:
//nothing.
private:
//nothing.
};
- benchMarkFactory.cpp
#include "benchMarkFactory.h"
BenchMarkBase * BenchMarkFactory::getBenchMark(int32_t requestType)
{
if (CONN == requestType)
{
return new BenchMarkConn;
}
else if (HTTP == requestType)
{
return new BenchMarkHttp;
}
return NULL;
}
3.6.6 BenchMarkInfo結(jié)構(gòu)體
- benchMarkCommon.h
#pragma once
#include <stdint.h>
#include <string>
using namespace std;
enum RequestType
{
CONN = 1, //基于連接
HTTP = 2 //基于http
};
struct BenchMarkInfo
{
string host;
int32_t port;
int32_t clients;
int32_t requestType;
int32_t times;
};
3.7 實測性能工具
3.7.1 編譯
[root@rookie_centos benchMark]# ls
Makefile benchMark.o benchMarkBase.o benchMarkConn.h benchMarkFactory.h benchMarkHttp.h http_parser.h robustIo.h
benchMark benchMarkBase.cpp benchMarkCommon.h benchMarkConn.o benchMarkFactory.o benchMarkHttp.o http_parser.o robustIo.o
benchMark.cpp benchMarkBase.h benchMarkConn.cpp benchMarkFactory.cpp benchMarkHttp.cpp http_parser.c robustIo.cpp
[root@rookie_centos benchMark]#
[root@rookie_centos benchMark]# make
cc -g -O2 -Wall -Werror -Wshadow -c http_parser.c -o http_parser.o
g++ -g -O2 -Wall -Werror -Wshadow -c benchMark.cpp -o benchMark.o
g++ -g -O2 -Wall -Werror -Wshadow -c benchMarkBase.cpp -o benchMarkBase.o
g++ -g -O2 -Wall -Werror -Wshadow -c benchMarkConn.cpp -o benchMarkConn.o
g++ -g -O2 -Wall -Werror -Wshadow -c benchMarkFactory.cpp -o benchMarkFactory.o
g++ -g -O2 -Wall -Werror -Wshadow -c benchMarkHttp.cpp -o benchMarkHttp.o
g++ -g -O2 -Wall -Werror -Wshadow -c robustIo.cpp -o robustIo.o
g++ -g -O2 -Wall -Werror -Wshadow ./http_parser.o ./benchMark.o ./benchMarkBase.o ./benchMarkConn.o ./benchMarkFactory.o ./benchMarkHttp.o ./robustIo.o -o benchMark
Type ./benchMark to execute the program.
3.7.2 運行測試
我們對百度主頁進行壓測
- 基于連接的壓測
[root@rookie_centos benchMark]# ./benchMark -h www.baidu.com -p 30 -c 30 -t 10
benchMark running.
host[www.baidu.com], port[30], clients[30], time[10], requestType[CONN]
benchMark report:
speed=453 conn/sec. success=4536, failed=0
[root@rookie_centos benchMark]#
- 基于http的壓測
[root@rookie_centos benchMark]# ./benchMark -h www.baidu.com -p 30 -c 30 -t 10 -r 2
benchMark running.
host[www.baidu.com], port[30], clients[30], time[10], requestType[HTTP]
benchMark report:
speed=104 pages/sec, 11327 kbytes/sec. success=1042, failed=0
[root@rookie_centos benchMark]#
3.8 性能工具擴展
除了對http接口進行壓測外,我們還可以擴展出其他的業(yè)務(wù)壓測類,只要編寫好相應(yīng)的壓測類,并放入項目中即可。