基于Go語言實現一個壓測工具
本篇主要是基于Go來實現一個壓測的工具,關于壓測的內容可以參考其他的文章,這里默認了解壓測的基本概念
整體架構

整體系統(tǒng)架構比較簡單
通用數據處理模塊
Http請求響應數據處理
本項目支持http協議、websocket協議、grpc協議、Remote Authentication Dial-In User Service協議,因此需要構造出一個通用的http請求和響應的結構體,進行一個通用的封裝:
// 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 // 驗證的狀態(tài)碼
Redirect bool // 是否重定向
}
這當中值得注意的是驗證的方法,這里是因為在進行壓測中,要判斷返回的響應是否是正確的響應,因此要進行判斷響應是否正確,所以要進行相應的函數的注冊,因此對于一個請求,是有必要找到一個對應的請求方法來判斷這個請求正確,之后進行記錄
這個model的核心功能,就是生成一個http請求的結構體,來幫助進行存儲
// 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
}
之后是對于對應的響應的封裝,結構體定義為:
// 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來獲取到指定的客戶端,建立映射關系
具體的核心成員為:
var (
mutex sync.RWMutex
// clients 客戶端
// key 客戶端id - value 客戶端
clients = make(map[uint64]*http.Client)
)
再具體的,對于客戶端的封裝,主要操作是,對于Client的構造
// createLangHTTPClient 初始化長連接客戶端參數
// 創(chuàng)建了一個配置了長連接的 HTTP 客戶端傳輸對象
func createLangHTTPClient(request *model.Request) *http.Client {
tr := &http.Transport{
// 使用 net.Dialer 來建立 TCP 連接
// Timeout 設置為 30 秒,表示如果連接在 30 秒內沒有建立成功,則超時
// KeepAlive 設置為 30 秒,表示連接建立后,如果 30 秒內沒有數據傳輸,則發(fā)送一個 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的構造來說,主要實現的功能是建立連接等,這些操作是較為簡單的操作,因此這里不具體講述
// 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客戶端處理
// 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調用,執(zhí)行了一個Hello World的函數,之后填充相應的數據作為請求的響應,最后將結果返回
// 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,
}
)
// 發(fā)送請求,獲得響應
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的測試,效果也基本類似,原理也基本相同
// 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
}
統(tǒng)計數據模塊
下面來看計算統(tǒng)計數據模塊
統(tǒng)計原理
這里需要統(tǒng)計的數據有以下:
耗時、并發(fā)數、成功數、失敗數、qps、最長耗時、最短耗時、平均耗時、下載字節(jié)、字節(jié)每秒、狀態(tài)碼
其中這里需要注意的,計算的數據有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是所有協程耗時總和
實現過程
這個模塊主要是定時進行一個統(tǒng)計壓測的結論并進行打印的工作,依賴的函數是
// 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))
}
// 平均時長 總耗時/總請求數/并發(fā)數 納秒=>毫秒
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)
}
以上就是基于Go語言實現一個壓測工具的詳細內容,更多關于Go壓測工具的資料請關注腳本之家其它相關文章!
相關文章
Go語言利用compress/gzip庫實現高效壓縮解決方案詳解
在數據存儲與傳輸領域,Gzip作為一種廣泛應用的無損壓縮格式,以其高效的壓縮比和跨平臺兼容性成為行業(yè)標準,下面我們就來看看Go語言如何利用該庫實現高效的數據壓縮與解壓縮吧2025-07-07

