通过《上篇》介绍,我们知道了如何通过编程和配置的方式设置相应的最大并发量,从而指导WCF的限流体系按照你设定的值对并发的服务调用请求进行限流控制。那么,在WCF框架体系内部,整个过程是如何实现的呢?这就是本篇文章需要为你讲述的内容。实际上,整个限流控制体系,主要涉及到两个对象:信道分发器(ChannelDispatcher)和ServiceThrottle。
一、信道分发器(ChannelDispatcher)与ServiceThrottle
从服务端整个消息监听、接收、分发和处理框架体系角度来讲,限流控制现在在信道分发器上。关于信道分发器在整个WCF服务端框架体系中所处的位置,由于在《WCF技术剖析(卷1)》的第2章和第7章均有过详细的介绍,在这里我只作一些概括性的介绍。
在服务寄宿的时候,我们基于服务类型创建相应的ServiceHost对象,并为之添加一到多个终结点。在开始ServiceHost的时候,整个服务端消息处理体系会被建立,而整个体系的核心由两个主要分发器(Dispatcher)构成,即信道分发器和终结点分发器。
WCF根据ServiceHost实际采用的监听地址(不一定是终结点地址)创建相应的信道分发器,也就是说,ServiceHost包含的信道分发器的数量和监听地址的数量相同。每个信道监听器具有各自的信道监听器,它们绑定到各自的监听地址进行请求消息的监听。
而终结点分发器与ServiceHost的终结点一一匹配,实际上可以看成是运行时的终结点。信道监听器通过创建的信道栈将接收到的消息递交给自己所在的信道分发器。信道分发器则通过消息承载的寻址信息将消息分发给相应的终结点分发器进行进一步处理。
举个例子,假设我们现在对一个服务进行寄宿,并采用如下所示的配置。该服务具有三个基于NetTcpBinding的终结点,它们的终结点地址对应的端口分别为7777,8888和9999。而对于第一个终结点,我们将监听地址设置成与第二个终结点的地址一样。
代码语言:javascript复制 1: <?xml version="1.0" encoding="utf-8" ?>
代码语言:javascript复制 2: <configuration>
代码语言:javascript复制 3: <system.serviceModel>
代码语言:javascript复制 4: <services>
代码语言:javascript复制 5: <service name="Artech.ThrottlingDemo.Service.CalculatorService">
代码语言:javascript复制 6: <endpoint address="net.tcp://127.0.0.1:7777/calculatorservice" binding="netTcpBinding" contract="Artech.ThrottlingDemo.Service.Interface.ICalculator" listenUri="net.tcp://127.0.0.1:8888/calculatorservice"/>
代码语言:javascript复制 7: <endpoint address="net.tcp://127.0.0.1:8888/calculatorservice" binding="netTcpBinding" contract="Artech.ThrottlingDemo.Service.Interface.ICalculator" />
代码语言:javascript复制 8: <endpoint address="net.tcp://127.0.0.1:9999/calculatorservice" binding="netTcpBinding" contract="Artech.ThrottlingDemo.Service.Interface.ICalculator" />
代码语言:javascript复制 9: </service>
代码语言:javascript复制 10: </services>
代码语言:javascript复制 11: </system.serviceModel>
代码语言:javascript复制 12: </configuration>
如果我们创建了基于服务类型CalculatorService的ServiceHost,并成功开启它,虽然该ServiceHost具有三个终结点,由于前两个共享相同的监听地址,所以实际采用的监听地址只有两个,即net.tcp://127.0.0.1:8888/calculatorservice和net.tcp://127.0.0.1:9999/calculatorservice。WCF会创建两个信道分发器,它们各自具有自己的信道监听器,上述的两个URI即为监听器对应的监听地址。此外,对应于ServiceHost的三个终结点,WCF会创建相应的终结点分发器。ServiceHost、信道分发器和终结点分发器之间的关系如图1所示。
图1 ServiceHost、信道分发器和终结点分发器之间的关系
而流量的控制就是实现在信道分发器上,也就是说当信道分发器将接收到的消息分发给相应的终结点分发器之前,就会进行流量的检测。至于实现流量控制的原理,我们会在后面讨论。在这里我们需要知道,WCF将所有限流相关的实现定义在ServiceThrottle类中。我们不妨来看看ServiceThrottle的定义。
代码语言:javascript复制 1: public sealed class ServiceThrottle
代码语言:javascript复制 2: {
代码语言:javascript复制 3: //其他成员
代码语言:javascript复制 4: public int MaxConcurrentCalls { get; set; }
代码语言:javascript复制 5: public int MaxConcurrentInstances { get; set; }
代码语言:javascript复制 6: public int MaxConcurrentSessions { get; set; }
代码语言:javascript复制 7: }
由于具体的限流逻辑实现在ServiceThrottle的内部,并没有通过公共方法的形式暴露出来(WCF甚至为ServiceThrottle定义了内部构造函数,我们不同直接通过new操作符创建ServiceThrottle对象),可见的只是三个我们熟悉的最大并发量。
如果我们查看ChannelDispatcher的成员列表,可以看到类型为ServiceThrottle的ServiceThrottle属性定义在ChannelDispatcher之中。当ChannelDispatcher进行消息分发之前对限流的控制就是通过该属性表示的ServiceThrottle对象实现的。
代码语言:javascript复制 1: public class ChannelDispatcher : ChannelDispatcherBase
代码语言:javascript复制 2: {
代码语言:javascript复制 3: // 其他成员
代码语言:javascript复制 4: public ServiceThrottle ServiceThrottle { get; set; }
代码语言:javascript复制 5: }
实际上服务行为ServiceThrottlingBehavior对限流控制的现实就是就是根据自身的设置(三个最大并发量)为信道分发器定义定制相应的ServiceThrottle。由于服务行为是针对服务级别的,即基于ServiceHost的,如果一个ServiceHost具有若干个信道分发器,ServiceThrottlingBehavior会为每一个信道分发器进行相同的设置。ServiceThrottlingBehavior对信道并发器ServiceThrottle的设置实现在ApplyDispatchBehavior方法中,大概得逻辑如下面的伪代码所示:
代码语言:javascript复制 1: public class ServiceThrottlingBehavior : IServiceBehavior
代码语言:javascript复制 2: {
代码语言:javascript复制 3: //其他成员
代码语言:javascript复制 4: void ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase)
代码语言:javascript复制 5: {
代码语言:javascript复制 6: ServiceThrottle serviceThrottle = new ServiceThrottle(serviceHostBase)
代码语言:javascript复制 7: serviceThrottle.MaxConcurrentCalls = this.MaxConcurrentCalls;
代码语言:javascript复制 8: serviceThrottle.MaxConcurrentSessions = this.MaxConcurrentSessions;
代码语言:javascript复制 9: serviceThrottle.MaxConcurrentInstances = this.MaxConcurrentInstances;
代码语言:javascript复制 10: foreach(ChannelDispatcher channelDispatcher in serviceHostBase.ChannelDispatchers)
代码语言:javascript复制 11: {
代码语言:javascript复制 12: channelDispatcher.ServiceThrottle = serviceThrottle;
代码语言:javascript复制 13: }
代码语言:javascript复制 14: }
代码语言:javascript复制 15: }
由于服务的限流控制最终是通过信道分发器的ServiceThrottle对象实现的,那么我们可以通过信道分发器的ServiceThrottle的属性,获取到我们通过编程或配置方式设置的三个最大并发量的值。假设我们通过配置的方式为CalculatorService服务进行了如下的限流设置。
代码语言:javascript复制 1: <?xml version="1.0" encoding="utf-8" ?>
代码语言:javascript复制 2: <configuration>
代码语言:javascript复制 3: <system.serviceModel>
代码语言:javascript复制 4: <behaviors>
代码语言:javascript复制 5: <serviceBehaviors>
代码语言:javascript复制 6: <behavior name="throttlingBehavior">
代码语言:javascript复制 7: <serviceThrottling maxConcurrentCalls="50" maxConcurrentInstances="30" maxConcurrentSessions="20"/>
代码语言:javascript复制 8: </behavior>
代码语言:javascript复制 9: </serviceBehaviors>
代码语言:javascript复制 10: </behaviors>
代码语言:javascript复制 11: <services>
代码语言:javascript复制 12: <service name="Artech.ThrottlingDemo.Service.CalculatorService" behaviorConfiguration="throttlingBehavior">
代码语言:javascript复制 13: <endpoint address="net.tcp://127.0.0.1:8888/calculatorservice" binding="netTcpBinding" contract="Artech.ThrottlingDemo.Service.Interface.ICalculator" />
代码语言:javascript复制 14: <endpoint address="net.tcp://127.0.0.1:9999/calculatorservice" binding="netTcpBinding" contract="Artech.ThrottlingDemo.Service.Interface.ICalculator" />
代码语言:javascript复制 15: </service>
代码语言:javascript复制 16: </services>
代码语言:javascript复制 17: </system.serviceModel>
代码语言:javascript复制 18: </configuration>
在寄宿过程中,我们可以通过如下的方式得到ServiceHost的每个信道分发器所有的ServiceThrottle对象,并将MaxConcurrentCalls、MaxConcurrentInstances和MaxConcurrentSessions三个最大并发量打印出来。
代码语言:javascript复制 1: using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
代码语言:javascript复制 2: {
代码语言:javascript复制 3: host.Open();
代码语言:javascript复制 4: for (int i = 0; i < host.ChannelDispatchers.Count; i )
代码语言:javascript复制 5: {
代码语言:javascript复制 6: ChannelDispatcher channelDispatcher = (ChannelDispatcher)host.ChannelDispatchers[i];
代码语言:javascript复制 7: ServiceThrottle serviceThrottle =channelDispatcher.ServiceThrottle;
代码语言:javascript复制 8: Console.WriteLine("ChannelDispatcher {0}: MaxConcurrentCalls = {1};MaxConcurrentInstances = {2};MaxConcurrentSessions = {3}",
代码语言:javascript复制 9: i 1,serviceThrottle.MaxConcurrentCalls,serviceThrottle.MaxConcurrentInstances,serviceThrottle.MaxConcurrentSessions);
代码语言:javascript复制 10: }
代码语言:javascript复制 11: }
输出结果:
代码语言:javascript复制ChannelDispatcher 1: MaxConcurrentCalls = 50;MaxConcurrentInstances = 30;MaxConc
代码语言:javascript复制urrentSessions = 20
代码语言:javascript复制ChannelDispatcher 2: MaxConcurrentCalls = 50;MaxConcurrentInstances = 30;MaxConc
代码语言:javascript复制urrentSessions = 20
二、 ServiceThrottle对限流实现原理揭秘
WCF对限流控制的实现原理,相对来说还是比较复杂的。由于涉及到很多的内部对象,要将限流控制机制具体的实现将清楚,也是一件不太容易的事情。接下来,我尽量用比较直白的描述简单地介绍一下WCF限流框架体系是如何将递交处理的请求控制在我们设置的范围的。无论是基于对并发会话的控制,还是对并发调用以及并发实例上下文的控制,都是采用相同的实现机制。WCF为此专门设计了一个内部组建,我们可以将其称为流量限制器(FlowThrottle)。
1、流量限制器(FlowThrottle)
流量限制器的设计大体上如图1所示。首先,它具有一个最大容量属性,表示最大流量;其内部维护一个队列和一个计数器,次队列被称为等待队列。当流量限制器初始化的时候,最大容量会被指定,等待队列为空,计数器置为零。当需要处理需要进行流量控制的请求的时候,调用者将请求递交给该流量限制器。流量限制器判断当前的计数器是否大于最大容量,如果没有则将其递交到相应的处理组建进行处理,与此同时计数器加1。如果计数器超出最大容量,则将请求放到等待队列中。如果之前的处理被正常处理,流量限制器的计数器会减1,如果此时等待队列不会空,则会提取第一个请求进行处理。
图2 流量限制器设计
2、ServiceThrottle与流量限制器
由于WCF的限流通过三个指标来控制,即最大并发请求、最大并发实例上下文和最大并发会话,所以ServiceThtottle内部会维护三个不同的流量限制器。这个三个流量限制器的最大容量就是我们通过ServiceThrottlingBehavior设置的三个最大并发量属性:MaxConcurrentCalls、MaxConcurrentInstances和MaxConcurrentSessions。图3揭示了信道分发器、ServiceThtottle和流量限制器之间的关系。
图3 ChannelDispatcher、ServiceThrottle和FlowThrottle之间的关系
3、限流控制实现
ServiceThrottle三个流量限制器就像是设置在信道分发器三道闸门。当信道监听器监测到请求消息,并创建信道栈接受消息,最后由信道监听器分发给相应的终结点分发器,必须经过这三道闸门。如果一道闸门不放行,将不能再进行后续的处理,必须等到之前的操作结束使并发的操作小于闸门限制的容量。
从整个消息接收、处理的流程来看,第一道闸门是限制并发会话的流量限制器。当信道监听器监听到抵达的详细请求后,创建信道栈对消息进行接收。如果创建的信道是会话信道,并发会话流量限制器会参与进来。并发会话流量限制器内部维护着一个会话信道计数器,如果该计数器超过通过通过ServiceThrottlingBehavior的MaxConcurrentSessions属性设置的最大并发量,如果没有继续处理,否则将请求添加到自己的等待队列中。关于会话信道,可以参阅《WCF技术剖析(卷1)》第9章关于会话的内容。
如果并发会话的流量限制器放行,对请求消息的处理进入第二道屏障,即并发调用流量限制器。原理与上面相似,如果该流量限制器的并发请求数超出了通过ServiceThrottlingBehavior的MaxConcurrentCalls属性设置的最大并发量,请求将会被添加到该自己的等待队列中,否则继续处理。
如果上面两个屏障顺利通过,WCF会通过实例上下文提供器(InstanceContext Provider)获取现有的或者创建新的实例上下文。此时,第三道屏障,即并发实例上下文流量控制器,开始发挥它的限流作用。与前面的并发限流机制一样,该流量限制器判断自身维护的并发实例上下文计数器是否超过了通过ServiceThrottlingBehavior的MaxConcurrentInstances属性设置的最大并发量,如果没有则继续处理,否则将请求添加到并发实例上下文流量控制器的等待队列中。