WCF技术剖析之二十三:服务实例(Service Instance)生命周期如何控制[中篇]

[第1篇]中,我们介绍了WCF关于实例管理一些基本的知识点,包括InstanceContext、InstanceContextMode、已经如何通过ServiceBehaviorAttribute应用不同的实例上下文模式给不同的服务。在[第1篇]中,对WCF采用的三种不同实例上下文模式进行了简单的比较,本篇的重点方法对单调(PerCall)模式为进行详细介绍。

在单调(Per-Call)实例上下文模式下,WCF总是创建一个新的服务实例上下文处理接收到的每一个服务调用请求,并在服务操作执行结束后,回收服务上下文和服务实例。换句话说,单调服务实例上下文模式使服务实例上下文的生命周期与服务调用本身绑定。我们首先来介绍单调模式下服务实例上下文具体有怎样的生命周期。

一、 单调模式下的服务实例上下文提供机制

对于单调模式,服务实例的生命周期大体上可以看成服务操作执行的生命周期。服务实例在服务操作执行前被创建,在操作完成之后被回收。下面的列表揭示了在单调模式下,对于每一次服务调用请求,WCF的整个服务实例激活过程:

  • WCF服务端接收到来自客户端的服务调用请求;
  • 通过实例上下文提供者(InstanceContextProvider)对象试图获取现有服务实例的实例上下文,对于单调模式,返回的实例上下文永远为空;
  • 如果获取实例上下文为空,则通过实例提供者(IntanceProvider)创建服务实例,封装到新创建的实例上下文中;
  • 通过InstanceContext的GetServiceInstance方法获取服务实例对象,借助操作选择器(OperationSelector)选择出相应的服务操作,最后通过操作执行器(OperationInvoker)对象执行相应的操作方法;
  • 操作方法执行完毕后,关闭被卸载InstanceContext对象。在此过程中,会调用InstanceProvider对象释放服务实例,如果服务类型实现了接口IDisposable,则会调用Disposable方法;
  • 服务实例成为垃圾对象,等待GC回收。

对于上述列表中提到的InstanceContextProvider、InstanceProvider等重要的对象,以及相关的实现机制,将在本系列后续的部分进行单独讲解。为了加深读者的理解,这里通过一个简单的例子来演示在单调模式下服务实例的整个激活流程。

二、 实例演示:单调模式下服务实例的生命周期

本案例依然沿用典型的4层结构和计算服务的场景,下面是服务契约和具体服务实现的定义。在CalculatorService类型上,通过ServiceBehaviorAttribute特性将实例上下文模式设为单调(Per-Call)模式。为了演示服务实例的创建、释放和回收,我们分别定义了无参构造函数,终止化器(Finalizer)以及实现的接口IDisposable,并在所有的方法中输出相应的指示性文字,以便更容易地观测到它们执行的先后顺序。

   1: using System.ServiceModel;
   2: namespace Artech.WcfServices.Contracts
   3: {
   4:     [ServiceContract(Namespace="http://www.artech.com/")]
   5:     public interface ICalculator
   6:     {
   7:         [OperationContract]
   8:         double Add(double x, double y);
   9:     }
  10: }
   1: using System;
   2: using System.ServiceModel;
   3: using Artech.WcfServices.Contracts;
   4: namespace Artech.WcfServices.Services
   5: {
   6:     [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
   7:     public class CalculatorService : ICalculator, IDisposable
   8:     {
   9:         public CalculatorService()
  10:         {
  11:             Console.WriteLine("Service object is instantiated.");
  12:         }
  13:         ~CalculatorService()
  14:         {
  15:             Console.WriteLine("Service object is finalized.");
  16:         }
  17:  
  18:         public void Dispose()
  19:         {
  20:             Console.WriteLine("Service object is disposed.");
  21:         }
  22:         public double Add(double x, double y)
  23:         {
  24:             Console.WriteLine("Operation method is invoked.");
  25:             return x + y;
  26:         }
  27:     }
  28: }

为了演示GC对服务实例的回收,在进行服务寄宿的时候,通过System.Threading.Timer使GC每隔10毫秒强制执行一次垃圾回收。

   1: using System;
   2: using System.ServiceModel;
   3: using System.Threading;
   4: using Artech.WcfServices.Services;
   5: namespace Artech.WcfServices.Hosting
   6: {
   7:     public class Program
   8:     {
   9:         private static Timer GCScheduler;
  10:  
  11:         static void Main(string[] args)
  12:         {
  13:             GCScheduler = new Timer(
  14:                 delegate
  15:                 {
  16:                     GC.Collect();
  17:                 }, null, 0, 100);
  18:             using (ServiceHost serviceHost = new ServiceHost(typeof(CalculatorService)))
  19:             {               
  20:                 serviceHost.Open();                
  21:                 Console.Read();
  22:             }
  23:         }
  24:     }
  25: }

通过一个控制台应用程序对服务进行成功寄宿后,客户端通过下面的代码,使用相同的服务代理对象进行两次服务调用。

   1: using System;
   2: using System.ServiceModel;
   3: using Artech.WcfServices.Contracts;
   4: namespace Artech.WcfServices.Clients
   5: {
   6:     class Program
   7:     {
   8:         static void Main(string[] args)
   9:         {
  10:             using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorservice"))
  11:             {
  12:                 ICalculator calculator = channelFactory.CreateChannel();
  13:                 Console.WriteLine("x + y = {2} when x = {0} and y = {1}", 1, 2, calculator.Add(1, 2));
  14:                 Console.WriteLine("x + y = {2} when x = {0} and y = {1}: {3}", 1, 2, calculator.Add(1, 2));
  15:             }
  16:         }
  17:     }
  18: }

从运行后服务端的输出可以看出,对于两次服务调用请求,服务端先后创建了两个服务实例,在操作方法成功执行后,Dispose方法得以执行。而终止化器(Finalizer)是被GC在后台执行的,所以执行的时机不能确定。不过有一点可以从中得到证实:当服务操作执行时,服务实例变成了“垃圾”对象,并可以被GC回收以腾出占据的内存空间。

Service object is instantiated.
Operation method is invoked.
Service object is disposed.
Service object is instantiated.
Operation method is invoked.
Service object is disposed.
Service object is finalized.
Service object is finalized.

三、 服务实例上下文的释放

如果服务实例须要引用一些非托管资源,比如数据库连接、文件句柄等,须要及时将其释放。在这种情况下,我们可以通过实现IDisposable接口,在Dispose方法中进行相应的资源回收工作。在单调实例上下文模式下,当服务操作执行时,Dispose方法会自动被执行,这一点已经通过上面的案例演示得到证实。

对于实现了IDisposable接口的Dispose方法,有一点值得注意的是:该方法是以与操作方法同步形式执行的。也就是说,服务操作和Dispose方法在相同的线程中执行。认识这一点很重要,因为无论采用怎样的实例模式,在支持会话(Session)的情况下如果服务请求来自于同一个服务代理,服务操作都会在一个线程下执行。对于单调模式就会出现这样的问题:由于Dispose方法同步执行的特性,如果该方法是一个比较耗时的操作,那么来自于同一个服务代理的服务后续调用请求将不能得到及时执行。WCF只能在上一个服务实例被成功释放之后,才能处理来自相同服务代理的下一个服务调用请求。为了让读者体会到同步方式释放服务实例在应用中的影响,并证明同步释放服务实例的现象,我们对上面的案例略加改动。

在CalculatorService中,通过线程休眠的方式模拟耗时的服务实例释放操作(5秒)。在Dispose和Add方法中,除了输出具体操作名称之外,还会输出当前的线程ID和执行的开始时间,代码如下所示。

   1: [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
   2: public class CalculatorService : ICalculator, IDisposable
   3: {
   4:     public void Dispose()
   5:     {
   6:         Console.WriteLine("Time: {0}; Thread ID: {1}; Service object is disposed.", DateTime.Now, Thread.CurrentThread.ManagedThreadId);
   7:         Thread.Sleep(5000);
   8:     }
   9:     public double Add(double x, double y)
  10:     {
  11:         Console.WriteLine("Time: {0}; Thread ID: {1}; Operation method is invoked.", DateTime.Now, Thread.CurrentThread.ManagedThreadId);
  12:         return x + y;
  13:     }
  14: }

在客户端,我们创建两个不同的服务代理,通过ThreadPool分别对它们进行2次异步调用。下面是相关的服务调用代码。

   1: using (ChannelFactory<ICalculator> channelFactory = new ChannelFactory<ICalculator>("calculatorservice"))
   2: {
   3:     ICalculator calculator = channelFactory.CreateChannel();
   4:     ThreadPool.QueueUserWorkItem(delegate
   5:     {
   6:         Console.WriteLine("{3}: x + y = {2} when x = {0} and y = {1}", 1, 2, calculator.Add(1, 2), DateTime.Now);
   7:     });
   8:     ThreadPool.QueueUserWorkItem(delegate
   9:     {
  10:         Console.WriteLine("{3}: x + y = {2} when x = {0} and y = {1}", 1, 2, calculator.Add(1, 2), DateTime.Now);
  11:     });
  12:     Console.Read();    
  13: } 

从客户端和服务端输出结果的比较,我们可以清晰地看出基于相同服务代理的操作方法和Dispose方法都执行在相同的线程下(线程ID为12),并且两次服务操作的间隔为服务实例释放的时间:5秒。由于服务操作和Dispose方法的同步执行,导致服务端忙于释放上一个服务实例,而不能及时处理来自相同服务代理的下一个服务调用请求。

客户端:

3/6/2009 7:12:34 PM: x + y = 3 when x = 1 and y = 2
3/6/2009 7:12:39 PM: x + y = 3 when x = 1 and y = 2

服务端:

Time: 3/6/2009 7:12:34 PM; Thread ID: 12; Operation method is invoked.
Time: 3/6/2009 7:12:34 PM; Thread ID: 12; Service object is disposed.
Time: 3/6/2009 7:12:39 PM; Thread ID: 12; Operation method is invoked.
Time: 3/6/2009 7:12:39 PM; Thread ID: 12; Service object is disposed.

关于服务实例的同步执行机制,还有一点需要说明是,在Dispose方法中,可以得到当前OperationContext,而OperationContext在会话(Per-Session)实例上下文模式下是不可得的。

四、单调模式与可扩展性

在单调模式下,如果不考虑GC对垃圾对象回收的滞后性,服务实例的数量可以看成是当前正在处理的服务调用请求的数量。相关的资源能够在服务操作执行完毕之后得到及时回收(通过实现IDisposable接口,将资源回收操作实现在Dispose方法中)。所以,单调模式具有的优势是能够最大限度地发挥资源的利用效率,避免了资源的闲置和相互争用。

这里的资源不仅仅包括服务实例本事占据的内存资源,也包括服务实例直接或间接引用的资源。由于单调模式采用基于服务调用的服务实例激活和资源分配方式,所以服务实例或被分配的资源自始至终都处于“工作”状态,不会造成资源的闲置。服务实例在完成其使命之后,能够对资源进行及时的释放,被释放的资源可以及时用于对其他服务请求的处理。

我们将单调模式和后面要讲的会话模式作一个对比,后者采用基于服务代理的实例激活和生命周期管理。也就是说,在不考虑WCF闲置请求策略(当服务实例在超出某个时间段没有被使用的情况下,WCF将其清理)的情况下,服务实例的生命始于通过服务实例进行第一次服务调用,或者调用Open方法开启服务代理之时,服务代理的关闭会通知WCF服务端框架将对应的服务实例进行释放。举一个极端的例子,服务实例在存续期间需要引用一个非托管资源,比如是数据库连接,假设最大允许的并发连接为100。现在,先后100个客户端(或者服务代理)进行服务调用请求,毫无疑问,100个服务实例会被创建并同时存在于服务端的内存之中,并且每一个服务实例引用一个开启状态的数据库连接,那么当来自第101个客户端服务调用请求抵达时,将得不到处理,除非在它的超时时限到达之前,有一个客户端自动将服务代理关闭。

但是,对于相同的场景,如果采用单调的模式,就能应付自如,因为在每次服务调用之后,数据库的连接可以及时地得到关闭和释放。

对于单调模式,很多读者一开始就会心存这样的疑问:服务实例的频繁创建,对性能不会造成影响吗?在前一章中,我们就说过:高性能(Performance)和高可扩展性(Scalability)是软件设计与架构中永远不可以同时兼顾的,原因很简单,高性能往往需要充足的资源,高扩展性又需要尽可能地节约资源。所以我们才说,软件设计与架构是一项“权衡”的艺术,我们的目的不是将各个方面都达到最优,因为这是不可能实现的任务,我们须要做的只是找到一个平衡点使整体最优。关于高扩展性和性能之间的平衡关系,我们很难有一个适合所有场景的黄金法则,这需要对具体场景的具体分析。

较之会话模式,单调模式能够处理更多的并发客户端,提供更好的吞吐量(Throughput)。对于量化我们的服务到底能够处理多少客户端,Juval Lowy在其著作《Programming WCF》中提出了这样一项经验性总结:在一个典型的企业应用中,并发量大概是所有客户端数量的1%(高并发情况下能达到3%),也就是如果服务端能够同时维持100个服务实例,那么意味着能为10 000个客户端提供服务。

关于服务实例的创建过程,其中会使用到诸如反射这样的相对影响性能的操作,但是在WCF应用中,真正影响性能是操作时信道的创建和释放。服务实例的激活和它们比起来,可以说是微不足道。但是,如果在应用中出现对基于相同服务代理的频繁调用,比如服务调用放在一个For循环中调用上百次,服务实例的创建带来的性能损失就不能不考虑了。


作者:蒋金楠 
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
上一篇:lambda表达式导致arthas无法redefine的问题


下一篇:分布式的CAP理论