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

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

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 中使用性能分析

自定义一个分析器

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

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

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);
  • 以上示例会为每个异步上下文自动创建一个性能分析会话(如果上下文拥有会话,则会重用该会话)。
  • 在某些单元测试中,可以使用以下代码来获取执行的操作和耗时:

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

例2

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

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

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:

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

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 了,注册方式如下:

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

常言道:学然后知不足,教然后知困。

我知道你的焦虑,一起共进加油:P

关不关注都无所谓,会根据生活节奏紧凑都定期分享些开发经验、搬砖生涯、痛点、感悟。

欢迎关注我的订阅号:P