go web压测工具实现

简介: go web压测工具实现

这篇Go实现单机压测工具博客分以下几个模块进行讲解,为了更加清楚的知道一个分布式Web压测实现,我们从单机单用户 -> 单机多用户 -> 分布式逐步实现。
(1)什么是web压力测试?
(2)压力测试中几个重要指标
(3)Go语言实现单机单用户压测
(4)GO语言实现单机多用户压测
(5)Go语言实现分布式压测
(6)相关参考资料

一、什么是web压力测试?
简单的说是测试一个web网站能够支撑多大的请求(也就是web网站的最大并发)
二、压力测试的几个重要指标
1)指标有哪些?
2)指标含义详解
https://blog.csdn.net/adparking/article/details/45673315

三、单机单用户压测Go实现
1)要知道的几个概念
并发连接数:
理解:并发连接数不等于并发数,真实的并发数只能在服务器中计算出来,这边的并发数等于处在从请求发出去,到收到服务器信息的这状态的个数总和。
总请求次数
响应时间
平均响应时间
成功次数
失败次数

2)代码实现

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
    "strconv"
    "sync"
    "time"
)

var (
    SBCNum     int           // 并发连接数
    QPSNum     int           // 总请求次数
    RTNum      time.Duration // 响应时间
    RTTNum     time.Duration // 平均响应时间
    SuccessNum int           // 成功次数
    FailNum    int           // 失败次数

    BeginTime time.Time // 开始时间
    SecNum    int       // 秒数

    RQNum int    // 最大并发数,由命令行传入
    Url   string // url,由命令行传入

    controlNum chan int // 控制并发数量
)

var mu sync.Mutex // 必须加锁

func init() {
    if len(os.Args) != 3 {
        log.Fatal("请求次数 url")
    }
    RQNum, _ = strconv.Atoi(os.Args[1])
    controlNum = make(chan int, RQNum)
    Url = os.Args[2]
}

func main() {
    go func() {
        for range time.Tick(1 * time.Second) {
            SecNum++
            fmt.Printf("并发数:%d,请求次数:%d,平均响应时间:%s,成功次数:%d,失败次数:%d\n",
                len(controlNum), SuccessNum+FailNum, RTNum/(time.Duration(SecNum)*time.Second), SuccessNum, FailNum)
        }
    }()
    requite()
}

func requite() {
    for {
        controlNum <- 1
        go func(c chan int) {
            var tb time.Time
            var el time.Duration
            for {
                tb = time.Now()
                _, err := http.Get(Url)
                if err == nil {
                    el = time.Since(tb)
                    mu.Lock() // 上锁
                    SuccessNum++
                    RTNum += el
                    mu.Unlock() // 解锁
                } else {
                    mu.Lock() // 上锁
                    FailNum++
                    mu.Unlock() // 解锁
                }
                time.Sleep(1 * time.Second)
            }
            <- c
        }(controlNum)
        time.Sleep(45 * time.Millisecond)
    }
}

四、单机多用户压测Go实现

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
    "strconv"
    "sync"
    "time"
)

var (
    BeginTime time.Time // 开始时间
    SecNum    int       // 秒数

    RQNum int    // 最大并发数,由命令行传入
    Url   string // url,由命令行传入

    userNum    int      // 用户数
)

var users []User

type User struct {
    UserId         int             // 用户id
    SBCNum     int           // 并发连接数
    QPSNum     int           // 总请求次数
    RTNum      time.Duration // 响应时间
    RTTNum     time.Duration // 平均响应时间
    SuccessNum int           // 成功次数
    FailNum    int           // 失败次数
    mu         sync.Mutex
}

func (u *User) request(url string) {
    var tb time.Time
    var el time.Duration
    for i := 0;i < u.QPSNum;i++ {
        u.SBCNum++
        go func(u *User) {
            for {
                tb = time.Now()
                _, err := http.Get(Url)
                if err == nil {
                    el = time.Since(tb)
                    u.mu.Lock() // 上锁
                    u.SuccessNum++
                    u.RTNum += el
                    u.mu.Unlock() // 解锁
                } else {
                    u.mu.Lock() // 上锁
                    u.FailNum++
                    u.mu.Unlock() // 解锁
                }
                time.Sleep(1 * time.Second)
            }
        }(u)
    }
}

func (u *User) show() {
    fmt.Printf("用户id:%d,并发数:%d,请求次数:%d,平均响应时间:%s,成功次数:%d,失败次数:%d\n",
        u.UserId,
        u.SBCNum,
        u.SuccessNum + u.FailNum,
        u.RTNum/(time.Duration(SecNum)*time.Second),
        u.SuccessNum,
        u.FailNum)
}

func showAll(us []User) {
    uLen := len(us)

    var SBCNum     int           // 并发连接数
    var RTNum      time.Duration // 响应时间
    var SuccessNum int           // 成功次数
    var FailNum    int           // 失败次数

    for i := 0;i < uLen;i++ {
        SBCNum += us[i].SBCNum
        SuccessNum += us[i].SuccessNum
        FailNum += us[i].FailNum
        RTNum += us[i].RTNum
        us[i].show()
    }
    fmt.Printf("并发数:%d,请求次数:%d,平均响应时间:%s,成功次数:%d,失败次数:%d\n",
        SBCNum,
        SuccessNum+FailNum,
        RTNum/(time.Duration(SecNum)*time.Second),
        SuccessNum,
        FailNum)
    fmt.Println()
}

func init() {
    if len(os.Args) != 4 {
        log.Fatal("用户数 请求次数 url")
    }
    userNum, _ = strconv.Atoi(os.Args[1])
    RQNum, _ = strconv.Atoi(os.Args[2])
    Url = os.Args[3]
    users = make([]User, userNum)
}

func main() {
    go func() {
        for range time.Tick(2 * time.Second) {
            SecNum += 2
            showAll(users)
        }
    }()
    for range time.Tick(1 * time.Second) {
        requite()
    }
}

func requite() {
    c := make(chan int)
    temp := 0
    for i := 0;i < userNum;i++ {
        if RQNum % userNum != 0 && i < RQNum % userNum {
            temp = 1
        } else {
            temp = 0
        }
        users[i].UserId = i
        users[i].QPSNum = RQNum / userNum + temp
        go users[i].request(Url)
        time.Sleep(45 * time.Millisecond)
    }
    <- c    // 阻塞
}

五、分布式压测Go
分主节点和从节点,现在分别实现以下功能
1)主节点功能
收集从节点的压测信息
显示压测信息
2)从节点功能
将压测信息发送给主节点
3)整个工作原理
一个主节点启动,设置监听端口,使用TCP方式,启动若干个从节点,每个从节点通过IP+端口连接到这个主节点,之后主节点记录连接上来的从节点信息。从节点将相关信息发往主节点,主节点在设定的时间里显示信息。
代码实现:
主节点代码实现

package main

import (
    "log"
    "net"
    "time"
    "os"
    "encoding/json"
    "fmt"
)

var ip string
var port string
var slaves []*slave

type slave struct {
    UserId string
    SBCNum     int           // 并发连接数
    QPSNum     int           // 总请求次数
    RTNum      time.Duration // 响应时间
    RTTNum     time.Duration // 响应时间
    SecNum       int             // 时间
    SuccessNum int           // 成功次数
    FailNum    int           // 失败次数
    Url string
    conn net.Conn
}

func (s *slave) Run() {
    var v interface{}
    buf := make([]byte, 1024)
    for {
        n, err := s.conn.Read(buf)
        if err != nil {
            log.Println(err)
            break
        }
        err = json.Unmarshal(buf[:n], &v)
        if err != nil {
            log.Println(err)
            continue
        }
        s.SBCNum = int(v.(map[string]interface{})["SBCNum"].(float64))    // 并发连接数
        s.RTNum = time.Duration(v.(map[string]interface{})["RTNum"].(float64))    // 响应时间
        s.SuccessNum = int(v.(map[string]interface{})["SuccessNum"].(float64))    //SuccessNum int           // 成功次数
        s.FailNum = int(v.(map[string]interface{})["FailNum"].(float64))            //FailNum    int           // 失败次数
        s.SecNum = int(v.(map[string]interface{})["SecNum"].(float64))
    }
}

func init() {
    if len(os.Args) != 3 {
        log.Fatal(os.Args[0] + " ip port")
    }
    ip = os.Args[1]
    port = os.Args[2]
}

func main() {
    s, err := net.Listen("tcp", ip + ":" + port)
    if err != nil {
        log.Fatal(err)
    }
    defer s.Close()
    buf := make([]byte, 128)
    fmt.Println("Run...")
    go func() {
        for range time.Tick(2 * time.Second) {
            show(slaves)
        }
    }()
    for {
        conn, err := s.Accept()
        if err != nil {
            log.Println(err)
            continue
        }
        n, err := conn.Read(buf)
        tempC := slave{conn:conn,UserId:conn.RemoteAddr().String(), Url:string(buf[:n])}
        go tempC.Run()
        slaves = append(slaves, &tempC)
    }
}

func show(clients []*slave) {
    if len(clients) == 0 {
        return
    }
    temp := slave{}
    num := 0
    for _, client := range clients {
        if client.SecNum == 0 {
            continue
        }
        num++
        fmt.Printf("用户id:%s,url: %s,并发数:%d,请求次数:%d,平均响应时间:%s,成功次数:%d,失败次数:%d\n",
            client.UserId,
            client.Url,
            client.SBCNum,
            client.SuccessNum + client.FailNum,
            client.RTNum / (time.Duration(client.SecNum) * time.Second),
            client.SuccessNum,
            client.FailNum)
        temp.SBCNum += client.SBCNum
        temp.RTNum += client.RTNum / (time.Duration(client.SecNum) * time.Second)
        temp.SecNum += client.SecNum
        temp.SuccessNum += client.SuccessNum
        temp.FailNum += client.FailNum
    }
    if num == 0 {
        return
    }
    fmt.Printf("并发数:%d,请求次数:%d,平均响应时间:%s,成功次数:%d,失败次数:%d\n",
        temp.SBCNum,
        temp.SuccessNum + temp.FailNum,
        temp.RTNum / time.Duration(num),
        temp.SuccessNum,
        temp.FailNum)
    fmt.Println()
}

func heartbeat(clients []slave) []slave {    // 标记耦合
    tempC := []slave{}
    for _, client := range clients {
        _, err := client.conn.Write([]byte(""))
        if err == nil {    // 删除
            tempC = append(tempC, client)
        }
    }
    return tempC
}

从节点

package main

import (
    "net"
    "github.com/lunny/log"
    "encoding/json"
    "time"
    "os"
    "net/http"
    "sync"
    "strconv"
    "fmt"
)

type master struct {
    ip string
    port string
    conn net.Conn
}

var (
    SBCNum     int           // 并发连接数
    QPSNum     int           // 总请求次数
    RTNum      time.Duration // 响应时间
    RTTNum     time.Duration // 平均响应时间
    SuccessNum int           // 成功次数
    FailNum    int           // 失败次数
    SecNum       int

    mt master
    err error
    mu sync.Mutex // 必须加锁
    RQNum int    // 最大并发数,由命令行传入
    Url   string // url,由命令行传入
)

func init() {
    if len(os.Args) != 5 {
        log.Fatalf("%s 并发数 url ip port", os.Args[0])
    }
    RQNum, err = strconv.Atoi(os.Args[1])
    if err != nil {
        log.Println(err)
    }
    Url = os.Args[2]
    mt.ip = os.Args[3]
    mt.port = os.Args[4]
}

func main() {
    mt.conn, err = net.Dial("tcp", mt.ip + ":" + mt.port)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("连接服务器成功。。。")
    _, err = mt.conn.Write([]byte(Url))
    if err != nil {
        log.Println(err)
    }
    go func() {
        for range time.Tick(1 * time.Second) {
            sendToMaster(mt, map[string]interface{}{
                "SBCNum": SBCNum,            // 并发连接数
                "RTNum": RTNum,                // 响应时间
                "SecNum": SecNum,            // 时间
                "SuccessNum": SuccessNum,    // 成功次数
                "FailNum": FailNum,            // 失败次数
            })
        }
    }()
    go func() {
        for range time.Tick(1 * time.Second) {
            SecNum++
        }
    }()
    requite(RQNum, Url)
}

func requite(RQNum int, url string) {
    c := make(chan int)
    for i := 0;i < RQNum;i++ {
        SBCNum = i + 1
        go func(url string) {
            var tb time.Time
            var el time.Duration
            for {
                tb = time.Now()
                _, err := http.Get(url)
                if err == nil {
                    el = time.Since(tb)
                    mu.Lock() // 上锁
                    SuccessNum++
                    RTNum += el
                    mu.Unlock() // 解锁
                } else {
                    mu.Lock() // 上锁
                    FailNum++
                    mu.Unlock() // 解锁
                }
                time.Sleep(1 * time.Second)
            }
        }(url)
        time.Sleep(45 * time.Millisecond)
    }
    <- c    // 阻塞
}

func sendToMaster(mt master, data map[string]interface{}) {
    r, err := json.Marshal(data)
    if err != nil {
        log.Println(err)
    }
    _, err = mt.conn.Write(r)
    if err != nil {
        log.Println(err)
        os.Exit(1)
    }
}

参考链接:
压测指标概念
(1)https://www.cnblogs.com/shijingjing07/p/6507317.html
(2)https://blog.csdn.net/adparking/article/details/45673315

github链接:https://github.com/laijinhang/WebRequest

有出错的地方,请指出,代码已上传到github,欢迎修改完善

相关实践学习
通过性能测试PTS对云服务器ECS进行规格选择与性能压测
本文为您介绍如何利用性能测试PTS对云服务器ECS进行规格选择与性能压测。
目录
相关文章
|
11天前
|
安全 网络协议 网络安全
【Docker项目实战】使用Docker部署web-check网站分析工具
【4月更文挑战第20天】使用Docker部署web-check网站分析工具
39 1
|
29天前
|
网络协议 Linux Go
分享一个go开发的工具-SNMP Server
分享一个go开发的工具-SNMP Server
27 0
|
2天前
|
缓存 监控 测试技术
【Go语言专栏】使用Go语言构建高性能Web服务
【4月更文挑战第30天】本文探讨了使用Go语言构建高性能Web服务的策略,包括Go语言在并发处理和内存管理上的优势、基本原则(如保持简单、缓存和并发控制)、标准库与第三方框架的选择、编写高效的HTTP处理器、数据库优化以及性能测试和监控。通过遵循最佳实践,开发者可以充分利用Go语言的特性,构建出高性能的Web服务。
|
2天前
|
中间件 Go API
【Go 语言专栏】Go 语言中的 Web 框架比较与选择
【4月更文挑战第30天】本文对比了Go语言中的四个常见Web框架:功能全面的Beego、轻量级高性能的Gin、简洁高效的Echo,以及各自的性能、功能特性、社区支持。选择框架时需考虑项目需求、性能要求、团队经验和社区生态。开发者应根据具体情况进行权衡,以找到最适合的框架。
|
4天前
|
中间件 Go API
Golang深入浅出之-Go语言标准库net/http:构建Web服务器
【4月更文挑战第25天】Go语言的`net/http`包是构建高性能Web服务器的核心,提供创建服务器和发起请求的功能。本文讨论了使用中的常见问题和解决方案,包括:使用第三方路由库改进路由设计、引入中间件处理通用逻辑、设置合适的超时和连接管理以防止资源泄露。通过基础服务器和中间件的代码示例,展示了如何有效运用`net/http`包。掌握这些最佳实践,有助于开发出高效、易维护的Web服务。
17 1
|
8天前
|
Cloud Native 网络协议 Go
[云原生] Go web工作流程
[云原生] Go web工作流程
|
20天前
|
安全 Linux iOS开发
【热门话题】 Fiddler:一款强大的Web调试代理工具——安装与使用详解
Fiddler是一款强大的Web调试工具,适用于Windows、macOS和Linux,用于捕获、记录和分析HTTP/HTTPS流量。本文详细介绍了Fiddler的安装步骤,包括下载、安装和配置,特别是信任根证书和代理设置。在使用方面,讲解了如何启动/停止捕获流量、查看和管理会话,以及重发请求、编辑请求/响应和清除会话。此外,还探讨了进阶功能,如自定义过滤规则、使用AutoResponder模拟服务器响应、性能分析和统计,以及插件扩展和脚本编写。Fiddler是学习HTTP协议和解决Web问题的得力工具。
43 1
|
28天前
|
资源调度 JavaScript 安全
Linux系统之部署web-check网站分析工具
【4月更文挑战第3天】Linux系统之部署web-check网站分析工具
70 9
|
29天前
|
搜索推荐 Linux Go
分享一个go开发的端口转发工具-port-forward
分享一个go开发的端口转发工具-port-forward
25 0
|
29天前
|
Go
推荐一个go写的RTSP转直播工具
推荐一个go写的RTSP转直播工具
12 0