广告位联系
返回顶部
分享到

基于Go语言实现一个压测工具

Golang 来源:互联网 作者:佚名 发布时间:2025-01-29 11:25:27 人浏览
摘要

整体架构 整体系统架构比较简单 通用数据处理模块 Http请求响应数据处理 本项目支持http协议、websocket协议、grpc协议、Remote Authentication Dial-In User Service协议,因此需要构造出一个通用的http请求

整体架构

整体系统架构比较简单

通用数据处理模块

Http请求响应数据处理

本项目支持http协议、websocket协议、grpc协议、Remote Authentication Dial-In User Service协议,因此需要构造出一个通用的http请求和响应的结构体,进行一个通用的封装:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

// Request 请求数据

type Request struct {

    URL       string            // URL

    Form      string            // http/webSocket/tcp

    Method    string            // 方法 GET/POST/PUT

    Headers   map[string]string // Headers

    Body      string            // body

    Verify    string            // 验证的方法

    Timeout   time.Duration     // 请求超时时间

    Debug     bool              // 是否开启Debug模式

    MaxCon    int               // 每个连接的请求数

    HTTP2     bool              // 是否使用http2.0

    Keepalive bool              // 是否开启长连接

    Code      int               // 验证的状态码

    Redirect  bool              // 是否重定向

}

这当中值得注意的是验证的方法,这里是因为在进行压测中,要判断返回的响应是否是正确的响应,因此要进行判断响应是否正确,所以要进行相应的函数的注册,因此对于一个请求,是有必要找到一个对应的请求方法来判断这个请求正确,之后进行记录

这个model的核心功能,就是生成一个http请求的结构体,来帮助进行存储

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

// NewRequest 生成请求结构体

// url 压测的url

// verify 验证方法 在server/verify中 http 支持:statusCode、json webSocket支持:json

// timeout 请求超时时间

// debug 是否开启debug

// path curl文件路径 http接口压测,自定义参数设置

func NewRequest(url string, verify string, code int, timeout time.Duration, debug bool, path string,

    reqHeaders []string, reqBody string, maxCon int, http2, keepalive, redirect bool) (request *Request, err error) {

    var (

        method  = "GET"

        headers = make(map[string]string)

        body    string

    )

    if path != "" {

        var curl *CURL

        curl, err = ParseTheFile(path)

        if err != nil {

            return nil, err

        }

        if url == "" {

            url = curl.GetURL()

        }

        method = curl.GetMethod()

        headers = curl.GetHeaders()

        body = curl.GetBody()

    } else {

        if reqBody != "" {

            method = "POST"

            body = reqBody

        }

        for _, v := range reqHeaders {

            getHeaderValue(v, headers)

        }

        if _, ok := headers["Content-Type"]; !ok {

            headers["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8"

        }

    }

    var form string

    form, url = getForm(url)

    if form == "" {

        err = fmt.Errorf("url:%s 不合法,必须是完整http、webSocket连接", url)

        return

    }

    var ok bool

    switch form {

    case FormTypeHTTP:

        // verify

        if verify == "" {

            verify = "statusCode"

        }

        key := fmt.Sprintf("%s.%s", form, verify)

        _, ok = verifyMapHTTP[key]

        if !ok {

            err = errors.New("验证器不存在:" + key)

            return

        }

    case FormTypeWebSocket:

        // verify

        if verify == "" {

            verify = "json"

        }

        key := fmt.Sprintf("%s.%s", form, verify)

        _, ok = verifyMapWebSocket[key]

        if !ok {

            err = errors.New("验证器不存在:" + key)

            return

        }

    }

    if timeout == 0 {

        timeout = 30 * time.Second

    }

    request = &Request{

        URL:       url,

        Form:      form,

        Method:    strings.ToUpper(method),

        Headers:   headers,

        Body:      body,

        Verify:    verify,

        Timeout:   timeout,

        Debug:     debug,

        MaxCon:    maxCon,

        HTTP2:     http2,

        Keepalive: keepalive,

        Code:      code,

        Redirect:  redirect,

    }

    return

}

之后是对于对应的响应的封装,结构体定义为:

1

2

3

4

5

6

7

8

9

// RequestResults 请求结果

type RequestResults struct {

    ID            string // 消息ID

    ChanID        uint64 // 消息ID

    Time          uint64 // 请求时间 纳秒

    IsSucceed     bool   // 是否请求成功

    ErrCode       int    // 错误码

    ReceivedBytes int64

}

Curl参数解析处理

对于这个模块,本项目中实现的逻辑是根据一个指定的Curl的文件,对于文件中的Curl进行解析,即可解析出对应的Http请求的参数,具体代码链接如下

https://gitee.com/zhaobohan/stress-testing/blob/master/model/curl_model.go

客户端模块

Http客户端处理

在该模块中主要是对于Http客户端进行处理,对于普通请求和Http2.0请求进行了特化处理,支持根据客户端ID来获取到指定的客户端,建立映射关系

具体的核心成员为:

1

2

3

4

5

6

var (

    mutex sync.RWMutex

    // clients 客户端

    // key 客户端id - value 客户端

    clients = make(map[uint64]*http.Client)

)

再具体的,对于客户端的封装,主要操作是,对于Client的构造

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

// createLangHTTPClient 初始化长连接客户端参数

// 创建了一个配置了长连接的 HTTP 客户端传输对象

func createLangHTTPClient(request *model.Request) *http.Client {

    tr := &http.Transport{

        // 使用 net.Dialer 来建立 TCP 连接

        // Timeout 设置为 30 秒,表示如果连接在 30 秒内没有建立成功,则超时

        // KeepAlive 设置为 30 秒,表示连接建立后,如果 30 秒内没有数据传输,则发送一个 keep-alive 探测包以保持连接

        DialContext: (&net.Dialer{

            Timeout:   30 * time.Second,

            KeepAlive: 30 * time.Second,

        }).DialContext,

        MaxIdleConns:        0,                // 最大连接数,默认0无穷大

        MaxIdleConnsPerHost: request.MaxCon,   // 对每个host的最大连接数量(MaxIdleConnsPerHost<=MaxIdleConns)

        IdleConnTimeout:     90 * time.Second, // 多长时间未使用自动关闭连接

        // InsecureSkipVerify 设置为 true,表示不验证服务器的 SSL 证书

        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},

    }

    if request.HTTP2 {

        // 使用真实证书 验证证书 模拟真实请求

        tr = &http.Transport{

            DialContext: (&net.Dialer{

                Timeout:   30 * time.Second,

                KeepAlive: 30 * time.Second,

            }).DialContext,

            MaxIdleConns:        0,                // 最大连接数,默认0无穷大

            MaxIdleConnsPerHost: request.MaxCon,   // 对每个host的最大连接数量(MaxIdleConnsPerHost<=MaxIdleConns)

            IdleConnTimeout:     90 * time.Second, // 多长时间未使用自动关闭连接

            // 配置 TLS 客户端设置,InsecureSkipVerify 设置为 false,表示验证服务器的 SSL 证书

            TLSClientConfig: &tls.Config{InsecureSkipVerify: false},

        }

        // 将 tr 配置为支持 HTTP/2 协议

        _ = http2.ConfigureTransport(tr)

    }

 

    client := &http.Client{

        Transport: tr,

    }

 

    // 禁止 HTTP 客户端自动重定向,而是让客户端在遇到重定向时停止并返回最后一个响应

    if !request.Redirect {

        client.CheckRedirect = func(req *http.Request, via []*http.Request) error {

            return http.ErrUseLastResponse

        }

    }

 

    return client

}

https://gitee.com/zhaobohan/stress-testing/blob/master/server/client/http_client.go

Grpc客户端处理

对于Grpc的构造来说,主要实现的功能是建立连接等,这些操作是较为简单的操作,因此这里不具体讲述

1

2

3

4

5

// GrpcSocket grpc

type GrpcSocket struct {

    conn    *grpc.ClientConn

    address string

}

conn和Address主要都是借助于两个类的成员函数来完成,解析地址和建立连接

其余模块可在代码中查看,这里不进行过多讲述

https://gitee.com/zhaobohan/stress-testing/blob/master/server/client/grpc_client.go

Websocket客户端处理

1

2

3

4

5

6

7

8

// WebSocket webSocket

type WebSocket struct {

    conn       *websocket.Conn

    URLLink    string

    URL        *url.URL

    IsSsl      bool

    HTTPHeader map[string]string

}

其余模块可在代码中查看,这里不进行过多讲述

https://gitee.com/zhaobohan/stress-testing/blob/master/server/client/websocket_client.go

连接处理模块

Grpc

对于Grpc的测试,这里模拟了一个rpc调用,执行了一个Hello World的函数,之后填充相应的数据作为请求的响应,最后将结果返回

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

// grpcRequest 请求

func grpcRequest(chanID uint64, ch chan<- *model.RequestResults, i uint64, request *model.Request,

    ws *client.GrpcSocket) {

    var (

        startTime = time.Now()

        isSucceed = false

        errCode   = model.HTTPOk

    )

    // 获取连接

    conn := ws.GetConn()

    if conn == nil {

        errCode = model.RequestErr

    } else {

        c := pb.NewApiServerClient(conn)

        var (

            ctx = context.Background()

            req = &pb.Request{

                UserName: request.Body,

            }

        )

        // 发送请求,获得响应

        rsp, err := c.HelloWorld(ctx, req)

        if err != nil {

            errCode = model.RequestErr

        } else {

            // 200 为成功

            if rsp.Code != 200 {

                errCode = model.RequestErr

            } else {

                isSucceed = true

            }

        }

    }

    requestTime := uint64(helper.DiffNano(startTime))

    requestResults := &model.RequestResults{

        Time:      requestTime,

        IsSucceed: isSucceed,

        ErrCode:   errCode,

    }

    requestResults.SetID(chanID, i)

    ch <- requestResults

}

Http

对于Http的测试,效果也基本类似,原理也基本相同

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

// HTTP 请求

func HTTP(ctx context.Context, chanID uint64, ch chan<- *model.RequestResults, totalNumber uint64, wg *sync.WaitGroup,

    request *model.Request) {

    defer func() {

        wg.Done()

    }()

    for i := uint64(0); i < totalNumber; i++ {

        if ctx.Err() != nil {

            break

        }

 

        list := getRequestList(request)

        isSucceed, errCode, requestTime, contentLength := sendList(chanID, list)

        requestResults := &model.RequestResults{

            Time:          requestTime,

            IsSucceed:     isSucceed,

            ErrCode:       errCode,

            ReceivedBytes: contentLength,

        }

        requestResults.SetID(chanID, i)

        ch <- requestResults

    }

 

    return

}

统计数据模块

下面来看计算统计数据模块

统计原理

这里需要统计的数据有以下:

耗时、并发数、成功数、失败数、qps、最长耗时、最短耗时、平均耗时、下载字节、字节每秒、状态码

其中这里需要注意的,计算的数据有QPS,其他基本都可以经过简单的计算得出

那QPS该如何进行计算呢?这里来这样进行计算:

QPS = 服务器每秒钟处理请求数量 (req/sec 请求数/秒)

定义:单个协程耗时T, 所有协程压测总时间 sumT,协程数 n

如果:只有一个协程,假设接口耗时为 2毫秒,每个协程请求了10次接口,每个协程耗总耗时210=20毫秒,sumT=20

QPS = 10/201000=500

如果:只有十个协程,假设接口耗时为 2毫秒,每个协程请求了10次接口,每个协程耗总耗时210=20毫秒,sumT=2010=200

QPS = 100/(200/10)*1000=5000

上诉两个示例现实中总耗时都是20毫秒,示例二 请求了100次接口,QPS应该为 示例一 的10倍,所以示例二的实际总QPS为5000

除以协程数的意义是,sumT是所有协程耗时总和

实现过程

这个模块主要是定时进行一个统计压测的结论并进行打印的工作,依赖的函数是

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

// calculateData 计算数据

func calculateData(concurrent, processingTime, requestTime, maxTime, minTime, successNum, failureNum uint64,

    chanIDLen int, errCode *sync.Map, receivedBytes int64) {

    if processingTime == 0 {

        processingTime = 1

    }

    var (

        qps              float64

        averageTime      float64

        maxTimeFloat     float64

        minTimeFloat     float64

        requestTimeFloat float64

    )

    // 平均 QPS 成功数*总协程数/总耗时 (每秒)

    if processingTime != 0 {

        qps = float64(successNum*concurrent) * (1e9 / float64(processingTime))

    }

    // 平均时长 总耗时/总请求数/并发数 纳秒=>毫秒

    if successNum != 0 && concurrent != 0 {

        averageTime = float64(processingTime) / float64(successNum*1e6)

    }

    // 纳秒=>毫秒

    maxTimeFloat = float64(maxTime) / 1e6

    minTimeFloat = float64(minTime) / 1e6

    requestTimeFloat = float64(requestTime) / 1e9

    // 打印的时长都为毫秒

    table(successNum, failureNum, errCode, qps, averageTime, maxTimeFloat, minTimeFloat, requestTimeFloat, chanIDLen,

        receivedBytes)

}


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 :
相关文章
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计