7

I am learning StackExchange.Redis, and Kubernetes, so I made a simple .net core app that reads a key/value from a Redis master+2slaves deployed on kubernetes. (so, everything, Redis and my app, run inside containers)

To connect to redis I use the syntax suggested in the doc:

ConnectionMultiplexer.Connect("server1:6379,server2:6379,server3:6379");

However, if I monitor the 3 containers with redis-cli MONITOR, the requests are processed always from the master, the 2 slaves do nothing, so there is no load balancing.

I have tried also to connect to a Kubernetes load balancer service which exposes the 3 Redis containers endpoints, the result is that when I start the .net app the request is processed randomly by one of the 3 Redis nodes, but then always on the same node. I have to restart the .net app and it will query another node but subsequent queries go always on that node.

What is the proper way to read key/values in a load balanced way using StackExchange.Redis with a master/slave Redis setup?

Thank you

Rico
  • 58,485
  • 12
  • 111
  • 141
Anton M
  • 798
  • 7
  • 17

3 Answers3

11

SE.Redis has a CommandFlags parameter that is optional on every command. There are some useful and relevant options here:

  • DemandPrimary
  • PreferPrimary
  • DemandReplica
  • PreferReplica

The default behaviour is PreferPrimary; write operations bump that to DemandPrimary, and there are a very few commands that actively prefer replicas (keyspace iteration, etc).

So: if you aren't specifying CommandFlags, then right now you're probably using the default: PreferPrimary. Assuming a primary exists and is reachable, then: it will use the primary. And there can only be one primary, so: it'll use one server.

A cheap option for today would be to add PreferReplica as a CommandFlags option on your high-volume read operations. This will push the work to the replicas if they can be resolved - or if no replicas can be found: the primary. Since there can be multiple replicas, it applies a basic rotation-based load-balancing scheme, and you should start to see load on multiple replicas.

If you want to spread load over all nodes including primaries and replicas... then I'll need to add new code for that. So if you want that, please log it as an issue on the github repo.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Thanks Marc, using PreferSlave change something, now the get is processed by one of the 2 slaves instead of the master as it was before, but it's always the same slave :( the 1st in the connect list... I'd like a LoadBalanced command flag that spread equally the load of read operations on master and all slaves , and ideally find a way to keep in sync the list of the M+S with the kubernetes service that expose them, which might change the list of endpoints. I will think a better way to express my request and eventually open an issue on GH. Big thanks for now ;) – Anton M Oct 22 '18 at 22:26
  • @AntonM it *should* currently rotate between the 2 slaves; if that isn't happening, something sounds "off", but hard to be specific without more context – Marc Gravell Oct 23 '18 at 07:46
  • I am inspecting again the code and eventually will point you to the source if I am unable to solve the problem. Thanks – Anton M Oct 23 '18 at 09:20
  • @AntonM question: what redis server version are you using, *exactly*? – Marc Gravell Oct 23 '18 at 09:22
  • I'm using the container "image": "redis:4.0.11-alpine" deployed in kubernetes (a stateful set composed of 1 master and 2 slaves) server version is: redis_version:4.0.11 – Anton M Oct 23 '18 at 09:36
  • @AntonM k; for a moment I was worried that 5.0 (released the other day) had implemented the planned nomenclature changes re "slave" / "replica", which might have confused the library; but I've checked, and 5.0 RTM doesn't yet include those – Marc Gravell Oct 23 '18 at 09:38
  • UPDATE I tested the app now and it's rotating/balancing between the 2 slaves :) It would be nice to balance also on the master but I will test later a workaround playing with the PreferMaster/Slave options. I'd like to update you on my findings, but without abusing the comments here: should we move to GH or SO chat? (never used SO char so far...) – Anton M Oct 23 '18 at 13:08
  • @AntonM I'd probably say "github issue or email" is the best option – Marc Gravell Oct 23 '18 at 14:10
  • @MarcGravell we did a change recently in our code that some get commands go to the replica ones however we did not see any decrease on get requests that go master, and the replica ones did not get any requests at all(Azure metric - Gets(Instance based)) . Do we need to so some setup explicitly other than setting up CommandFlags as PreferSlave. – fiskra Sep 17 '20 at 13:11
  • @fiskra preferslave (/preferreplica) should do the job by itself; if it isn't working, we'd need to look into that, but: it should, *if* the server (azure here?) actually advertises them – Marc Gravell Sep 17 '20 at 13:23
  • @MarcGravell it turned to the redis instance has to be clustered if you want to enable replica. We run "cluster nodes" command and it tells : ERR This instance has cluster support disabled we tried it on the others that are clustered and we are able to see slave and master nodes – fiskra Sep 18 '20 at 13:11
  • @MarcGravell we also have one question. We appreciate your effort answering all questions in here stackoverflow community. How stale the data we read from slave ? Is there any period we need to know before enable this feature for reads ww. – fiskra Sep 18 '20 at 13:49
  • That depends on load, but usually "not very", and there are redis commands related to waiting for replication guarantees on write (although they aren't provided via SE-Redis for technical reasons) – Marc Gravell Sep 18 '20 at 15:30
  • @MarcGravell So if I can live with stale data for a while then all reads would be better using PreferSlave obviously? SE.Redis will check by itself which address is the slave and which is master, correct?(meaning i don't need to configure it)? – omriman12 Mar 06 '21 at 12:28
  • 1
    @omriman12 yes, the library will identify the replication topology – Marc Gravell Mar 06 '21 at 12:55
  • I have verified the PreferSlave command and it seems to be working fine. – Taha Ali May 24 '22 at 05:08
1

As per the docs it should automatically detect master/slaves. It may be that StackExchange.Redis is detecting that all your nodes are masters, thus just selecting one using its own tiebreaker rules.

I would also check the request logs on your redis-pods if there are some invalid commands that are being sent by StackExchange.Redis, maybe you don't have the right permissions for it to detect master/slaves.

Maybe you also have Sentinel enabled and StackExchange doesn't support sentinel

If you see something is not working you can open an issue on Github

Finally, you could also try twemproxy.

Rico
  • 58,485
  • 12
  • 111
  • 141
  • 1
    while what you say isn't *wrong*, I suspect the actual issue here is simply `CommandFlags`, as per my answer – Marc Gravell Oct 22 '18 at 19:58
  • ahh cool, thx!. Where is that in the docs or you have to know from the code? – Rico Oct 22 '18 at 20:06
  • 1
    I don't know if we've written a `.md` covering it, but it is explained in the intellisense documentation of `CommandFlags` – Marc Gravell Oct 22 '18 at 20:08
  • Thanks Rico, the kube. part works flawlessly with other tools, I am trying to get the same results from .net apps using SE.Redis. I guess tweaking the commandFlags as Marc suggested should get the desired result. – Anton M Oct 22 '18 at 22:32
0

My Redis Implementation

namespace Asset.Car.Cache
{
    public class Redis : IRedis
    {
        private readonly IConnectionMultiplexer _iConnectionMultiplexer;

        public Redis(IConnectionMultiplexer connectionMultiplexer)
        {
            _iConnectionMultiplexer = connectionMultiplexer;
        }

        public async Task<T> Get<T>(string key) where T : class
        {
            var cachedResponse = await _iConnectionMultiplexer.GetDatabase().StringGetAsync(key, CommandFlags.PreferReplica);
            return cachedResponse == RedisValue.Null ? null : JsonConvert.DeserializeObject<T>(cachedResponse);
        }

        public async Task Set<T>(string key, T value, int expiryInMins = 1440) where T : class
        {
            string serVal = JsonConvert.SerializeObject(value);
            await _iConnectionMultiplexer.GetDatabase().StringSetAsync(key, serVal, System.TimeSpan.FromMinutes(expiryInMins));
        }

        public async Task Delete(string key)
        {
            await _iConnectionMultiplexer.GetDatabase().KeyDeleteAsync(key);
        }

        public async Task FlushAll()
        {
            var endpoints = _iConnectionMultiplexer.GetEndPoints();
            var server = _iConnectionMultiplexer.GetServer(endpoints[0]);
            await server.FlushDatabaseAsync(); // allowAdmin must be true
        }
    }
}

Startup.cs -

        var redisConfig = Configuration.GetSection("Redis").Get<RedisConfig>();
        services.AddSingleton<IConnectionMultiplexer>(ConnectionMultiplexer.Connect(
            new ConfigurationOptions()
            {
                EndPoints = { redisConfig.Hosts.Primary, redisConfig.Hosts.Replica },
                SyncTimeout = redisConfig.SyncTimeout,
                ConnectRetry = redisConfig.ConnectRetry,
                Password = redisConfig.Password,
                Ssl = redisConfig.Ssl,
                AllowAdmin = redisConfig.AllowAdmin
            }));

IMPORTANT DECISION FACTOR - Wrappers like IDistributedCache and StackExchangeRedis.Extensions do not include all the functions possible in the original stackexchange.Redis library, In particular I required to delete All the keys in Redis Cache, which was not exposed in these wrappers.

Taha Ali
  • 457
  • 1
  • 5
  • 7
  • I currently have a Feature Request open to the `Microsoft.Extensions.Caching.StackExchangeRedis` nuget to expose the ability to pass `StackExchange.Redis.CommandFlags` to the `RedisCache` implementation. Please add your feedback if you are also interested - https://github.com/dotnet/aspnetcore/issues/41948 – Dave Black Jul 28 '22 at 15:55