108 lines
3.2 KiB
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]
|
|
}
|