WCF 4.0 进阶系列 – 第十三章以更好的性能实现WCF服务(上)

前言

良好的性能是大多数应用程序和服务的关键因素,你可以通过周密地设计,以及选择合适的特性以确保WCF服务维持其吞吐量,保持响应并具有可扩展性。到目前为止,这些技术包括事务(),session状态可靠地消息传递,以及异步操作()。
还有其他一些影响性能的方面,比如安全(企业内部WCF的安全因特网环境下WCF的安全)。如同我们在前面章节中讨论的那样,实现消息级别的安全性和安全地进行会话会导致复杂的消息交换,以协商其使用的协议以及交换身份信息;此外消息内容本身也变得比较大,这是因为额外的安全信息包含在消息的头部中—这些信息导致消息在网络中传输时需要更长的时间和更多的内存去处理。加密和解密同样也是十分消耗资源的任务。但是,所有这些对于一个安全的系统都是必须的,因此多数人都牺牲性能以确保数据的隐秘性和身份信息的隐秘性。(如果解密非常迅速并且非常容易,那么执行加密就没有什么意义了。加密消息时消耗越多的资源,便可以更好地保护消息)
维持性能的一个重要的方面是需要确保服务不会耗尽宿主计算机上可用的资源,进而导致计系统变慢,甚至停止响应。WCF提供服务阀值以帮助控制资源的利用。使用该特性可以最大程度的维持服务的最大扩展性。负载均衡是另外一个技术你可以用来分发请求至多个服务器并保持相同的输出。第十四章"发现服务和路由消息"描述了通过创建一个特别的WCF服务以实现一个简单的负载均衡。你还可以基于Windows网络负载均衡和WindowsServer AppFabric(Windows云平台中间件)创建一个负载均衡的架构,尽管该技术的详细信息超出本书的讨论范围。
在传输数据时,使用合适的编码机制对提高性能亦有显著的影响。你已经了解到,WCF支持使用文本和二进制对消息进行编码。两者相比较,二进制经常更具有影响力并且占用较少的网络流量,但是二进制编码格式是与平台相关的,因此运行在非Windows平台上的服务和客户端程序不能轻易地使用它。但是,WCF还支持消息传输优化机制(MTOM),该机制为传输大块的二进制数据提供了一个标准的,互操作的,并且有效的格式。
如果你知道服务传输数据的大小,那么此时MTOM是非常有用的。有些服务发送未知大小的数据块,这种类型的数据最好使用流进行传输,WCF同样支持从服务输出流。
在本章中,你将了解如何使用服务阀值以维护服务的扩展性,如何使用MTOM编码数据以减少大二进制数据对象的系统开销,以及何如启动流以最大化利用网络带宽。

使用服务端的阀值控制资源使用

你可以使用服务阀值以禁止过度消耗WCF服务的资源。你可能记得在第十一章"编写代码控制配置和通信"中,当一条消息由服务的宿主接收到后,该消息被传送至通道堆栈的顶部,然后它把消息传送至一个ChannelDispatcher对象,该对象将消息传递至对于的EndpointDispatcher对象,该对象接着调用服务实例中对应的方法。然而,在传送请求至EndpointDispatcher对象之前,ChannelDispatcher对象可以检查服务当前的负载,如果该请求会导致服务超出许可的负载那么选择延迟该请求。在这种情况下,请求将被阻塞并且保存在内部的队列中直到服务的负载得以释放。ChannelDispacher对象有一个名为ServiceThrottle的属性,你可以使用该属性来帮助ChannelDispatcher对象决定阻塞并将请求加入到等待队列或者将请求直接传送至服务实例并让其执行。ServiceThrottle属性是ServiceThrottle类的一个实例变量,该实例变量本身对外暴露三个属性:
  • MaxConcurrentInstances, 该属性设定并发服务实例的最大数量。
  • MaxConcurrentCalls, 该属性设定服务可以处理的并发消息的最大数量。如果客户端程序产生了大量并发调用,无论使用单向调用操作或在客户端使用多线程,都会导致其迅速耗用服务资源。在这种情况下,你可能希望限制每个客户端均访问服务的单个线程实例,这可以通过设置ConcurrencyMode.Single属性为True来完成。这样客户端程序继续异步地执行,而且服务还可以响应其他用户;但是客户端提交的请求将被服务以各种方式处理。
  • MaxConcurrentSessions, 该属性设定并发会话的最大数量。客户端程序负责建立和终止会话,并在在会话中调用服务的操作。创建长时间运行会话的客户端可能会导致其他客户端被阻塞,因此尽量保持短暂的会话,而且会话应该避免执行等待用户输入这样的任务。

配置服务阀值

默认情况下,ChannelDispatcher对象的ServiceThrottle属性为null;并且WCF运行时使用最大并发实例、方法调用、服务会话的默认值。为了控制扩展性,你应该设置WCF运行时创建一个ServiceThrottle对象并显示地设定这些属性以适合你环境,此外还应考虑到并发客户端数量以及客户端程序可能执行的任务数量。你可以通过代码来创建一个ServiceThrottle对象,然后设置该对象的属性,最后添加该对象到ServiceHost对象的行为集合中,以完成该项任务。请注意,你必须在打开ServiceHost对象之前完成上述任务。下面的例子展示了如果执行该任务:
但是,应该警告你的地方是ServiceThrottle对象的属性可能会对服务的响应时间和服务的带来剧烈的影响;因此你应该实时监控WCF服务的性能,如果寄宿服务的计算机性能下降,立即更改之前的设置。此外,如果这些属性的值设置的过低,那么会导致大量客户端被阻塞甚至发生超时或者在客户端程序中发生其他错误,也有可能在客户端堆栈处发生其他错误;因此你需要设置捕获异常的代码并处理这些异常。
因为你可能需要方便地更改ServiceThrottle对象的值,一个较为复杂地创建ServiceThrottle对象并设置该对象属性的方式是添加一个包含<serviceThrottle>元素的服务行为到服务配置文件中。我们将在下面的练习中使用这种方式。此外,在练习中,你还需要修改服务宿主程序以显示当前的服务阀值信息。
练习:在 ShoppingCartService服务中应用服务阀值
1. 使用Visual Studio打开位于*\WCF\Step.by.Step\Solutions\Chapter13\ServiceThrottle目录下的ShoppingCart.sln解决方案。
该解决方案包含一个简单的非事务版本的ShoppingCartService服务,该服务并不更新数据库。该服务还包含一个扩展版本的客户端程序,其与服务之间建立多个并发的会话。
2. 打开ShoppingCartService项目下的IShoppingCartService.cs文件。请注意该服务通过ServiceContract特性类为接口IShoppingCartService指定SessionMode为SessionMode.Required。打开ShoppingCartService.cs文件,你可以看到服务实现类的服务实例模式为PerSession。
3. 检查ShoppingCartService.cs文件的AddItemToCart方法,该方法首先调用WriteLine语句在屏幕上显示方法的名称。在该方法中每个可能终止方法的地方都添加了一个对应的WriteLine语句。你将使用这些语句追踪每个服务实例运行的过程。你还应该注意到该方法在第一个WriteLine语句后调用 System.Threading.Thread.Sleep(10000)以使当前线程停止10秒钟,以模拟执行数据库更新的操作。这么做的目的也方便了观察服务阀值参数带来的效果。另外的public方法RemoveItemFromCart,GetShoppingCart和Checkout都按照上述方式实现。
4. 打开ShoppingCartClient项目下的Programm.cs文件,然后找到doClientWork方法。该方法创建一个新的代理对象实例,然后通过代理对象实例调用ShoppingCartService服务的各种操作。该方法包含多个WriteLine语句,它们用以在控制台窗口中显示方法处理的进度。WirteLine输出指明当前连接到服务的客户端数。此外,客户端使用标准的TCP绑定连接至服务。
5. 再来检查Programm.cs的main方法。该方法使用Parallel.For构造方法,该方法10次异步地调用doClientWork方法。每次调用都创建一个并行的任务。它模拟10个不同的但是具有相同身份的客户端在同一时间连接至服务。
6. 打开ShoppingCartHost项目的Programm.cs文件,该程序寄宿ShoppingCart服务。在该文件的头部添加下面的引用。
7. 添加下面的代码至Main方法
上述代码获取服务ChannelDispatcher对象的引用(在本例中,服务仅仅只是有一个绑定,因此当宿主程序启动服务运行时,WCF运行时仅仅创建单个ChannelDispatcher对象)。然后,代码检查ChannelDispatcher对象的ServiceThrottle属性,如果其为null,那么表明管理员或者开发人员没有指定任何自定义的服务阀值,因此使用默认的服务阀值。如果ServiceThrottle属性不为null,那么服务使用自定义的服务阀值行为,控制台窗口将显示管理员或开发人员所设定的服务阀值信息。
8. 在非调适模式下,运行解决方案。在服务控制台窗口中,你将发现服务当前使用默认的服务阀值行为。
在客户端控制台窗口中按ENTER键后,控制台窗口将显示如下图所示结果
此时,在服务宿主控制台窗口,你应该会当每个客户端发送一个AddItemToCart的请求后看到"AddItemToCart operation started"出现:
这就意味着服务同时处理多个客户端的请求。当每个方法完成后,服务宿主控制台窗口将显示"AddItemToCart Operation completed",然后客户端将调用其他的操作。在此时,控制台输出会变得有点混乱,但最重要的一点是还没有突破服务阀值,因为服务不会拒绝10个客户端中任何一个调用操作(默认的最大并发调用数为10)。下图显示了所有操作执行完成后的界面。
当消息"Tests complete:Press ENTER to finish"处客户端控制台窗口中出现,按下ENTER键以关闭客户端程序控制台窗口,然后在服务宿主程序中按下ENTER键停止服务。
上述练习使用服务阀值的默认值,接下来,你将自定义服务阀值。
9. 在Visual Studio中,使用WCF服务配置编辑器打开ShoppingCartHost项目的App.config文件
10. 在控制面板中,展开高级文件夹,然后选择服务行为。然后在右边面板中,点击创建新的服务行为配置。
11. 在右边面板中,更改新行为的名字为ThrottleBeahavior。
12. 在右下面板中,点击添加按钮添加serviceThrottlig元素之刚创建的服务行为ThrottleBahavior
13. 在配置面板,点击serviceThrottling行为元素。该元素的三个属性机器默认值将在右边面板中显示:
14. 更改MaxConcurrentCalls值为3.
15. 在配置面板,点击ShoppingCartService.ShoppingCartServiceImple服务,然后在右边面板中,设置BehaviorConfiguration的属性为ThrottleBehavior
16. 保存配置文件;
17. 在Visual Studio中,在非调适模式下运行解决方案。在服务技术控制台窗口中,你将看到现在服务使用你指定的阀值行为。
你可能会非常惊讶在服务寄宿控制台窗口中显示的值。在服务配置编辑器中,为serviceThrottling元素的属性生成的值并不是服务寄宿程序所使用的实际值,除非你修改它们。在WCF 4.0中,默认值是基于宿主计算机的可用资源决定的。举例来说,一台电脑使用单核CPU,那么最大的并发实例数为116,最大的的并发调用数为16, 最大的并发会话数为100.
在一个双核的电脑上,你将发现这些默认值变为单核默认值的两倍—但serviceThrottle默认值将仍然为第十三步中显示的值。此外,serviceTrhottling三个属性的值之间存在一个关系,那就是MaxConcurrentInstances = MaxConcurrentCalls + MaxConcurrentSessions.这就意味着如果你仅仅更改MaxConcurrentClass属性的值,而不修改另外两个属性的值,那么WCF运行时将自动为其他两个属性生成值。请注意,我目前使用的电脑是四核的CPU,因此最大并发会话数为100×4=400;所以最大并发实例为400+3 = 403.
当然,你还可以修改MaxConcurrentCalls和MaxConcurrentSession属性的值,那么WCF运行时将不会再计算这两个属性的值,而是直接使用你指定的值。
18. 在客户端程序控制台中按下ENTER键。
在客户端控制台窗口中,所有10个客户端输出"Clinet n:1st AddItemToCart",但是服务宿主控制台与之前显示的不一样;最开始,仅仅显示3条"AddItemToCart operation started"消息。这是因为服务现在仅仅支持3个并发操作调用。ChannelDispatcher将后续的请求放入等待队列中。当每个请求完成,会显示"AddItemToCart operation completed"消息,然后ChannelDispatcher从等待队列中释放下一个请求,你将看到消息"AddItemToCart operation started"。此后,每次操作完成,ChannelDispatcher都释放下一个请求。你将看到"Completed"和"Started"消息交替显示,直到所有的客户端完成了它们的工作。
当客户端程序结束,按下ENTER键关闭客户端程序控制台窗口,然后在服务宿主控制台窗口按ENTER键停止服务。
19. 返回到WCF服务配置编辑器,在左边面板中点击serviceThrottling服务行为元素。然后在右边面板中,修改MaxConcurrentCalls属性值为16,并设置MaxConcurrentSessions属性为3。然后保存配置文件并退出WCF服务配置编辑器。
20. 回到Visual Studio,在为调适模式下运行解决方案。在服务宿主控制台窗口中,你将看到服务现在使用阀值的最大并发实例数为19. 请注意这里为什么是19;因为你如果手动修改MaxConcurrentCalls的值为16,即使该属性的默认值也为16.但是你的修改的背后,会在配置文件中产生如下的结果:(也就是系统认为你手动设置了最大并发调用数为16)
根据MaxConcurrentInstances = MaxConcurrentCalls + MaxConcurrentSessions;所以控制台窗口显示19。如果你删除maxConcurrentCalls="16";然后再运行方案,你会发现此时maxConcurrentCalls不再为19了;如果你是双核电脑,那么现在maxConcurrentCalls的值应该为16*2+3=35;如果你使用4核电脑,那么maxConcurrentCalls的值为16*4+3=67。
在客户端控制台窗口中,按下ENTER键。
同样,在客户端窗口中,所有10个客户端都输出消息"Client n: 1st AddItemToCart";服务控制台窗口显示3个AddItemToCart操作开始和操作结束。但是,当这些操作结束完成后,如果你查看客户端控制台窗口的消息,你你将会发现仅仅第一批客户端(先被服务处理的三个客户端)可以第二次调用AddItemToCart操作。其他的客户端由ChannelDispacther管理并处于pending状态,这是因为服务已经达到了所允许的最大会话数。第一批客户端(也就是头三个客户端)完成整套工作,第三次调用AddItemToCart,随后执行GetShoppingCart操作,最后执行Checkout操作。只有当Checkout完成而且客户端在关闭自己的会话后,下一个客户端才允许继续。你应该在客户端中看到消息始终以三条为循环的方式出现。
之后的一些会话将在客户端控制台窗口中显示如下的异常消息:
这是因为它们提交AddItemToCart之后,一直处于等待状态;在轮到它们被服务处理前,已经发生超时了。
当测试结束,按下ENTER键关闭客户端程序控制台窗口,然后在服务宿主控制台窗口按ENTER键停止服务。
 
上述练习演示了使用服务阀值控制服务所允许的最大并发调用和最大并发会话的效果。很不幸,该练习并不能告诉你应该为你的服务设定具体的服务阀值。你需要在真实负载环境中测试你的服务,并观察客户端程序是否被阻塞了相当长的时间。请记住服务阀值的目的是为了阻止服务因大量的请求造成"洪水泛滥"—因为服务没有足够的资源处理如此大量的请求。你应该设置服务阀值的属性以确保当客户端请求被接受后,便开始处理该请求,而且寄宿服务的计算机在客户端发生超时之前拥有足够的可用资源来完成操作。否则,你的更改会进一步妨碍整个服务的性能。请注意如果服务支持事务,失败的客户端要求服务执行更多的工作,因为当超时发生,服务必须回滚之前已经执行的事务。
 

WCF和服务实例池

WCF运行时创建服务实例以处理客户端请求。如果服务使用PerSession实例模式,那么服务实例可以跨越多个操作而存在。如果服务使用PerCall实例模式,每个操作都将产生一个新的服务实例,当操作完成时该服务实例将被丢弃并销毁。创建和销毁服务实例不仅非常消耗资源的,而且还可能耗费较长时间。在这种场景下,服务实例池非常有用。
当使用服务实例池,WCF运行时在服务启动时创建一个服务实例对象池。如果服务使用PerCall实例模式,当客户端程序访问调用操作,WCF运行时将从池中获取一个预先创建的服务实例,并在操作完成后将该服务实例放回池中。如果使用PerSession实例模式时,从语义来讲工作模式与PerCall是相同的,但是WCF运行时在会话开始时才从池中获取一个服务实例,在会话结束时将该服务实例放回池中。处于安全目的,服务实例持有的所有数据(服务实现类中的成员变量)都将在返回池中时全部清除。
实际上,WCF并不直接提供服务实例池,但是可以通过定义自定义行为扩展WCF从而实现服务实例池。WCF通过System.ServiceModel.Dispatcher命名空间提供IInstanceProvider接口,你可以使用该接口来定义自定义服务实例分发机制。这是一个非常有用的技术,但是其详细信息超出本书讨论的范畴。更多关于服务实例池的内容,请参考Visual Studio帮助文档的服务实例池主题。该主题同样可以在MSDN查阅到:http://msdn.microsoft.com/en-gb/library/ms751482.aspx
 

指定内存需求

应用服务阀值行为允许你限制会话的数量和连接的数量以使服务维持其吞吐量。但是,服务是运行在电脑上的应用程序,并且服务执行耗费资源的操作,因此有必要确保其在开始运行时拥有足够的可用资源。
一个常见并且紧缺的资源就是内存。正因为这个原因,WCF运行时允许你指定服务在激活前可用内存的最小值。你可以在服务配置文件中,通过<serviceHostingEnvironment>元素下的minFreeMemoryPercentageToActivateService特性来定制该值。其默认值为5,在本例中我们将其修改为10.
在这种情况下,当WCF运行时尝试激活服务时,如果可用的总内存小于总内存的10%,那么WCF运行时将失败并抛出ServiceActivationException异常。
当然,你还可以通过WCF服务配置编辑器来设定该参数的值。在配置面板中,展开高级文件夹,然后点击宿主环境。在宿主环境面板,指定所需内存的最小数目。如下图所示:
 
 
posted @ 2011-08-24 09:57  On the road....  阅读(3080)  评论(10编辑  收藏  举报