性能分析

StackExchange.Redis 公开了少量的方法和类型来开启性能分析。由于其异步性和多路复用行为,性能分析是一个有点复杂的话题。

接口

性能分析接口是由这些组成的:IProfiler,ConnectionMultiplexer.RegisterProfiler(IProfiler),ConnectionMultiplexer.BeginProfiling(object), ConnectionMultiplexer.FinishProfiling(object) 还有 IProfiledCommand

你可以用 ConnectionMultiplexer 的实例来注册一个 IProfiler 接口,注册后它不能被更改。通过调用 BeginProfiling(object) 方法开始分析一个给定的上下文(例如:Thread,HttpRequest等等),然后调用 FinishProfiling(object) 方法完成分析;FinishProfiling(object) 方法返回一个 IProfiledCommand 的集合,该集合包含计时信息的所有命令都被发送到Redis;使用给定的上下文参数,通过已配置的 ConnectionMultiplexer 对像来调用 (Begin|Finish)Profiling (也就是BeginProfiling & FinishProfiling) 方法。

在具体的应用中什么样的 “上下文” 对象应该使用。

Available Timings

StackExchange.Redis公共的信息有:

  • 涉及的Redis服务器
  • 对Redis数据库的查询
  • 运行的Redis命令
  • 路由命令的使用标志
  • 命令的初始化创建时间
  • 用了多长时间来排队命令
  • 在排队之后,用了多长时间来发送命令
  • 在命令被发送后,用了多长时间接受来自Redis的响应
  • 在接受响应后,用了多长时间来处理响应
  • 如果命令被发送以回应一个集群 ASK 或 MOVED 的响应
    • 如果这样,那么原始的命令的 TimeSpan 是高精确度的, 如果运行时支持。DateTimeDateTime.UtcNow 精确度是一样的。

选择上下文

由于StackExchange.Redis的异步接口,分析需要外部协助来组织相关的命令。开始分析和结束分析都是通过给定的上下文对象来实现的(通过 BeginProfiling(object) & FinishProfiling(object) 方法实现),通过 IProfiler 接口的 GetContext 方法取得上下文对象。

下面是一个从很多不同的线程发出相关命令的示例:

  1. class ToyProfiler : IProfiler
  2. {
  3. public ConcurrentDictionary<Thread, object> Contexts = new ConcurrentDictionary<Thread, object>();
  4. public object GetContext()
  5. {
  6. object ctx;
  7. if(!Contexts.TryGetValue(Thread.CurrentThread, out ctx)) ctx = null;
  8. return ctx;
  9. }
  10. }
  11. // ...
  12. ConnectionMultiplexer conn = /* initialization */;
  13. var profiler = new ToyProfiler();
  14. var thisGroupContext = new object();
  15. //注册实现了IProfiler接口的对象
  16. conn.RegisterProfiler(profiler);
  17. var threads = new List<Thread>();
  18. for (var i = 0; i < 16; i++)
  19. {
  20. var db = conn.GetDatabase(i);
  21. var thread =
  22. new Thread(
  23. delegate()
  24. {
  25. var threadTasks = new List<Task>();
  26. for (var j = 0; j < 1000; j++)
  27. {
  28. var task = db.StringSetAsync("" + j, "" + j);
  29. threadTasks.Add(task);
  30. }
  31. Task.WaitAll(threadTasks.ToArray());
  32. }
  33. );
  34. profiler.Contexts[thread] = thisGroupContext;
  35. threads.Add(thread);
  36. }
  37. //分析开始
  38. conn.BeginProfiling(thisGroupContext);
  39. threads.ForEach(thread => thread.Start());
  40. threads.ForEach(thread => thread.Join());
  41. //分析结束,并且返回了含定时信息的所有命令集合
  42. IEnumerable<IProfiledCommand> timings = conn.FinishProfiling(thisGroupContext);

在结束后,timings 包含了16,000个 IProfiledCommand 对象:每一个命令都会被发送到Redis。

替代方案,你可以按照如下做:

  1. ConnectionMultiplexer conn = /* initialization */;
  2. var profiler = new ToyProfiler();
  3. conn.RegisterProfiler(profiler);
  4. var threads = new List<Thread>();
  5. var perThreadTimings = new ConcurrentDictionary<Thread, List<IProfiledCommand>>();
  6. for (var i = 0; i < 16; i++)
  7. {
  8. var db = conn.GetDatabase(i);
  9. var thread =
  10. new Thread(
  11. delegate()
  12. {
  13. var threadTasks = new List<Task>();
  14. conn.BeginProfiling(Thread.CurrentThread);
  15. for (var j = 0; j < 1000; j++)
  16. {
  17. var task = db.StringSetAsync("" + j, "" + j);
  18. threadTasks.Add(task);
  19. }
  20. Task.WaitAll(threadTasks.ToArray());
  21. perThreadTimings[Thread.CurrentThread] = conn.FinishProfiling(Thread.CurrentThread).ToList();
  22. }
  23. );
  24. profiler.Contexts[thread] = thread;
  25. threads.Add(thread);
  26. }
  27. threads.ForEach(thread => thread.Start());
  28. threads.ForEach(thread => thread.Join());

perThreadTimings 最终会包含16项1,000个 IProfilingCommand 记录,以线程作为键来获取perThreadTimings集合中的值来发送它们。

让我们忘记玩具示例,这里展示的是一个在MVC5应用中配置StackExchange.Redis的示例:

首先注册 IProfiler 接口,而不是 ConnectionMultiplexer

  1. public class RedisProfiler : IProfiler
  2. {
  3. const string RequestContextKey = "RequestProfilingContext";
  4. public object GetContext()
  5. {
  6. var ctx = HttpContext.Current;
  7. if (ctx == null) return null;
  8. return ctx.Items[RequestContextKey];
  9. }
  10. public object CreateContextForCurrentRequest()
  11. {
  12. var ctx = HttpContext.Current;
  13. if (ctx == null) return null;
  14. object ret;
  15. ctx.Items[RequestContextKey] = ret = new object();
  16. return ret;
  17. }
  18. }

那么,添加下面的代码到你的Global.asax.cs文件中:

  1. protected void Application_BeginRequest()
  2. {
  3. var ctxObj = RedisProfiler.CreateContextForCurrentRequest();
  4. if (ctxObj != null)
  5. {
  6. RedisConnection.BeginProfiling(ctxObj);
  7. }
  8. }
  9. protected void Application_EndRequest()
  10. {
  11. var ctxObj = RedisProfiler.GetContext();
  12. if (ctxObj != null)
  13. {
  14. var timings = RedisConnection.FinishProfiling(ctxObj);
  15. // 在这里你可以使用`timings`做你想做的
  16. }
  17. }

这些实现会组织所有的Redis命令,包括 async/await 并随着http请求初始化它们。