StackExchange.Redis 系列 10:Profiling | 性能分析

  • 本系列博文是“伪”官方文档翻译,并非完全将官方文档进行翻译,而是我在查阅、测试原始文档并转换为自己东西后进行的“准”翻译。

  • 原始文档见此:https://stackexchange.github.io/StackExchange.Redis/

  • 本系列本博文基于 redis 5.0.6,系列中部分博文跟官方文档有出入,有不同见解 / 说明不当的地方,还请大家不吝拍砖。

StackExchange.Redis公开了一些方法和类型来启用性能分析。

性能分析接口由以下几个接口和方法组成:

  • IProfiler

  • ConnectionMultiplexer.RegisterProfiler(IProfiler)

  • ConnectionMultiplexer.BeginProfiling(object)

  • ConnectionMultiplexer.FinishProfiling(object),

  • IProfiledCommand

你可以通过 ConnectionMultiplexer 来注册一个 IProfiler 实例,无法被更改。

您可以通过调用 BeginProfiling(object) 方法对给定的上下文(如线程,Http Request等)进行概要分析,并通过调用 FinishProfiling(object) 方法来完成概要分析。

FinishProfiling(object) 返回 IProfiledCommands 的集合,其中包含所有发送给 Redis 的命令的时间信息。

  • 这里的 Context 依赖于特定的应用程序。

StackExchange.Redis 目前提供的性能计时器信息包括:

  • Redis 服务器信息。

  • 被查询的 Redis 数据库信息。

  • 执行的 redis 命令。

  • 路由命令的标记信息。

  • 所执行命令的初始化连接所用时间。

  • 命令入队所耗时间。

  • 命令入队之后再发送命令所耗的时间。

  • 从命令发送后,从 redis 服务器接收到响应所耗的时间。

  • 接收到响应后,处理响应所耗的时间。

  • 如果命令是响应集群 ASK 或 MOVED 响应,则获取原始命令是什么。

通过 BeginProfiling(object),FinishProfiling(object) 和 IProfiler.GetContext() 进行性能分析

因为 StackExchange.Redis 采用的是异步接口,因此在性能分析的时候,需要外部协助才能将相关命令组合在一起:这里可以在调用 BeginProfiling(object),FinishProfiling(object) 和 IProfiler.GetContext() 方法的时候,使用上下文对象来实现。

测试用例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class ToyProfiler : IProfiler
{
public ConcurrentDictionary<Thread, object> Contexts = new ConcurrentDictionary<Thread, object>();

public object GetContext()
{
object ctx;
if(!Contexts.TryGetValue(Thread.CurrentThread, out ctx)) ctx = null;

return ctx;
}
}

// ...

ConnectionMultiplexer conn = /* initialization */;
var profiler = new ToyProfiler();
var thisGroupContext = new object();

conn.RegisterProfiler(profiler);

var threads = new List<Thread>();

for (var i = 0; i < 16; i++)
{
var db = conn.GetDatabase(i);

var thread =
new Thread(
delegate()
{
var threadTasks = new List<Task>();

for (var j = 0; j < 1000; j++)
{
var task = db.StringSetAsync("" + j, "" + j);
threadTasks.Add(task);
}

Task.WaitAll(threadTasks.ToArray());
}
);

profiler.Contexts[thread] = thisGroupContext;

threads.Add(thread);
}

conn.BeginProfiling(thisGroupContext);

threads.ForEach(thread => thread.Start());
threads.ForEach(thread => thread.Join());

IEnumerable<IProfiledCommand> timings = conn.FinishProfiling(thisGroupContext);
  • 以上例子将包含 16000 个 IProfiledCommand 对象—每个命令一个。

测试用例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
ConnectionMultiplexer conn = /* initialization */;
var profiler = new ToyProfiler();

conn.RegisterProfiler(profiler);

var threads = new List<Thread>();

var perThreadTimings = new ConcurrentDictionary<Thread, List<IProfiledCommand>>();

for (var i = 0; i < 16; i++)
{
var db = conn.GetDatabase(i);

var thread =
new Thread(
delegate()
{
var threadTasks = new List<Task>();

conn.BeginProfiling(Thread.CurrentThread);

for (var j = 0; j < 1000; j++)
{
var task = db.StringSetAsync("" + j, "" + j);
threadTasks.Add(task);
}

Task.WaitAll(threadTasks.ToArray());

perThreadTimings[Thread.CurrentThread] = conn.FinishProfiling(Thread.CurrentThread).ToList();
}
);

profiler.Contexts[thread] = thread;

threads.Add(thread);
}

threads.ForEach(thread => thread.Start());
threads.ForEach(thread => thread.Join());
  • perThreadTimings 最终将带有 16 个条目,每个条目包含 1000 个 IProfilingCommand,由发出它们的线程键入。

在 MVC5 中使用性能分析

自定义一个分析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RedisProfiler : IProfiler
{
const string RequestContextKey = "RequestProfilingContext";

public object GetContext()
{
var ctx = HttpContext.Current;
if (ctx == null) return null;

return ctx.Items[RequestContextKey];
}

public object CreateContextForCurrentRequest()
{
var ctx = HttpContext.Current;
if (ctx == null) return null;

object ret;
ctx.Items[RequestContextKey] = ret = new object();

return ret;
}
}

在 Global.ascx.cs 中调用 RedisProfiler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected void Application_BeginRequest()
{
var ctxObj = RedisProfiler.CreateContextForCurrentRequest();
if (ctxObj != null)
{
RedisConnection.BeginProfiling(ctxObj);
}
}

protected void Application_EndRequest()
{
var ctxObj = RedisProfiler.GetContext();
if (ctxObj != null)
{
var timings = RedisConnection.FinishProfiling(ctxObj);

// do what you will with `timings` here
}
}
  • 以上代码会将所有的 redis 命令(包括同步和异步方法)与初始划它们的 http 请求 进行分组。

通过ProfilingSession, ConnectionMultiplexer.RegisterProfiler(Func), ProfilingSession.FinishProfiling() 和 IProfiledCommand 进行性能分析

您需要注册一个回调(Func ),该回调函数为 ProfilingSession 提供了一个 ConnectionMultiplexer 实例。

必要时,StackExchange.Redis 会调用该回调函数,如果返回非空,则会将此操作附加到 session 中。

在特定性能分析会话上调用 FinishProfiling 将会返回一个 IProfiledCommands 的集合,其中包含 ConnectionMultiplexer 发送到 Redis 的所有命令的耗时信息。

目前提供的时间信息见上方(已罗列出)。

例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class AsyncLocalProfiler
{
private readonly AsyncLocal<ProfilingSession> perThreadSession = new AsyncLocal<ProfilingSession>();

public ProfilingSession GetSession()
{
var val = perThreadSession.Value;
if (val == null)
{
perThreadSession.Value = val = new ProfilingSession();
}
return val;
}
}
...
var profiler = new AsyncLocalProfiler();
multiplexer.RegisterProfiler(profiler.GetSession);
  • 以上示例会为每个异步上下文自动创建一个性能分析会话(如果上下文拥有会话,则会重用该会话)。

  • 在某些单元测试中,可以使用以下代码来获取执行的操作和耗时:

1
var commands = profiler.GetSession().FinishProfiling();

例2

将不同线程发出的命令关联到一起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class ToyProfiler
{
// note this won't work over "await" boundaries; "AsyncLocal" would be necessary there
private readonly ThreadLocal<ProfilingSession> perThreadSession = new ThreadLocal<ProfilingSession>();
public ProfilingSession PerThreadSession
{
get => perThreadSession.Value;
set => perThreadSession.Value = value;
}
}

// ...

ConnectionMultiplexer conn = /* initialization */;
var profiler = new ToyProfiler();
var sharedSession = new ProfilingSession();

conn.RegisterProfiler(() => profiler.PerThreadSession);

var threads = new List<Thread>();

for (var i = 0; i < 16; i++)
{
var db = conn.GetDatabase(i);

var thread =
new Thread(
delegate()
{
// set each thread to share a session
profiler.PerThreadSession = sharedSession;

var threadTasks = new List<Task>();

for (var j = 0; j < 1000; j++)
{
var task = db.StringSetAsync("" + j, "" + j);
threadTasks.Add(task);
}

Task.WaitAll(threadTasks.ToArray());
}
);

threads.Add(thread);
}

threads.ForEach(thread => thread.Start());
threads.ForEach(thread => thread.Join());

var timings = sharedSession.FinishProfiling();
  • 以上示例最终将包含 16000 个 IProfiledCommand 对象,每个命令一个。

例3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
ConnectionMultiplexer conn = /* initialization */;
var profiler = new ToyProfiler();

conn.RegisterProfiler(() => profiler.PerThreadSession);

var threads = new List<Thread>();

var perThreadTimings = new ConcurrentDictionary<Thread, List<IProfiledCommand>>();

for (var i = 0; i < 16; i++)
{
var db = conn.GetDatabase(i);

var thread =
new Thread(
delegate()
{
var threadTasks = new List<Task>();
profiler.PerThreadSession = new ProfilingSession();

for (var j = 0; j < 1000; j++)
{
var task = db.StringSetAsync("" + j, "" + j);
threadTasks.Add(task);
}

Task.WaitAll(threadTasks.ToArray());

perThreadTimings[Thread.CurrentThread] = profiler.PerThreadSession.FinishProfiling().ToList();
}
);
threads.Add(thread);
}

threads.ForEach(thread => thread.Start());
threads.ForEach(thread => thread.Join());
  • 以上示例中,perThreadTimings 最终将会创建 16 个条目,每个条目 1000 个 IProfilingCommand ,由发出它们的线程键入。

在 MVC5 中使用性能分析

首先针对 ConnectionMultiplexer 注册一个 IProfiler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RedisProfiler
{
const string RequestContextKey = "RequestProfilingContext";

public ProfilingSession GetSession()
{
var ctx = HttpContext.Current;
if (ctx == null) return null;

return (ProfilingSession)ctx.Items[RequestContextKey];
}

public void CreateSessionForCurrentRequest()
{
var ctx = HttpContext.Current;
if (ctx != null)
{
ctx.Items[RequestContextKey] = new ProfilingSession();
}
}
}

在 Global.ascx.cs 中调用 RedisProfiler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void Application_BeginRequest()
{
//_redisProfiler 是 RedisProfiler 的一个实例
_redisProfiler.CreateSessionForCurrentRequest();
}

protected void Application_EndRequest()
{
var session = _redisProfiler.GetSession();
if (session != null)
{
var timings = session.FinishProfiling();

// do what you will with `timings` here
}
}
  • 需要确保该连接创建的时候已经注册 Profiler 了,注册方式如下:

1
connection.RegisterProfiler(() => _redisProfiler.GetSession());
  • 以上代码会将所有的 redis 命令(包括同步和异步方法)与初始划它们的 http 请求 进行分组。