基于Go語言實現(xiàn)一個壓測工具
本篇主要是基于Go來實現(xiàn)一個壓測的工具,關于壓測的內容可以參考其他的文章,這里默認了解壓測的基本概念
整體架構
整體系統(tǒng)架構比較簡單
通用數(shù)據(jù)處理模塊
Http請求響應數(shù)據(jù)處理
本項目支持http協(xié)議、websocket協(xié)議、grpc協(xié)議、Remote Authentication Dial-In User Service協(xié)議,因此需要構造出一個通用的http請求和響應的結構體,進行一個通用的封裝:
// Request 請求數(shù)據(jù) 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 // 每個連接的請求數(shù) HTTP2 bool // 是否使用http2.0 Keepalive bool // 是否開啟長連接 Code int // 驗證的狀態(tài)碼 Redirect bool // 是否重定向 }
這當中值得注意的是驗證的方法,這里是因為在進行壓測中,要判斷返回的響應是否是正確的響應,因此要進行判斷響應是否正確,所以要進行相應的函數(shù)的注冊,因此對于一個請求,是有必要找到一個對應的請求方法來判斷這個請求正確,之后進行記錄
這個model的核心功能,就是生成一個http請求的結構體,來幫助進行存儲
// NewRequest 生成請求結構體 // url 壓測的url // verify 驗證方法 在server/verify中 http 支持:statusCode、json webSocket支持:json // timeout 請求超時時間 // debug 是否開啟debug // path curl文件路徑 http接口壓測,自定義參數(shù)設置 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參數(shù)解析處理
對于這個模塊,本項目中實現(xiàn)的邏輯是根據(jù)一個指定的Curl的文件,對于文件中的Curl進行解析,即可解析出對應的Http請求的參數(shù),具體代碼鏈接如下
https://gitee.com/zhaobohan/stress-testing/blob/master/model/curl_model.go
客戶端模塊
Http客戶端處理
在該模塊中主要是對于Http客戶端進行處理,對于普通請求和Http2.0請求進行了特化處理,支持根據(jù)客戶端ID來獲取到指定的客戶端,建立映射關系
具體的核心成員為:
var ( mutex sync.RWMutex // clients 客戶端 // key 客戶端id - value 客戶端 clients = make(map[uint64]*http.Client) )
再具體的,對于客戶端的封裝,主要操作是,對于Client的構造
// createLangHTTPClient 初始化長連接客戶端參數(shù) // 創(chuàng)建了一個配置了長連接的 HTTP 客戶端傳輸對象 func createLangHTTPClient(request *model.Request) *http.Client { tr := &http.Transport{ // 使用 net.Dialer 來建立 TCP 連接 // Timeout 設置為 30 秒,表示如果連接在 30 秒內沒有建立成功,則超時 // KeepAlive 設置為 30 秒,表示連接建立后,如果 30 秒內沒有數(shù)據(jù)傳輸,則發(fā)送一個 keep-alive 探測包以保持連接 DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, MaxIdleConns: 0, // 最大連接數(shù),默認0無窮大 MaxIdleConnsPerHost: request.MaxCon, // 對每個host的最大連接數(shù)量(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, // 最大連接數(shù),默認0無窮大 MaxIdleConnsPerHost: request.MaxCon, // 對每個host的最大連接數(shù)量(MaxIdleConnsPerHost<=MaxIdleConns) IdleConnTimeout: 90 * time.Second, // 多長時間未使用自動關閉連接 // 配置 TLS 客戶端設置,InsecureSkipVerify 設置為 false,表示驗證服務器的 SSL 證書 TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, } // 將 tr 配置為支持 HTTP/2 協(xié)議 _ = 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的構造來說,主要實現(xiàn)的功能是建立連接等,這些操作是較為簡單的操作,因此這里不具體講述
// GrpcSocket grpc type GrpcSocket struct { conn *grpc.ClientConn address string }
conn和Address主要都是借助于兩個類的成員函數(shù)來完成,解析地址和建立連接
其余模塊可在代碼中查看,這里不進行過多講述
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的函數(shù),之后填充相應的數(shù)據(jù)作為請求的響應,最后將結果返回
// 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)計數(shù)據(jù)模塊
下面來看計算統(tǒng)計數(shù)據(jù)模塊
統(tǒng)計原理
這里需要統(tǒng)計的數(shù)據(jù)有以下:
耗時、并發(fā)數(shù)、成功數(shù)、失敗數(shù)、qps、最長耗時、最短耗時、平均耗時、下載字節(jié)、字節(jié)每秒、狀態(tài)碼
其中這里需要注意的,計算的數(shù)據(jù)有QPS,其他基本都可以經過簡單的計算得出
那QPS該如何進行計算呢?這里來這樣進行計算:
QPS = 服務器每秒鐘處理請求數(shù)量 (req/sec 請求數(shù)/秒)
定義:單個協(xié)程耗時T, 所有協(xié)程壓測總時間 sumT,協(xié)程數(shù) n
如果:只有一個協(xié)程,假設接口耗時為 2毫秒,每個協(xié)程請求了10次接口,每個協(xié)程耗總耗時210=20毫秒,sumT=20
QPS = 10/201000=500
如果:只有十個協(xié)程,假設接口耗時為 2毫秒,每個協(xié)程請求了10次接口,每個協(xié)程耗總耗時210=20毫秒,sumT=2010=200
QPS = 100/(200/10)*1000=5000
上訴兩個示例現(xiàn)實中總耗時都是20毫秒,示例二 請求了100次接口,QPS應該為 示例一 的10倍,所以示例二的實際總QPS為5000
除以協(xié)程數(shù)的意義是,sumT是所有協(xié)程耗時總和
實現(xiàn)過程
這個模塊主要是定時進行一個統(tǒng)計壓測的結論并進行打印的工作,依賴的函數(shù)是
// calculateData 計算數(shù)據(jù) 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 成功數(shù)*總協(xié)程數(shù)/總耗時 (每秒) if processingTime != 0 { qps = float64(successNum*concurrent) * (1e9 / float64(processingTime)) } // 平均時長 總耗時/總請求數(shù)/并發(fā)數(shù) 納秒=>毫秒 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語言實現(xiàn)一個壓測工具的詳細內容,更多關于Go壓測工具的資料請關注腳本之家其它相關文章!
相關文章
Go函數(shù)使用(函數(shù)定義、函數(shù)聲明、函數(shù)調用等)
本文主要介紹了Go函數(shù)使用,包括函數(shù)定義、函數(shù)聲明、函數(shù)調用、可變參數(shù)函數(shù)、匿名函數(shù)、遞歸函數(shù)、高階函數(shù)等,感興趣的可以了解一下2023-11-11go?gin?正確讀取http?response?body內容并多次使用詳解
這篇文章主要為大家介紹了go?gin?正確讀取http?response?body內容并多次使用解決思路,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-01-01Golang?channel關閉后是否可以讀取剩余的數(shù)據(jù)詳解
這篇文章主要介紹了Golang?channel關閉后是否可以讀取剩余的數(shù)據(jù),文章通過一個測試例子給大家詳細的介紹了是否可以讀取剩余的數(shù)據(jù),需要的朋友可以參考下2023-09-09