服务器CPU负载过重或线程池繁忙时WCF可靠会话故障
在WCF可靠会话中似乎有一个设计缺陷,当服务器处于高CPU负载(80-100%范围)或没有立即IO线程池线程可用来处理消息时,它会阻止基础设施保持活动消息的发布或接受。症状表现为由于可靠的会话不活动超时而导致随机通道中断。然而,中止逻辑似乎以更高的优先级或通过不同的机制运行,因为中止计时器似乎被触发,即使keep-alive计时器不能运行。
深入研究引用源,可以发现ChannelReliableSession似乎使用了一个InterruptableTimer类来处理inactivityTimer。作为响应,它触发由ReliableOutputSessionChannel设置的PollingCallback,它创建ACKRequestedMessage并将其发送到远程端点。InactivityTimer使用WCF内部的IOThreadTimer/IOThreadScheduler来调度自己。这取决于一个可用的(非繁忙的)IO ThreadPool线程来服务计时器。如果CPU负载很高,似乎线程池不会生成新线程。因此,如果几个线程正在执行(在我的4核机器上似乎是8个线程;15秒的非活动超时(7将中止并失败),那么没有线程可用来发送keep-alive。但是,如果您将客户端上的可靠会话非活动超时修改为比服务器长,即使在这些条件下,服务器仍然会单方面中止通道,因为它期望在更短的时间内收到消息。因此,中止逻辑似乎以更高的优先级运行,或者将异常抛出到其中一个执行线程中(不确定是哪个);我预计服务器上的中止会因为高CPU而延迟,并且客户端的超时时间更长,但事实并非如此。如果CPU负载较低,那么即使并发调用需要30-90秒才能返回,这种情况也完全可以正常工作。
无关你的InstanceMode是什么,最大并发连接,会话,或实例是什么,任何其他超时值是什么(除了receivetimeout必须大于inactivityTimeout)。这完全是WCF实现的设计缺陷;它应该使用一个隔离的高优先级或实时线程来服务keep-alive消息,这样就不会产生虚假的中止。
简短的版本是:只要CPU负载保持较低,我可以发出1000个并发请求,这些请求需要60秒才能完成,并且有15秒的可靠会话非活动超时,没有问题。一旦CPU负载变得沉重,调用将随机开始中止,包括不占用任何CPU时间的调用或等待使用的双工会话空闲。如果传入调用也增加了CPU负载,那么服务将进入死亡螺旋,因为执行时间浪费在保证中止的请求上,而其他请求则位于入站队列中。在所有请求停止、所有正在运行的线程完成以及CPU负载下降之前,服务无法返回到正常状态。这种行为似乎自相矛盾地使可靠会话成为最不可靠的通信机制之一。
同样的行为也适用于客户端;在这种情况下,WCF客户端可能会受到机器上其他进程的摆布,但在高CPU负载下,它将随机中止可靠会话,除非所有操作花费的时间少于inactivityTimeout来完成,尽管如果您不快速发出新的调用,WCF可能仍然无法发送keep-alive并且通道可能出现故障。
记录我的答案:
如果使用ThreadPool,可以稍微缓解这个问题。SetMinThreads(X, Y),其中Y是大于执行并发WCF请求的线程数的某个数字。然后可能会有一个线程可用来为keep-alive服务,并且可靠的会话可能不会超时,即使在持续的100% CPU负载下也是如此,但这也有其局限性。在我的测试中,我将IO线程从最少2个增加到20个,然后发出大量并发(但不做任何事情的请求,只是休眠10秒)。在那之后,我重新运行了我的客户端,但是使用了cpu浪费调用,并且我能够成功地同时执行所有8个。由于线程池的延迟初始化,重新启动服务然后立即执行相同的客户端测试失败。我最终在14个同时调用时再次遇到超时(10个调用被中止),这可能仅仅是因为调度器没有获得足够的CPU片来正确执行。我怀疑如果你能抓住IO线程并提高它们的优先级,你可能能够解决这个问题。
++由于池使用延迟初始化,您必须从客户端发出足够的并发调用,这些调用需要时间来完成,但不使用任何CPU(例如:Thread.Sleep(5000))来强制池创建最小的线程数,而不触发高CPU块-新线程逻辑,否则不会创建最小的线程数,问题仍然发生。
另一个可能的修复方法是将inactivityTimeout设置为一个非常大的值。这将有助于缓解问题,但引入了一个新的拒绝服务漏洞,甚至从客户端意外失败关闭连接。
否则,目前这个问题似乎没有修复;由于这个缺陷,我个人建议不要使用可靠会话,因为它使中止在中止的连接和中止开始发生的情况下都是随机的。