StackExchange.Redis icon indicating copy to clipboard operation
StackExchange.Redis copied to clipboard

Question: Is there a need for implementing a Multiplexer connection pool?

Open jufa2401 opened this issue 11 months ago • 3 comments

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?

jufa2401 avatar Feb 26 '24 08:02 jufa2401

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 avatar Feb 27 '24 16:02 teemka

@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 avatar Mar 05 '24 16:03 NickCraver

@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 |

teemka avatar Mar 06 '24 10:03 teemka