StackExchange.Redis
StackExchange.Redis copied to clipboard
Question: Is there a need for implementing a Multiplexer connection pool?
I have see projects such as https://github.com/mataness/StackExchange.Redis.MultiplexerPool that implement a connection pool for ConnectionMultiplexer instances.
Is there any need for such layers on top of StackExchange.Redis, and if so what are there any obvious scenarios for such?
Hi,
I and my team have been researching this as well because we experienced slower than expected behavior while using single connection. We've found out that using multiple connections is beneficial on .NET Framework and not on .NET 6+. I don't know why, maybe some maintainers can shed some light.
Anyway here are some benchmark results of 10 parallel upload and download of 10Mb payload. Those were run on Amazon EC2 instance with Up to 12.5 bandwith
// * Summary *
BenchmarkDotNet v0.13.9+228a464e8be6c580ad9408e98f18813f6407fb5a, Windows 10 (10.0.17763.5328/1809/October2018Update/Redstone5)
Intel Xeon Platinum 8375C CPU 2.90GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 8.0.201
[Host] : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job-JQEXCP : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX2
Job-ASJAON : .NET Framework 4.8 (4.8.3761.0), X64 RyuJIT VectorSize=256
Platform=X64 IterationCount=5 RunStrategy=Monitoring
| Method | Runtime | ConnectionCount | Mean | Error | StdDev | Allocated |
|------- |------------------- |---------------- |-----------:|------------:|----------:|----------:|
| Upload | .NET 8.0 | 1 | 672.6 ms | 1,031.71 ms | 267.93 ms | 282.39 MB |
| Upload | .NET Framework 4.8 | 1 | 3,065.0 ms | 416.06 ms | 108.05 ms | 531.97 MB |
| Upload | .NET 8.0 | 2 | 851.1 ms | 1,272.22 ms | 330.39 ms | 282.76 MB |
| Upload | .NET Framework 4.8 | 2 | 1,435.9 ms | 320.23 ms | 83.16 ms | 565.77 MB |
| Upload | .NET 8.0 | 3 | 604.9 ms | 802.26 ms | 208.34 ms | 277.88 MB |
| Upload | .NET Framework 4.8 | 3 | 1,359.9 ms | 40.38 ms | 10.49 ms | 559.69 MB |
| Upload | .NET 8.0 | 10 | 1,090.9 ms | 1,755.86 ms | 455.99 ms | 287.98 MB |
| Upload | .NET Framework 4.8 | 10 | 1,161.7 ms | 278.37 ms | 72.29 ms | 566.22 MB |
@teemka Do you have code to go with the benchmark? We might be able to comment a little but hard to tell without seeing what's running.
@NickCraver sure. Here is the minimal benchmark.
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
using StackExchange.Redis;
var runtimes = new Runtime[]
{
ClrRuntime.Net48,
CoreRuntime.Core80,
};
BenchmarkRunner.Run<Benchmark>(ManualConfig
.CreateMinimumViable()
.AddJob(runtimes.Select(r => Job.Default
.WithRuntime(r)
.WithPlatform(Platform.X64)
.WithStrategy(RunStrategy.Monitoring)
.WithWarmupCount(1)
.WithIterationCount(5))
.ToArray()));
public class Benchmark : IAsyncDisposable
{
private const string Key = "testing/benchmark/connections";
private static readonly Random Random = new(2137);
private readonly List<IConnectionMultiplexer> _connections = [];
private readonly byte[] _data = new byte[10 * 1024 * 1024];
[Params(1, 2, 3, 10)]
public int ConnectionCount;
[GlobalSetup]
public void Setup()
{
Random.NextBytes(_data);
for (int i = 0; i < ConnectionCount; i++)
{
var configuration = new ConfigurationOptions
{
EndPoints = { { "*.cache.amazonaws.com", 6379 } },
Ssl = true,
Password = "*",
};
var connectionMultiplexer = ConnectionMultiplexer.Connect(configuration);
_connections.Add(connectionMultiplexer);
}
}
[Benchmark]
public async Task SetGetConcurrent()
{
var tasks = new List<Task>();
for (int i = 0; i < 10; i++)
{
var connection = _connections[i % _connections.Count];
var database = connection.GetDatabase();
var dt = DateTime.UtcNow.Ticks;
var task = Task.Run(async () =>
{
var key = $"{Key}{dt}";
await database.StringSetAsync(key, _data, TimeSpan.FromSeconds(30));
await database.StringGetAsync(key);
});
tasks.Add(task);
}
await Task.WhenAll(tasks);
}
public async ValueTask DisposeAsync()
{
foreach (var connection in _connections)
{
await connection.DisposeAsync();
}
GC.SuppressFinalize(this);
}
}
csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net48;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.9" />
<PackageReference Include="StackExchange.Redis" Version="2.7.20" />
</ItemGroup>
</Project>
And here are updated results:
// * Summary *
BenchmarkDotNet v0.13.9+228a464e8be6c580ad9408e98f18813f6407fb5a, Windows 10 (10.0.17763.5458/1809/October2018Update/Redstone5)
Intel Xeon Platinum 8375C CPU 2.90GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 8.0.201
[Host] : .NET 8.0.2 ([8.0.224.67](http://8.0.224.67/)11), X64 RyuJIT AVX2
Job-GYLMUN : .NET 8.0.2 ([8.0.224.67](http://8.0.224.67/)11), X64 RyuJIT AVX2
Job-ZMKRES : .NET Framework 4.8 (4.8.3761.0), X64 RyuJIT VectorSize=256
Platform=X64 IterationCount=10 RunStrategy=Monitoring
WarmupCount=1
| Method | Runtime | ConnectionCount | Mean | Error | StdDev |
|----------------- |------------------- |---------------- |-----------:|----------:|----------:|
| SetGetConcurrent | .NET 8.0 | 1 | 741.7 ms | 306.78 ms | 202.92 ms |
| SetGetConcurrent | .NET Framework 4.8 | 1 | 3,102.5 ms | 89.94 ms | 59.49 ms |
| SetGetConcurrent | .NET 8.0 | 2 | 517.4 ms | 108.59 ms | 71.82 ms |
| SetGetConcurrent | .NET Framework 4.8 | 2 | 1,457.6 ms | 144.76 ms | 95.75 ms |
| SetGetConcurrent | .NET 8.0 | 3 | 721.6 ms | 597.01 ms | 394.88 ms |
| SetGetConcurrent | .NET Framework 4.8 | 3 | 1,442.9 ms | 265.49 ms | 175.60 ms |
| SetGetConcurrent | .NET 8.0 | 10 | 518.9 ms | 118.88 ms | 78.63 ms |
| SetGetConcurrent | .NET Framework 4.8 | 10 | 1,269.7 ms | 128.96 ms | 85.30 ms |