《Programming WCF Services》翻译笔记之四
本书第四章介绍了关于实例管理的相关技术。“WCF支持三种实例激活的类型:单调服务(Per- Call Service)会为每次的客户端请求分配(销毁)一个新的服务实例。会话服务(Sessionful Service)则为每次客户端连接分配一个服务实例。最后一种是单例服务(Singleton Service),所有的客户端会为所有的连接和激活对象共享一个相同的服务实例。”
对于Per-Call Service的翻译,我踌躇良久,最后还是决定按照Singleton服务的翻译,将其译为单调服务,意即为每次调用创建一个服务实例,与单例服务相对 应。不知是否妥当?其实最好的翻译就是保持原文不变,但对于整本书而言,如果保持英文术语,也有许多不便的地方。
实例模式的配置通过ServiceBehavior完成,以下是ServiceBehaviorAttribute的定义:
{
PerCall,
PerSession,
Single
}
[AttributeUsage(AttributeTargets.Class)]
public sealed class ServiceBehaviorAttribute : Attribute,...
{
public InstanceContextMode InstanceContextMode
{get;set;}
//More members
}
单调服务(Per-Call Service)
单调服务如图所示:
执行步骤如下:
1. 客户端调用代理,代理将调用转发给服务。
2. WCF创建一个服务实例,然后调用服务实例的方法。
3. 当方法调用返回时,如果对象实现了IDisposable接口,WCF将调用IDisposable.Dispose()方法。
4. 客户端调用代理,代理将调用转发给服务。
5. WCF创建一个对象,然后调用对象的方法。
单调服务的一个最重要优势在于它能够节省资源,支持系统的可伸缩性。由于服务实例的生命周期只存在于一次调用期间,特别对于那些持有昂贵资源的服务 实例而言,这种方式可以有效地提高系统性能。而且,销毁服务实例时,WCF不会断开与客户端(通过客户端的代理)的连接,这比创建实例与连接所消耗的资源 要少得多。
单调服务体现的优势在事务编程与队列服务中更为明显,它可以保证在事务编程中实例状态的同步;而对于队列断开调用而言,则单调服务能够建立服务实例与离散队列消息之间的简单映射。
单调服务的配置通过ServiceBehavior,如下所示:
interface IMyContract
{...}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyService : IMyContract
{...}
注意,ServiceBehavior特性只能应用到类上面,这在前面介绍的特性定义可以看出。实际上,这也是合理的约束,因为如果将ServiceBehavior应用到接口上,由于接口是不能实例化的,自然会出现错误。
单调服务实例是状态相关的,但是由于实例会在调用之初创建,而在调用之后被销毁,因而这样的单调服务实例是无法保存状态的。为了解决这一问题,我们 可以引入数据库或者文件保存状态,或者利用全局变量临时存储状态。那么为了获取状态,潜在的含义是单调服务的每个操作应该定义一个参数,用来传递状态或状 态的ID。
书中有一句话非常重要:“如果单调服务真的与状态无关,就根本不需要单调激活模式。准确地讲,正是因为状态,特别是代价昂贵的状态,才需要使用单调 模式。”很多时候,我们认为单调服务实例的生命周期存在于每次调用,因而想当然的认为这样的服务实例应该是无状态的。这样的操作可能只是简单执行某项任 务,而不会对服务对象的属性进行操作。表面看起来确实如此,但在这里我们却忽略了我们采用单调服务的根本原因,是在于单调激活模式的本质就在于能够适时释 放实例所持有的昂贵资源,这里的资源大体上讲就是一种状态。如果不需要维护状态,则以为着性能上没有太大的损耗,我们就没有必要采用单调激活模式了,毕竟 频繁地创建与销毁实例,仍然会对性能造成一定的影响。
对于WCF服务而言,单调服务可以算是最佳的实例激活模式。书中介绍:“一个有力的论据是单调服务更利于系统的可伸缩性。为了更好的支持可伸缩性, 服务设计有一个黄金法则是10X,即设计出的每个服务应该能够处理至少多于需求一个量级以上的负载。这是一个工程学准则,工程师在设计系统时,绝不能够“ 鼠目寸光”,只考虑当前指定负载的处理。如果一幢大楼,只能够支撑当前需求确定的承重,还会有人胆敢居住吗?如果一座电梯,只能够承受规定的六位乘客的重 量,还会有人愿意乘坐吗?软件系统同样如此。为什么不能针对当前指定的负载进行系统设计?假设采用这样的设计方式,一旦系统的每位用户增加了业务量,那么 系统就会变得岌岌可危。设计良好的系统必须能够经久不衰,经得起时间的考验。为了实现这一目的,就需要应用10X的黄金法则,有效地利用单调服务所能提供 的可伸缩性。采用单调服务的另一个有力论据是关于事务的处理。正如第7章介绍的那样,事务绝对是每个系统所必需的,单调服务有利于实现事务编程模型,而不 用考虑系统的负载。”
会话服务
从执行方式与激活方式来看,会话服务相当于.NET Remoting中的客户端激活模式。也就是为每个客户端创建一个专门的服务实例。只要会话没有结束,该实例就不会被销毁。
“客户端会话是一个代理对应一个服务终结点。如果客户端为相同或不同的终结点创建了另外的代理,则新建的代理就会与新的实例和会话建立关联。”根据 这句话的内容,可以理解到对于会话服务而言,是一个客户端代理对应一个服务实例。也就是说,会话服务中的服务是与代理相对应的,而不是对应于一个客户端。 这是它与.NET Remoting的客户端激活模式不同的地方。
此外,会话服务存在可伸缩性的问题。由于每个客户端都需要维护一个会话,如果存在多个独立的客户端,则创建专门的服务实例的代价太大。
配置会话服务的方式仍然是使用ServiceBehavior特性,如下所示:
class MyService : IMyContract
{...}
然而,InstanceContextMode的默认值为InstanceContextMode.PerSession,如果没有设置InstanceContextMode,则服务默认为会话服务。
仅仅为服务配置InstanceContextMode是不够的,因为会话服务必须要求客户端维持一个会话,这就需要让客户端的WCF运行时知道服 务是否使用了会话,因此,我们需要通过ServiceContract特性提供的SessionMode属性,设置服务契约。SessionMode的定 义如下:
{
Allowed,
Required,
NotAllowed
}
“SessionMode的默认值为SessionMode.Allowed。当客户端导入契约元数据时,服务元数据将包含SessionMode值,并会如实地反映它的内容。”
如果服务的SessionMode被配置为SessionMode.Allowed,并不必然代表服务为会话服务。以下是对各种情况的说明:
1、 如果服务被配置为单调服务,则服务与SessionMode无关;
2、 如果服务被配置为会话服务,且SessionMode为Allowed,则:
(1)如果服务使用的绑定为BasicHttpBinding,服务为单调服务;
(2)如果服务使用的绑定为没有包含安全与可靠消息传输的WSHttpBinding绑定,服务为单调服务;
(3)如果服务使用的WSHttpBinding绑定包含了安全(为默认配置)或者可靠的消息传输,或者使用NetTcpBinding绑定、NetNamedPipeBinding绑定,服务为会话服务。
当SessionMode为Required时,服务不能使用BasicHttpBinding绑定或者没有包含安全与可靠消息传输的 WSHttpBinding绑定,在装载服务时会对此进行验证。作者建议,“若要设计一个会话契约,我主张使用 SessionMode.Required,而非SessionMode.Allowed默认值。”
如果SessionMode为NotAllowed,则不管服务配置如何,它总是采用单调服务方式。但如果契约使用了NetTcpBinding或 NetNamedPipeBinding绑定,则不能将服务的SessionMode配置为NotAllowed。作者的建议是“是在选择使用 SessionMode.NotAllowed的同时,总是将服务配置为单调服务”。
应该避免将单调服务与会话契约混合定义在相同的会话服务类型中,即使WCF允许这样的配置:
interface IMyContract
{...}
[ServiceContract(SessionMode = SessionMode.NotAllowed)]
interface IMyOtherContract
{...}
//Avoid
class MyService : IMyContract,IMyOtherContract
{...}
会话应该保证是可靠的,一个实现了会话契约的服务,它包含的所有终结点所公开的契约都应该使用支持可靠传输会话的绑定。
“通常,一旦客户端关闭了代理,会话就会终止。但是,客户端也可以强行终止会话,也可能因为通信故障而终止会话。每个会话还包含了一个空闲超时值, 默认为10分钟。如果客户端在10分钟内没有任何操作,那么即使客户端期望继续使用该会话,会话仍然会自动终止。会话如果是因为空闲超时的原因被终止,那 么当客户端试图使用它的代理时,会获得一个CommunicationObjectFaultedException异常。在绑定中通过配置不同的值,可 以为客户端和服务配置不同的超时值。支持可靠传输层会话的绑定提供了ReliableSession属性,类型为ReliableSession或者 OptionalReliableSession。ReliableSession类定义了InactivityTimeout属性,属于 TimeSpan类型,通过它可以配置一个新的空闲超时值。”
注意,InactivityTimeout属性的默认值为10分钟。不能将该值设置为小于或等于0的值,否则会抛出ArgumentOutOfRangeException异常。
例如,下面的代码利用编程方式将TCP绑定的空闲超时值配置为25分钟:
tcpSessionBinding.ReliableSession.Enabled = true;
tcpSessionBinding.ReliableSession.InactivityTimeout = TimeSpan.FromMinutes(25);
这等同于配置config文件:
<binding name = "TCPSession">
<reliableSession enabled = "true" inactivityTimeout = "00:25:00"/>
</binding>
</netTcpBinding>
如果客户端与服务都配置了超时值,则以短的超时值为准。
单例服务
如果我们熟悉设计模式,可以以单例模式的方式思考单例服务。所谓单例服务,就是针对所有客户端而言,都只有一个服务实例。“单例服务的生存期是无限的,只有在关闭宿主时,才会被释放。创建宿主时,单例服务会被创建,并且只能被创建一次。”
可以通过InstanceContextMode.Single的InstanceContextMode属性配置单例服务:
class MySingleton : ...
{...}
只要是单例服务,即使该服务支持多个契约,这些契约中有的需要会话,有的不需要会话,在不同终结点的调用仍然是通过相同的实例进行传递。即使关闭了代理,也不会终止单例服务。
在实例化单例服务对象时,可能需要执行一些初始化的工作。如果使用默认的构造函数,并通过ServiceHost托管服务,是没有办法做到这一点 的。当然,我们也可以在默认构造函数中实现这些初始化的工作,然而如果初始化工作需要一些特别的定制步骤,特别是需要操作状态或者需要传入参数时,默认的 构造函数就显得捉襟见肘了。
WCF提供了另外一种初始化单例服务的办法,就是利用ServiceHost类提供的专门的构造函数,可以接收一个object对象:
{
public ServiceHost(object singletonInstance,
params Uri[] baseAddresses);
public virtual object SingletonInstance
{get;}
//More members
}
注意,构造函数中的singletonInstance必须是配置为单例方式的服务对象。因此,初始化以及托管单例服务的方式可以如下实现:
[ServiceContract]
interface IMyContract
{
[OperationContract]
void MyMethod( );
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
class MySingleton : IMyContract
{
int m_Counter = 0;
public int Counter
{
get
{
return m_Counter;
}
set
{
m_Counter = value;
}
}
public void MyMethod( )
{
m_Counter++;
Trace.WriteLine("Counter = " + Counter);
}
}
//Host code
MySingleton singleton = new MySingleton( );
singleton.Counter = 42;
ServiceHost host = new ServiceHost(singleton);
host.Open( );
//Do some blocking calls then
host.Close( );
//Client code
MyContractClient proxy = new MyContractClient( );
proxy.MyMethod( );
proxy.Close( );
//Output:
Counter = 43
很显然,ServiceHost提供的这个构造函数还有改进的余地,那就是object类型参数显然不具备类型安全,因而本书作者定义了新的ServiceHost类,引入了泛型:
{
public ServiceHost(T singleton,params Uri[] baseAddresses)
: base(singleton,baseAddresses)
{}
public virtual T Singleton
{
get
{
if(SingletonInstance == null)
{
return default(T);
}
return (T)SingletonInstance;
}
}
//More members
}
单例服务与可伸缩性之间的关系可谓“水火不容”。因而除非是在特殊情况,应尽量避免使用单例服务。
分步操作
WCF提供了一种被称之为分步操作(Demarcating Operation)的方法,以应对服务契约的操作需要指定执行顺序的情况。分步操作是使用OperationContract特性的IsInitiating和IsTerminating属性:
public sealed class OperationContractAttribute : Attribute
{
public bool IsInitiating
{get;set;}
public bool IsTerminating
{get;set;}
//More members
}
一个分步操作的应用如下:
interface IOrderManager
{
[OperationContract]
void SetCustomerId(int customerId);
[OperationContract(IsInitiating = false)]
void AddItem(int itemId);
[OperationContract(IsInitiating = false)]
decimal GetTotal( );
[OperationContract(IsInitiating = false,IsTerminating = true)]
bool ProcessOrders( );
}
此时,操作AddItem()、GetTotal()以及ProcessOrder()都不能是启动会话的第一个操作。同时,ProcessOrder()操作则会成为终止会话的操作。这与业务的要求是一脉相承的。
IsInitiating属性的默认值为true,IsTerminating属性的默认值则为false。
如 果IsInitiating值为true,并不必然代表该操作必然是启动会话的第一个操作。如果其它相同设置的操作首先被调用,就会启动一个会话,而原操 作则在调用时被加入会话,成为会话的一部分。但如果IsTermination的值为true,则代表该操作必须是终止会话的操作。虽然在服务契约定义 时,允许将多个操作的IsTerminating值设置为true,但一旦调用了IsTerminating值为true的方法,就不能再调用服务实例的 其它方法,除非在客户端重新创建一个代理对象。此外,即使操作的IsTermination值为true,它也可以是启动会话的第一个操作,但在操作执行 后它会终止会话。因此,如下的两个操作定义是等效的:
void StartAndEndSession();
[OperationContract(IsInitiating=true, IsTerminating = true)]
void StartAndEndSession();
然而,如下的两个操作则是不等效的,因为后者要求该操作不能为启动会话的第一个操作:
void StartAndEndSession();
[OperationContract(IsInitiating=false, IsTerminating = true)]
void StartAndEndSession();
实例停用(Instance Deactivation)
实例停用只针对会话服务而言。单例服务虽然也可以应用,但却无效。
“ 会话实际要做的不仅是关联客户端消息,同时还要关联托管了服务的上下文。启动会话时,宿主会创建一个新的上下文。会话终止时,上下文也随之而终止。默认情 况下,上下文的生命周期与发布的服务实例的生命周期相同。然而,出于优化的目的,WCF为服务设计者提供了一个分离两种生命周期的选项,该选项允许WCF 独立地停用实例,而不必依赖于它的上下文。实际上,WCF还允许不包含实例的上下文存在,如下图所示。”
这种实例管理技术称为上下文停用(Context Deactivation)。控制上下文停用的最常见办法是通过OperationBehavior特性的ReleaseInstanceMode属性:
{
None,
BeforeCall,
AfterCall,
BeforeAndAfterCall,
}
[AttributeUsage(AttributeTargets.Method)]
public sealed class OperationBehaviorAttribute : Attribute,...
{
public ReleaseInstanceMode ReleaseInstanceMode
{get;set;}
//More members
}
“通常,我们只需要将实例停用应用到部分服务方法上,而不是全部方法;或者为不同的方法设置不同的值。”例如:
{
[OperationBehavior(ReleaseInstanceMode = ReleaseInstanceMode.AfterCall)]
public void MyMethod( )
{...}
public void MyOtherMethod( )
{...}
public void Dispose( )
{...}
}
ReleaseInstanceMode属性的默认值为ReleaseInstanceMode.None。关于ReleaseInstanceMode属性值,以下四张图片足以说明它们的工作方式:
ReleaseInstanceMode.None
ReleaseInstanceMode.BeforeCall
ReleaseInstanceMode.AfterCall
ReleaseInstanceMode.BeforeAndAfterCall
准确地选择ReleaseInstanceMode,可以有效地提高系统的性能,优化内存以及资源的利用。
除 了可以通过应用OperationBehaviorAttribute应用ReleaseInstanceMode,也可以通过编程方式在服务的操作中显 式地完成对实例的停止。方法是利用InstanceContext的ReleaseServiceInstance()方法:
{
public void MyMethod( )
{
//Do some work then
OperationContext.Current.InstanceContext.ReleaseServiceInstance( );
}
public void Dispose( )
{...}
}
这两种实现实例停止的方式还可以结合使用,例如在应用了OperationBehavior特性,并将ReleaseInstanceMode设置为 BeforeCall的方法中,如果显式调用ReleaseServiceInstance()方法,其效果就相当于将 ReleaseInstanceMode设置为BeforeAndAfterCall。
限流(Throttling)
限流“允许开发者限制客户端连接数以及服务的负荷。限流可以避免服务的最大化,以及分配与使用重要资源的最大化。引入限流技术后,一旦超出配置的设置 值,WCF就会自动地将等待处理的调用者放入到队列中,然后依次从队列中取出。在队列中等待处理调用时,如果客户端的调用超时,客户端就会获得一个 TimeoutException异常。每个服务类型都可以应用限流技术,也就是说,它会影响到服务的所有实例以及服务类型的所有终结点。实现方式是为限 流与服务使用的每个通道分发器建立关联。”
限流由ServiceThrottlingBehavior类定义,包括三个重要的属性:MaxConcurrentCalls、MaxConcurrentSessions、MaxConcurrentInstances,它们分别的默 认值为16,10和Int.MaxValue。
在翻译过程中,我在查阅MSDN时,发现MaxConcurrentSessions的默 认值为64,这让我感觉很奇怪,莫非作者在这里出现了错误。然而经过我仔细地查阅相关资料,发现在WCF的早期版本 中,MaxConcurrentSessions的默认值确实为64,但在2006年6月的CTP版本中已经被修改为16。
设置限流值可以通过配置文件,也可以通过编码方式。前者例如:
<services>
<service name = "MyService" behaviorConfiguration = "ThrottledBehavior">
...
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name = "ThrottledBehavior">
<serviceThrottling
maxConcurrentCalls = "12"
maxConcurrentSessions = "34"
maxConcurrentInstances = "56"
/>
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
WCF并没有提供关于限流的特性。但实现该特性的方法非常简单,如下内容是我定义的关于限流的特性,本书并没有提供:
{
private ServiceThrottlingBehavior throttle;
public ServiceThrottlingAttribute(
int maxConcurrentCalls,
int maxConcurrentInstances,
int maxConcurrentSessions)
{
this.throttle = new ServiceThrottlingBehavior();
throttle.MaxConcurrentCalls = maxConcurrentCalls;
throttle.MaxConcurrentInstances = maxConcurrentInstances;
throttle.MaxConcurrentSessions = maxConcurrentSessions;
}
#region IServiceBehavior Members
void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase,
System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints,
System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{ }
void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{
ServiceThrottlingBehavior currentThrottle = serviceDescription.Behaviors.Find<ServiceThrottlingBehavior>();
if (currentThrottle == null)
{
serviceDescription.Behaviors.Add(this.throttle);
}
}
void IServiceBehavior.Validate(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{ }
#endregion
}
定义的ServiceThrottlingAttribute特性继承了Attribute,并实现了IServiceBehavior接口。在特性内,则 使用了ServiceThrottlingBehavior类,以设置限流的相关值。如果要配置服务的限流值,就可以应用该特性,例如:
class MyService : IMyContract,IDisposable
{
public void MyMethod( )
{
ChannelDispatcher dispatcher = OperationContext.Current.Host.ChannelDispatchers[0] as ChannelDispatcher;
ServiceThrottle serviceThrottle = dispatcher.ServiceThrottle;
Trace.WriteLine("MaxConcurrentCalls = " + serviceThrottle.MaxConcurrentCalls);
Trace.WriteLine("MaxSessions = " + serviceThrottle.MaxConcurrentSessions);
Trace.WriteLine("MaxInstances = " + serviceThrottle.MaxConcurrentInstances);
}
}
则输出结果为:
MaxConcurrentCalls = 12
MaxSessions = 56
MaxInstances = 34