[unistore] Add benchmark for write throughput (#101345)
* Add generic benchmark * address comments
This commit is contained in:
@@ -40,6 +40,9 @@ type BackendOptions struct {
|
||||
PollingInterval time.Duration
|
||||
WatchBufferSize int
|
||||
IsHA bool
|
||||
|
||||
// testing
|
||||
SimulatedNetworkLatency time.Duration // slows down the create transactions by a fixed amount
|
||||
}
|
||||
|
||||
func NewBackend(opts BackendOptions) (Backend, error) {
|
||||
@@ -58,15 +61,16 @@ func NewBackend(opts BackendOptions) (Backend, error) {
|
||||
opts.WatchBufferSize = defaultWatchBufferSize
|
||||
}
|
||||
return &backend{
|
||||
isHA: opts.IsHA,
|
||||
done: ctx.Done(),
|
||||
cancel: cancel,
|
||||
log: log.New("sql-resource-server"),
|
||||
tracer: opts.Tracer,
|
||||
dbProvider: opts.DBProvider,
|
||||
pollingInterval: opts.PollingInterval,
|
||||
watchBufferSize: opts.WatchBufferSize,
|
||||
batchLock: &batchLock{running: make(map[string]bool)},
|
||||
isHA: opts.IsHA,
|
||||
done: ctx.Done(),
|
||||
cancel: cancel,
|
||||
log: log.New("sql-resource-server"),
|
||||
tracer: opts.Tracer,
|
||||
dbProvider: opts.DBProvider,
|
||||
pollingInterval: opts.PollingInterval,
|
||||
watchBufferSize: opts.WatchBufferSize,
|
||||
batchLock: &batchLock{running: make(map[string]bool)},
|
||||
simulatedNetworkLatency: opts.SimulatedNetworkLatency,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -95,6 +99,9 @@ type backend struct {
|
||||
pollingInterval time.Duration
|
||||
watchBufferSize int
|
||||
notifier eventNotifier
|
||||
|
||||
// testing
|
||||
simulatedNetworkLatency time.Duration
|
||||
}
|
||||
|
||||
func (b *backend) Init(ctx context.Context) error {
|
||||
@@ -249,7 +256,9 @@ func (b *backend) create(ctx context.Context, event resource.WriteEvent) (int64,
|
||||
return fmt.Errorf("update resource rv: %w", err)
|
||||
}
|
||||
newVersion = rv
|
||||
|
||||
if b.simulatedNetworkLatency > 0 {
|
||||
time.Sleep(b.simulatedNetworkLatency)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
infraDB "github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl"
|
||||
test "github.com/grafana/grafana/pkg/storage/unified/testing"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestBackend(b *testing.B) resource.StorageBackend {
|
||||
dbstore := infraDB.InitTestDB(b)
|
||||
eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil)
|
||||
require.NoError(b, err)
|
||||
require.NotNil(b, eDB)
|
||||
|
||||
backend, err := sql.NewBackend(sql.BackendOptions{
|
||||
DBProvider: eDB,
|
||||
IsHA: true,
|
||||
SimulatedNetworkLatency: 5 * time.Millisecond, // to simulate some network latency
|
||||
})
|
||||
require.NoError(b, err)
|
||||
require.NotNil(b, backend)
|
||||
err = backend.Init(context.Background())
|
||||
require.NoError(b, err)
|
||||
return backend
|
||||
}
|
||||
|
||||
func BenchmarkSQLStorageBackend(b *testing.B) {
|
||||
opts := test.DefaultBenchmarkOptions()
|
||||
if infraDB.IsTestDbSQLite() {
|
||||
opts.Concurrency = 1 // to avoid SQLite database is locked error
|
||||
}
|
||||
test.BenchmarkStorageBackend(b, newTestBackend(b), opts)
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// BenchmarkOptions configures the benchmark parameters
|
||||
type BenchmarkOptions struct {
|
||||
NumResources int // total number of resources to write
|
||||
Concurrency int // number of concurrent writers
|
||||
NumNamespaces int // number of different namespaces
|
||||
NumGroups int // number of different groups
|
||||
NumResourceTypes int // number of different resource types
|
||||
}
|
||||
|
||||
// DefaultBenchmarkOptions returns the default benchmark options
|
||||
func DefaultBenchmarkOptions() *BenchmarkOptions {
|
||||
return &BenchmarkOptions{
|
||||
NumResources: 1000,
|
||||
Concurrency: 20,
|
||||
NumNamespaces: 1,
|
||||
NumGroups: 1,
|
||||
NumResourceTypes: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkResult contains the benchmark metrics
|
||||
type BenchmarkResult struct {
|
||||
TotalDuration time.Duration
|
||||
WriteCount int
|
||||
Throughput float64 // writes per second
|
||||
P50Latency time.Duration
|
||||
P90Latency time.Duration
|
||||
P99Latency time.Duration
|
||||
}
|
||||
|
||||
// runStorageBackendBenchmark runs a write throughput benchmark
|
||||
func runStorageBackendBenchmark(ctx context.Context, backend resource.StorageBackend, opts *BenchmarkOptions) (*BenchmarkResult, error) {
|
||||
if opts == nil {
|
||||
opts = DefaultBenchmarkOptions()
|
||||
}
|
||||
|
||||
// Create channels for workers
|
||||
jobs := make(chan int, opts.NumResources)
|
||||
results := make(chan time.Duration, opts.NumResources)
|
||||
errors := make(chan error, opts.NumResources)
|
||||
|
||||
// Fill the jobs channel
|
||||
for i := 0; i < opts.NumResources; i++ {
|
||||
jobs <- i
|
||||
}
|
||||
close(jobs)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Initialize each group and resource type combination in the init namespace
|
||||
namespace := "ns-init"
|
||||
for g := 0; g < opts.NumGroups; g++ {
|
||||
group := fmt.Sprintf("group-%d", g)
|
||||
for r := 0; r < opts.NumResourceTypes; r++ {
|
||||
resourceType := fmt.Sprintf("resource-%d", r)
|
||||
_, err := writeEvent(ctx, backend, "init", resource.WatchEvent_ADDED,
|
||||
WithNamespace(namespace),
|
||||
WithGroup(group),
|
||||
WithResource(resourceType),
|
||||
WithValue([]byte("init")))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize backend: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Start workers
|
||||
startTime := time.Now()
|
||||
for workerID := 0; workerID < opts.Concurrency; workerID++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for jobID := range jobs {
|
||||
// Calculate a unique ID for this job that's guaranteed to be unique across all workers
|
||||
uniqueID := jobID
|
||||
|
||||
// Generate deterministic and unique resource details
|
||||
namespace := fmt.Sprintf("ns-%d", uniqueID%opts.NumNamespaces)
|
||||
group := fmt.Sprintf("group-%d", uniqueID%opts.NumGroups)
|
||||
resourceType := fmt.Sprintf("resource-%d", uniqueID%opts.NumResourceTypes)
|
||||
// Ensure name is unique by using the global uniqueID
|
||||
name := fmt.Sprintf("item-%d", uniqueID)
|
||||
|
||||
writeStart := time.Now()
|
||||
_, err := writeEvent(ctx, backend, name, resource.WatchEvent_ADDED,
|
||||
WithNamespace(namespace),
|
||||
WithGroup(group),
|
||||
WithResource(resourceType),
|
||||
WithValue([]byte(strings.Repeat("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 20)))) // ~1.21 KiB
|
||||
|
||||
if err != nil {
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
|
||||
results <- time.Since(writeStart)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all workers to complete
|
||||
wg.Wait()
|
||||
close(results)
|
||||
close(errors)
|
||||
|
||||
// Check for errors
|
||||
if len(errors) > 0 {
|
||||
return nil, <-errors // Return the first error encountered
|
||||
}
|
||||
|
||||
// Collect all latencies
|
||||
latencies := make([]time.Duration, 0, opts.NumResources)
|
||||
for latency := range results {
|
||||
latencies = append(latencies, latency)
|
||||
}
|
||||
|
||||
// Sort latencies for percentile calculation
|
||||
sort.Slice(latencies, func(i, j int) bool {
|
||||
return latencies[i] < latencies[j]
|
||||
})
|
||||
|
||||
totalDuration := time.Since(startTime)
|
||||
throughput := float64(opts.NumResources) / totalDuration.Seconds()
|
||||
|
||||
return &BenchmarkResult{
|
||||
TotalDuration: totalDuration,
|
||||
WriteCount: opts.NumResources,
|
||||
Throughput: throughput,
|
||||
P50Latency: latencies[len(latencies)*50/100],
|
||||
P90Latency: latencies[len(latencies)*90/100],
|
||||
P99Latency: latencies[len(latencies)*99/100],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BenchmarkStorageBackend runs a benchmark test for a storage backend implementation
|
||||
func BenchmarkStorageBackend(b *testing.B, backend resource.StorageBackend, opts *BenchmarkOptions) {
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := runStorageBackendBenchmark(ctx, backend, opts)
|
||||
require.NoError(b, err)
|
||||
|
||||
b.ReportMetric(result.Throughput, "writes/sec")
|
||||
b.ReportMetric(float64(result.P50Latency.Milliseconds()), "p50-latency-ms")
|
||||
b.ReportMetric(float64(result.P90Latency.Milliseconds()), "p90-latency-ms")
|
||||
b.ReportMetric(float64(result.P99Latency.Milliseconds()), "p99-latency-ms")
|
||||
|
||||
// Also log the results for better visibility
|
||||
b.Logf("Benchmark Configuration: Workers=%d, Resources=%d, Namespaces=%d, Groups=%d, Resource Types=%d", opts.Concurrency, opts.NumResources, opts.NumNamespaces, opts.NumGroups, opts.NumResourceTypes)
|
||||
b.Logf("")
|
||||
b.Logf("Benchmark Results:")
|
||||
b.Logf("Total Duration: %v", result.TotalDuration)
|
||||
b.Logf("Write Count: %d", result.WriteCount)
|
||||
b.Logf("Throughput: %.2f writes/sec", result.Throughput)
|
||||
b.Logf("P50 Latency: %v", result.P50Latency)
|
||||
b.Logf("P90 Latency: %v", result.P90Latency)
|
||||
b.Logf("P99 Latency: %v", result.P99Latency)
|
||||
}
|
||||
Reference in New Issue
Block a user