与基准测试相比,StackExchange redis 客户端非常慢

本文关键字:redis 客户端 非常 StackExchange 基准测试 | 更新日期: 2023-09-27 18:36:42

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

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

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 个集合/获取:

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);
     }
}

这将使用以下类通过堆栈交换客户端执行 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 测试数字快得多?

谢谢保罗

与基准测试相比,StackExchange redis 客户端非常慢

我从下面的代码中得到的结果:

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");
        }
    }
}

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

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

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

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

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