C# · 12月 28, 2021

c# – CPU未充分利用.由于阻塞I / O?

我试图找到哪里是C#服务器应用程序的瓶颈,这个应用程序利用了cpu.我认为这可能是由于磁盘I / O性能不佳,与应用程序本身无关,但是我无法想出这个假设.

应用程序从本地MSMQ队列读取消息,对每个消息执行一些处理,并在处理消息后,将响应消息发送到另一个本地MSMQ队列.

我正在使用异步循环来读取队列中的消息,尽可能快地发出消息,并使用Task.Run调度它们进行处理,以启动每个消息的处理(而不是等待这个Task.Run ..只是附加一个连续只有错误才能记录错误).每个消息被并发处理,即在处理下一个消息之前不需要等待消息被完全处理.

在消息处理结束时,我使用MessageQueue的Send方法(以某种方式异步但不是真的因为在返回之前必须等待磁盘写入–see System.Messaging – why MessageQueue does not offer an asynchronous version of Send).

对于基准测试,我排队队列中的100K消息(100K消息总共大约100MB),然后启动该程序.在我的两台个人电脑(一台SSD HD,另一款采用i7 cpu quadcores -8逻辑处理器)上,我在程序生命周期内达到〜95%的cpu使用率(出列100K消息,处理它们和发送回复).消息可以尽可能快地出现,尽可能快地处理(这里涉及cpu),然后响应发送到不同本地队列的每个消息.

现在在运行非HT双核cpu的虚拟机上(不知道什么是底层磁盘,但在基准测试中似乎远低于矿山的性能),Perfmon可以看到平均磁盘秒/写在10-15毫秒VM,而在我的个人机器上是2ms的)当我运行相同的台式机时,我只能达到〜55%的cpu(当我在机器上运行相同的台式机而不发送响应消息到队列时,我达到〜90%的cpu ).

我真的不明白这里有什么问题.似乎很清楚,发送消息到队列是问题,并减慢了程序的全局处理(并且要处理的消息的出队),但是为什么会考虑我正在使用Task.Run来启动每个出队消息的处理并最终响应发送,我不会指望cpu未充分利用.除非一个线程发送消息,否则阻止其他线程在等待返回(磁盘写入)的同一个核心上运行,在这种情况下,考虑到等待时间远远高于我的个人计算机,这可能是有意义的,但是线程等待I / O不应阻止其他线程运行.

我真的想了解为什么我没有达到这台机器上至少95%的cpu使用率.我盲目地说这是由于磁盘I / O性能较差,但是我仍然不明白为什么会导致cpu利用率不足,因为我正在使用Task.Run同时运行处理.它也可能是与磁盘完全无关的一些系统问题,但考虑到MessageQueue.Send似乎是问题,并且该方法最终将消息写入内存映射文件磁盘,我看不到性能问题可能来自其他比磁盘.

当然这是一个系统性能问题,因为该程序可以最大限度地在我自己的计算机上使用cpu,但是我需要找到VM系统上的瓶颈,以及为什么它会影响我的应用程序的并发性/速度.

任何想法 ?

解决方法 为了检查光盘和/或cpu的利用率差,只有一个工具:Windows Performance Toolkit.有关如何使用它的示例,请参阅 here.
您应该从Windows 8.1 SDK(需要.NET 4.5.1)中获得最新的功能,它可以为您提供最多的功能,但Windows 8 SDK中的功能也很好.

您可以获得图表%cpu利用率和%光盘利用率.如果任一个在100%,另一个是低,那么你已经找到了瓶颈.由于它是一个系统范围的分析器,您可以检查msmq服务是否严重使用光盘,或者您或其他人(例如病毒扫描程序是常见问题).

你可以直接访问你的调用堆栈,并检查哪个进程和线程唤醒你的工作线程,这应该是全速运行的.然后,您可以跳转到准备好的线程并处理并检查它在做好线程之前做了什么.这样你就可以直接验证什么是阻碍它的时间.

没有更多的猜测.你真的可以看到系统正在做什么.

要分析进一步启用cpu使用精确度查看以下列:

> NewProcess
> NewThreadId
> NewThreadStack(框架标签)
> ReadyingProcess
> ReadyingThreadId
准备(我们)总和
等等(我们)总和
>等(我们)
> cpu占用率

然后在进程中向下钻取一个调用堆栈,以查看哪里高等待(我们)的时间确实发生在应该以全速运行的线程中.您可以深入到一个单一的事件,直到你不再进一步.然后,您将在Reading Process和ReadyingThreadId中看到值.转到该进程/线程(它可以是您自己的)并重复该过程,直到最终导致某些阻塞操作涉及到磁盘IO或睡眠或长时间运行的设备驱动程序调用(例如病毒扫描程序或vm驱动程序).