当服务器处于高 CPU 负载或线程池繁忙时 WCF 可靠会话故障

rus*_*hop 5 c# wcf reliablesession

当服务器处于高 CPU 负载或线程池繁忙时 WCF 可靠会话故障

WCF 可靠会话中似乎存在一个设计缺陷,当服务器处于高 CPU 负载(80-100% 范围)或没有立即可用的 IO 线程池线程时,该缺陷会阻止基础结构保持活动消息的发布或接受来处理消息。由于可靠的会话不活动超时,这些症状表现为明显的随机通道中止。然而,中止逻辑似乎以更高的优先级或通过不同的机制运行,因为即使保持活动计时器无法运行,中止计时器似乎也会触发。

深入研究参考源,似乎 ChannelReliableSession 使用 InterruptableTimer 类来处理 inactivityTimer。作为响应,它触发由 ReliableOutputSessionChannel 设置的 PollingCallback,它创建一个 ACK​​RequestedMessage 并将其发送到远程端点。InactivityTimer 使用 WCF 内部 IOThreadTimer/IOThreadScheduler 来调度自己。这取决于一个可用(非繁忙)IO ThreadPool 线程来为计时器提供服务。如果 CPU 负载很高,则线程池似乎不会产生新线程。因此,如果有多个线程正在执行(在我的 4 核机器上似乎是 8 个线程;如果 15 秒 inactivityTimeout 7 将中止并失败),则没有线程可用于发送保持活动状态。但是,如果您将客户端上的可靠会话不活动超时修改为长于服务器,即使在这些情况下,服务器仍将单方面中止通道,因为它期望在更短的时间内收到消息。因此,中止逻辑似乎以更高的优先级运行或将异常抛出到正在执行的线程之一(不确定是哪个);我预计服务器上的中止会由于 CPU 高而延迟,并且客户端的超时时间最终会达到,但事实并非如此。如果 CPU 负载较低,那么即使并发调用需要 30-90 秒才能返回,这个完全相同的场景也能正常工作。

您的 InstanceMode 是什么、最大并发连接数、会话数或实例数、任何其他超时值(除了 recieveTimeout 必须大于 inactivityTimeout)都无关紧要。这完全是 WCF 实现的设计缺陷;它应该使用一个隔离的高优先级或实时线程来为保持活动的消息提供服务,这样就不会产生虚假的中止。

简短版本是:我可以发出 1000 个并发请求,需要 60 秒才能完成,15 秒可靠会话不活动超时没有问题,只要 CPU 负载保持低。一旦 CPU 负载变重,调用将随机开始中止,包括不占用任何 CPU 时间的调用或空闲等待使用的双工会话如果传入调用也增加了 CPU 负载,那么服务将进入死亡螺旋,因为执行时间浪费在保证中止的请求上,而其他请求则位于入站队列中。在所有请求都停止、所有进行中的线程完成并且 CPU 负载下降之前,服务无法恢复到正常状态。这种行为似乎自相矛盾地使可靠会话成为最不可靠的通信机制之一。

同样的行为也适用于客户端;在这种情况下,WCF 客户端可能会受其他进程的支配,但在高 CPU 负载下,它会随机中止可靠会话,除非所有操作的完成时间都少于 inactivityTimeout,但如果您不发出新调用很快 WCF 可能仍然无法发送 keep-alive,并且通道可能会出现故障。

rus*_*hop 5

记录我的回答:

如果您使用 ThreadPool.SetMinThreads(X, Y),其中 Y 比执行并发 WCF 请求的线程数大一些,您可以稍微缓解这个问题。然后可能有一个线程可用++来为保持活动提供服务,即使在持续的 100% CPU 负载下,可靠会话也可能不会超时,但这也有其局限性。在我的测试中,我将 IO 线程从 2 个最小增加到 20 个,然后发出大量并发(但不做任何请求,只是休眠 10 秒)。之后,我重新运行了我的客户端,但使用了 CPU 浪费调用,并且我能够同时成功执行所有 8 个。由于线程池的延迟初始化,重新启动服务然后立即执行相同的客户端测试失败。遇到这种情况,我最终在 14 个同时调用(10 个调用中止)时再次开始超时,这可能只是调度程序没有获得足够的 CPU 切片来正确执行。我怀疑如果您可以获取 IO 线程并提高它们的优先级,您可能能够解决这个问题。

++因为池使用延迟初始化,您必须从客户端发出足够多的并发调用,这些调用需要时间才能完成但不使用任何 CPU(例如:Thread.Sleep(5000))来强制池创建不触发高CPU-blocks-new-threads逻辑的最小线程数,否则将不会创建最小线程数并且问题仍然存在。

另一个潜在的解决方法是使 inactivityTimeout 成为一个非常大的值。这将有助于缓解问题,但会引入新的拒绝服务漏洞,即使客户端意外失败关闭连接也是如此。

否则目前似乎没有解决此问题的方法;由于这个缺陷,我个人建议不要使用 Reliable Sessions,因为它使中止在连接中止和中止开始发生的情况下都是随机的。