与基准测试相比,StackExchange redis 客户端非常慢
StackExchange redis client very slow compared to benchmark tests
我正在使用 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 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);
}
}
这使用以下 class 通过 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 测试数据快得多吗?
谢谢,
保罗
您正在以同步方式获取数据(并行 50 个客户端,但每个客户端的请求是同步而不是异步发出的)
一种选择是使用 async/await 方法(StackExchange.Redis 支持)。
如果您需要一次获取多个键(例如,假设您保存了每天键的访问者计数器,则构建网站访问者的每日图表)那么您应该尝试使用 redis pipelining,这应该会给你带来更好的性能。
我从下面的代码中得到的结果:
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 bytes payload
接下来,您需要查看并行性:
50 parallel clients
不清楚您的测试是否是并行的,但如果不是,我们应该绝对期望看到更少的原始吞吐量。方便的是,SE.Redis 被设计成易于并行化:你可以启动多个线程与 同一个连接 对话(这实际上也有避免数据包碎片的优点,因为您最终可能会在每个数据包中收到多条消息,而单线程同步方法保证每个数据包最多使用一条消息。
最后,我们需要了解列出的基准测试在做什么。它在做什么:
(send, receive) x n
或者正在做
send x n, receive separately until all n are received
?两种选择都是可能的。您的 sync API 用法是第一个,但第二个测试同样定义明确,据我所知:这就是它所测量的。有两种模拟第二个设置的方法:
- 使用 "fire and forget" 标志发送前 (n-1) 条消息,因此您实际上 等待最后一条
- 对所有消息使用
*Async
API,仅 Wait()
或 await
最后 Task
这是我在上面使用的基准,它显示了 "sync per op"(通过同步 API)和 "pipeline per thread"(使用 *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");
}
}
}
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
我正在使用 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 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);
}
}
这使用以下 class 通过 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 测试数据快得多吗?
谢谢, 保罗
您正在以同步方式获取数据(并行 50 个客户端,但每个客户端的请求是同步而不是异步发出的)
一种选择是使用 async/await 方法(StackExchange.Redis 支持)。
如果您需要一次获取多个键(例如,假设您保存了每天键的访问者计数器,则构建网站访问者的每日图表)那么您应该尝试使用 redis pipelining,这应该会给你带来更好的性能。
我从下面的代码中得到的结果:
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 bytes payload
接下来,您需要查看并行性:
50 parallel clients
不清楚您的测试是否是并行的,但如果不是,我们应该绝对期望看到更少的原始吞吐量。方便的是,SE.Redis 被设计成易于并行化:你可以启动多个线程与 同一个连接 对话(这实际上也有避免数据包碎片的优点,因为您最终可能会在每个数据包中收到多条消息,而单线程同步方法保证每个数据包最多使用一条消息。
最后,我们需要了解列出的基准测试在做什么。它在做什么:
(send, receive) x n
或者正在做
send x n, receive separately until all n are received
?两种选择都是可能的。您的 sync API 用法是第一个,但第二个测试同样定义明确,据我所知:这就是它所测量的。有两种模拟第二个设置的方法:
- 使用 "fire and forget" 标志发送前 (n-1) 条消息,因此您实际上 等待最后一条
- 对所有消息使用
*Async
API,仅Wait()
或await
最后Task
这是我在上面使用的基准,它显示了 "sync per op"(通过同步 API)和 "pipeline per thread"(使用 *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");
}
}
}
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