WCF 节流
WCF 节流不仅仅是关于节流选项。
引言
WCF 节流对于新手来说可能很奇怪,特别是当不同的设置与各种其他选项以不同的方式交互时,这确实使一切都变得相当混乱。
我通过 Google 搜索了关于 WCF 中节流整个概念的资源。如果你使用 Google, there are some very good pages,可惜 MSDN 绝对 **不是** 其中之一,因为它除了类定义之外几乎没有用处。我决定在一篇文档中概述节流的工作原理,并提供一些示例项目来帮助你理解它。
我假设你已经掌握了基本知识,并且在你的生活中至少创建过一个 WCF 服务。该服务将是一个自托管的 WCF 服务,虽然我将在配置中设置节流,但我还将向你展示如何在代码中进行设置。
理论
配置中有三个设置,以及几个我们将重点关注的 ServiceBehaviour
设置。我没有深入探讨类、属性和类层次结构等的具体细节,而是更容易通过代码来理解它们之间的关系。
1. MaxConcurrentCalls
默认值为 16,16 是默认数量。这是由通道处理的最大消息数。这也受 ServiceBehavior
属性的 ConcurrencyMode
影响。
1.1.1. Single
默认值。服务在一个线程上运行。这确实意味着,无论有多少调用线程,调用都是一次处理一个。
1.1.2. Reentrant
(我从 MSDN 借用了这个描述,因为它几乎说明了一切。) 服务实例是单线程的,并接受重入调用。重入服务在调用另一个服务时接受调用;因此,在你进行外呼之前,你需要负责保持对象状态一致,并且在外呼之后必须确认操作本地数据有效。请注意,服务实例仅在通过 WCF 通道调用另一个服务时才会被解锁。在这种情况下,被调用的服务可以通过回调重新进入第一个服务。如果第一个服务不是重入的,调用序列将导致死锁。
1.1.3. Multiple
多个线程可以同时访问服务,并且你需要自己管理多线程。
2. MaxConcurrentInstances
默认值为 Int32.Max
(对于那些不记得的人来说,可以写成 2147483647。我完全记得。完全记得。)。正如你所见,这是一个 **非常** 大的数字,所以我们实际上不会去调整它。它的行为很大程度上取决于 ServiceBehavior
属性中设置的 InstanceContextMode
的使用情况。
2.1.1. PerSession
默认值。当对服务进行调用时,会为客户端创建一个会话,每个会话都有自己的服务实例。
2.1.2. PerCall
每次调用服务都会导致该服务的新实例,然后在调用完成后将其处置。
2.1.3. Single
只有一个服务实例。非常简单。还要记住 ConcurrencyMode
也会影响 InstanceContextMode
选项的行为。
3. MaxConcurrentSessions
如果启用了会话(并且你使用的绑定支持会话),那么每个代理将有效地有一个会话,并且每次数据库调用直到不活动超时都会在一个会话中完成。如果对单个会话进行了多次调用,那么在排队发生之前允许的最大调用数将是……是的,MaxConcurrentCalls
。
服务和主机
我将不深入探讨此服务的具体细节,它是一个自托管的 WCF 服务,带有一个休眠一秒钟的单个方法,以及一个手工制作的代理。(有关代理的更多信息,请参阅我的另一篇文章 此处。)服务中唯一有点意思的是,你会注意到我们在创建服务时生成一个 Guid
,并将该 Guid
返回给 Ping()
方法的调用者。这意味着当我们重用类的一个实例时,我们会得到与之前相同的 Guid
,但如果类在每次调用时都被处置和创建,则会返回不同的 GUID。
public class Service1 : IService1
{
Guid sessionGuid = Guid.NewGuid();
public Guid Ping()
{
Thread.Sleep(1*1000);
return sessionGuid;
}
}
配置
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<bindings>
<netTcpBinding>
<binding name="tcpBinding" portSharingEnabled="True"/>
</netTcpBinding>
</bindings>
<services>
<service name="WCFThrottling.Service1"
behaviorConfiguration="WCFThrottling.Service1Behavior">
<endpoint address ="net.tcp:///Throttle"
binding="netTcpBinding"
contract="WCFThrottling.IService1"
bindingName="tcpBinding"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="WCFThrottling.Service1Behavior">
<serviceDebug includeExceptionDetailInFaults="true" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
客户端和测试代码
我添加了一个客户端项目,允许我们生成多个线程。每个线程将调用 Ping()
方法并返回一个 GUID,然后将其输出到屏幕。
class Program
{
public static int Threads;
public static int DoneThreads;
public static int ErrorThreads;
static void Main(string[] args)
{
Threads = 20;
DoneThreads = 0;
SpawnThreads(Threads);
Console.ReadLine();
}
public delegate bool ThreadDelegate();
public static void SpawnThreads(int threads)
{
for (int i = 0; i < threads; i++)
{
ThreadDelegate del = new ThreadDelegate(CallProxy);
IAsyncResult result = del.BeginInvoke(delegate(IAsyncResult r)
{ EndCall(del.EndInvoke(r)); }, null);
}
}
public static bool CallProxy()
{
try
{
Console.WriteLine( new Proxy("ThrottleTCP").Ping());
return true;
}
catch (Exception exc)
{
Console.WriteLine(-1);
//Console.WriteLine(exc.Message);//If you want some debugging
return false;
}
}
public static void EndCall(bool result)
{
if (!result)
{
ErrorThreads++;
}
DoneThreads++;
if (DoneThreads == Threads)
{
Console.WriteLine(string.Format("The {0} threads completed " +
"of which {1} failed.", DoneThreads, ErrorThreads));
}
}
}
是的,我确实意识到 DoneThreads
可能会在不正确的时间被递增,并且 (DoneThreads == Threads)
可能永远不会为 true
。这种情况发生的可能性足够小,我并不介意,而且这也不是世界末日。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<bindings>
<netTcpBinding>
<binding name="tcpBinding" sendTimeout="00:00:05"
receiveTimeout="00:00:05"/>
</netTcpBinding>
</bindings>
<client>
<endpoint address="net.tcp:///Throttle" binding="netTcpBinding"
contract="WCFThrottling.IService1"
bindingConfiguration="tcpBinding"
name="ThrottleTCP"/>
</client>
</system.serviceModel>
</configuration>
目前所有与节流相关的设置都保留为默认值(即,未设置)。
简单明了。注意 **非常** 短的超时时间。我们希望服务尽早超时,而我们只调用 Thread.Sleep()
一秒钟。
测试
运行此代码,正如它在不到一秒的时间内生成 20 个线程,为每个线程生成代理,并调用 Ping()
方法。这隐式地为每个代理创建了一个会话,最多为默认的 10 个。前 10 个调用然后成功并返回,而接下来的 10 个调用失败。失败的原因是前 10 个线程分别获得了 10 个可用会话中的一个,接下来的 10 个线程等待会话被释放,但在 5 秒后超时并失败。每个会话只有在显式结束会话或达到 SessionTimeout
值后才会释放。
你还会注意到每个 Guid
都不同,因为每次调用都会创建一个新的服务实例。
解决此问题的最简单方法(假设这是一个问题,你可能出于某种原因希望它按此行为)是增加可能的会话数量。这可以通过在配置中通过将以下行添加到 ServiceBehavior 部分(服务部分,以便清楚)来完成,如下所示
<serviceThrottling maxConcurrentSessions="20"/>
或者,你也可以在服务主机中进行。我现在提到这种方法,并且在其余的示例中,它将放在配置中。哦,选择一种方法,WCF 不喜欢你同时使用两种方法。
ServiceThrottlingBehavior behaviour = new ServiceThrottlingBehavior();
behaviour.MaxConcurrentSessions = 20;
host.Description.Behaviors.Add(behaviour);
完成此操作后,再次运行它,一切都正常工作。
现在,如果你将此行添加到服务中……什么都不会改变。
[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerCall)]
这是因为(我找不到明确说明这一点的内容,但这是唯一有意义的)TCP 通道 **本质上** 是有状态的。你无法将其关闭(尝试将 ServiceContract
属性设置为 SessionMode.NotAllowed
,它将无法编译),因此它只是假装你没有尝试从 PerSession
更改为 PerCall
。我在弄清楚发生了什么事情时,这个地方 **非常** 烦人了一个小时。
在不支持会话的绑定上(例如 basicHttpBinding
),这是默认行为。
现在,在不更改配置的情况下,我们可以更改服务的整个行为。
在此行添加到服务实现上方
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single,
ConcurrencyMode=ConcurrencyMode.Single)]
(请记住 ConcurrencyMode.Single
是默认值。)
现在,当你运行项目时,即使每个线程(来自客户端)都接收自己的会话,也只有一个服务实例在处理每个请求(你会注意到它是按顺序处理的),每次都返回相同的 Guid
。此外,一些以前成功的线程会失败,而另一些以前失败的线程会成功,因为有更多的会话可用。由于只有一个服务在处理 **所有** 请求,Thread.Sleep()
实际上会锁定整个服务,而不仅仅是特定会话的服务(我希望这句话有意义)。
现在,我们将 ConcurrencyMode
更改为 ConcurrencyMode.Multiple
,允许多个线程访问同一个实例。(这里有明显的线程安全问题,但超出了本文的范围。)
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single,
ConcurrencyMode=ConcurrencyMode.Multiple)]
我们首先注意到的是,我们 **快得多** 地获得了结果,为多线程点赞。Guid
s 仍然相同,所以仍然是同一个服务;它只是允许多个线程同时访问它。最多 10 个会话仍然导致最后 10 个线程失败。
现在,我们改变游戏的玩法。我们不测试我们可以拥有的会话数量,而是测试每个会话的消息数量(MaxConcurrentCalls
)。我们不为每个线程生成新的代理,而是使用相同的代理并在每个线程上调用 Ping()
方法。因此,我们得到一个会话,并在其上进行多次调用。不幸的是,由于服务恢复为单线程模式,并且所有请求都通过同一个会话传入并按顺序处理,这在给定的超时时间内根本行不通。
所以,是时候让这个服务再次成为 ConcurrencyMode.Multiple
了。
这导致我们的 Guid
s 的返回速度 **快得多**(请注意,它们都是相同的,单个服务实例,即使我们的 ContextMode
设置回 ContextMode.PerSession
)。
为了真正说明 MaxConcurrentCalls
如何限制事物,真的不那么容易,我能想到的最好的方法是将 MaxConcurrentCalls
限制为一次 2 个。
<serviceThrottling maxConcurrentCalls="2"/>
运行项目,并留意项目运行一段时间后,结果开始成批返回,因为服务一次允许两个线程同时访问它。
关注点
要制作一个能展示 MaxConcurrentCalls
区别的项目确实相当困难,以上版本是我能想到的最好的版本了,我相信有更好的方法可以解决这个问题。
此外,这是本文的第二次尝试。CodeProject 编辑器吃掉了第一个版本,我不得不重新做一遍。我认为这篇文章不如第一次那么好,抱歉 : (