後端研發菜鳥成長記 第三章 小試牛刀,編寫性能測試工具

3 小試牛刀,編寫性能測試工具

做爲一名後端研發人員,必須具有系統性能評估和分析能力,由於只有對系統整體性能瞭如指掌,才能知道系統何時須要擴容,系統哪裏有性能瓶頸須要優化。node

本章將介紹如何宏觀的評估系統的整體性能,並重點介紹如何編寫性能測試工具對系統性能作一個實測,畢竟理論歸理論,理論上性能指標仍是須要靠實際壓測來檢驗。ios

3.1 系統的整體性能簡述

一般咱們使用QPS、平均響應時間、併發數,這三個指標來衡量一個系統的整體性能。git

  • QPS:表示系統每秒請求數。github

  • 平均響應時間:表示系統平均單個請求耗時,單位爲秒。web

  • 併發數:表示系統能同時處理的請求數。算法

系統能支持的最大QPS是由平均響應時間和併發數決定的,即QPS=併發數/平均響應時間。純理論的解說可能不太好理解,咱們這邊舉一個例子,好比咱們去銀行營業廳辦存款業務,這時有10個櫃檯能夠辦理存款業務,每一個人辦理存款業務平均須要5分鐘,那麼這個營業廳每小時能夠處理多少的存款業務呢?這裏10個櫃檯能夠辦理業務也就是說「併發數」爲10,咱們單位時間設定爲小時,那麼辦理存款業務平均須要5分鐘也就是說「平均響應時間」爲12分之1小時,那麼這個營業廳每小時能夠處理的存款業務數也就是QPS=10/(1/12),即120。數據庫

由上面的公式「QPS=併發數/平均響應時間」,咱們能夠看出要提升系統的QPS,能夠從兩個方面着手,一方面是提升併發數,一方面是下降平均響應時間。後端

  • 提升併發數的方法
    新增更多的服務器,採用多核服務器,單服務器上運行多進程提供服務,進程採用更高效性的IO模型。centos

  • 下降平均響應時間的方法
    響應時間一般由:網絡通信時間(網絡io時間)+計算處理時間(cpu時間)+磁盤讀寫時間(磁盤io時間)構成的。能夠經過使用更大的網絡帶寬,更好的網卡來下降網絡通信時間,可使用更強大的cpu或者優化算法來減小計算處理時間,可使用SSD硬盤來替代普通硬盤來下降單次讀寫磁盤的io時間,能夠在業務層和數據庫持久化層之間添加一個緩存層來減小磁盤io次數。api

3.2 性能測試工具

咱們的性能測試工具是命令行工具,命名爲「benchMark」,它用於測試一個系統提供的網絡服務的整體性能,它具有命令參數功能、支持多客戶端併發、支持基於鏈接的壓測、支持基於特定業務請求的壓測等的功能。

3.2.1 命令參數功能

和全部Linux的命令同樣,咱們的工具也支持命令參數,在Linux下有一個getopt_long函數用於支持對命令行參數的解析,經過這個函數咱們能輕易是實現對長短參數的解析和獲取。getopt_long函數所在的頭文件和函數原型以下:

#include <getopt.h>

int getopt_long(int argc, char * const argv[], const char *optstring, const struct option *longopts, int *longindex);

做爲C/C++入口函數,main的標準函數原型爲「int main(int argc, char * argv[])」,命令行參數也正是經過main函數的argc、argv這兩個參數傳遞進來的。

  • argc與argv參數

getopt_long函數開頭須要傳入的兩個參數就是main函數的argc和argv參數,argc表示要解析參數的個數,argv是具體要解析的參數列表。

  • optstring參數

optstring爲選項聲明串,其中一個字符表明一個選項,若是字符後面跟上一個英文冒號代表這個選項必須有一個選項參數,這個選項參數能夠和選項在同一個命令行參數中,即「-oarg」,也能夠在下一個命令行參數中,即「-o arg」,這個選項參數的值能夠從全局變量optarg中獲取到;若是字符後面跟上兩個冒號代表這個選項有一個可選的選項參數,此時可選的選項參數要和選項在同一個命令行參數中,好比咱們設置o爲有可選的選項參數的選項,這時要設置可選的選項參數,可選的選項參數必須和選項在同一個參數中,即「-oarg」,其中arg爲o選項的可選的選項參數,它們之間不能有空格,一樣的這個可選的選項參數能夠在全局變量optarg中獲取,沒有設置可選的選項參數時,optarg的值爲0。

  • longopts參數

optstring中包含的只有短選項,當要支持長選項的時候就須要使用longopts參數了,longopts參數是一個指向struct option數組的指針,在struct option數組中設置了長選項的相關信息。長選項在設置選項參數時和短選項有稍微的不一樣,短選項爲「-oarg」或者「-o arg」,而長選項爲「--arg=param」或者「--arg param」,短選項設置可選參數時只能使用「-oarg」的形式,長選項設置可選參數時只能使用「--arg=param」的形式。下面咱們看一下struct option中各個字段的含義。

struct option 
{
    const char *name; 
    int         has_arg; 
    int        *flag;
    int         val;
};

name字段:表示長選項的名稱。

has_arg字段:表示長選項參數設置,若是設置成no_argument(或者0)表示長選項沒有參數,若是設置成required_argument(或者1)表示長選項必須有一個參數,若是設置成optional_argument(或者3)表示長選項有一個可選項參數,長選項的可選參數必須使用「--arg=param」來設置。

flag字段:一般爲NULL,若是flag爲NULL,則當解析到name設置的長選項時getopt_long返回最後一個val字段設置的值,經過這個方式可使短選項和長選項具有相同的功能;若是flag不爲NULL,則當解析到name設置的長選項時getopt_long返回0,flag指向的變量的值將被設置成val設置的值,長選項沒解析到,則flag指向的變量的值不會被改變。

val字段:做爲匹配到長選項時getopt_long的返回值(flag爲NULL),或者做爲設置flag指向的變量的值(flag不爲NULL)。

  • longindex參數

能夠設置爲NULL,不爲NULL時用於返回匹配的長選項在長選項數組中的索引值。

3.2.2 多客戶端併發

咱們使用fork的方式來模擬多客戶端併發的場景,子進程和父進程經過pipe函數建立的匿名管道來進行通訊,父進程對測試結果進行彙總統計,在全部子進程結束後在父進程中打印測試的彙總結果。

3.2.3 基於鏈接仍是基於請求

咱們的測試工具支持基於鏈接(建立網絡鏈接成功後就立刻關閉網絡鏈接)的併發壓測;也支持基於請求(建立網絡鏈接成功後還會發起業務請求,並在接收完應答數據後才關閉網絡鏈接)的併發壓測,能夠針對不一樣的業務設置不一樣請求數據和應答數據不一樣的校驗邏輯。

3.2.4 性能工具的侷限性

咱們的性能工具只能運行在單服務器上,故併發壓測能力受限於單服務器的併發能力,若是是測試外網服務還受限於服務器的外網帶寬,外網帶寬一旦跑滿併發數就難再有所提升。

3.3 數據交互過程

性能測試工具和網絡服務系統的數據交互過程以下圖:

clipboard.png

[圖3-1 數據交互]

3.4 類關係圖

benchMark主要由5個類和1個結構體構成。其中結構體BenchMarkInfo用於保存測試相關的輸入參數;BenchMarkFactory是一個簡單的工廠類,它用於生成具體的壓測類BenchMarkConn(基於鏈接)和BenchMarkHttp(基於http協議),BenchMarkBase是壓測基類;RobustIo類是對網絡io的封裝,它自帶讀緩衝區。這5個類和1個結構體的關係以下類關係圖:

clipboard.png

[圖3-2 類關係圖]

3.5 http的請求與應答

在BenchMarkHttp類中涉及到了http協議,在BenchMarkHttp中咱們壓測的是獲取web站點根目錄接口的性能,發起的是GET請求,url爲「/」。http應答解析方面咱們使用開源的C語言的http解析api,它沒有其餘特別庫的依賴,支持流式的解析,在流式解析過程當中採用回調的方式來通知http協議相關的字段信息。在第9章「網絡通訊與併發」中咱們將本身實現一個簡單的http協議解析類,並對http協議作詳細的講解,加深你們對http協議的理解。

3.5.1 發起http請求

http請求報文由三部分組成,它們分別爲:請求行(request_line)、請求頭集合(headers)、請求體(body);請求行和請求頭都是以「rn」爲結尾,請求頭集合則以一個 空行(「rn」)做爲請求頭集合結束的標誌,其中body爲可選部分,咱們http壓測使用的GET請求就沒有body部分。接下來看一下具體的請求數據生成函數。

void BenchMarkHttp::getRequestData(string & data, BenchMarkInfo & info)
{
    //http GET請求
    data = "GET / HTTP/1.1\r\n"           //請求行,表示發起GET請求,url爲"/",使用http 1.1協議
        "User-Agent: benchMark v1.0\r\n"  //User-Agent請求頭,表示當前客戶端代理信息
        "Host: " + info.host +"\r\n"      //Host請求頭,表示請求的服務器域名
        "Accept: */*\r\n"                 //Accept請求頭,表示接受任意格式的應答數據
        "\r\n";                           //一個空行表示請求頭集合的結束
}

3.5.3 解析http應答

咱們使用的C語言的http解析api託管在github上,項目連接爲:https://github.com/nodejs/htt...

主要分爲如下幾個步驟來使用開源的http解析api,下面是咱們在BenchMarkHttp使用http解析api的相關代碼:

static int http_rsp_complete(http_parser * parser)
{
    //取出以前在解析變量中設置的私有數據,它爲bool指針,
    //它指向dealHttpReq函數中的finish變量
    bool * pFinish = (bool *)parser->data;
    //設置finish的值爲true,表示已經完成了一個http應答的解析。
    *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;    //表示應答是否解析完畢,它的指針會被傳遞給http解析變量

    http_parser parser;             //聲明http解析api的解析變量parser
    http_parser_settings settings;  //聲明http解析api的解析設置變量settings
    
    http_parser_init(&parser, HTTP_RESPONSE);   //初始化http解析api的解析變量parser
    http_parser_settings_init(&settings);       //初始化http解析api的解析設置變量settings
    
    settings.on_message_complete = http_rsp_complete; //設置http應答解析完成回調函數
    parser.data = (void *)(&finish);   //在解析變量parser中設置咱們的私有數據,在回調函數中會使用

    getRequestData(data, info); //獲取http GET請求的發送數據
    //使用封裝好的網絡io類發送http GET請求
    ret = rio.rioWrite(sock, (void *)data.c_str(), data.size());
    //發送失敗則返回false
    if (ret != data.size())
    {
        return false;
    }
    //統計發送字節數
    bytes += data.size();

    char c;
    int status;
    //初始化io讀緩存區大小
    rio.rioInit(1034);
    //只要還沒解析完應答則一直從網絡中獲取數據,並解析
    while (!finish)
    {
        //從網絡中讀取一個字節,rioRead是自帶緩衝區的
        //故不會頻繁的調用系統read函數,帶來性能影響
        if (rio.rioRead(sock, &c, 1) != 1)
        {
            break;
        }
        //統計接收字節數
        ++bytes; 
        //調用http解析核心api對流式數據進行解析,每次解析從網絡流中獲取的一個字節
        //這個api函數返回解析成功的字節數,若是返回值和傳入的要解析的字節數不一致
        //則代表解析過程當中發送錯誤,則返回false
        if (http_parser_execute(&parser, &settings, &c, 1) != 1)
        {
            return false;
        }
    }

    //獲取http應答的狀態碼
    status = parser.status_code;
    if (2 == (status / 100)) //狀態碼爲2xx的都表示請求成功,故返回true?
    {
        return true;
    }
    else
    {
        return false;
    }
}

從上面代碼咱們能夠看到http解析api使用的變量爲:parser和settings,它們的類型分別爲http_parser和http_parser_settings,在使用它們以前咱們須要分別使用http_parser_init和http_parser_settings_init進行初始化,在settings中設置瞭解析完畢的回調函數http_rsp_complete,在http_rsp_complete函數中咱們將dealHttpReq中的finish變量的值設置爲true代表http應答已經解析完畢,dealHttpReq就能退出http解析循環。在dealHttpReq函數中咱們使用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;
    //常規選項
    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;
    //併發選項
    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}  //長選項數組必須以一個空的設置爲結束元素
    };

    //一直解析參數直到參數解析完畢
    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;          //默認併發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); //解析輸入參數
    if (ret != 0)
    {
        return -1;
    }

    ret = checkArgv(info);  //校驗參數值是否合法
    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); //初始化讀緩衝區
    int32_t childReportData[3]; //子進程的報告數據爲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; //讀失敗或者報告所有讀完
        }
        //統計成功次數,成功次數放在第一個int32_t
        success += childReportData[0];
        //統計失敗次數,失敗次數放在第二個int32_t
        failed += childReportData[1];
        //統計上下行流量,上下行流量放在第三個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;
    }
}

/*
    核心壓測函數
 */
void BenchMarkBase::run(BenchMarkInfo & info)
{
    int fd[2];
    int ret = 0;
    int sockfd = 0;
    pid_t childPid = 0;
    RobustIo rio;

    //先校驗在指定的host和port上是否開放了服務。
    //建立tcp鏈接失敗說明沒開放服務,直接返回終止壓測
    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); //建立匿名管道用於父子進程間通訊
    if (ret != 0)
    {
        cout << "call pipe() failed! error message = " << strerror(errno) << endl;
        return;
    }

    //調用fork建立指定的子進程數
    for (int32_t i = 0; i < info.clients; ++i)
    {
        childPid = fork();
        if (childPid <= 0) //從子進程中返回,或者父進程調用fork失敗
        {
            break;
        }
    }

    if (0 == childPid) //從子進程中返回
    {
        close(fd[0]); //關閉匿名管道讀端
        childDeal(fd[1], info); //在子進程中進行壓測,並傳入匿名管道寫fd
        return; //壓測完子進程返回,並在返回main函數後結束進程運行
    }

    //在父進程中返回(調用fork失敗,或者調用fork所有成功)
    
    if (childPid < 0) //調用fork失敗的話,打印一下調用失敗緣由
    {
        cout << "fork childs failed. error message = " << strerror(errno) << endl;
    }

    //父進程關閉匿名管道的寫端,這裏必須關閉寫端,
    //不然在父進程接收子進程的壓測報告數據時,沒法讀到文件結束標誌,
    //阻塞在read調用,致使父進程沒法退出。
    close(fd[1]); 
    //在父進程中接收子進程報告,彙總統計後輸出最後的壓測報告
    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;
    //子進程壓測報告數據,第一個int32_t是成功次數,
    //第二個int32_t是失敗次數,第三個int32_t是上下行流量統計
    int32_t statData[3];
    int32_t beginTime = time(NULL);
    memset(statData, 0x0, sizeof(statData)); //初始化統計數據爲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]; //鏈接失敗數統計在statData[1]
            }
        }
        else
        {
            ++statData[0]; //鏈接成功數統計在statData[0]
        }
        close(sock);
    }

    //向匿名管道寫入壓測報告數據
    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"           //請求行,表示發起GET請求,url爲"/",使用http 1.1協議
        "User-Agent: benchMark v1.0\r\n"  //User-Agent請求頭,表示當前客戶端代理信息
        "Host: " + info.host +"\r\n"      //Host請求頭,表示請求的服務器域名
        "Accept: */*\r\n"                 //Accept請求頭,表示接受任意格式的應答數據
        "\r\n";                           //一個空行表示請求頭集合的結束
}

static int http_rsp_complete(http_parser * parser)
{
    //取出以前在解析變量中設置的私有數據,它爲bool指針,
    //它指向dealHttpReq函數中的finish變量
    bool * pFinish = (bool *)parser->data;
    //設置finish的值爲true,表示已經完成了一個http應答的解析。
    *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;    //表示應答是否解析完畢,它的指針會被傳遞給http解析變量

    http_parser parser;             //聲明http解析api的解析變量parser
    http_parser_settings settings;  //聲明http解析api的解析設置變量settings
    
    http_parser_init(&parser, HTTP_RESPONSE);   //初始化http解析api的解析變量parser
    http_parser_settings_init(&settings);       //初始化http解析api的解析設置變量settings
    
    settings.on_message_complete = http_rsp_complete; //設置http應答解析完成回調函數
    parser.data = (void *)(&finish);   //在解析變量parser中設置咱們的私有數據,在回調函數中會使用

    getRequestData(data, info); //獲取http GET請求的發送數據
    //使用封裝好的網絡io類發送http GET請求
    ret = rio.rioWrite(sock, (void *)data.c_str(), data.size());
    //發送失敗則返回false
    if (ret != data.size())
    {
        return false;
    }
    //統計發送字節數
    bytes += data.size();

    char c;
    int status;
    //初始化io讀緩存區大小
    rio.rioInit(1024);
    //只要還沒解析完應答則一直從網絡中獲取數據,並解析
    while (!finish)
    {
        //從網絡中讀取一個字節,rioRead是自帶緩衝區的
        //故不會頻繁的調用系統read函數,帶來性能影響
        if (rio.rioRead(sock, &c, 1) != 1)
        {
            break;
        }
        //統計接收字節數
        ++bytes; 
        //調用http解析核心api對流式數據進行解析,每次解析從網絡流中獲取的一個字節
        //這個api函數返回解析成功的字節數,若是返回值和傳入的要解析的字節數不一致
        //則代表解析過程當中發送錯誤,則返回false
        if (http_parser_execute(&parser, &settings, &c, 1) != 1)
        {
            return false;
        }
    }

    //獲取http應答的狀態碼
    status = parser.status_code;
    if (2 == (status / 100)) //狀態碼爲2xx的都表示請求成功,故返回true?
    {
        return true;
    }
    else
    {
        return false;
    }
}

void BenchMarkHttp::childDeal(int writeFd, BenchMarkInfo & info)
{
    int sock = 0;
    RobustIo rio;
    //子進程壓測報告數據,第一個int32_t是成功次數,
    //第二個int32_t是失敗次數,第三個int32_t是上下行流量統計
    int32_t statData[3];
    int32_t beginTime = time(NULL);
    memset(statData, 0x0, sizeof(statData)); //初始化統計數據爲0
    

    while (time(NULL) <= beginTime + info.times)
    {
        sock = rio.newSocket((char *)info.host.c_str(), info.port);
        if (sock < 0)
        {
            ++statData[1]; //發起鏈接失敗統計在statData[1]
        }
        else
        {
            //發起http請求,並統計上下行流量
            if (dealHttpReq(sock, info, statData[2]))
            {
                ++statData[0]; //http請求成功統計在statData[0]
            }
            else
            {
                ++statData[1]; //http請求失敗統計在statData[1]
            }
        }
        close(sock);
    }

    //向匿名管道寫入壓測報告數據
    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結構體

  • 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 運行測試

咱們對百度www.baidu.com站點進行壓測

  • 基於鏈接的壓測

[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接口進行壓測外,咱們還能夠擴展出其餘的業務壓測類,只要編寫好相應的壓測類,並放入項目中便可。

相關文章
相關標籤/搜索