9

我目前正在执行我开发的服务器应用程序的一些基准测试,很大程度上依赖于 C#5 async/await 结构。

这是一个控制台应用程序,因此没有同步上下文,也没有在代码中显式创建线程。应用程序正在尽可能快地从 MSMQ 队列中出列请求(异步出列循环),并在通过 HttpClient 发送处理后的请求之前处理每个请求。

依赖 async/await 的 I/O 从 MSMSQ 出列,读取数据/将数据写入 SQL 服务器数据库,最后在链的末端发送 HttpClient 请求。

目前,对于我的基准测试,数据库是完全伪造的(结果直接通过 Task.FromResult 返回)并且 HttpClient 也是伪造的(等待 0-50 毫秒之间的随机 Task.Delay 并返回响应),所以只有真正的I/O 是从 MSMQ 出列。

通过看到大量时间花在 GC 上,我已经大大提高了应用程序的吞吐量,因此我使用了 CLR Profiler 并找到了可以优化的地方。

我现在正在尝试看看我是否仍然可以提高吞吐量,我认为这是可能的。

有两件事我不明白,也许这背后有一些提高吞吐量的可能性:

1)我有4个CPU核心(实际上只有2个真正的...... i7 CPU),当应用程序运行时,它最多只使用3个CPU核心(在VS2012并发可视化器中我可以清楚地看到只有3个核心是正在使用,在 windows perfmon 中我可以看到 CPU 使用率大约为 75/80%)。知道为什么吗?我无法控制线程,因为我没有明确地创建它们,只依赖于任务,那么为什么任务调度程序在我的情况下不能最大化 CPU 使用率?有人经历过吗?

2)使用 VS2012 并发可视化工具,我可以看到非常长的同步时间(大约 20% 的执行和 80% 的同步)。仅供参考 正在创建大约 15 个线程。

大约 60% 的同步来自以下调用堆栈:

clr.dll!ThreadPoolMgr::WorkerThreadStart
clr.dll!CLRSemaphore::Wait
kernelbase.dll!WaitForSingleObjectEx

clr.dll!ThreadPoolMgr::WorkerThreadStart
clr.dll!ThreadPoolMgr::UnfairSemaphore::Wait
clr.dll!CLRSemaphore::Wait 
kernelbase.dll!WaitForSingleObjectEx

大约 30% 的同步来自:

clr.dll!ThreadPoolMgr::CompletionPortThreadStart
kernel32.dll!GetQueueCompletionStatusStub
kernelbase.dll!GetQueuedCompletionStatus
ntdll.dll!ZwRemoveIoCompletion 
..... blablabla 
ntoskrnl.exe!KeRemoveQueueEx

不知道这样高同步是不是正常。

编辑:根据斯蒂芬的回答,我正在添加有关我的实施的更多细节:

事实上,我的服务器是完全异步的。然而,一些 CPU 工作是为了处理每条消息(我承认不是那么多,但仍然是一些)。从 MSMQ 队列接收到消息后,首先对其进行反序列化(大部分 CPU/内存成本似乎都发生在这一点上),然后经过处理/验证的各个阶段,消耗一些 CPU,最后到达“结束”管道”,其中处理后的消息通过 HttpClient 发送到外部世界。

我的实现不是在从队列中取出下一个消息之前等待消息被完全处理。事实上,我的消息泵,从队列中出列消息,非常简单,并且立即“转发”消息以便能够出列下一个消息。简化的代码如下所示(省略异常管理、取消......):

while (true)
{
    var message = await this.queue.ReceiveNextMessageAsync();
    this.DeserializeDispatchMessageAsync();
}

private async void DeserializeDispatchMessageAsync()
{
    // Immediately yield to avoid blocking the asynchronous messaging pump
    // while deserializing the body which would otherwise impact the throughput.
    await Task.Yield();

    this.messageDispatcher.DispatchAsync(message).ForgetSafely();
}

ReceiveNextMessageAsync是一种使用TaskCompletionSource.NET的自定义方法MessageQueue,在 .NET Framework 4.5 中未提供任何异步方法。所以我只是将BeginReceive/EndReceiveTaskCompletionSource.

这是我的代码中唯一不等待异步方法的地方之一。循环尽可能快地出列。它甚至不等待消息反序列化(当显式访问 Body 属性时,消息反序列化由 Message 的 .NET FCL 实现延迟完成)。我立即执行 Task.Yield() 以将反序列化/消息处理分叉到另一个任务并立即释放循环。

现在,在我的工作台上,正如我之前所说的,所有的 I/O(仅限数据库访问)都是伪造的。所有从数据库中获取数据的异步方法调用都只返回一个带有假数据的 Task.FromResult。在处理消息期间大约有 20 个 DB 调用,它们现在都是伪造的/同步的。唯一的异步点是在消息处理结束时,它通过 HttpClient 发送。HttpClient 发送也是伪造的,但此时我正在执行随机(0-50 毫秒)“等待 Task.Delay”。无论如何,由于数据库的伪造,每个消息处理都可以看作是一个单独的任务。

对于我的工作台,我在队列中存储了大约 30 万条消息,然后我启动了服务器应用程序。它出列非常快,淹没了服务器应用程序,并且所有消息都同时处理。这就是为什么我不明白为什么我没有达到 100% CPU 和 4 个核心,但只使用了 75% 和 3 个核心(同步问题除外)。

当我只出队而不进行任何反序列化或处理消息时(注释掉对我的调用,DeserializeDispatchMessageAsync我达到了大约 20K 消息/秒的吞吐量。当我进行整个处理时,我达到了大约 10K 消息/秒的吞吐量。

消息从队列中快速出列并且消息反序列化+处理是在一个单独的任务中完成的事实使我在脑海中想象了很多任务(每条消息一个)在任务调度程序(这里是线程池......没有同步上下文),所以我希望线程池将所有这些消息发送到最大数量的核心和所有 4 个核心完全忙于处理所有任务,但我似乎不是这样。

无论如何,欢迎任何答案,我正在寻找任何想法/提示。

4

1 回答 1

6

听起来您的服务器几乎是完全异步的(异步 MSMQ、异步 DB、异步 HttpClient)。所以在那种情况下,我不觉得你的结果令人惊讶。

首先,几乎没有 CPU 工作要做。我完全希望每个线程池线程大部分时间都在等待工作。请记住,在自然异步操作期间不使用 CPU。

Task异步 MSMQ/DB/ 操作返回的不在HttpClient线程池线程上执行;它只是代表一个 I/O 操作的完成。您看到的唯一线程池工作是异步方法中的少量同步工作,通常只是为 I/O 安排缓冲区。

就吞吐量而言,您确实有一些扩展空间(假设您的测试正在淹没您现有的服务)。您的代码可能只是(异步)从 MSMQ 检索单个值,然后(异步)在检索另一个值之前对其进行处理;在这种情况下,您肯定会看到不断从 MSMQ 读取数据的改进。请记住,async代码是异步的,但它仍然是序列化的;您的async方法可能会暂停await

如果是这种情况,您可能会受益于设置 TPL 数据流管道(MaxDegreeOfParallelism设置为Unbounded)并运行从 MSMQ 异步读取并将数据推送到管道中的紧密循环。这比自己进行重叠处理更容易。

更新编辑:

我有一些建议:

  1. 使用Task.Run而不是await Task.Yield. Task.Run有更明确的意图。
  2. Begin/ Endwrappers 可以使用Task.Factory.FromAsyncTCS 来代替,这样可以为您提供更简洁的代码。

但我看不出有什么理由不使用最后一个核心——除非有明显的原因,比如分析器或其他应用程序让它忙起来。您最终应该得到的是async等效于动态并行性,这是 .NET 线程池专门设计用于处理的情况之一。

于 2013-07-26T22:45:18.843 回答