关于c#:.NET任务的性能指标/诊断

Performance Metrics/Diagnostics of .NET Tasks

有没有办法从.NET获取数据(C#5或更新,所以后异步/等待)执行等待执行的任务,以及类似的指标,用于诊断生产服务器发生的问题?

我正在讨论的案例是一个异步全能的系统(例如一个大规模并行套接字服务器,其中每个请求从一开始就是异步运行),其中初始任务产生多个任务,每个任务都需要时间来处理(或者每个启动更多任务),或者生成一些任务阻塞的任务(如第三方代码),其中一些正确地处理异步。我看到有两种情况难以有效诊断:

  • 在正常负载下,一切正常,但如果有足够的请求,那么CPU会很快跳到100%并且所有请求都会变得越来越慢。当负载减少时,CPU将保持100%,直到大多数待处理任务逐渐完成,然后CPU降至正常水平。
  • 在正常负载下,一切正常,但如果有足够的请求,那么某些请求(所有这些都是正确的异步)都不再完成或非常慢。当负载减少时,CPU将在处理完毕后保持100%,但任务完成率会出现速度颠簸,并且在短时间内会大幅减速。

我已经尝试为此编写一个简单的测试,但没有明显的方法来限制执行程序的数量和我需要创建的任务数来测试它使得解析信息非常困难。通过尝试注销调试信息也很难不干扰测试本身。我会继续尝试创建一个更好的测试用例,并在需要时修改我的问题。

根据我对问题和异步任务系统的理解,这两者实际上都是对实际运行任务的执行程序的争用。

第一种情况发生是因为创建的任务比实际完成的更多,在这种情况下,即使在负载足够高以锁定服务之前,待处理任务的计数器也可用于诊断它。

第二种情况发生是因为一组任务足够长而不会随着时间的推移而产生(有足够的负载)所有执行程序最终同时运行这些任务。一旦完成,它将处理一些任务,但很快就会被另一个长期运行的任务所取代。在这种情况下,挂起的任务计数器将是有用的,以及一些其他指标。

有什么类型可用,或者是否有一些未记录/ hacky方法将一些代码移植到应用程序中启动的每个任务的开始/结束,以使其注销/测量这些内容并在任务编号时抛出警告正在爆炸?


您可以从EventListener继承一个类来处理Task Parallel Library生成的事件。也许,您可以用ConcurrentDictionary这种方式计算排队和运行的任务并存储与任务相关的分析信息。但是,存在一些复杂性,例如任务ID的非唯一性或此分析的性能影响。

示例实现:

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
public class TplEventListener : EventListener
{
    static readonly Guid _tplSourceGuid = new Guid("2e5dba47-a3d2-4d16-8ee0-6671ffdcd7b5");
    readonly EventLevel _handledEventsLevel;

    public TplEventListener(EventLevel handledEventsLevel)
    {
        _handledEventsLevel = handledEventsLevel;
    }

    protected override void OnEventSourceCreated(EventSource eventSource)
    {
        if (eventSource.Guid == _tplSourceGuid)
            EnableEvents(eventSource, _handledEventsLevel);
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        if (eventData.EventSource.Guid != _tplSourceGuid)
            return;

        switch (eventData.EventId)
        {
            // TODO: Add case for each relevant EventId (such as TASKSCHEDULED_ID and TASKWAITBEGIN_ID)
            // and explore relevant data (such as task Id) in eventData.Payload. Payload is described by
            // eventData.PayloadNames.
            // For event ids and payload meaning explore TplEtwProvider source code
            // (https://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/TPLETWProvider.cs,183).
            default:
                var message = new StringBuilder();
                message.Append(eventData.EventName);
                message.Append("(");
                message.Append(eventData.EventId);
                message.Append(") {");
                if (!string.IsNullOrEmpty(eventData.Message))
                {
                    message.Append("Message = "");
                    message.AppendFormat(eventData.Message, eventData.Payload.ToArray());
                    message.Append("
",");
                }
                for (var i = 0; i < eventData.Payload.Count; ++i)
                {
                    message.Append(eventData.PayloadNames[i]);
                    message.Append(" =");
                    message.Append(eventData.Payload[i]);
                    message.Append(",");
                }
                message[message.Length - 2] = ' ';
                message[message.Length - 1] = '}';
                Console.WriteLine(message);
                break;
        }
    }
}

在每个AppDomain中初始化并存储new TplEventListener(EventLevel.LogAlways),您将得到类似于的日志:

NewID(26) { TaskID = 1 }
TaskScheduled(7) { Message ="Task 1 scheduled to TaskScheduler 1.", OriginatingTaskSchedulerID = 1, OriginatingTaskID = 0, TaskID = 1, CreatingTaskID = 0, TaskCreationOptions = 8192 }
NewID(26) { TaskID = 2 }
TraceOperationBegin(14) { TaskID = 2, OperationName = Task.ContinueWith: < SendAsync > b__0, RelatedContext = 0 }
TaskStarted(8) { Message ="Task 1 executing.", OriginatingTaskSchedulerID = 1, OriginatingTaskID = 0, TaskID = 1 }
AwaitTaskContinuationScheduled(12) { OriginatingTaskSchedulerID = 1, OriginatingTaskID = 0, ContinuwWithTaskId = 2 }
NewID(26) { TaskID = 3 }
TraceOperationBegin(14) { TaskID = 3, OperationName = Async: < Main > d__3, RelatedContext = 0 }
NewID(26) { TaskID = 4 }
TaskWaitBegin(10) { Message ="Beginning wait (2) on Task 4.", OriginatingTaskSchedulerID = 1, OriginatingTaskID = 0, TaskID = 4, Behavior = 2, ContinueWithTaskID = 3 }
TaskWaitBegin(10) { Message ="Beginning wait (1) on Task 3.", OriginatingTaskSchedulerID = 1, OriginatingTaskID = 0, TaskID = 3, Behavior = 1, ContinueWithTaskID = 0 }
TraceSynchronousWorkBegin(17) { TaskID = 1, Work = 2 }
TraceSynchronousWorkEnd(18) { Work = 2 }
TraceOperationEnd(15) { TaskID = 1, Status = 1 }
RunningContinuation(20) { TaskID = 1, Object = 0 }
TaskCompleted(9) { Message ="Task 1 completed.", OriginatingTaskSchedulerID = 1, OriginatingTaskID = 0, TaskID = 1, IsExceptional = False }

有关更多信息,请检查

  • Andrew Stasyuk撰写的Async Causality Chain Tracking文章
  • Stephen Cleary撰写的关于Task.Id(和TaskScheduler.Id)的文章。
  • 如何在StackOverflow上监听TPL TaskStarted / TaskCompleted ETW事件讨论
  • System.Threading.Tasks.TplEtwProvider源代码


在生产环境中,Metrics.NET库非常方便。您可以检测代码并定期将收集的数据写入本地文件或数据库。在开发环境中,您可以使用Visual Studio分析器来探索CPU和地址空间使用情况。请参阅Stephen Toub撰写的使用Visual Studio 2012的.NET内存分配概要文章。

Metrics.NET wiki的相关摘录:

The Metrics.NET library provides five types of metrics that can be recorded:

  • Meters record the rate at which an event occurs
  • Histograms measure the distribution of values in a stream of data
  • Timers keep a histogram of the duration of a type of event and a meter of the rate of its occurrence
  • Counters 64 bit integers that can be incremented or decremented
  • Gauges instantaneous values

和仪表示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SampleMetrics
{
    private readonly Timer timer = Metric.Timer("Requests", Unit.Requests);
    private readonly Counter counter = Metric.Counter("ConcurrentRequests", Unit.Requests);

    public void Request(int i)
    {
        this.counter.Increment();
        using (this.timer.NewContext()) // measure until disposed
        {
            // do some work
        }
        this.counter.Decrement();
    }
}

有关更多信息,请检查

  • 异步性能:了解异步和等待的成本作者:Stephen Toub
  • 异步/等待高性能服务器应用程序?关于StackOverflow的讨论
  • Coda Hale提供的度量,指标,无处不在的视频


似乎Leonid Vasilyev的回答对你来说已经足够了,但我想分享我的经验,当涉及到任务失败或有时比平时更长时间。

这里最大的罪魁祸首是**** Context Switching ****,启动的任务越多,CPU就越需要跟踪上下文。 相信我,CPU执行100个重任务比100个轻量级任务更有效率。

对我来说,诀窍是根据提交的请求(我们使用消息队列)分析负载模式,并根据模式保持最佳位置。我还在每个任务结束时进行手动ForceGC收集。

我知道你正在寻找帮助分析的工具,我认为这可能会有所帮助。 我相信对于这类问题,最好先关注传入流量,而不是分析我们对流量做了什么。


据我所知,在调用任何await之前,我们是否可以使用方法名称,datatime和sessionID添加某种类型的日志记录(首选DBlogging)。在await语句之后清除记录,当await完成时,记录被删除(插入/更新记录等待时间)。因此,实时可以分析等待方法的会话数。我希望这有效。我尝试过只使用txt文件创建和删除它并且有效。


最简单的方法是创建Task.RunTask.Start等替代方案,1)调用实际的run / start和2)记录ConcurrentDictionary中的信息(例如任务本身,线程等) 。)

实际上,您正在编写一个专门的探查器:

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
public static class TaskExtensions
{
    Task Run(
        Action action,
        [CallerMemberName] string caller = null,
        [CallerFilePath] string callerFile = null,
        [CallerLineNumber] int lineNumber = 0)
    {
        tasks.Add(new Entry { ... });
        return Task.Run(action);
    }

    // etc.
}

class Entry
{
    Task Task { get; set; }
    string CallerMemberName { get; set; }
    string CallerFilePath { get; set; }
    string CallerFileNumber { get; set; }
    int ThreadId { get; set; }
    DateTime Started { get; set; }
    DateTime Stopped { get; set; }
}

var tasks = new ConcurrentDictionary<string, Entry>();

我们使用调用者信息为我们提供唯一密钥,因为不保证任务ID是唯一的。

在另一个线程或任务中,迭代检查其状态(IsCompletedIsFaultedIsCanceled)的所有任务并记录指标,例如它们运行了多长时间等。

您可能希望轮询之间有一个短暂的延迟,因此这会限制您在指标精度方面的延迟持续时间。由于民意调查是在他们自己的任务中运行的,因此您的主线代码不应受到太大影响,您应该能够了解正在发生的事情。

顺便说一句,既然你提到了套接字,你可能会遇到套接字进入TIME_WAIT的情况。当发生这种情况时,您可能会遇到您正在谈论的缓慢下降。我以前见过这个,这肯定是在负载下发生的。

为了满足不将指标记录投入生产的需要,在指标代码周围的较低(测试)环境中使用编译器指令,并为这些环境创建具有该指令的构建配置。