mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-16 12:16:36 +00:00
* test: add self-contained S3 read/write load tool Concurrent PUT/GET against the S3 gateway, reporting requests/sec, transfer rate, and latency percentiles. Built on the aws-sdk-go-v2 client the S3 tests already use, so no extra benchmark binary is needed. * ci: add performance workflow Three parallel jobs: cpu/heap pprof of the server under write load, native throughput via weed benchmark plus the Go micro-benchmarks, and an S3 read/write benchmark against the gateway. Runs on push to master and manual dispatch with tunable duration, object count, size, and concurrency.
224 lines
5.9 KiB
Go
224 lines
5.9 KiB
Go
// Command s3_benchmark drives concurrent PUT and GET load against an S3
|
|
// gateway and reports throughput and latency. It is a self-contained load
|
|
// generator for the performance CI, using the aws-sdk-go-v2 client the rest
|
|
// of the S3 tests already depend on.
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"slices"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/config"
|
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
)
|
|
|
|
func main() {
|
|
endpoint := flag.String("endpoint", "http://localhost:8000", "S3 gateway endpoint")
|
|
accessKey := flag.String("access-key", "some_access_key1", "S3 access key")
|
|
secretKey := flag.String("secret-key", "some_secret_key1", "S3 secret key")
|
|
region := flag.String("region", "us-east-1", "S3 region")
|
|
bucket := flag.String("bucket", "perf-benchmark", "bucket to write into")
|
|
objects := flag.Int("objects", 10000, "number of objects")
|
|
size := flag.Int("size", 1024, "object size in bytes")
|
|
concurrency := flag.Int("concurrency", 16, "concurrent workers")
|
|
mode := flag.String("mode", "both", "write, read, or both")
|
|
flag.Parse()
|
|
|
|
cfg, err := config.LoadDefaultConfig(context.Background(),
|
|
config.WithRegion(*region),
|
|
config.WithRetryMaxAttempts(1),
|
|
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(*accessKey, *secretKey, "")),
|
|
)
|
|
if err != nil {
|
|
fatalf("load aws config: %v", err)
|
|
}
|
|
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
|
|
o.BaseEndpoint = aws.String(*endpoint)
|
|
o.UsePathStyle = true
|
|
})
|
|
|
|
if err := ensureBucket(client, *bucket); err != nil {
|
|
fatalf("ensure bucket: %v", err)
|
|
}
|
|
|
|
payload := make([]byte, *size)
|
|
for i := range payload {
|
|
payload[i] = byte(i)
|
|
}
|
|
|
|
doWrite := *mode == "both" || *mode == "write"
|
|
doRead := *mode == "both" || *mode == "read"
|
|
|
|
if doWrite {
|
|
report("S3 WRITE", *concurrency, *size, run(*concurrency, *objects, func(key string) error {
|
|
_, err := client.PutObject(context.Background(), &s3.PutObjectInput{
|
|
Bucket: bucket,
|
|
Key: aws.String(key),
|
|
Body: bytes.NewReader(payload),
|
|
ContentLength: aws.Int64(int64(*size)),
|
|
})
|
|
return err
|
|
}))
|
|
}
|
|
|
|
if doRead {
|
|
report("S3 READ", *concurrency, *size, run(*concurrency, *objects, func(key string) error {
|
|
out, err := client.GetObject(context.Background(), &s3.GetObjectInput{
|
|
Bucket: bucket,
|
|
Key: aws.String(key),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Body.Close()
|
|
_, err = io.Copy(io.Discard, out.Body)
|
|
return err
|
|
}))
|
|
}
|
|
}
|
|
|
|
type result struct {
|
|
completed int64
|
|
failed int64
|
|
elapsed time.Duration
|
|
latencies []time.Duration
|
|
firstErr error
|
|
}
|
|
|
|
// run fans `total` keyed operations across `workers` goroutines, timing each.
|
|
func run(workers, total int, op func(key string) error) result {
|
|
var (
|
|
completed, failed, nextKey atomic.Int64
|
|
mu sync.Mutex
|
|
firstErr error
|
|
latencies = make([]time.Duration, 0, total)
|
|
wg sync.WaitGroup
|
|
)
|
|
start := time.Now()
|
|
for w := 0; w < workers; w++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
local := make([]time.Duration, 0, total/workers+1)
|
|
for {
|
|
i := nextKey.Add(1) - 1
|
|
if i >= int64(total) {
|
|
break
|
|
}
|
|
key := fmt.Sprintf("obj-%d", i)
|
|
opStart := time.Now()
|
|
err := op(key)
|
|
local = append(local, time.Since(opStart))
|
|
if err != nil {
|
|
failed.Add(1)
|
|
mu.Lock()
|
|
if firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
mu.Unlock()
|
|
continue
|
|
}
|
|
completed.Add(1)
|
|
}
|
|
mu.Lock()
|
|
latencies = append(latencies, local...)
|
|
mu.Unlock()
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
return result{
|
|
completed: completed.Load(),
|
|
failed: failed.Load(),
|
|
elapsed: time.Since(start),
|
|
latencies: latencies,
|
|
firstErr: firstErr,
|
|
}
|
|
}
|
|
|
|
func report(label string, concurrency, size int, r result) {
|
|
secs := r.elapsed.Seconds()
|
|
transferred := r.completed * int64(size)
|
|
rps := 0.0
|
|
kbps := 0.0
|
|
if secs > 0 {
|
|
rps = float64(r.completed) / secs
|
|
kbps = float64(transferred) / 1024 / secs
|
|
}
|
|
fmt.Printf("\n%s results:\n", label)
|
|
fmt.Printf("Concurrency Level: %d\n", concurrency)
|
|
fmt.Printf("Time taken for tests: %.3f seconds\n", secs)
|
|
fmt.Printf("Completed requests: %d\n", r.completed)
|
|
fmt.Printf("Failed requests: %d\n", r.failed)
|
|
fmt.Printf("Total transferred: %d bytes\n", transferred)
|
|
fmt.Printf("Requests per second: %.2f [#/sec]\n", rps)
|
|
fmt.Printf("Transfer rate: %.2f [Kbytes/sec]\n", kbps)
|
|
p := percentiles(r.latencies)
|
|
fmt.Printf("Latency (ms): avg=%.2f p50=%.2f p90=%.2f p99=%.2f max=%.2f\n",
|
|
ms(p.avg), ms(p.p50), ms(p.p90), ms(p.p99), ms(p.max))
|
|
if r.failed > 0 && r.firstErr != nil {
|
|
fmt.Printf("First error: %v\n", r.firstErr)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
type latencyStats struct {
|
|
avg, p50, p90, p99, max time.Duration
|
|
}
|
|
|
|
func percentiles(d []time.Duration) latencyStats {
|
|
if len(d) == 0 {
|
|
return latencyStats{}
|
|
}
|
|
slices.Sort(d)
|
|
var sum time.Duration
|
|
for _, v := range d {
|
|
sum += v
|
|
}
|
|
at := func(q float64) time.Duration {
|
|
idx := int(q * float64(len(d)))
|
|
if idx >= len(d) {
|
|
idx = len(d) - 1
|
|
}
|
|
return d[idx]
|
|
}
|
|
return latencyStats{
|
|
avg: sum / time.Duration(len(d)),
|
|
p50: at(0.50),
|
|
p90: at(0.90),
|
|
p99: at(0.99),
|
|
max: d[len(d)-1],
|
|
}
|
|
}
|
|
|
|
func ms(d time.Duration) float64 { return float64(d) / float64(time.Millisecond) }
|
|
|
|
func ensureBucket(client *s3.Client, bucket string) error {
|
|
_, err := client.CreateBucket(context.Background(), &s3.CreateBucketInput{Bucket: aws.String(bucket)})
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
var owned *types.BucketAlreadyOwnedByYou
|
|
var exists *types.BucketAlreadyExists
|
|
if errors.As(err, &owned) || errors.As(err, &exists) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func fatalf(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
|
os.Exit(1)
|
|
}
|