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