【问题标题】:StackExchange redis client very slow compared to benchmark tests与基准测试相比,StackExchange redis 客户端非常慢
【发布时间】:2016-06-12 23:35:30
【问题描述】:

我正在使用 Stackexchange Redis 客户端实现 Redis 缓存层,现在的性能几乎无法使用。

我有一个本地环境,其中 Web 应用程序和 redis 服务器在同一台机器上运行。我对我的 Redis 服务器进行了 Redis 基准测试,结果实际上非常好(我只是在我的文章中包含了 set 和 get 操作):

C:\Program Files\Redis>redis-benchmark -n 100000
====== PING_INLINE ======
  100000 requests completed in 0.88 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1

====== SET ======
  100000 requests completed in 0.89 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1

99.70% <= 1 milliseconds
99.90% <= 2 milliseconds
100.00% <= 3 milliseconds
111982.08 requests per second

====== GET ======
  100000 requests completed in 0.81 seconds
  50 parallel clients
  3 bytes payload
  keep alive: 1

99.87% <= 1 milliseconds
99.98% <= 2 milliseconds
100.00% <= 2 milliseconds
124069.48 requests per second

因此,根据基准,我正在查看每秒超过 100,000 组和 100,000 次获取。我写了一个单元测试来做 300,000 set/gets:

private string redisCacheConn = "localhost:6379,allowAdmin=true,abortConnect=false,ssl=false";


[Fact]
public void PerfTestWriteShortString()
{
    CacheManager cm = new CacheManager(redisCacheConn);

    string svalue = "t";
    string skey = "testtesttest";
    for (int i = 0; i < 300000; i++)
    {
        cm.SaveCache(skey + i, svalue);
        string valRead = cm.ObtainItemFromCacheString(skey + i);
     }

}

这使用以下类通过 Stackexchange 客户端执行 Redis 操作:

using StackExchange.Redis;    

namespace Caching
{
    public class CacheManager:ICacheManager, ICacheManagerReports
    {
        private static string cs;
        private static ConfigurationOptions options;
        private int pageSize = 5000;
        public ICacheSerializer serializer { get; set; }

        public CacheManager(string connectionString)
        {
            serializer = new SerializeJSON();
            cs = connectionString;
            options = ConfigurationOptions.Parse(connectionString);
            options.SyncTimeout = 60000;
        }

        private static readonly Lazy<ConnectionMultiplexer> lazyConnection = new Lazy<ConnectionMultiplexer>(() => ConnectionMultiplexer.Connect(options));
        private static ConnectionMultiplexer Connection => lazyConnection.Value;
        private static IDatabase cache => Connection.GetDatabase();

        public string ObtainItemFromCacheString(string cacheId)
        {
            return cache.StringGet(cacheId);
        }

        public void SaveCache<T>(string cacheId, T cacheEntry, TimeSpan? expiry = null)
        {
            if (IsValueType<T>())
            {
                cache.StringSet(cacheId, cacheEntry.ToString(), expiry);
            }
            else
            {
                cache.StringSet(cacheId, serializer.SerializeObject(cacheEntry), expiry);
            }
        }

        public bool IsValueType<T>()
        {
            return typeof(T).IsValueType || typeof(T) == typeof(string);
        }

    }
}

我的 JSON 序列化器只是使用 Newtonsoft.JSON:

using System.Collections.Generic;
using Newtonsoft.Json;

namespace Caching
{
    public class SerializeJSON:ICacheSerializer
    {
        public string SerializeObject<T>(T cacheEntry)
        {
            return JsonConvert.SerializeObject(cacheEntry, Formatting.None,
                new JsonSerializerSettings()
                {
                    ReferenceLoopHandling = ReferenceLoopHandling.Ignore
                });
        }

        public T DeserializeObject<T>(string data)
        {
            return JsonConvert.DeserializeObject<T>(data, new JsonSerializerSettings()
            {
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore
            });

        }


    }
}

我的测试时间约为 21 秒(300,000 组和 300,000 次获取)。这使我每秒可以进行大约 28,500 次操作(至少比使用基准测试时预期的慢 3 倍)。我正在转换为使用 Redis 的应用程序非常健谈,某些繁重的请求可能会对 Redis 进行大约 200,000 次总操作。显然,我没想到会出现与使用系统运行时缓存相同的时间,但是这种更改后的延迟很明显。我的实现是否有问题?有人知道为什么我的基准测试数据比我的 Stackechange 测试数据快得多吗?

谢谢, 保罗

【问题讨论】:

    标签: c# redis stackexchange.redis


    【解决方案1】:

    以下代码的结果:

    Connecting to server...
    Connected
    PING (sync per op)
        1709ms for 1000000 ops on 50 threads took 1.709594 seconds
        585137 ops/s
    SET (sync per op)
        759ms for 500000 ops on 50 threads took 0.7592914 seconds
        658761 ops/s
    GET (sync per op)
        780ms for 500000 ops on 50 threads took 0.7806102 seconds
        641025 ops/s
    PING (pipelined per thread)
        3751ms for 1000000 ops on 50 threads took 3.7510956 seconds
        266595 ops/s
    SET (pipelined per thread)
        1781ms for 500000 ops on 50 threads took 1.7819831 seconds
        280741 ops/s
    GET (pipelined per thread)
        1977ms for 500000 ops on 50 threads took 1.9772623 seconds
        252908 ops/s
    

    ===

    服务器配置:确保禁用持久性等

    在基准测试中您应该做的第一件事是:对一件事进行基准测试。目前,您包含大量序列化开销,这无助于获得清晰的画面。理想情况下,对于同类基准,您应该使用 3 字节的固定负载,因为:

    3 字节有效负载

    接下来,您需要了解并行性:

    50 个并行客户端

    尚不清楚您的测试是否是并行的,但如果不是,我们应该绝对期望看到更少的原始吞吐量。方便的是,SE.Redis 被设计为易于并行化:您可以启动多个线程与同一个连接进行通信(这实际上还具有避免数据包碎片的优点,因为您最终可能会得到每个数据包多条消息,而单线程同步方法保证每个数据包最多使用一条消息)。

    最后,我们需要了解列出的基准在做什么。是否在做:

    (send, receive) x n
    

    或者它在做什么

    send x n, receive separately until all n are received
    

    ?这两种选择都是可能的。您的同步 API 使用是第一个,但第二个测试同样定义明确,据我所知:这就是它所测量的。有两种方法可以模拟第二种设置:

    • 发送带有“即发即弃”标志的第一条 (n-1) 条消息,因此您只需实际上等待最后一条
    • 对所有消息使用*Async API,并且只使用Wait()await 最后一个Task

    这是我在上面使用的一个基准,它显示了“每个操作同步”(通过同步 API)和“每个线程的管道”(使用 *Async API 并等待每个线程的最后一个任务) ,都使用 50 个线程:

    using StackExchange.Redis;
    using System;
    using System.Diagnostics;
    using System.Threading;
    using System.Threading.Tasks;
    
    static class P
    {
        static void Main()
        {
            Console.WriteLine("Connecting to server...");
            using (var muxer = ConnectionMultiplexer.Connect("127.0.0.1"))
            {
                Console.WriteLine("Connected");
                var db = muxer.GetDatabase();
    
                RedisKey key = "some key";
                byte[] payload = new byte[3];
                new Random(12345).NextBytes(payload);
                RedisValue value = payload;
                DoWork("PING (sync per op)", db, 1000000, 50, x => { x.Ping(); return null; });
                DoWork("SET (sync per op)", db, 500000, 50, x => { x.StringSet(key, value); return null; });
                DoWork("GET (sync per op)", db, 500000, 50, x => { x.StringGet(key); return null; });
    
                DoWork("PING (pipelined per thread)", db, 1000000, 50, x => x.PingAsync());
                DoWork("SET (pipelined per thread)", db, 500000, 50, x => x.StringSetAsync(key, value));
                DoWork("GET (pipelined per thread)", db, 500000, 50, x => x.StringGetAsync(key));
            }
        }
        static void DoWork(string action, IDatabase db, int count, int threads, Func<IDatabase, Task> op)
        {
            object startup = new object(), shutdown = new object();
            int activeThreads = 0, outstandingOps = count;
            Stopwatch sw = default(Stopwatch);
            var threadStart = new ThreadStart(() =>
            {
                lock(startup)
                {
                    if(++activeThreads == threads)
                    {
                        sw = Stopwatch.StartNew();
                        Monitor.PulseAll(startup);
                    }
                    else
                    {
                        Monitor.Wait(startup);
                    }
                }
                Task final = null;
                while (Interlocked.Decrement(ref outstandingOps) >= 0)
                {
                    final = op(db);
                }
                if (final != null) final.Wait();
                lock(shutdown)
                {
                    if (--activeThreads == 0)
                    {
                        sw.Stop();
                        Monitor.PulseAll(shutdown);
                    }
                }
            });
            lock (shutdown)
            {
                for (int i = 0; i < threads; i++)
                {
                    new Thread(threadStart).Start();
                }
                Monitor.Wait(shutdown);
                Console.WriteLine($@"{action}
        {sw.ElapsedMilliseconds}ms for {count} ops on {threads} threads took {sw.Elapsed.TotalSeconds} seconds
        {(count * 1000) / sw.ElapsedMilliseconds} ops/s");
            }
        }
    }
    

    【讨论】:

    • 我想知道为什么流水线产生的吞吐量低于同步处理?
    • @Kobynet 公平问题;在这种特殊情况下可能会产生 TPL 开销;我现在没有时间把它拆开
    • 感谢 Marc,这真的帮了我大忙!我需要将此(旧版)应用程序重构为多线程和异步的,以获得最佳性能。我很抱歉,我粘贴的代码并不清楚,但我在测试用例中避免了所有序列化。你完全正确,我没有比较同类(使用不同的有效负载并且我的单元测试不是并行的)所以我完全明白为什么这些数字如此不同。再次感谢!
    • @MarcGravell 您是否在 x64 发布模式下运行它?对我来说,异步至少会变得更快
    【解决方案2】:

    您正在以同步方式获取数据(50 个客户端并行,但每个客户端的请求是同步而不是异步发出的)

    一种选择是使用 async/await 方法(StackExchange.Redis 支持该方法)。

    如果您需要一次获取多个键(例如,假设您保存了每天的访问者计数器键,则构建网站访问者的每日图表),那么您应该尝试使用 redis pipelining 以异步方式从 redis 获取数据,这应该会给你更好的表现。

    【讨论】:

    • 感谢 Kobynet,感谢您的解释!我正在转换的当前 Web 应用程序非常严格和顺序,因此我可能会采取双重方法,将 System.Runtime 缓存与 Redis 缓存一起使用,这将允许我逐渐将应用程序重构为更加异步(这是无论如何都是最终转换的一部分)。这将使我能够在未来努力实现更好的可扩展性。再次感谢您的帮助!
    【解决方案3】:

    StackExchange redis 客户端旧版本存在性能问题。 升级到最新版本。在这里阅读更多: https://www.gitmemory.com/issue/mgravell/Pipelines.Sockets.Unofficial/28/479932064

    在这篇文章中: https://blog.marcgravell.com/2019/02/fun-with-spiral-of-death.html

    这是 repo 中的问题: https://github.com/StackExchange/StackExchange.Redis/issues/1003

    【讨论】:

      猜你喜欢
      • 2018-03-22
      • 2017-02-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-02-13
      • 2017-05-07
      • 1970-01-01
      相关资源
      最近更新 更多