mirror of
https://github.com/redis/go-redis.git
synced 2025-12-02 06:22:31 +03:00
* perf(pool): replace hookManager RWMutex with atomic.Pointer and add predefined state slices
- Replace hookManager RWMutex with atomic.Pointer for lock-free reads in hot paths
- Add predefined state slices to avoid allocations (validFromInUse, validFromCreatedOrIdle, etc.)
- Add Clone() method to PoolHookManager for atomic updates
- Update AddPoolHook/RemovePoolHook to use copy-on-write pattern
- Update all hookManager access points to use atomic Load()
Performance improvements:
- Eliminates RWMutex contention in Get/Put/Remove hot paths
- Reduces allocations by reusing predefined state slices
- Lock-free reads allow better CPU cache utilization
* perf(pool): eliminate mutex overhead in state machine hot path
The state machine was calling notifyWaiters() on EVERY Get/Put operation,
which acquired a mutex even when no waiters were present (the common case).
Fix: Use atomic waiterCount to check for waiters BEFORE acquiring mutex.
This eliminates mutex contention in the hot path (Get/Put operations).
Implementation:
- Added atomic.Int32 waiterCount field to ConnStateMachine
- Increment when adding waiter, decrement when removing
- Check waiterCount atomically before acquiring mutex in notifyWaiters()
Performance impact:
- Before: mutex lock/unlock on every Get/Put (even with no waiters)
- After: lock-free atomic check, only acquire mutex if waiters exist
- Expected improvement: ~30-50% for Get/Put operations
* perf(pool): use predefined state slices to eliminate allocations in hot path
The pool was creating new slice literals on EVERY Get/Put operation:
- popIdle(): []ConnState{StateCreated, StateIdle}
- putConn(): []ConnState{StateInUse}
- CompareAndSwapUsed(): []ConnState{StateIdle} and []ConnState{StateInUse}
- MarkUnusableForHandoff(): []ConnState{StateInUse, StateIdle, StateCreated}
These allocations were happening millions of times per second in the hot path.
Fix: Use predefined global slices defined in conn_state.go:
- validFromInUse
- validFromCreatedOrIdle
- validFromCreatedInUseOrIdle
Performance impact:
- Before: 4 slice allocations per Get/Put cycle
- After: 0 allocations (use predefined slices)
- Expected improvement: ~30-40% reduction in allocations and GC pressure
* perf(pool): optimize TryTransition to reduce atomic operations
Further optimize the hot path by:
1. Remove redundant GetState() call in the loop
2. Only check waiterCount after successful CAS (not before loop)
3. Inline the waiterCount check to avoid notifyWaiters() call overhead
This reduces atomic operations from 4-5 per Get/Put to 2-3:
- Before: GetState() + CAS + waiterCount.Load() + notifyWaiters mutex check
- After: CAS + waiterCount.Load() (only if CAS succeeds)
Performance impact:
- Eliminates 1-2 atomic operations per Get/Put
- Expected improvement: ~10-15% for Get/Put operations
* perf(pool): add fast path for Get/Put to match master performance
Introduced TryTransitionFast() for the hot path (Get/Put operations):
- Single CAS operation (same as master's atomic bool)
- No waiter notification overhead
- No loop through valid states
- No error allocation
Hot path flow:
1. popIdle(): Try IDLE → IN_USE (fast), fallback to CREATED → IN_USE
2. putConn(): Try IN_USE → IDLE (fast)
This matches master's performance while preserving state machine for:
- Background operations (handoff/reauth use UNUSABLE state)
- State validation (TryTransition still available)
- Waiter notification (AwaitAndTransition for blocking)
Performance comparison per Get/Put cycle:
- Master: 2 atomic CAS operations
- State machine (before): 5 atomic operations (2.5x slower)
- State machine (after): 2 atomic CAS operations (same as master!)
Expected improvement: Restore to baseline ~11,373 ops/sec
* combine cas
* fix linter
* try faster approach
* fast semaphore
* better inlining for hot path
* fix linter issues
* use new semaphore in auth as well
* linter should be happy now
* add comments
* Update internal/pool/conn_state.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* address comment
* slight reordering
* try to cache time if for non-critical calculation
* fix wrong benchmark
* add concurrent test
* fix benchmark report
* add additional expect to check output
* comment and variable rename
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
357 lines
8.9 KiB
Go
357 lines
8.9 KiB
Go
package redis_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
// HSET Benchmark Tests
|
|
//
|
|
// This file contains benchmark tests for Redis HSET operations with different scales:
|
|
// 1, 10, 100, 1000, 10000, 100000 operations
|
|
//
|
|
// Prerequisites:
|
|
// - Redis server running on localhost:6379
|
|
// - No authentication required
|
|
//
|
|
// Usage:
|
|
// go test -bench=BenchmarkHSET -v ./hset_benchmark_test.go
|
|
// go test -bench=BenchmarkHSETPipelined -v ./hset_benchmark_test.go
|
|
// go test -bench=. -v ./hset_benchmark_test.go # Run all benchmarks
|
|
//
|
|
// Example output:
|
|
// BenchmarkHSET/HSET_1_operations-8 5000 250000 ns/op 1000000.00 ops/sec
|
|
// BenchmarkHSET/HSET_100_operations-8 100 10000000 ns/op 100000.00 ops/sec
|
|
//
|
|
// The benchmarks test three different approaches:
|
|
// 1. Individual HSET commands (BenchmarkHSET)
|
|
// 2. Pipelined HSET commands (BenchmarkHSETPipelined)
|
|
|
|
// BenchmarkHSET benchmarks HSET operations with different scales
|
|
func BenchmarkHSET(b *testing.B) {
|
|
ctx := context.Background()
|
|
|
|
// Setup Redis client
|
|
rdb := redis.NewClient(&redis.Options{
|
|
Addr: "localhost:6379",
|
|
DB: 0,
|
|
})
|
|
defer rdb.Close()
|
|
|
|
// Test connection
|
|
if err := rdb.Ping(ctx).Err(); err != nil {
|
|
b.Skipf("Redis server not available: %v", err)
|
|
}
|
|
|
|
// Clean up before and after tests
|
|
defer func() {
|
|
rdb.FlushDB(ctx)
|
|
}()
|
|
|
|
scales := []int{1, 10, 100, 1000, 10000, 100000}
|
|
|
|
for _, scale := range scales {
|
|
b.Run(fmt.Sprintf("HSET_%d_operations", scale), func(b *testing.B) {
|
|
benchmarkHSETOperations(b, rdb, ctx, scale)
|
|
})
|
|
}
|
|
}
|
|
|
|
// benchmarkHSETOperations performs the actual HSET benchmark for a given scale
|
|
func benchmarkHSETOperations(b *testing.B, rdb *redis.Client, ctx context.Context, operations int) {
|
|
hashKey := fmt.Sprintf("benchmark_hash_%d", operations)
|
|
|
|
b.ResetTimer()
|
|
b.StartTimer()
|
|
totalTimes := []time.Duration{}
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
b.StopTimer()
|
|
// Clean up the hash before each iteration
|
|
rdb.Del(ctx, hashKey)
|
|
b.StartTimer()
|
|
|
|
startTime := time.Now()
|
|
// Perform the specified number of HSET operations
|
|
for j := 0; j < operations; j++ {
|
|
field := fmt.Sprintf("field_%d", j)
|
|
value := fmt.Sprintf("value_%d", j)
|
|
|
|
err := rdb.HSet(ctx, hashKey, field, value).Err()
|
|
if err != nil {
|
|
b.Fatalf("HSET operation failed: %v", err)
|
|
}
|
|
}
|
|
totalTimes = append(totalTimes, time.Since(startTime))
|
|
}
|
|
|
|
// Stop the timer to calculate metrics
|
|
b.StopTimer()
|
|
|
|
// Report operations per second
|
|
opsPerSec := float64(operations*b.N) / b.Elapsed().Seconds()
|
|
b.ReportMetric(opsPerSec, "ops/sec")
|
|
|
|
// Report average time per operation
|
|
avgTimePerOp := b.Elapsed().Nanoseconds() / int64(operations*b.N)
|
|
b.ReportMetric(float64(avgTimePerOp), "ns/op")
|
|
// report average time in milliseconds from totalTimes
|
|
sumTime := time.Duration(0)
|
|
for _, t := range totalTimes {
|
|
sumTime += t
|
|
}
|
|
avgTimePerOpMs := sumTime.Milliseconds() / int64(len(totalTimes))
|
|
b.ReportMetric(float64(avgTimePerOpMs), "ms")
|
|
}
|
|
|
|
// benchmarkHSETOperationsConcurrent performs the actual HSET benchmark for a given scale
|
|
func benchmarkHSETOperationsConcurrent(b *testing.B, rdb *redis.Client, ctx context.Context, operations int) {
|
|
hashKey := fmt.Sprintf("benchmark_hash_%d", operations)
|
|
|
|
b.ResetTimer()
|
|
b.StartTimer()
|
|
totalTimes := []time.Duration{}
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
b.StopTimer()
|
|
// Clean up the hash before each iteration
|
|
rdb.Del(ctx, hashKey)
|
|
b.StartTimer()
|
|
|
|
startTime := time.Now()
|
|
// Perform the specified number of HSET operations
|
|
|
|
wg := sync.WaitGroup{}
|
|
timesCh := make(chan time.Duration, operations)
|
|
errCh := make(chan error, operations)
|
|
|
|
for j := 0; j < operations; j++ {
|
|
wg.Add(1)
|
|
go func(j int) {
|
|
defer wg.Done()
|
|
field := fmt.Sprintf("field_%d", j)
|
|
value := fmt.Sprintf("value_%d", j)
|
|
|
|
err := rdb.HSet(ctx, hashKey, field, value).Err()
|
|
if err != nil {
|
|
errCh <- err
|
|
return
|
|
}
|
|
timesCh <- time.Since(startTime)
|
|
}(j)
|
|
}
|
|
|
|
wg.Wait()
|
|
close(timesCh)
|
|
close(errCh)
|
|
|
|
// Check for errors
|
|
for err := range errCh {
|
|
b.Errorf("HSET operation failed: %v", err)
|
|
}
|
|
|
|
for d := range timesCh {
|
|
totalTimes = append(totalTimes, d)
|
|
}
|
|
}
|
|
|
|
// Stop the timer to calculate metrics
|
|
b.StopTimer()
|
|
|
|
// Report operations per second
|
|
opsPerSec := float64(operations*b.N) / b.Elapsed().Seconds()
|
|
b.ReportMetric(opsPerSec, "ops/sec")
|
|
|
|
// Report average time per operation
|
|
avgTimePerOp := b.Elapsed().Nanoseconds() / int64(operations*b.N)
|
|
b.ReportMetric(float64(avgTimePerOp), "ns/op")
|
|
// report average time in milliseconds from totalTimes
|
|
|
|
sumTime := time.Duration(0)
|
|
for _, t := range totalTimes {
|
|
sumTime += t
|
|
}
|
|
avgTimePerOpMs := sumTime.Milliseconds() / int64(len(totalTimes))
|
|
b.ReportMetric(float64(avgTimePerOpMs), "ms")
|
|
}
|
|
|
|
// BenchmarkHSETPipelined benchmarks HSET operations using pipelining for better performance
|
|
func BenchmarkHSETPipelined(b *testing.B) {
|
|
ctx := context.Background()
|
|
|
|
// Setup Redis client
|
|
rdb := redis.NewClient(&redis.Options{
|
|
Addr: "localhost:6379",
|
|
DB: 0,
|
|
})
|
|
defer rdb.Close()
|
|
|
|
// Test connection
|
|
if err := rdb.Ping(ctx).Err(); err != nil {
|
|
b.Skipf("Redis server not available: %v", err)
|
|
}
|
|
|
|
// Clean up before and after tests
|
|
defer func() {
|
|
rdb.FlushDB(ctx)
|
|
}()
|
|
|
|
scales := []int{1, 10, 100, 1000, 10000, 100000}
|
|
|
|
for _, scale := range scales {
|
|
b.Run(fmt.Sprintf("HSET_Pipelined_%d_operations", scale), func(b *testing.B) {
|
|
benchmarkHSETPipelined(b, rdb, ctx, scale)
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkHSET_Concurrent(b *testing.B) {
|
|
ctx := context.Background()
|
|
|
|
// Setup Redis client
|
|
rdb := redis.NewClient(&redis.Options{
|
|
Addr: "localhost:6379",
|
|
DB: 0,
|
|
PoolSize: 100,
|
|
})
|
|
defer rdb.Close()
|
|
|
|
// Test connection
|
|
if err := rdb.Ping(ctx).Err(); err != nil {
|
|
b.Skipf("Redis server not available: %v", err)
|
|
}
|
|
|
|
// Clean up before and after tests
|
|
defer func() {
|
|
rdb.FlushDB(ctx)
|
|
}()
|
|
|
|
// Reduced scales to avoid overwhelming the system with too many concurrent goroutines
|
|
scales := []int{1, 10, 100, 1000}
|
|
|
|
for _, scale := range scales {
|
|
b.Run(fmt.Sprintf("HSET_%d_operations_concurrent", scale), func(b *testing.B) {
|
|
benchmarkHSETOperationsConcurrent(b, rdb, ctx, scale)
|
|
})
|
|
}
|
|
}
|
|
|
|
// benchmarkHSETPipelined performs HSET benchmark using pipelining
|
|
func benchmarkHSETPipelined(b *testing.B, rdb *redis.Client, ctx context.Context, operations int) {
|
|
hashKey := fmt.Sprintf("benchmark_hash_pipelined_%d", operations)
|
|
|
|
b.ResetTimer()
|
|
b.StartTimer()
|
|
totalTimes := []time.Duration{}
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
b.StopTimer()
|
|
// Clean up the hash before each iteration
|
|
rdb.Del(ctx, hashKey)
|
|
b.StartTimer()
|
|
|
|
startTime := time.Now()
|
|
// Use pipelining for better performance
|
|
pipe := rdb.Pipeline()
|
|
|
|
// Add all HSET operations to the pipeline
|
|
for j := 0; j < operations; j++ {
|
|
field := fmt.Sprintf("field_%d", j)
|
|
value := fmt.Sprintf("value_%d", j)
|
|
pipe.HSet(ctx, hashKey, field, value)
|
|
}
|
|
|
|
// Execute all operations at once
|
|
_, err := pipe.Exec(ctx)
|
|
if err != nil {
|
|
b.Fatalf("Pipeline execution failed: %v", err)
|
|
}
|
|
totalTimes = append(totalTimes, time.Since(startTime))
|
|
}
|
|
|
|
b.StopTimer()
|
|
|
|
// Report operations per second
|
|
opsPerSec := float64(operations*b.N) / b.Elapsed().Seconds()
|
|
b.ReportMetric(opsPerSec, "ops/sec")
|
|
|
|
// Report average time per operation
|
|
avgTimePerOp := b.Elapsed().Nanoseconds() / int64(operations*b.N)
|
|
b.ReportMetric(float64(avgTimePerOp), "ns/op")
|
|
// report average time in milliseconds from totalTimes
|
|
sumTime := time.Duration(0)
|
|
for _, t := range totalTimes {
|
|
sumTime += t
|
|
}
|
|
avgTimePerOpMs := sumTime.Milliseconds() / int64(len(totalTimes))
|
|
b.ReportMetric(float64(avgTimePerOpMs), "ms")
|
|
}
|
|
|
|
// add same tests but with RESP2
|
|
func BenchmarkHSET_RESP2(b *testing.B) {
|
|
ctx := context.Background()
|
|
|
|
// Setup Redis client
|
|
rdb := redis.NewClient(&redis.Options{
|
|
Addr: "localhost:6379",
|
|
Password: "", // no password docs
|
|
DB: 0, // use default DB
|
|
Protocol: 2,
|
|
})
|
|
defer rdb.Close()
|
|
|
|
// Test connection
|
|
if err := rdb.Ping(ctx).Err(); err != nil {
|
|
b.Skipf("Redis server not available: %v", err)
|
|
}
|
|
|
|
// Clean up before and after tests
|
|
defer func() {
|
|
rdb.FlushDB(ctx)
|
|
}()
|
|
|
|
scales := []int{1, 10, 100, 1000, 10000, 100000}
|
|
|
|
for _, scale := range scales {
|
|
b.Run(fmt.Sprintf("HSET_RESP2_%d_operations", scale), func(b *testing.B) {
|
|
benchmarkHSETOperations(b, rdb, ctx, scale)
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkHSETPipelined_RESP2(b *testing.B) {
|
|
ctx := context.Background()
|
|
|
|
// Setup Redis client
|
|
rdb := redis.NewClient(&redis.Options{
|
|
Addr: "localhost:6379",
|
|
Password: "", // no password docs
|
|
DB: 0, // use default DB
|
|
Protocol: 2,
|
|
})
|
|
defer rdb.Close()
|
|
|
|
// Test connection
|
|
if err := rdb.Ping(ctx).Err(); err != nil {
|
|
b.Skipf("Redis server not available: %v", err)
|
|
}
|
|
|
|
// Clean up before and after tests
|
|
defer func() {
|
|
rdb.FlushDB(ctx)
|
|
}()
|
|
|
|
scales := []int{1, 10, 100, 1000, 10000, 100000}
|
|
|
|
for _, scale := range scales {
|
|
b.Run(fmt.Sprintf("HSET_Pipelined_RESP2_%d_operations", scale), func(b *testing.B) {
|
|
benchmarkHSETPipelined(b, rdb, ctx, scale)
|
|
})
|
|
}
|
|
}
|