StackExchange / StackExchange.Redis

General purpose redis client
https://stackexchange.github.io/StackExchange.Redis/
Other
5.88k stars 1.51k forks source link

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

Open jufa2401 opened 7 months ago

jufa2401 commented 7 months ago

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?

teemka commented 7 months ago

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 |
NickCraver commented 7 months ago

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

teemka commented 7 months ago

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