13

We are using C1 Azure Redis Cache in our application. Recently we are experiencing lots of time-outs on GET operations.

According to this article, one of possible solutions is to implement pool of ConnectionMultiplexer objects.

Another possible solution is to use a pool of ConnectionMultiplexer objects in your client, and choose the “least loaded” ConnectionMultiplexer when sending a new request. This should prevent a single timeout from causing other requests to also timeout.

How would implementation of a pool of ConnectionMultiplexer objects using C# look like?

Edit:

Related question that I asked recently.

Community
  • 1
  • 1
Jakub Holovsky
  • 6,543
  • 10
  • 54
  • 98
  • Are you performing any particularly long-running operations? Before leaping to a pool, I'd love to understand if this is latency, bandwidth saturation, server congestion, etc... – Marc Gravell Apr 13 '15 at 16:09
  • 1
    @MarcGravell - we have almost solved all our time-out issues by simply re-writing some of our code that would result in better performance. This is not really needed but I would still be interested in seeing a piece of code that would implement pool of ConnectionMultiplexers. – Jakub Holovsky Apr 16 '15 at 10:17
  • @JakubHolovsky can you share some experience about how to re-write to result better performance? – huangcd Sep 03 '16 at 07:10
  • 1
    @huangcd - yep, sure, have a look at my answer here http://stackoverflow.com/questions/29569997/azure-redis-cache-multiple-errors-timeoutexception-timeout-performing-get-ke – Jakub Holovsky Sep 03 '16 at 17:41
  • @JakubHolovsky thank you! – huangcd Sep 04 '16 at 03:23

2 Answers2

6

If you're using StackExchange.Redis, according to this github issue, you can use the TotalOutstanding property on the connection multiplexer object.

Here is a implementation I came up with, that is working correctly:

public static int POOL_SIZE = 100;
private static readonly Object lockPookRoundRobin = new Object();
private static Lazy<Context>[] lazyConnection = null;

//Static initializer to be executed once on the first call
private static void InitConnectionPool()
{
    lock (lockPookRoundRobin)
    {
        if (lazyConnection == null) {
             lazyConnection = new Lazy<Context>[POOL_SIZE];
        }


        for (int i = 0; i < POOL_SIZE; i++){
            if (lazyConnection[i] == null)
                lazyConnection[i] = new Lazy<Context>(() => new Context("YOUR_CONNECTION_STRING", new CachingFramework.Redis.Serializers.JsonSerializer()));
        }
    }
}

private static Context GetLeastLoadedConnection()
{
    //choose the least loaded connection from the pool
    /*
    var minValue = lazyConnection.Min((lazyCtx) => lazyCtx.Value.GetConnectionMultiplexer().GetCounters().TotalOutstanding);
    var lazyContext = lazyConnection.Where((lazyCtx) => lazyCtx.Value.GetConnectionMultiplexer().GetCounters().TotalOutstanding == minValue).First();
    */

    // UPDATE following @Luke Foust comment below
    Lazy<Connection> lazyContext;

    var loadedLazys = lazyConnection.Where((lazy) => lazy.IsValueCreated);
    if(loadedLazys.Count()==lazyConnection.Count()){
        var minValue = loadedLazys.Min((lazy) => lazy.Value.TotalOutstanding);
        lazyContext = loadedLazys.Where((lazy) => lazy.Value.TotalOutstanding == minValue).First();
    }else{
        lazyContext = lazyConnection[loadedLazys.Count()];
    }
    return lazyContext.Value;
}

private static Context Connection
{
    get
    {
        lock (lockPookRoundRobin)
        {
            return GetLeastLoadedConnection();
        }
    }
}

public RedisCacheService()
{
    InitConnectionPool();
}
Gomino
  • 12,127
  • 4
  • 40
  • 49
  • 2
    Won't the call to lazyConnection.Min just acause the whole pool to connect/initialize thus invalidating the use of Lazy? – Luke Foust Jun 07 '18 at 19:28
  • You're totally right, I updated the answer to fix this issue and properly use the connection pool. You can test this code online: [here](https://dotnetfiddle.net/UCJFeA) – Gomino Jun 10 '18 at 11:00
  • 1
    @Gomino, what is Context here. I can see it coming from just one package Microsoft.AspNetCore.Hosting.Internal.HostingApplication. which doesn't have argument in its constructor. Which package you are referring to get Context object. Also in this line Lazy lazyContext; from where you are getting Connection Class? – Anil Purswani Apr 03 '19 at 06:03
  • @AnilPurswani you should now use the `RedisContext` class. Indeed the `Context` class came from `CachingFramework.Redis` but was marked obsolete in May 2018 by commit [a0a83e0](https://github.com/thepirat000/CachingFramework.Redis/blob/a0a83e0e18f57ddf6f8515f25797f991cc281bab/src/CachingFramework.Redis/Context.cs) and has been removed by commit [023a4b3](https://github.com/thepirat000/CachingFramework.Redis/commits/023a4b3475375b5540e988eaeedffcbfee95919b) – Gomino Apr 03 '19 at 08:27
  • @Gomino, I am using StackExchange.Redis, so I used ConnectionMultiplexer and it worked fine. And also like in the link you provided, I used "GetCounters().TotalOutstanding" – Anil Purswani Apr 04 '19 at 05:03
  • @Gomino be very careful with GetCounters(). Make sure you do not call this twice for same multiplexer because it's values can change – Anubis Apr 06 '20 at 12:45
6

You can also accomplish this in a easier way by using StackExchange.Redis.Extensions

Sample code:

    using StackExchange.Redis;
    using StackExchange.Redis.Extensions.Core.Abstractions;
    using StackExchange.Redis.Extensions.Core.Configuration;
    using System;
    using System.Collections.Concurrent;
    using System.Linq;

    namespace Pool.Redis
    {
    /// <summary>
    /// Provides redis pool
    /// </summary>
    public class RedisConnectionPool : IRedisCacheConnectionPoolManager
    {
        private static ConcurrentBag<Lazy<ConnectionMultiplexer>> connections;
        private readonly RedisConfiguration redisConfiguration;

        public RedisConnectionPool(RedisConfiguration redisConfiguration)
        {
            this.redisConfiguration = redisConfiguration;
            Initialize();
        }

        public IConnectionMultiplexer GetConnection()
        {
            Lazy<ConnectionMultiplexer> response;
            var loadedLazys = connections.Where(lazy => lazy.IsValueCreated);

            if (loadedLazys.Count() == connections.Count)
            {
                response = connections.OrderBy(x => x.Value.GetCounters().TotalOutstanding).First();
            }
            else
            {
                response = connections.First(lazy => !lazy.IsValueCreated);
            }

            return response.Value;
        }

        private void Initialize()
        {
            connections = new ConcurrentBag<Lazy<ConnectionMultiplexer>>();

            for (int i = 0; i < redisConfiguration.PoolSize; i++)
            {
                connections.Add(new Lazy<ConnectionMultiplexer>(() => ConnectionMultiplexer.Connect(redisConfiguration.ConfigurationOptions)));
            }
        }

        public void Dispose()
        {
            var activeConnections = connections.Where(lazy => lazy.IsValueCreated).ToList();
            activeConnections.ForEach(connection => connection.Value.Dispose());
            Initialize();
        }
    }
}

Where RedisConfiguration is something like this:

            return new RedisConfiguration()
        {
            AbortOnConnectFail = true,
            Hosts = new RedisHost[] {
                                      new RedisHost() 
                                      {
                                          Host = ConfigurationManager.AppSettings["RedisCacheAddress"].ToString(),
                                          Port = 6380
                                      },
                                    },
            ConnectTimeout = Convert.ToInt32(ConfigurationManager.AppSettings["RedisTimeout"].ToString()),
            Database = 0,
            Ssl = true,
            Password = ConfigurationManager.AppSettings["RedisCachePassword"].ToString(),
            ServerEnumerationStrategy = new ServerEnumerationStrategy()
            {
                Mode = ServerEnumerationStrategy.ModeOptions.All,
                TargetRole = ServerEnumerationStrategy.TargetRoleOptions.Any,
                UnreachableServerAction = ServerEnumerationStrategy.UnreachableServerActionOptions.Throw
            },
            PoolSize = 50
        };
godot
  • 3,422
  • 6
  • 25
  • 42
rafael.braga
  • 392
  • 4
  • 11