blob: d71ed11ca45b039b52b34598becf9d4d60a59fbe [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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"
"encoding/json"
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
)
import (
"github.com/dubbogo/gost/log/logger"
"go.yaml.in/yaml/v4"
)
import (
"dubbo.apache.org/dubbo-go/v3/tools/benchmark/client/clients"
"dubbo.apache.org/dubbo-go/v3/tools/benchmark/client/engine"
"dubbo.apache.org/dubbo-go/v3/tools/benchmark/client/monitor"
"dubbo.apache.org/dubbo-go/v3/tools/benchmark/client/payload"
)
const (
FrameworkDubboGo = "dubbo-go"
FrameworkDubboJava = "dubbo-java"
FrameworkGRPC = "grpc"
Separator = "========================================"
MaxPayloadSize = 16 * 1024 * 1024 // 16MB
MinPayloadSize = 1
MinConcurrency = 1
MaxConcurrency = 10000
)
var (
framework = flag.String("framework", FrameworkDubboGo, "Framework: dubbo-go / dubbo-java / grpc")
payloadSize = flag.Int("payload", 1024, "Payload size (bytes)")
serialization = flag.String("serialization", "protobuf", "Serialization protocol: hessian2 / protobuf / msgpack")
compression = flag.String("compression", "none", "Compression strategy: none / default / fastest")
concurrency = flag.Int("concurrency", 100, "Concurrency level")
callMode = flag.String("mode", "unary", "Call mode: unary / streaming")
testDuration = flag.String("duration", "60s", "Test duration")
warmupDuration = flag.String("warmup", "10s", "Warmup duration")
serverAddr = flag.String("addr", "", "Server address")
serverPID = flag.Int("pid", 0, "Server process PID (for system monitoring)")
outputDir = flag.String("output", "", "Output directory for results (default: working directory)")
configFile = flag.String("config", "", "Path to config.yaml")
)
type BenchmarkConfig struct {
Service struct {
Name string `yaml:"name"`
Port struct {
DubboGo int `yaml:"dubbo-go"`
DubboJava int `yaml:"dubbo-java"`
Grpc int `yaml:"grpc"`
} `yaml:"port"`
} `yaml:"service"`
PayloadSizes []string `yaml:"payload_sizes"`
Serializations []string `yaml:"serializations"`
Compressions []string `yaml:"compressions"`
CallModes []string `yaml:"call_modes"`
ConcurrencyLevels []string `yaml:"concurrency_levels"`
Benchmark struct {
WarmupDuration string `yaml:"warmup_duration"`
TestDuration string `yaml:"test_duration"`
RequestTimeout string `yaml:"request_timeout"`
} `yaml:"benchmark"`
}
type Caller interface {
Call(ctx context.Context) error
Close() error
String() string
}
type BenchmarkResult struct {
Framework string `json:"framework"`
PayloadSize int `json:"payload_size"`
Serialization string `json:"serialization"`
Compression string `json:"compression"`
Concurrency int `json:"concurrency"`
CallMode string `json:"call_mode"`
Timestamp string `json:"timestamp"`
QPS float64 `json:"qps"`
SuccessRate float64 `json:"success_rate"`
TotalRequests int64 `json:"total_requests"`
SuccessRequests int64 `json:"success_requests"`
FailureRequests int64 `json:"failure_requests"`
LatencyP50 float64 `json:"latency_p50_ms"`
LatencyP90 float64 `json:"latency_p90_ms"`
LatencyP95 float64 `json:"latency_p95_ms"`
LatencyP99 float64 `json:"latency_p99_ms"`
LatencyMin float64 `json:"latency_min_ms"`
LatencyMax float64 `json:"latency_max_ms"`
LatencyAvg float64 `json:"latency_avg_ms"`
CPUAvg float64 `json:"cpu_avg_percent"`
MemoryPeak float64 `json:"memory_peak_mb"`
}
var (
validFrameworks = map[string]bool{FrameworkDubboGo: true, FrameworkDubboJava: true, FrameworkGRPC: true}
validSerializations = map[string]bool{"hessian2": true, "protobuf": true, "msgpack": true}
validCompressions = map[string]bool{"none": true, "default": true, "fastest": true}
validCallModes = map[string]bool{"unary": true, "streaming": true}
)
func validateParams() {
if !validFrameworks[*framework] {
logger.Fatalf("Invalid framework: %s. Valid values: dubbo-go, dubbo-java, grpc", *framework)
}
if *payloadSize < MinPayloadSize || *payloadSize > MaxPayloadSize {
logger.Fatalf("Invalid payload size: %d. Must be between %d and %d bytes", *payloadSize, MinPayloadSize, MaxPayloadSize)
}
if *concurrency < MinConcurrency || *concurrency > MaxConcurrency {
logger.Fatalf("Invalid concurrency: %d. Must be between %d and %d", *concurrency, MinConcurrency, MaxConcurrency)
}
if !validSerializations[*serialization] {
logger.Fatalf("Invalid serialization: %s. Valid values: hessian2, protobuf, msgpack", *serialization)
}
if !validCompressions[*compression] {
logger.Fatalf("Invalid compression: %s. Valid values: none, default, fastest", *compression)
}
if !validCallModes[*callMode] {
logger.Fatalf("Invalid call mode: %s. Valid values: unary, streaming", *callMode)
}
if *framework == FrameworkDubboJava && *callMode != "unary" {
logger.Fatalf("Invalid call mode for dubbo-java: %s. Only unary is supported", *callMode)
}
if *framework == FrameworkDubboJava && *serialization != "protobuf" {
logger.Fatalf("Invalid serialization for dubbo-java: %s. Only protobuf is supported", *serialization)
}
if _, err := time.ParseDuration(*testDuration); err != nil {
logger.Fatalf("Invalid test duration: %v", err)
}
if _, err := time.ParseDuration(*warmupDuration); err != nil {
logger.Fatalf("Invalid warmup duration: %v", err)
}
}
func main() {
flag.Parse()
loadConfig(*configFile)
validateParams()
logger.Info(Separator)
logger.Info(" Dubbo-Go Benchmark Client")
logger.Info(Separator)
logger.Infof("Framework: %s", *framework)
logger.Infof("Payload Size: %d bytes", *payloadSize)
logger.Infof("Serialization: %s", *serialization)
logger.Infof("Compression: %s", *compression)
logger.Infof("Concurrency: %d", *concurrency)
logger.Infof("Call Mode: %s", *callMode)
logger.Infof("Warmup Duration: %s", *warmupDuration)
logger.Infof("Test Duration: %s", *testDuration)
if *serverAddr != "" {
logger.Infof("Server Address: %s", *serverAddr)
}
if *serverPID != 0 {
logger.Infof("Server PID: %d", *serverPID)
}
logger.Info(Separator)
testDur, err := time.ParseDuration(*testDuration)
if err != nil {
logger.Fatalf("Invalid test duration: %v", err)
}
warmupDur, err := time.ParseDuration(*warmupDuration)
if err != nil {
logger.Fatalf("Invalid warmup duration: %v", err)
}
pg := payload.NewPayloadGenerator()
data := pg.Generate(*payloadSize)
logger.Infof("[INFO] Payload data generated, size: %d bytes", len(data))
caller, err := createCaller(data)
if err != nil {
logger.Fatalf("Failed to create client: %v", err)
}
defer caller.Close()
var sysMonitor *monitor.SystemMonitor
if *serverPID != 0 {
sysMonitor = monitor.NewSystemMonitor(*serverPID, 1*time.Second)
sysMonitor.Start()
defer sysMonitor.Stop()
logger.Infof("[INFO] System monitor started, monitoring PID: %d", *serverPID)
}
benchEngine := engine.NewEngine(*concurrency, warmupDur, testDur, 30*time.Second)
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigCh
logger.Infof("[INFO] Received signal %v, stopping benchmark...", sig)
benchEngine.Stop()
}()
logger.Info("[INFO] Starting benchmark...")
stats := benchEngine.Run(func(ctx context.Context) (time.Duration, error) {
start := time.Now()
err := caller.Call(ctx)
return time.Since(start), err
})
logger.Info(stats.String())
cpuAvg, memoryPeakBytes := 0.0, uint64(0)
if sysMonitor != nil {
cpuAvg, memoryPeakBytes = sysMonitor.GetSummary()
logger.Info(sysMonitor.String())
}
saveResults(stats, cpuAvg, float64(memoryPeakBytes)/1024/1024)
}
func loadConfig(configPath string) {
if configPath == "" {
return
}
data, err := os.ReadFile(configPath)
if err != nil {
logger.Warnf("[WARN] Failed to read config file: %v", err)
return
}
var config BenchmarkConfig
if err := yaml.Unmarshal(data, &config); err != nil {
logger.Warnf("[WARN] Failed to parse config file: %v", err)
return
}
logger.Infof("[INFO] Loaded config from %s", configPath)
}
func createCaller(data []byte) (Caller, error) {
addr := *serverAddr
if addr == "" {
switch *framework {
case FrameworkDubboGo:
addr = "127.0.0.1:20000"
case FrameworkDubboJava:
addr = "127.0.0.1:20001"
case FrameworkGRPC:
addr = "127.0.0.1:50051"
default:
addr = "127.0.0.1:20000"
}
}
switch *framework {
case FrameworkDubboGo, FrameworkDubboJava:
return clients.NewDubboGoClient(addr, *serialization, *compression, *callMode, data)
case FrameworkGRPC:
return clients.NewGrpcClient(addr, *callMode, data)
default:
return nil, fmt.Errorf("unsupported framework: %s", *framework)
}
}
func saveResults(stats *engine.Statistics, cpuAvg, memoryPeak float64) {
result := &BenchmarkResult{
Framework: *framework,
PayloadSize: *payloadSize,
Serialization: *serialization,
Compression: *compression,
Concurrency: *concurrency,
CallMode: *callMode,
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
QPS: stats.QPS,
SuccessRate: stats.SuccessRate,
TotalRequests: stats.Total,
SuccessRequests: stats.Success,
FailureRequests: stats.Failure,
LatencyP50: float64(stats.P50) / float64(time.Millisecond),
LatencyP90: float64(stats.P90) / float64(time.Millisecond),
LatencyP95: float64(stats.P95) / float64(time.Millisecond),
LatencyP99: float64(stats.P99) / float64(time.Millisecond),
LatencyMin: float64(stats.Min) / float64(time.Millisecond),
LatencyMax: float64(stats.Max) / float64(time.Millisecond),
LatencyAvg: float64(stats.Avg) / float64(time.Millisecond),
CPUAvg: cpuAvg,
MemoryPeak: memoryPeak,
}
dataDir := *outputDir
if dataDir == "" {
wd, err := os.Getwd()
if err != nil {
logger.Warnf("[WARN] Failed to get working directory: %v", err)
return
}
dataDir = filepath.Join(wd, "data")
}
if mkdirErr := os.MkdirAll(dataDir, 0755); mkdirErr != nil {
logger.Warnf("[WARN] Failed to create data directory: %v", mkdirErr)
return
}
filename := fmt.Sprintf("%s_%d_%s_%s_%d_%s.json",
*framework, *payloadSize, *serialization, *compression, *concurrency, *callMode)
path := filepath.Join(dataDir, filename)
dataBytes, err := json.MarshalIndent(result, "", " ")
if err != nil {
logger.Warnf("[WARN] Failed to serialize result: %v", err)
return
}
if err := os.WriteFile(path, dataBytes, 0644); err != nil {
logger.Warnf("[WARN] Failed to write result file: %v", err)
return
}
logger.Infof("[INFO] Test results saved to: %s", path)
}