dragonfly/test/tools/stress/main.go

289 lines
6.3 KiB
Go

/*
* Copyright 2020 The Dragonfly Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"context"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"sort"
"sync"
"syscall"
"time"
"github.com/go-echarts/statsview"
"github.com/go-echarts/statsview/viewer"
"github.com/montanaflynn/stats"
"d7y.io/dragonfly/v2/client/config"
"d7y.io/dragonfly/v2/pkg/net/ip"
"d7y.io/dragonfly/v2/pkg/unit"
)
var (
target string
output string
proxy string
con int
duration *time.Duration
)
func init() {
flag.StringVar(&target, "url", "", "target url for stress testing, example: http://localhost")
flag.StringVar(&output, "output", "/tmp/statistics.txt", "all request statistics")
flag.StringVar(&proxy, "proxy", "", "target proxy for downloading, example: http://127.0.0.1:65001")
flag.IntVar(&con, "connections", 100, "concurrency count of connections")
duration = flag.Duration("duration", 100*time.Second, "testing duration")
}
type Result struct {
StatusCode int
StartTime time.Time
EndTime time.Time
Cost time.Duration
TaskID string
PeerID string
Size int64
Message string
}
func main() {
go debug()
flag.Parse()
var (
wgProcess = &sync.WaitGroup{}
wgCollect = &sync.WaitGroup{}
)
ctx, cancel := context.WithCancel(context.Background())
resultCh := make(chan *Result, 1024)
if proxy != "" {
pu, err := url.Parse(proxy)
if err != nil {
panic(err)
}
http.DefaultClient.Transport = &http.Transport{
Proxy: http.ProxyURL(pu),
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
}
wgCollect.Add(1)
go collect(wgCollect, resultCh)
for i := 0; i < con; i++ {
wgProcess.Add(1)
go process(ctx, wgProcess, resultCh)
}
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
go forceExit(signals)
loop:
for {
select {
case <-time.After(*duration):
break loop
case sig := <-signals:
log.Printf("receive signal: %v", sig)
break loop
}
}
cancel()
wgProcess.Wait()
close(resultCh)
wgCollect.Wait()
}
func debug() {
debugAddr := fmt.Sprintf("%s:%d", ip.IPv4.String(), 18066)
viewer.SetConfiguration(viewer.WithAddr(debugAddr))
if err := statsview.New().Start(); err != nil {
log.Println("stat view start failed", err)
}
}
func forceExit(signals chan os.Signal) {
var count int
for {
select {
case <-signals:
count++
if count > 2 {
log.Printf("force exit")
os.Exit(1)
}
}
}
}
func process(ctx context.Context, wg *sync.WaitGroup, result chan *Result) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
default:
}
start := time.Now()
resp, err := http.Get(target)
if err != nil {
log.Printf("connect target error: %s", err)
continue
}
var msg string
n, err := io.Copy(io.Discard, resp.Body)
if err != nil {
msg = err.Error()
log.Printf("discard data error: %s", err)
}
end := time.Now()
result <- &Result{
StatusCode: resp.StatusCode,
StartTime: start,
EndTime: end,
Cost: end.Sub(start),
Size: n,
TaskID: resp.Header.Get(config.HeaderDragonflyTask),
PeerID: resp.Header.Get(config.HeaderDragonflyPeer),
Message: msg,
}
resp.Body.Close()
}
}
func collect(wg *sync.WaitGroup, resultCh chan *Result) {
defer wg.Done()
var results = make([]*Result, 0, 1000)
loop:
for {
select {
case result, ok := <-resultCh:
if !ok {
break loop
}
results = append(results, result)
}
}
printStatistics(results)
saveToOutput(results)
}
func printStatistics(results []*Result) {
sort.Slice(results, func(i, j int) bool {
return results[i].Cost < results[j].Cost
})
printLatency(results)
printStatus(results)
printThroughput(results)
}
func printStatus(results []*Result) {
var status = make(map[int]int)
for _, v := range results {
status[v.StatusCode]++
}
fmt.Printf("HTTP codes\n")
for code, count := range status {
fmt.Printf("\t%d\t %d\n", code, count)
}
}
func printLatency(results []*Result) {
var dur []int64
for _, v := range results {
if v.StatusCode == 200 {
dur = append(dur, v.EndTime.Sub(v.StartTime).Nanoseconds())
}
}
if len(dur) == 0 {
log.Printf("empty result with 200 status")
return
}
d := stats.LoadRawData(dur)
min, _ := stats.Min(d)
max, _ := stats.Max(d)
mean, _ := stats.Mean(d)
fmt.Printf("Latency\n")
fmt.Printf("\tavg\t %v\n", time.Duration(int64(mean)))
fmt.Printf("\tmin\t %v\n", time.Duration(int64(min)))
fmt.Printf("\tmax\t %v\n", time.Duration(int64(max)))
fmt.Printf("Latency Distribution\n")
for _, p := range []float64{50, 75, 90, 95, 99} {
percentile, err := stats.Percentile(d, p)
if err != nil {
panic(err)
}
fmt.Printf("\t%.0f%%\t%v\n", p, time.Duration(int64(percentile)))
}
}
func printThroughput(results []*Result) {
var total int64
for _, v := range results {
total += v.Size
}
fmt.Printf("Throughput\t%v\n", unit.Bytes(total/int64(*duration/time.Second)))
fmt.Printf("Request\t\t%d/s\n", len(results)/int(*duration/time.Second))
}
func saveToOutput(results []*Result) {
out, err := os.OpenFile(output, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
if err != nil {
panic(err)
}
defer out.Close()
for _, v := range results {
if v.TaskID == "" {
v.TaskID = "unknown"
}
if v.PeerID == "" {
v.PeerID = "unknown"
}
if _, err := out.WriteString(fmt.Sprintf("%s %s %d %v %d %d %s\n",
v.TaskID, v.PeerID, v.StatusCode, v.Cost,
v.StartTime.UnixNano()/100, v.EndTime.UnixNano()/100, v.Message)); err != nil {
log.Panicln("write string failed", err)
}
}
}