Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance/Caching Recommendations? #19

Closed
micahasmith opened this issue Apr 17, 2014 · 12 comments
Closed

Performance/Caching Recommendations? #19

micahasmith opened this issue Apr 17, 2014 · 12 comments

Comments

@micahasmith
Copy link

Background

We run the latest stable redis on linux and our .NET stuff is on Windows Server 2012 all in windows azure, all within the same region.

How do i plan on using Redis? Well, I plan on having it cache the layer that feeds the http Response. Its not output caching because i'm not caching the actual JSON that the api is returning-- it's the layer that builds the object that will end up being returned as JSON.

Story

I'm testing out using StackExchange.Redis for caching instead of our current ObjectCache-based impl. For full disclosure I'm going to provide both here:

ObjectCache Based

using Printmee.Infra.Caching;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Caching;
using System.Text;
using System.Threading.Tasks;

namespace Widget.Caching
{
    public class HttpCache:IBigCache
    {
        private ObjectCache _cache
        {
            get
            {
                if (__cache == null)
                {
                    __cache = System.Runtime.Caching.MemoryCache.Default;
                }
                return __cache;
            }
        }
        private ObjectCache __cache = null;
        public async Task<T> Get<T>(string key) where T : class
        {
            return _cache.Get(key) as T;
        }

        public async Task<T> Get<T>(string key, params object[] args) where T : class
        {
            return _cache.Get(string.Format(key,args)) as T;
        }

        public async Task<T> Get<T>(CacheKey key) where T : class
        {
            return _cache.Get(key.Full) as T;
        }

        public async Task<T> GetCategorized<T>(string category, string key) where T : class
        {
            return _cache.Get(category+key) as T;
        }

        public async Task Remove(string key)
        {
            _cache.Remove(key);
        }

        public async Task<T> Set<T>(string key, T obj, TimeSpan keepAlive)
        {
            _cache.Set(key, obj, new CacheItemPolicy() { AbsoluteExpiration = DateTime.UtcNow.Add(keepAlive) });
            return obj;
        }

        public async Task<T> Set<T>(CacheKey key, T obj) where T : class
        {
            _cache.Set(key.Full, obj, new CacheItemPolicy() { AbsoluteExpiration = DateTime.UtcNow.AddDays(1) });
            return obj;
        }

        public async Task<T> SetCategorized<T>(string category, string key, T obj) where T : class
        {
            _cache.Set(category+key, obj, new CacheItemPolicy() { AbsoluteExpiration = DateTime.UtcNow.AddDays(1) });
            return obj;
        }
    }
}

StackExchange.Redis

using Printmee.Infra.Caching;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using StackExchange.Redis;
using ServiceStack.Text;
using ServiceStack.Text.Json;
using NLog;

namespace Widget.Caching
{
    public class RedisCache : Widget.Caching.IBigCache 
    {
        private static Logger _Log = LogManager.GetCurrentClassLogger();
        private StackExchange.Redis.IDatabase __Redis;
        private StackExchange.Redis.IDatabase _Redis 
        { 
            get 
            {
                if (__Redis == null)
                    __Redis = _Connection.GetDatabase();
                return __Redis;
            } 
        }
        private RedisConfig _Config;
        private ConnectionMultiplexer _Connection;
        public RedisCache(ConnectionMultiplexer con, RedisConfig config)
        {
            _Connection = con;
            _Config = config;
        }

        public async Task<T> Get<T>(string key) where T: class
        {
            if (!_Config.IsOn)
                return null;
            var val= await _Redis.StringGetWithExpiryAsync(key);
            if (val.Value.IsNull)
                return null;
            return JsonSerializer.DeserializeFromString<T>(val.Value);
        }



        public async Task<T> Get<T>(string key, params object[] args) where T:class
        {
            return await Get<T>(string.Format(key, args));
        }

        public async Task<T> GetCategorized<T>(string category, string key) where T : class
        {
            if (!_Config.IsOn)
                return null;
            _Log.Debug("lookup {0}{1}", category, key);
            var val = await _Redis.HashGetAsync(category, key);
            _Log.Debug("done {0}{1}", category, key);
            if (val.IsNull)
                return null;
            _Log.Debug("ser {0}{1}", category, key);
            var final = JsonSerializer.DeserializeFromString<T>(val);
            _Log.Debug("done ser {0}{1}", category, key);
            return final;
        }

        public async Task<T> Get<T>(CacheKey key) where T:class
        {
            if (key.IsCategorized)
                return await GetCategorized<T>(key.Category, key.Key);
            return await Get<T>(key.Key);
        }

        public async Task Remove(string key)
        {
            if (!_Config.IsOn)
                return;
            await _Redis.KeyDeleteAsync(key);
        }

        public async Task<T> Set<T>(string key, T obj, TimeSpan keepAlive)
        {
            if (!_Config.IsOn)
                return obj;

            await _Redis.StringSetAsync(key, JsonSerializer.SerializeToString<T>(obj));
            await _Redis.KeyExpireAsync(key, DateTime.Now.ToUniversalTime().Add(keepAlive));

            return obj;
        }
        public async Task<T> SetCategorized<T>(string category, string key, T obj) where T : class
        {
            if (!_Config.IsOn)
                return obj;

            await _Redis.HashSetAsync(category,key, JsonSerializer.SerializeToString<T>(obj));
            return obj;
        }

        public async Task<T> Set<T>(CacheKey key, T obj) where T : class
        {
            if (key.IsCategorized)
                return await SetCategorized<T>(key.Category, key.Key,obj);
            return await Set<T>(key.Key,obj, TimeSpan.FromDays(2));
        }
    }
}

Here's an example of me using this IBigCache interface:

var key = new CacheKey()
    .SetCategory("ProductVariant-{0}", sku)
    .SetKey("BuildInfo");

var cached = await _BigCache.Get<ProductBuildInfoResponseV1>(key);
if (cached != null) 
    return cached;

// the computation
var res = await GetBuildInfo(sku);

//proxying the result through cache setting
return await _BigCache.Set<ProductBuildInfoResponseV1>(key,res);

Ok ok ok. So the thing that is bothering me is the performance change between the two. Things I know I need to account for when comparing the two:

  1. Object cache isnt json serializing, obviously this will come at a cost
  2. Network latency and the fact that my data isn't on the same box anymore

So I'm thinking "object cache will be faster" as I go into this. The thing that surprise me though is the cost of using Redis here--

Benchmark of ObjectCache at -c 50

Percentage of the requests served within a certain time (ms)
  50%    211
  66%    337
  75%    388
  80%    423
  90%    500
  95%    519
  98%    587
  99%    592
 100%    592 (longest request)

Benchmark of StackExchange.Redis (async) at -c 50

Percentage of the requests served within a certain time (ms)
  50%   6899
  66%   6941
  75%   7838
  80%   8381
  90%   9904
  95%  10448
  98%  12104
  99%  12607
 100%  12607 (longest request)

It's a lot higher than I expected. Am I doing this wrong?

@mgravell
Copy link
Collaborator

What is the average latency to your Redis server? Note that Ping() returns
the latency.

Yes, there's no way it should be taking seconds unless there is a problem.
We don't use azure, but a Redis hit in the same DC for us is basically
unmeasurable (unless we are saturating).

As a side note: we generally use a hybrid 2-tier cache: check local memory
first, then use Redis as the centralised cache. We use pub/sub to achieve
cache invalidation. I only mention it as a side topic - I'd still like to
understand what is happening here.

Marc
On 17 Apr 2014 17:47, "Micah Smith" [email protected] wrote:

Background

We run the latest stable redis on linux and our .NET stuff is on Windows
Server 2012 all in windows azure, all within the same region.

How do i plan on using Redis? Well, I plan on having it cache the layer
that feeds the http Response. Its not output caching because i'm not
caching the actual JSON that the api is returning-- it's the layer that
builds the object that will end up being returned as JSON.
Story

I'm testing out using StackExchange.Redis for caching instead of our
current ObjectCache-based impl. For full disclosure I'm going to provide
both here:
ObjectCache Based

using Printmee.Infra.Caching;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Caching;
using System.Text;
using System.Threading.Tasks;

namespace Widget.Caching
{
public class HttpCache:IBigCache
{
private ObjectCache _cache
{
get
{
if (__cache == null)
{
__cache = System.Runtime.Caching.MemoryCache.Default;
}
return __cache;
}
}
private ObjectCache __cache = null;
public async Task Get(string key) where T : class
{
return _cache.Get(key) as T;
}

    public async Task<T> Get<T>(string key, params object[] args) where T : class
    {
        return _cache.Get(string.Format(key,args)) as T;
    }

    public async Task<T> Get<T>(CacheKey key) where T : class
    {
        return _cache.Get(key.Full) as T;
    }

    public async Task<T> GetCategorized<T>(string category, string key) where T : class
    {
        return _cache.Get(category+key) as T;
    }

    public async Task Remove(string key)
    {
        _cache.Remove(key);
    }

    public async Task<T> Set<T>(string key, T obj, TimeSpan keepAlive)
    {
        _cache.Set(key, obj, new CacheItemPolicy() { AbsoluteExpiration = DateTime.UtcNow.Add(keepAlive) });
        return obj;
    }

    public async Task<T> Set<T>(CacheKey key, T obj) where T : class
    {
        _cache.Set(key.Full, obj, new CacheItemPolicy() { AbsoluteExpiration = DateTime.UtcNow.AddDays(1) });
        return obj;
    }

    public async Task<T> SetCategorized<T>(string category, string key, T obj) where T : class
    {
        _cache.Set(category+key, obj, new CacheItemPolicy() { AbsoluteExpiration = DateTime.UtcNow.AddDays(1) });
        return obj;
    }
}

}

StackExchange.Redis

using Printmee.Infra.Caching;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using StackExchange.Redis;
using ServiceStack.Text;
using ServiceStack.Text.Json;
using NLog;

namespace Widget.Caching
{
public class RedisCache : Widget.Caching.IBigCache
{
private static Logger _Log = LogManager.GetCurrentClassLogger();
private StackExchange.Redis.IDatabase __Redis;
private StackExchange.Redis.IDatabase _Redis
{
get
{
if (__Redis == null)
__Redis = _Connection.GetDatabase();
return __Redis;
}
}
private RedisConfig _Config;
private ConnectionMultiplexer _Connection;
public RedisCache(ConnectionMultiplexer con, RedisConfig config)
{
_Connection = con;
_Config = config;
}

    public async Task<T> Get<T>(string key) where T: class
    {
        if (!_Config.IsOn)
            return null;
        var val= await _Redis.StringGetWithExpiryAsync(key);
        if (val.Value.IsNull)
            return null;
        return JsonSerializer.DeserializeFromString<T>(val.Value);
    }



    public async Task<T> Get<T>(string key, params object[] args) where T:class
    {
        return await Get<T>(string.Format(key, args));
    }

    public async Task<T> GetCategorized<T>(string category, string key) where T : class
    {
        if (!_Config.IsOn)
            return null;
        _Log.Debug("lookup {0}{1}", category, key);
        var val = await _Redis.HashGetAsync(category, key);
        _Log.Debug("done {0}{1}", category, key);
        if (val.IsNull)
            return null;
        _Log.Debug("ser {0}{1}", category, key);
        var final = JsonSerializer.DeserializeFromString<T>(val);
        _Log.Debug("done ser {0}{1}", category, key);
        return final;
    }

    public async Task<T> Get<T>(CacheKey key) where T:class
    {
        if (key.IsCategorized)
            return await GetCategorized<T>(key.Category, key.Key);
        return await Get<T>(key.Key);
    }


    public async Task Remove(string key)
    {
        if (!_Config.IsOn)
            return;
        await _Redis.KeyDeleteAsync(key);
    }

    public async Task<T> Set<T>(string key, T obj, TimeSpan keepAlive)
    {
        if (!_Config.IsOn)
            return obj;

        await _Redis.StringSetAsync(key, JsonSerializer.SerializeToString<T>(obj));
        await _Redis.KeyExpireAsync(key, DateTime.Now.ToUniversalTime().Add(keepAlive));

        return obj;
    }
    public async Task<T> SetCategorized<T>(string category, string key, T obj) where T : class
    {
        if (!_Config.IsOn)
            return obj;

        await _Redis.HashSetAsync(category,key, JsonSerializer.SerializeToString<T>(obj));
        return obj;
    }

    public async Task<T> Set<T>(CacheKey key, T obj) where T : class
    {
        if (key.IsCategorized)
            return await SetCategorized<T>(key.Category, key.Key,obj);
        return await Set<T>(key.Key,obj, TimeSpan.FromDays(2));
    }
}

}

Here's an example of me using this IBigCache interface:

        var key = new CacheKey()
            .SetCategory("ProductVariant-{0}", sku)
            .SetKey("BuildInfo");

        var cached = await _BigCache.Get<ProductBuildInfoResponseV1>(key);
        if (cached != null)
            return cached;

        // the computation
        var res = await GetBuildInfo(sku);

        //proxying the result through cache setting
        return await _BigCache.Set<ProductBuildInfoResponseV1>(key,res);

Ok ok ok. So the thing that is bothering me is the performance change
between the two. Things I know I need to account for when comparing the two:

  1. Object cache isnt json serializing, obviously this will come at a
    cost
  2. Network latency and the fact that my data isn't on the same box
    anymore

So I'm thinking "object cache will be faster" as I go into this. The thing
that surprise me though is the cost of using Redis here--
Benchmark of ObjectCache at -c 50

Percentage of the requests served within a certain time (ms)
50% 211
66% 337
75% 388
80% 423
90% 500
95% 519
98% 587
99% 592
100% 592 (longest request)

Benchmark of StackExchange.Redis (async) at -c 50

Percentage of the requests served within a certain time (ms)
50% 6899
66% 6941
75% 7838
80% 8381
90% 9904
95% 10448
98% 12104
99% 12607
100% 12607 (longest request)

It's a lot higher than I expected. Am I doing this wrong?


Reply to this email directly or view it on GitHubhttps://github.com//issues/19
.

@micahasmith
Copy link
Author

I added a method to my server API so as to test within the data center and both Ping() and PingAsync() return either 0 or 1ms, consistently.

Going to have to put the hybrid approach in mine as well.... smart ;)

Another benchmark, this time of sync instead of async--

Benchmark of StackExchange.Redis (sync) at -c 50

Concurrency Level:      50
Time taken for tests:   51.411 seconds
Complete requests:      100
Failed requests:        47
   (Connect: 0, Receive: 0, Length: 47, Exceptions: 0)
Non-2xx responses:      47
Total transferred:      4443412 bytes
HTML transferred:       4409721 bytes
Requests per second:    1.95 [#/sec] (mean)
Time per request:       25705.471 [ms] (mean)
Time per request:       514.109 [ms] (mean, across all concurrent requests)
Transfer rate:          84.40 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       16   57 300.7     27    3033
Processing:   636 13342 12365.1   6960   42126
Waiting:      416 13152 12495.2   6703   42126
Total:        663 13399 12341.4   6981   42148

Percentage of the requests served within a certain time (ms)
  50%   6981
  66%  15508
  75%  24217
  80%  27089
  90%  34190
  95%  39235
  98%  41383
  99%  42148
 100%  42148 (longest request)

Included the top so that you can see I start getting errors when using sync methods.

The error is:

System.TimeoutException: Timeout performing HGET Recipe-00000000-0000-0000-0000-000000000000, inst: 4, queue: 12, qu=0, qs=12, qc=0, wr=0/0
   at StackExchange.Redis.ConnectionMultiplexer.ExecuteSyncImpl[T](Message message, ResultProcessor`1 processor, ServerEndPoint server) in c:\TeamCity\buildAgent\work\18a91a3757cef937\StackExchange.Redis\StackExchange\Redis\ConnectionMultiplexer.cs:line 1720

@micahasmith
Copy link
Author

It looks like you have hooks for logging in certain places... want me to send any log data your way?

@mgravell
Copy link
Collaborator

Sorry for delay; 4 day weekend here. I wonder, then, if this is bandwidth saturation - I don't actually track the bytes sent at the moment. This is all very odd. I don't suppose it can be illustrated in a runnable test rig? (i.e. where I can repro)

@micahasmith
Copy link
Author

Let me see if i can put some time together to create a test setup in Azure.

@gopimails
Copy link

About your side note: "As a side note: we generally use a hybrid 2-tier cache: check local memory
first, then use Redis as the centralised cache."
Is this feature available in latest StackExchange Redis NuGet.

@NickCraver
Copy link
Collaborator

@gopimails It's not...we've talked about an API for this but it's significant amounts of work. Currently it's in a very-SO-specific library calling StackRedis. We see the general usefulness to everyone for such a system, just a lack of time.

@MichaCo
Copy link
Contributor

MichaCo commented Jan 26, 2016

If you want to use redis just as a plain object cache and need those features like multiple layers of caching etc..., you can take a look at CacheManager which does all that.

@GaTechThomas
Copy link

@micahasmith Did you ever discover what was causing the long durations?

@micahasmith
Copy link
Author

@GaTechThomas not at all! i'm currently investigating if it was from me abusing HttpClient in other parts of my codebase a la https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

@NickCraver
Copy link
Collaborator

Slight update here: there's talk of client-side caching in general with redis here and how it'll interact with library. We're watching the discussion here and will see how the ecosystem wants to head before making calls: https://groups.google.com/forum/#!topic/redis-db/xfcnYkbutDw

@NickCraver
Copy link
Collaborator

One more update - in case I wrote up how we do caching at Stack Overflow on this library here: https://nickcraver.com/blog/2019/08/06/stack-overflow-how-we-do-app-caching/ - hope that helps the curious finding this.

Closing out just to cleanup!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants