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,欢迎修改完善

上一篇:GO基础深入总结


下一篇:Go语言 字符串拼接性能优化