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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,335评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,895评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,766评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,918评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,042评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,169评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,219评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,976评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,393评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,711评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,876评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,562评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,193评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,903评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,699评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,764评论 2 351