Files
configcenter/cmd/loadtest/main.go

108 lines
3.2 KiB
Go

package main
import (
"context"
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"sort"
"sync"
"time"
)
type result struct {
duration time.Duration
ok bool
}
func main() {
baseURL := flag.String("base-url", "http://127.0.0.1:8080", "Config Center base URL")
env := flag.String("env", "DEV", "environment code")
app := flag.String("app", "demo-service", "application code")
namespace := flag.String("namespace", "application", "namespace")
token := flag.String("token", "", "optional bearer token")
concurrency := flag.Int("concurrency", 32, "concurrent workers")
duration := flag.Duration("duration", 30*time.Second, "test duration")
maxErrorRate := flag.Float64("max-error-rate", 0.01, "failure threshold")
maxP95 := flag.Duration("max-p95", 200*time.Millisecond, "p95 latency threshold")
flag.Parse()
if *concurrency <= 0 || *duration <= 0 {
fmt.Fprintln(os.Stderr, "concurrency and duration must be positive")
os.Exit(2)
}
query := url.Values{"env": {*env}, "app": {*app}, "namespace": {*namespace}}
endpoint := fmt.Sprintf("%s/v1/config?%s", *baseURL, query.Encode())
client := &http.Client{Transport: &http.Transport{MaxIdleConns: *concurrency * 2, MaxIdleConnsPerHost: *concurrency, IdleConnTimeout: 30 * time.Second}, Timeout: 5 * time.Second}
ctx, cancel := context.WithTimeout(context.Background(), *duration)
defer cancel()
results := make(chan result, *concurrency*128)
var workers sync.WaitGroup
for i := 0; i < *concurrency; i++ {
workers.Add(1)
go func() {
defer workers.Done()
for ctx.Err() == nil {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return
}
if *token != "" {
request.Header.Set("Authorization", "Bearer "+*token)
}
begin := time.Now()
response, err := client.Do(request)
elapsed := time.Since(begin)
if err != nil && ctx.Err() != nil {
return
}
ok := err == nil && response.StatusCode == http.StatusOK
if response != nil {
_, _ = io.Copy(io.Discard, response.Body)
response.Body.Close()
}
select {
case results <- result{duration: elapsed, ok: ok}:
case <-ctx.Done():
return
}
}
}()
}
go func() {
workers.Wait()
close(results)
}()
latencies := make([]time.Duration, 0)
failures := 0
for item := range results {
latencies = append(latencies, item.duration)
if !item.ok {
failures++
}
}
if len(latencies) == 0 {
fmt.Fprintln(os.Stderr, "no requests completed")
os.Exit(1)
}
sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
p50, p95, p99 := percentile(latencies, 0.50), percentile(latencies, 0.95), percentile(latencies, 0.99)
errorRate := float64(failures) / float64(len(latencies))
qps := float64(len(latencies)) / duration.Seconds()
fmt.Printf("requests=%d failures=%d error_rate=%.4f qps=%.1f p50=%s p95=%s p99=%s\n", len(latencies), failures, errorRate, qps, p50, p95, p99)
if errorRate > *maxErrorRate || p95 > *maxP95 {
fmt.Fprintf(os.Stderr, "SLO failed: error_rate<=%.4f p95<=%s\n", *maxErrorRate, *maxP95)
os.Exit(1)
}
}
func percentile(values []time.Duration, quantile float64) time.Duration {
index := int(float64(len(values)-1) * quantile)
return values[index]
}