WCF学习之:利用Throttling提高服务器性能
如果WCF服务用到服务器的稀缺资源,该资源的创建和销毁极耗服务器时间和性能,这种情况如果提高服务器性能呢?
我们知道WCF的实例模式有三种:Per-Call 、Per-Session 、Singleton。我们来分析一下:第一种Per-Call ,每一个Client Call都会一个服务实例和资源对象,这样的性能是无法忍受的;第二种Per-Session ,这种情况稍为改善了服务器性能,但是海量并发请求的时候比前者好不到哪去;第三种Singleton单件模式,保证只有一个服务实例和资源对象,但是并发访问时候会发生线程冲突。
在这种情况下,资源池派上了用场,把大操作对象(资源对象)放到一个池里面,这个池是启动服务之后提供服务之前创建好的,响应客户端请求时从池里获取一个可用的资源对象,请求响应过后释放该资源,使之响应下一个请求。
关键点在于:当并发请求数大于资源对象池大小时,如何排队等候。我们来看WCF是怎么处理这种情况的。WCF有三种Service Concurrency Mode:Single、Multiple、Reentrant。默认是Single,每次只能响应一个请求,其它请求被阻塞;Multiple服务实例是多线程的,所有并发请求立刻被响应,无同步保证,但是我们可以用Throttling节流措施来限制并发请求数,使并发数刚好等于资源池的大小,其它的请求排队等候,保证每个请求都有可用资源。看下面的例子:
以GDI+为例,我们创建Graphic绘图对象池,Client请求服务器给它绘制一幅图。
1、先设计一个服务契约:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace Tloner.Throttling.WcfServiceLibrary1
{
// 注意: 如果更改此处的接口名称“IService1”,也必须更新 App.config 中对“IService1”的引用。
[ServiceContract]
public interface IService1
{
[OperationContract]
byte[] DrawImg(string str);
}
}
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
namespace Tloner.Throttling.WcfServiceLibrary1
{
// 注意: 如果更改此处的接口名称“IService1”,也必须更新 App.config 中对“IService1”的引用。
[ServiceContract]
public interface IService1
{
[OperationContract]
byte[] DrawImg(string str);
}
}
2、设计一个资源类和资源池
public class MyResource
{
public Bitmap bitmap;
public Graphics graphics;
public bool usable;
public int resourceIndex;
public MyResource(int index)
{
bitmap = new Bitmap(500, 500);
graphics = Graphics.FromImage(bitmap);
usable = true;
resourceIndex = index;
}
}
public class ResourceCollection : List<MyResource>
{
public ResourceCollection(int capacity)
{
for (int i = 0; i < capacity; i++)
{
this.Add(new MyResource(i));
}
}
public MyResource CatchResource()
{
lock (this)
{
for (int i = 0; i < this.Count; i++)
{
if (this[i].usable == true)
{
this[i].usable = false;
Console.WriteLine("catch resource index:{0}", i);
return this[i];
}
}
}
return null;
}
public void ReleaseResource(int index)
{
lock (this)
{
Console.WriteLine("release resource index:{0}", index);
this[index].usable = true;
}
}
}
在ResourceCollection资源池的构建函数里按照容量添加资源对象{
public Bitmap bitmap;
public Graphics graphics;
public bool usable;
public int resourceIndex;
public MyResource(int index)
{
bitmap = new Bitmap(500, 500);
graphics = Graphics.FromImage(bitmap);
usable = true;
resourceIndex = index;
}
}
public class ResourceCollection : List<MyResource>
{
public ResourceCollection(int capacity)
{
for (int i = 0; i < capacity; i++)
{
this.Add(new MyResource(i));
}
}
public MyResource CatchResource()
{
lock (this)
{
for (int i = 0; i < this.Count; i++)
{
if (this[i].usable == true)
{
this[i].usable = false;
Console.WriteLine("catch resource index:{0}", i);
return this[i];
}
}
}
return null;
}
public void ReleaseResource(int index)
{
lock (this)
{
Console.WriteLine("release resource index:{0}", index);
this[index].usable = true;
}
}
}
CatchResource()方法用于获取一个可用资源来响应请求
ReleaseResource(int index)方法释放一个资源,使它为其它请求服务
3、服务实现
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
public class Service1 : IService1
{
ResourceCollection resourceCollection;
public Service1()
{
resourceCollection = new ResourceCollection(3);
Console.WriteLine("create a ResourceCollection");
Console.WriteLine("create a service instance");
}
public byte[] DrawImg(string str)
{
MyResource resource = null;
MemoryStream stream = new MemoryStream(); ;
try
{
resource = resourceCollection.CatchResource();
System.Diagnostics.Debug.Assert(resource != null, "no free resource");
Console.WriteLine("using resource index:{0}", resource.resourceIndex);
resource.graphics.Clear(Color.White);
resource.graphics.DrawRectangle(new Pen(Color.Red), 0, 0, 498, 498);
Font drawFont = new Font("Arial", 30);
SolidBrush drawBrush = new SolidBrush(Color.Red);
resource.graphics.DrawString(str, drawFont, drawBrush, 100, 100);
resource.bitmap.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
Thread.Sleep(3000);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
resourceCollection.ReleaseResource(resource.resourceIndex);
}
return stream.GetBuffer();
}
~Service1()
{
Console.WriteLine("destroy a service instance");
}
}
实例模式设为InstanceContextMode = InstanceContextMode.Single,保证只有一个服务实例,该实例是在服务Host的时候创建的。响应DrawImg(string str)请求的时候catch一个可用Graphic对象资源用来绘制,绘制完了把它Release释放掉。为了更好展现请求状态,在每次DrawImg时让线程等待3秒。public class Service1 : IService1
{
ResourceCollection resourceCollection;
public Service1()
{
resourceCollection = new ResourceCollection(3);
Console.WriteLine("create a ResourceCollection");
Console.WriteLine("create a service instance");
}
public byte[] DrawImg(string str)
{
MyResource resource = null;
MemoryStream stream = new MemoryStream(); ;
try
{
resource = resourceCollection.CatchResource();
System.Diagnostics.Debug.Assert(resource != null, "no free resource");
Console.WriteLine("using resource index:{0}", resource.resourceIndex);
resource.graphics.Clear(Color.White);
resource.graphics.DrawRectangle(new Pen(Color.Red), 0, 0, 498, 498);
Font drawFont = new Font("Arial", 30);
SolidBrush drawBrush = new SolidBrush(Color.Red);
resource.graphics.DrawString(str, drawFont, drawBrush, 100, 100);
resource.bitmap.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
Thread.Sleep(3000);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
resourceCollection.ReleaseResource(resource.resourceIndex);
}
return stream.GetBuffer();
}
~Service1()
{
Console.WriteLine("destroy a service instance");
}
}
4、Host服务
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using Tloner.Throttling.WcfServiceLibrary1;
namespace host
{
class Program
{
static void Main(string[] args)
{
using (ServiceHost serivceHost = new ServiceHost(typeof(Service1)))
{
serivceHost.Opened += delegate
{
Console.WriteLine("Service has begun to listen ");
};
serivceHost.Open();
Console.Read();
}
}
}
}
Configuration:using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using Tloner.Throttling.WcfServiceLibrary1;
namespace host
{
class Program
{
static void Main(string[] args)
{
using (ServiceHost serivceHost = new ServiceHost(typeof(Service1)))
{
serivceHost.Opened += delegate
{
Console.WriteLine("Service has begun to listen ");
};
serivceHost.Open();
Console.Read();
}
}
}
}
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.web>
<compilation debug="true" />
</system.web>
<system.serviceModel>
<services>
<service name="Tloner.Throttling.WcfServiceLibrary1.Service1" behaviorConfiguration="WcfServiceLibrary1.Service1Behavior">
<host>
<baseAddresses>
<add baseAddress = "http://localhost:8732/Design_Time_Addresses/WcfServiceLibrary1/Service1/"/>
</baseAddresses>
</host>
<endpoint address ="" binding="basicHttpBinding" contract="Tloner.Throttling.WcfServiceLibrary1.IService1">
<identity>
<dns value="localhost"/>
</identity>
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="WcfServiceLibrary1.Service1Behavior">
<serviceThrottling maxConcurrentCalls="3"/>
<serviceMetadata httpGetEnabled="True"/>
<serviceDebug includeExceptionDetailInFaults="False" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
注意,这里的<serviceThrottling maxConcurrentCalls="3"/>设置为跟服务实现里的resourceCollection = new ResourceCollection(3)的值一致,并发数和池大小相等才能保证每个请求都有可用的资源。<configuration>
<system.web>
<compilation debug="true" />
</system.web>
<system.serviceModel>
<services>
<service name="Tloner.Throttling.WcfServiceLibrary1.Service1" behaviorConfiguration="WcfServiceLibrary1.Service1Behavior">
<host>
<baseAddresses>
<add baseAddress = "http://localhost:8732/Design_Time_Addresses/WcfServiceLibrary1/Service1/"/>
</baseAddresses>
</host>
<endpoint address ="" binding="basicHttpBinding" contract="Tloner.Throttling.WcfServiceLibrary1.IService1">
<identity>
<dns value="localhost"/>
</identity>
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="WcfServiceLibrary1.Service1Behavior">
<serviceThrottling maxConcurrentCalls="3"/>
<serviceMetadata httpGetEnabled="True"/>
<serviceDebug includeExceptionDetailInFaults="False" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
5、client调用服务
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.ServiceModel;
using Tloner.Throttling.WcfServiceLibrary1;
using System.ServiceModel.Channels;
using System.Drawing;
using System.IO;
namespace client
{
class Program
{
static void Main(string[] args)
{
try
{
for (int i = 0; i < 10; i++)
{
ThreadStart ts = new ThreadStart(ClientCall);
Thread th = new Thread(ts);
th.Start();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.Read();
}
static void ClientCall()
{
using (ThrottlingClient client = new ThrottlingClient("throttling"))
{
byte[] data = client.DrawImg(Thread.CurrentThread.ManagedThreadId.ToString());
MemoryStream stream = new MemoryStream(data);
Bitmap bitmap = new Bitmap(stream);
bitmap.Save(@"c:\" + Thread.CurrentThread.ManagedThreadId.ToString() + ".png", System.Drawing.Imaging.ImageFormat.Png);
Console.WriteLine("CurrentThreadID: {0}", Thread.CurrentThread.ManagedThreadId.ToString());
}
}
}
class ThrottlingClient : ClientBase<IService1>, IService1
{
public ThrottlingClient()
: base()
{ }
public ThrottlingClient(string endpointConfigurationName)
: base(endpointConfigurationName)
{ }
public ThrottlingClient(Binding binding, EndpointAddress address)
: base(binding, address)
{ }
public byte[] DrawImg(string str)
{
return this.Channel.DrawImg(str);
}
}
}
创建10个线程模拟10个并发请求,把服务器绘制返回的图保存到本地。using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.ServiceModel;
using Tloner.Throttling.WcfServiceLibrary1;
using System.ServiceModel.Channels;
using System.Drawing;
using System.IO;
namespace client
{
class Program
{
static void Main(string[] args)
{
try
{
for (int i = 0; i < 10; i++)
{
ThreadStart ts = new ThreadStart(ClientCall);
Thread th = new Thread(ts);
th.Start();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.Read();
}
static void ClientCall()
{
using (ThrottlingClient client = new ThrottlingClient("throttling"))
{
byte[] data = client.DrawImg(Thread.CurrentThread.ManagedThreadId.ToString());
MemoryStream stream = new MemoryStream(data);
Bitmap bitmap = new Bitmap(stream);
bitmap.Save(@"c:\" + Thread.CurrentThread.ManagedThreadId.ToString() + ".png", System.Drawing.Imaging.ImageFormat.Png);
Console.WriteLine("CurrentThreadID: {0}", Thread.CurrentThread.ManagedThreadId.ToString());
}
}
}
class ThrottlingClient : ClientBase<IService1>, IService1
{
public ThrottlingClient()
: base()
{ }
public ThrottlingClient(string endpointConfigurationName)
: base(endpointConfigurationName)
{ }
public ThrottlingClient(Binding binding, EndpointAddress address)
: base(binding, address)
{ }
public byte[] DrawImg(string str)
{
return this.Channel.DrawImg(str);
}
}
}
Configuration:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="throttlingBinding"
sendTimeout="00:10:00"
transferMode="Streamed"
maxReceivedMessageSize="92233720368">
<readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="963840000"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="http://localhost:8732/Design_Time_Addresses/WcfServiceLibrary1/Service1/" binding="basicHttpBinding" bindingConfiguration="throttlingBinding"
contract="Tloner.Throttling.WcfServiceLibrary1.IService1" name="throttling" />
</client>
</system.serviceModel>
</configuration>
<configuration>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="throttlingBinding"
sendTimeout="00:10:00"
transferMode="Streamed"
maxReceivedMessageSize="92233720368">
<readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="963840000"
maxBytesPerRead="4096" maxNameTableCharCount="16384" />
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="http://localhost:8732/Design_Time_Addresses/WcfServiceLibrary1/Service1/" binding="basicHttpBinding" bindingConfiguration="throttlingBinding"
contract="Tloner.Throttling.WcfServiceLibrary1.IService1" name="throttling" />
</client>
</system.serviceModel>
</configuration>
我们来看看运行结果:
我们可以看到每个资源都是catch-->using-->release,资源释放之前不会被cactht和using.
也许有考虑欠周到的地方,欢迎大家给点意见,共同研究
源码下载:download