WCF性能,延迟和可扩展性 [英] WCF performance, latency and scalability

查看:104
本文介绍了WCF性能,延迟和可扩展性的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我试图端口一个简单的异步TCP在F#到C#4.服务器接收到一个连接服务器,读取单个请求并关闭连接前流回来的响应序列。

在异步C#4看起来繁琐和容易出错的,所以我想我会尝试使用WCF来代替。此服务器是不是不太可能看到在野外1000并发的请求,所以我认为无论是吞吐量和延迟感兴趣。

我已经写在C#中最小的双工WCF的Web服务和控制台客户端。虽然我使用WCF,而不是原始套接字,这已经是175线$ C $的则为C〜80行原。但我更关心的是性能和可伸缩性:


  • 延迟更糟糕的是使用WCF 154×。

  • 吞吐量更糟糕的是使用WCF 54×。

  • TCP处理1000个并发连接,但很容易WCF扼流圈只有20。

首先,我使用的是默认设置的一切,所以我想知道如果有什么我可以调整,以提高这些性能数字?

其次,我是否有人使用WCF为这种事情还是想知道,如果它是错误的工作工具?

下面是在C#中我的WCF服务器:

IService1.cs

  [DataContract]
公共类股票
{
  [数据成员]
  公众的DateTime FirstDealDate {搞定;组; }
  [数据成员]
  公众的DateTime LastDealDate {搞定;组; }
  [数据成员]
  公开日期时间起始日期{搞定;组; }
  [数据成员]
  公共DateTime的结束日期{搞定;组; }
  [数据成员]
  公共小数打开{搞定;组; }
  [数据成员]
  公共小数高{搞定;组; }
  [数据成员]
  公共小数低{搞定;组; }
  [数据成员]
  公共小数关闭{搞定;组; }
  [数据成员]
  公共小数VolumeWeightedPrice {搞定;组; }
  [数据成员]
  公共小数TotalQuantity {搞定;组; }
}[的ServiceContract(CallbackContract = typeof运算(IPutStock))]
公共接口IStock
{
  [OperationContract的]
  无效GetStocks();
}公共接口IPutStock
{
  [OperationContract的]
  无效PutStock(股份股票);
}

Service1.svc

 <%@ ServiceHost的语言=C#调试=真正的服务=DuplexWcfService2.StockscodeBehind =Service1.svc.cs%GT;

Service1.svc.cs

  [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple)
 公共类股票istock提供
 {
   IPutStock回调;   #区域IStock成员
   公共无效GetStocks()
   {
     回调= OperationContext.Current.GetCallbackChannel< IPutStock>();
     ST股票= NULL;
     ST =新股票
     {
       FirstDealDate = System.DateTime.Now,
       LastDealDate = System.DateTime.Now,
       起始日期= System.DateTime.Now,
       结束日期= System.DateTime.Now,
       打开= 495,
       高= 495,
       低= 495,
       关闭= 495,
       VolumeWeightedPrice = 495,
       TotalQuantity = 495
     };
     的for(int i = 0; I< 1000; ++ I)
       callback.PutStock(ST);
   }
   #endregion
 }

的Web.config

 <?XML版本=1.0&GT?;
<结构>
  <&的System.Web GT;
    <编译调试=真targetFramework =4.0/>
  < /system.web>
  < system.serviceModel>
    <服务和GT;
      <服务名称=DuplexWcfService2.Stocks>
        <端点地址=绑定=wsDualHttpBinding合同=DuplexWcfService2.IStock>
          <同一性GT;
            < D​​NS值=本地主机/>
          < /身份>
        < /端点>
        <端点地址=MEX绑定=mexHttpBinding合同=IMetadataExchange接口/>
      < /服务>
    < /服务>
    <&行为GT;
      < serviceBehaviors>
        <&行为GT;
          < serviceMetadata httpGetEnabled =真/>
          < serviceDebug includeExceptionDetailInFaults =真/>
        < /行为>
      < / serviceBehaviors>
    < /行为>
    < serviceHostingEnvironment multipleSiteBindingsEnabled =真/>
  < /system.serviceModel>
  < system.webServer>
    <模块runAllManagedModulesForAllRequests =真/>
  < /system.webServer>
< /结构>

这里的C#WCF客户端:

的Program.cs

  [CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Multiple,UseSynchronizationContext = FALSE)]
 一流的回调:DuplexWcfService2.IStockCallback
 {
   System.Diagnostics.Stopwatch定时器;
   INT N;   公共回调(System.Diagnostics.Stopwatch T)
   {
     定时器= T;
     N = 0;
   }   公共无效PutStock(DuplexWcfService2.Stock ST)
   {
     ++ N;
     如果(N == 1)
       Console.WriteLine(第一结果在+ this.timer.Elapsed.TotalSeconds +的s);
     如果(N = = 1000)
       Console.WriteLine(1000的结果为+ this.timer.Elapsed.TotalSeconds +的s);
   }
 } 类节目
 {
   静态无效测试(int i)以
   {
     VAR定时器= System.Diagnostics.Stopwatch.StartNew();
     VAR CTX =新的InstanceContext(新回拨(定时器));
     VAR代理=新DuplexWcfService2.StockClient(CTX);
     proxy.GetStocks();
     Console.WriteLine第(i +连接);
   }   静态无效的主要(字串[] args)
   {
     对(INT I = 0; I&小于10 ++ⅰ)
     {
       INT J =;
       新System.Threading.Thread(()=>测试(J))。开始();
     }
   }
 }

下面是我的异步TCP客户端和服务器$ C $ F#中的C:

 键入AggregatedDeals =
  {
    FirstDealTime:System.DateTime的
    LastDealTime:System.DateTime的
    开始时间:System.DateTime的
    结束时间:System.DateTime的
    开放时间:十进制
    高:十进制
    低:十进制
    关闭:十进制
    VolumeWeightedPrice:十进制
    TotalQuantity:十进制
  }让阅读(流:的System.IO.Stream)= {异步
  让!标题= stream.AsyncRead 4
  让长度= System.BitConverter.ToInt32(头,0)
  让!身体= stream.AsyncRead长
  让FMT = System.Runtime.Serialization.Formatters.Binary.BinaryFormatter()
  使用流=新System.IO.MemoryStream(体)
  返回fmt.Deserialize(流)
}让写(流:的System.IO.Stream)值= {异步
  让身体=
    让FMT = System.Runtime.Serialization.Formatters.Binary.BinaryFormatter()
    使用流=新System.IO.MemoryStream()
    fmt.Serialize(流值)
    stream.ToArray()
  让头部= System.BitConverter.GetBytes body.Length
  做! stream.AsyncWrite头
  做! stream.AsyncWrite体
}让终点= System.Net.IPEndPoint(System.Net.IPAddress.Loopback,4502)让服务器()= {异步
  让听者= System.Net.Sockets.TcpListener(端点)
  listener.Start()
  而真正的做
    让客户端= listener.AcceptTcpClient()
    异步{
      使用流= client.GetStream()
      让! _ = stream.AsyncRead 1
      因为我在1..1000做
        让aggregatedDeals =
          {
            FirstDealTime = System.DateTime.Now
            LastDealTime = System.DateTime.Now
            开始时间= System.DateTime.Now
            结束时间= System.DateTime.Now
            打开=1米
            高=1米
            低=1米
            关闭=1米
            VolumeWeightedPrice =1米
            TotalQuantity =1米
          }
        做!写流aggregatedDeals
    } |> Async.Start
}让客户端()= {异步
  让计时器= System.Diagnostics.Stopwatch.StartNew()
  使用客户=新System.Net.Sockets.TcpClient()
  client.Connect端点
  使用流= client.GetStream()
  做! stream.AsyncWrite [| 0uy |]
  因为我在1..1000做
    让! _ =读流
    若i = 1,则锁定标准输出(乐趣() - GT;
      printfn首先导致%FStimer.Elapsed.TotalSeconds)
  锁定标准输出(乐趣() - GT;
    printfn1000结果%FStimer.Elapsed.TotalSeconds)
}做
  服务器()|> Async.Start
  以次为{我1..100 - >客户端()}
  |> Async.Parallel
  |> Async.RunSynchronously
  |>忽视


解决方案

WCF几乎所有的默认选择非常安全值。在此之前的不要让新手开发者搬起石头砸自己的理念。但是,如果你知道要改变油门和绑定使用,你可以得到合理的性能和扩展能力。

在我的核心i5-2400(四核,没有启用超线程,3.10 GHz)的解决方案将在下面运行的1000个客户有1000回调为每20秒的平均总运行时间。这是百万WCF在20秒内调用。

不幸的是我不能让你的F#程序来进行直接比较运行。如果您在设备上运行我的解决方案,你能不能请张贴一些F#VS C#WCF性能比较数字?


免责声明:以下旨在成为一个概念证明。其中的一些设置没有意义进行生产。

我做了什么:


  • 删除了双螺旋结合并有客户创造他们自己的
    服务承载接收回调。实际上,这就是一个
    双工绑定引擎盖下做的。 (这也是PRATIK的
    建议)

  • 改变的结合NetTcpBinding的。

  • 改变限制值​​:

    • WCF:maxConcurrentCalls,maxConcurrentSessions,
      <一href=\"http://msdn.microsoft.com/en-us/library/system.servicemodel.description.servicethrottlingbehavior.maxconcurrentinstances.aspx\">maxConcurrentInstances所有1000

    • TCP绑定:MAXCONNECTIONS = 1000

    • 线程池:最小工作线程= 1000,最小的IO线程= 2000


  • IsOneWay 服务操作

请注意,在这个原型的所有服务和客户端在同一个应用程序域和共享相同的线程池。

我所学到的:


  • 当有一个无连接可以作出,因为目标机器积极拒绝,客户例外

    • 可能的原因:

      1. WCF已经达到极限

      2. TCP已经达到极限

      3. 有可用于处理呼叫没有I / O线程。


    • 为#3的解决方案是要么:

      1. 增加分钟IO线程数-OR -

      2. 拥有的StockService做它的一个工作线程回调(这确实增加总运行时间)



  • 添加IsOneWay削减运行时间缩短了一半(从40秒到20秒)。

这是一个核心的i5-2400正在运行的程序输出。
注意计时器的使用不同比原来的问题(见code)。

 所有客户端主机打开。
服务主机打开。启动计时器...
preSS ENTER关闭主机一台,你看全部完成。
客户端100#1000完成结果0.0542168小号
客户端200#1000完成结果0.0794684小号
客户端300#1000完成结果0.0673078小号
客户端400#1000完成结果0.0527753小号
客户端500#1000完成结果0.0581796小号
客户端600#1000完成结果0.0770291小号
客户端700#1000完成结果0.0681298小号
客户端800#1000完成结果0.0649353小号
客户端900#1000完成结果0.0714947小号
客户#1000完成1000结果0.0450857小号
全做完了。客户端总数:1000总运行时间:19323毫秒

code都在一个控制台应用程序文件:

 使用系统;
使用System.Collections.Generic;
使用System.ServiceModel;
使用System.Diagnostics程序;
使用的System.Threading;
使用System.Runtime.Serialization;命名空间StockApp
{
    [DataContract]
    公共类股票
    {
        [数据成员]
        公众的DateTime FirstDealDate {搞定;组; }
        [数据成员]
        公众的DateTime LastDealDate {搞定;组; }
        [数据成员]
        公开日期时间起始日期{搞定;组; }
        [数据成员]
        公共DateTime的结束日期{搞定;组; }
        [数据成员]
        公共小数打开{搞定;组; }
        [数据成员]
        公共小数高{搞定;组; }
        [数据成员]
        公共小数低{搞定;组; }
        [数据成员]
        公共小数关闭{搞定;组; }
        [数据成员]
        公共小数VolumeWeightedPrice {搞定;组; }
        [数据成员]
        公共小数TotalQuantity {搞定;组; }
    }    [服务合同]
    公共接口IStock
    {
        [OperationContract的(IsOneWay =真)]
        无效GetStocks(字符串地址);
    }    [服务合同]
    公共接口IPutStock
    {
        [OperationContract的(IsOneWay =真)]
        无效PutStock(股份股票);
    }    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)
    公共类StocksService istock提供
    {
        公共无效SendStocks(obj对象)
        {
            字符串的地址=(字符串)OBJ;
            &的ChannelFactory LT; IPutStock&GT;厂=新的ChannelFactory&LT; IPutStock&GT;(CallbackClientEndpoint);
            IPutStock回调= factory.CreateChannel(新的EndpointAddress(地址));            ST股票= NULL; ST =新股票
            {
                FirstDealDate = System.DateTime.Now,
                LastDealDate = System.DateTime.Now,
                起始日期= System.DateTime.Now,
                结束日期= System.DateTime.Now,
                打开= 495,
                高= 495,
                低= 495,
                关闭= 495,
                VolumeWeightedPrice = 495,
                TotalQuantity = 495
            };            的for(int i = 0; I&LT; 1000; ++ I)
                callback.PutStock(ST);            //Console.WriteLine(\"Done调用{0},地址);            ((ICommunicationObject)回调).Shutdown();
            factory.Shutdown();
        }        公共无效GetStocks(字符串地址)
        {
            /// WCF服务方法执行上的IO线程。
            ///传递事推到工作线程提高服务响应速度......在总运行时间可衡量的成本。
            System.Threading.ThreadPool.QueueUserWorkItem(新System.Threading.WaitCallback(SendStocks),地址);            // SendStocks(地址);
        }
    }    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)
    公共类回调:IPutStock
    {
        公共静态INT CallbacksCompleted = 0;
        System.Diagnostics.Stopwatch定时器= Stopwatch.StartNew();
        INT N = 0;        公共无效PutStock(ST股票)
        {
            ++ N;
            如果(N = = 1000)
            {
                //Console.WriteLine(\"1,000结果为+ this.timer.Elapsed.TotalSeconds +的s);                INT compelted = Interlocked.Increment(REF CallbacksCompleted);
                如果(compelted%100 == 0)
                {
                    Console.WriteLine(客户端#{0}完成1000结果{1}的,compelted,this.timer.Elapsed.TotalSeconds);                    如果(compelted == Program.CLIENT_COUNT)
                    {
                        Console.WriteLine(客户端的所有DONE总数:{0}总运行时间:{1}毫秒。Program.CLIENT_COUNT,Program.ProgramTimer.ElapsedMilliseconds);
                    }
                }
            }
        }
    }    类节目
    {
        公共const int的CLIENT_COUNT = 1000; //使用不同的值测试        公共静态System.Diagnostics.Stopwatch ProgramTimer;        静态无效StartCallPool(对象uriObj)
        {
            字符串callbackUri =(字符串)uriObj;
            &的ChannelFactory LT; IStock&GT;厂=新的ChannelFactory&LT; IStock&GT;(StockClientEndpoint);
            IStock代理= factory.CreateChannel();            proxy.GetStocks(callbackUri);            ((ICommunicationObject)代理).Shutdown();
            factory.Shutdown();
        }        静态无效测试()
        {
            ThreadPool.SetMinThreads(CLIENT_COUNT,CLIENT_COUNT * 2);            //创建所有将收到回调的主机。
            清单&LT;&ServiceHost的GT; callBackHosts =新的List&LT;&ServiceHost的GT;();
            的for(int i = 0; I&LT; CLIENT_COUNT ++ I)
            {
                串口=的String.Format({0},I).PadLeft(3,'0');
                字符串baseAddress =的net.tcp://本地主机:7+端口+/;
                ServiceHost的callbackHost =新的ServiceHost(typeof运算(回调),新的URI [] {新的URI(baseAddress)});
                callbackHost.Open();
                callBackHosts.Add(callbackHost);
            }
            Console.WriteLine(所有客户端主机打开。);            ServiceHost的stockHost =新的ServiceHost(typeof运算(StocksService));
            stockHost.Open();            Console.WriteLine(服务主机打开启动计时器...。);
            ProgramTimer = Stopwatch.StartNew();            的foreach(在callBackHosts VAR callbackHost)
            {
                ThreadPool.QueueUserWorkItem(新WaitCallback(StartCallPool),callbackHost.BaseAddresses [0] .AbsoluteUri);
            }            Console.WriteLine(preSS ENTER一旦你看到全部完成,关闭主机。);
            到Console.ReadLine();            的foreach(在callBackHosts VAR H)
                h.Shutdown();
            stockHost.Shutdown();
        }        静态无效的主要(字串[] args)
        {
            测试();
        }
    }    公共静态类扩展
    {
        静态公共无效关机(这ICommunicationObject OBJ)
        {
            尝试
            {
                obj.Close();
            }
            赶上(异常前)
            {
                Console.WriteLine(关机异常:{0},ex.Message);
                obj.Abort();
            }
        }
    }
}

的app.config:

 &LT;?XML版本=1.0编码=UTF-8&GT?;
&LT;结构&gt;
  &LT; system.serviceModel&GT;
    &LT;服务和GT;
      &LT;服务名称=StockApp.StocksService&GT;
        &LT;主机&GT;
          &LT; baseAddresses&GT;
            &LT;添加baseAddress =的net.tcp://本地主机:8123 / StockApp //&GT;
          &LT; / baseAddresses&GT;
        &LT; /主机&GT;
        &LT;端点地址=绑定=NetTcpBinding的bindingConfiguration =TCPCONFIG合同=StockApp.IStock&GT;
          &LT;同一性GT;
            &LT; D​​NS值=本地主机/&GT;
          &LT; /身份&GT;
        &LT; /端点&GT;
      &LT; /服务&GT;      &LT;服务名称=StockApp.Callback&GT;
        &LT;主机&GT;
          &LT; baseAddresses&GT;
            &LT;! - 基址在运行时定义。 - &GT;
          &LT; / baseAddresses&GT;
        &LT; /主机&GT;
        &LT;端点地址=绑定=NetTcpBinding的bindingConfiguration =TCPCONFIG合同=StockApp.IPutStock&GT;
          &LT;同一性GT;
            &LT; D​​NS值=本地主机/&GT;
          &LT; /身份&GT;
        &LT; /端点&GT;
      &LT; /服务&GT;
    &LT; /服务&GT;    &LT;客户端&GT;
      &LT;端点名称=StockClientEndpoint
                地址=的net.tcp://本地主机:8123 / StockApp /
                                绑定=NetTcpBinding的
                bindingConfiguration =TCPCONFIG
                                合同=StockApp.IStock&GT;
      &LT; /端点&GT;      &所述;! - CallbackClientEndpoint地址在运行时定义的。 - &GT;
      &LT;端点名称=CallbackClientEndpoint
                绑定=NetTcpBinding的
                bindingConfiguration =TCPCONFIG
                合同=StockApp.IPutStock&GT;
      &LT; /端点&GT;
    &LT; /客户&GT;    &LT;&行为GT;
      &LT; serviceBehaviors&GT;
        &LT;&行为GT;
          &所述;! - &下; serviceMetadata httpGetEnabled =真/&GT - →;
          &LT; serviceDebug includeExceptionDetailInFaults =真/&GT;
          &LT; serviceThrottling maxConcurrentCalls =1000maxConcurrentSessions =1000maxConcurrentInstances =1000/&GT;
        &LT; /行为&GT;
      &LT; / serviceBehaviors&GT;
    &LT; /行为&GT;    &LT;&绑定GT;
      &LT;&NetTcpBinding的GT;
        &LT;绑定名称=TCPCONFIG将ListenBackLog =100MAXCONNECTIONS =1000&GT;
          &LT;安全模式=无/&GT;
          &LT;启用的ReliableSession =FALSE/&GT;
        &LT; /&结合GT;
      &LT; / NetTcpBinding的&GT;
    &LT; /绑定&GT;
  &LT; /system.serviceModel>
&LT; /结构&gt;


更新
我只是想用netNamedPipeBinding上述溶液:

 &LT; netNamedPipeBinding&GT;
    &LT;绑定名称=pipeConfigMAXCONNECTIONS =1000&GT;
      &LT;安全模式=无/&GT;
    &LT; /&结合GT;
  &LT; / netNamedPipeBinding&GT;

这实际上得到3秒慢(从20到23秒)。由于这个特殊的例子是所有进程间,我不知道为什么。如果任何人有一些见解,请评论。

I'm trying to port a simple async TCP server in F# to C# 4. The server receives a connection, reads a single request and streams back a sequence of responses before closing the connection.

Async in C# 4 looks tedious and error prone so I thought I'd try using WCF instead. This server is not unlikely to see 1,000 simultaneous requests in the wild so I think both throughput and latency are of interest.

I've written a minimal duplex WCF web service and console client in C#. Although I'm using WCF instead of raw sockets, this is already 175 lines of code compared to 80 lines for the original. But I'm more concerned about the performance and scalability:

  • Latency is 154× worse with WCF.
  • Throughput is 54× worse with WCF.
  • TCP handles 1,000 simultaneous connections easily but WCF chokes on just 20.

Firstly, I'm using the default settings for everything so I'm wondering if there is anything I can tweak to improve these performance figures?

Secondly, I'm wondering if anyone is using WCF for this kind of thing or if it is the wrong tool for the job?

Here's my WCF server in C#:

IService1.cs

[DataContract]
public class Stock
{
  [DataMember]
  public DateTime FirstDealDate { get; set; }
  [DataMember]
  public DateTime LastDealDate { get; set; }
  [DataMember]
  public DateTime StartDate { get; set; }
  [DataMember]
  public DateTime EndDate { get; set; }
  [DataMember]
  public decimal Open { get; set; }
  [DataMember]
  public decimal High { get; set; }
  [DataMember]
  public decimal Low { get; set; }
  [DataMember]
  public decimal Close { get; set; }
  [DataMember]
  public decimal VolumeWeightedPrice { get; set; }
  [DataMember]
  public decimal TotalQuantity { get; set; }
}

[ServiceContract(CallbackContract = typeof(IPutStock))]
public interface IStock
{
  [OperationContract]
  void GetStocks();
}

public interface IPutStock
{
  [OperationContract]
  void PutStock(Stock stock);
}

Service1.svc

<%@ ServiceHost Language="C#" Debug="true" Service="DuplexWcfService2.Stocks" CodeBehind="Service1.svc.cs" %>

Service1.svc.cs

 [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple)]
 public class Stocks : IStock
 {
   IPutStock callback;

   #region IStock Members
   public void GetStocks()
   {
     callback = OperationContext.Current.GetCallbackChannel<IPutStock>();
     Stock st = null;
     st = new Stock
     {
       FirstDealDate = System.DateTime.Now,
       LastDealDate = System.DateTime.Now,
       StartDate = System.DateTime.Now,
       EndDate = System.DateTime.Now,
       Open = 495,
       High = 495,
       Low = 495,
       Close = 495,
       VolumeWeightedPrice = 495,
       TotalQuantity = 495
     };
     for (int i=0; i<1000; ++i)
       callback.PutStock(st);
   }
   #endregion
 }

Web.config

<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>
  <system.serviceModel>
    <services>
      <service name="DuplexWcfService2.Stocks">
        <endpoint address="" binding="wsDualHttpBinding" contract="DuplexWcfService2.IStock">
          <identity>
            <dns value="localhost"/>
          </identity>
        </endpoint>
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
  </system.serviceModel>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
  </system.webServer>
</configuration>

Here's the C# WCF client:

Program.cs

 [CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false)]
 class Callback : DuplexWcfService2.IStockCallback
 {
   System.Diagnostics.Stopwatch timer;
   int n;

   public Callback(System.Diagnostics.Stopwatch t)
   {
     timer = t;
     n = 0;
   }

   public void PutStock(DuplexWcfService2.Stock st)
   {
     ++n;
     if (n == 1)
       Console.WriteLine("First result in " + this.timer.Elapsed.TotalSeconds + "s");
     if (n == 1000)
       Console.WriteLine("1,000 results in " + this.timer.Elapsed.TotalSeconds + "s");
   }
 }

 class Program
 {
   static void Test(int i)
   {
     var timer = System.Diagnostics.Stopwatch.StartNew();
     var ctx = new InstanceContext(new Callback(timer));
     var proxy = new DuplexWcfService2.StockClient(ctx);
     proxy.GetStocks();
     Console.WriteLine(i + " connected");
   }

   static void Main(string[] args)
   {
     for (int i=0; i<10; ++i)
     {
       int j = i;
       new System.Threading.Thread(() => Test(j)).Start();
     }
   }
 }

Here's my async TCP client and server code in F#:

type AggregatedDeals =
  {
    FirstDealTime: System.DateTime
    LastDealTime: System.DateTime
    StartTime: System.DateTime
    EndTime: System.DateTime
    Open: decimal
    High: decimal
    Low: decimal
    Close: decimal
    VolumeWeightedPrice: decimal
    TotalQuantity: decimal
  }

let read (stream: System.IO.Stream) = async {
  let! header = stream.AsyncRead 4
  let length = System.BitConverter.ToInt32(header, 0)
  let! body = stream.AsyncRead length
  let fmt = System.Runtime.Serialization.Formatters.Binary.BinaryFormatter()
  use stream = new System.IO.MemoryStream(body)
  return fmt.Deserialize(stream)
}

let write (stream: System.IO.Stream) value = async {
  let body =
    let fmt = System.Runtime.Serialization.Formatters.Binary.BinaryFormatter()
    use stream = new System.IO.MemoryStream()
    fmt.Serialize(stream, value)
    stream.ToArray()
  let header = System.BitConverter.GetBytes body.Length
  do! stream.AsyncWrite header
  do! stream.AsyncWrite body
}

let endPoint = System.Net.IPEndPoint(System.Net.IPAddress.Loopback, 4502)

let server() = async {
  let listener = System.Net.Sockets.TcpListener(endPoint)
  listener.Start()
  while true do
    let client = listener.AcceptTcpClient()
    async {
      use stream = client.GetStream()
      let! _ = stream.AsyncRead 1
      for i in 1..1000 do
        let aggregatedDeals =
          {
            FirstDealTime = System.DateTime.Now
            LastDealTime = System.DateTime.Now
            StartTime = System.DateTime.Now
            EndTime = System.DateTime.Now
            Open = 1m
            High = 1m
            Low = 1m
            Close = 1m
            VolumeWeightedPrice = 1m
            TotalQuantity = 1m
          }
        do! write stream aggregatedDeals
    } |> Async.Start
}

let client() = async {
  let timer = System.Diagnostics.Stopwatch.StartNew()
  use client = new System.Net.Sockets.TcpClient()
  client.Connect endPoint
  use stream = client.GetStream()
  do! stream.AsyncWrite [|0uy|]
  for i in 1..1000 do
    let! _ = read stream
    if i=1 then lock stdout (fun () ->
      printfn "First result in %fs" timer.Elapsed.TotalSeconds)
  lock stdout (fun () ->
    printfn "1,000 results in %fs" timer.Elapsed.TotalSeconds)
}

do
  server() |> Async.Start
  seq { for i in 1..100 -> client() }
  |> Async.Parallel
  |> Async.RunSynchronously
  |> ignore

解决方案

WCF selects very safe values for almost all its defaults. This follows the philosophy of don’t let the novice developer shoot themselves. However if you know the throttles to change and the bindings to use, you can get reasonable performance and scaling.

On my core i5-2400 (quad core, no hyper threading, 3.10 GHz) the solution below will run 1000 clients with a 1000 callbacks each for an average total running time of 20 seconds. That’s 1,000,000 WCF calls in 20 seconds.

Unfortunately I couldn’t get your F# program to run for a direct comparison. If you run my solution on your box, could you please post some F# vs C# WCF performance comparison numbers?


Disclaimer: The below is intended to be a proof of concept. Some of these settings don’t make sense for production.

What I did:

  • Removed the duplex binding and had the clients create their own service hosts to receive the callbacks. This is essentially what a duplex binding is doing under the hood. (It’s also Pratik’s suggestion)
  • Changed the binding to netTcpBinding.
  • Changed throttling values:
    • WCF: maxConcurrentCalls, maxConcurrentSessions, maxConcurrentInstances all to 1000
    • TCP binding: maxConnections=1000
    • Threadpool: Min worker threads = 1000, Min IO threads = 2000
  • Added IsOneWay to the service operations

Note that in this prototype all services and clients are in the same App Domain and sharing the same thread pool.

What I learned:

  • When a client got a "No connection could be made because the target machine actively refused it" exception
    • Possible causes:

      1. WCF limit had been reached
      2. TCP limit had been reached
      3. There was no I/O thread available to handle the call.

    • The solution for #3 was either to:

      1. Increase the min IO thread count -OR-
      2. Have the StockService do its callbacks on a worker thread (this does increase total runtime)

  • Adding IsOneWay cut the running time in half (from 40 sec to 20 sec).

Program output running on a core i5-2400. Note the timers are used differently than in the original question (see the code).

All client hosts open.
Service Host opened. Starting timer...
Press ENTER to close the host one you see 'ALL DONE'.
Client #100 completed 1,000 results in 0.0542168 s
Client #200 completed 1,000 results in 0.0794684 s
Client #300 completed 1,000 results in 0.0673078 s
Client #400 completed 1,000 results in 0.0527753 s
Client #500 completed 1,000 results in 0.0581796 s
Client #600 completed 1,000 results in 0.0770291 s
Client #700 completed 1,000 results in 0.0681298 s
Client #800 completed 1,000 results in 0.0649353 s
Client #900 completed 1,000 results in 0.0714947 s
Client #1000 completed 1,000 results in 0.0450857 s
ALL DONE. Total number of clients: 1000 Total runtime: 19323 msec

Code all in one console application file:

using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Diagnostics;
using System.Threading;
using System.Runtime.Serialization;

namespace StockApp
{
    [DataContract]
    public class Stock
    {
        [DataMember]
        public DateTime FirstDealDate { get; set; }
        [DataMember]
        public DateTime LastDealDate { get; set; }
        [DataMember]
        public DateTime StartDate { get; set; }
        [DataMember]
        public DateTime EndDate { get; set; }
        [DataMember]
        public decimal Open { get; set; }
        [DataMember]
        public decimal High { get; set; }
        [DataMember]
        public decimal Low { get; set; }
        [DataMember]
        public decimal Close { get; set; }
        [DataMember]
        public decimal VolumeWeightedPrice { get; set; }
        [DataMember]
        public decimal TotalQuantity { get; set; }
    }

    [ServiceContract]
    public interface IStock
    {
        [OperationContract(IsOneWay = true)]
        void GetStocks(string address);
    }

    [ServiceContract]
    public interface IPutStock
    {
        [OperationContract(IsOneWay = true)]
        void PutStock(Stock stock);
    } 

    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    public class StocksService : IStock
    {
        public void SendStocks(object obj)
        {
            string address = (string)obj;
            ChannelFactory<IPutStock> factory = new ChannelFactory<IPutStock>("CallbackClientEndpoint");
            IPutStock callback = factory.CreateChannel(new EndpointAddress(address));

            Stock st = null; st = new Stock
            {
                FirstDealDate = System.DateTime.Now,
                LastDealDate = System.DateTime.Now,
                StartDate = System.DateTime.Now,
                EndDate = System.DateTime.Now,
                Open = 495,
                High = 495,
                Low = 495,
                Close = 495,
                VolumeWeightedPrice = 495,
                TotalQuantity = 495
            };

            for (int i = 0; i < 1000; ++i)
                callback.PutStock(st);

            //Console.WriteLine("Done calling {0}", address);

            ((ICommunicationObject)callback).Shutdown();
            factory.Shutdown();
        }

        public void GetStocks(string address)
        {
            /// WCF service methods execute on IO threads. 
            /// Passing work off to worker thread improves service responsiveness... with a measurable cost in total runtime.
            System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(SendStocks), address);

            // SendStocks(address);
        }
    } 

    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
    public class Callback : IPutStock
    {
        public static int CallbacksCompleted = 0;
        System.Diagnostics.Stopwatch timer = Stopwatch.StartNew();
        int n = 0;

        public void PutStock(Stock st)
        {
            ++n;
            if (n == 1000)
            {
                //Console.WriteLine("1,000 results in " + this.timer.Elapsed.TotalSeconds + "s");

                int compelted = Interlocked.Increment(ref CallbacksCompleted);
                if (compelted % 100 == 0)
                {
                    Console.WriteLine("Client #{0} completed 1,000 results in {1} s", compelted, this.timer.Elapsed.TotalSeconds);

                    if (compelted == Program.CLIENT_COUNT)
                    {
                        Console.WriteLine("ALL DONE. Total number of clients: {0} Total runtime: {1} msec", Program.CLIENT_COUNT, Program.ProgramTimer.ElapsedMilliseconds);
                    }
                }
            }
        }
    }

    class Program
    {
        public const int CLIENT_COUNT = 1000;           // TEST WITH DIFFERENT VALUES

        public static System.Diagnostics.Stopwatch ProgramTimer;

        static void StartCallPool(object uriObj)
        {
            string callbackUri = (string)uriObj;
            ChannelFactory<IStock> factory = new ChannelFactory<IStock>("StockClientEndpoint");
            IStock proxy = factory.CreateChannel();

            proxy.GetStocks(callbackUri);

            ((ICommunicationObject)proxy).Shutdown();
            factory.Shutdown();
        }

        static void Test()
        {
            ThreadPool.SetMinThreads(CLIENT_COUNT, CLIENT_COUNT * 2);

            // Create all the hosts that will recieve call backs.
            List<ServiceHost> callBackHosts = new List<ServiceHost>();
            for (int i = 0; i < CLIENT_COUNT; ++i)
            {
                string port = string.Format("{0}", i).PadLeft(3, '0');
                string baseAddress = "net.tcp://localhost:7" + port + "/";
                ServiceHost callbackHost = new ServiceHost(typeof(Callback), new Uri[] { new Uri( baseAddress)});
                callbackHost.Open();
                callBackHosts.Add(callbackHost);            
            }
            Console.WriteLine("All client hosts open.");

            ServiceHost stockHost = new ServiceHost(typeof(StocksService));
            stockHost.Open();

            Console.WriteLine("Service Host opened. Starting timer...");
            ProgramTimer = Stopwatch.StartNew();

            foreach (var callbackHost in callBackHosts)
            {
                ThreadPool.QueueUserWorkItem(new WaitCallback(StartCallPool), callbackHost.BaseAddresses[0].AbsoluteUri);
            }

            Console.WriteLine("Press ENTER to close the host once you see 'ALL DONE'.");
            Console.ReadLine();

            foreach (var h in callBackHosts)
                h.Shutdown();
            stockHost.Shutdown(); 
        }

        static void Main(string[] args)
        {
            Test();
        }
    }

    public static class Extensions
    {
        static public void Shutdown(this ICommunicationObject obj)
        {
            try
            {
                obj.Close();
            }
            catch (Exception ex)
            {
                Console.WriteLine("Shutdown exception: {0}", ex.Message);
                obj.Abort();
            }
        }
    }
}

app.config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="StockApp.StocksService">
        <host>
          <baseAddresses>
            <add baseAddress="net.tcp://localhost:8123/StockApp/"/>
          </baseAddresses>
        </host>
        <endpoint address="" binding="netTcpBinding" bindingConfiguration="tcpConfig" contract="StockApp.IStock">
          <identity>
            <dns value="localhost"/>
          </identity>
        </endpoint>
      </service>

      <service name="StockApp.Callback">
        <host>
          <baseAddresses>
            <!-- Base address defined at runtime. -->
          </baseAddresses>
        </host>
        <endpoint address="" binding="netTcpBinding" bindingConfiguration="tcpConfig" contract="StockApp.IPutStock">
          <identity>
            <dns value="localhost"/>
          </identity>
        </endpoint>
      </service>
    </services>

    <client>
      <endpoint name="StockClientEndpoint"
                address="net.tcp://localhost:8123/StockApp/"
                                binding="netTcpBinding"
                bindingConfiguration="tcpConfig"
                                contract="StockApp.IStock" >
      </endpoint>

      <!-- CallbackClientEndpoint address defined at runtime. -->
      <endpoint name="CallbackClientEndpoint"
                binding="netTcpBinding"
                bindingConfiguration="tcpConfig"
                contract="StockApp.IPutStock" >
      </endpoint>
    </client>

    <behaviors>
      <serviceBehaviors>
        <behavior>
          <!--<serviceMetadata httpGetEnabled="true"/>-->
          <serviceDebug includeExceptionDetailInFaults="true"/>
          <serviceThrottling maxConcurrentCalls="1000" maxConcurrentSessions="1000" maxConcurrentInstances="1000" />
        </behavior>
      </serviceBehaviors>
    </behaviors>

    <bindings>
      <netTcpBinding>
        <binding name="tcpConfig" listenBacklog="100" maxConnections="1000">
          <security mode="None"/>
          <reliableSession enabled="false" />
        </binding>
      </netTcpBinding>
    </bindings>
  </system.serviceModel>
</configuration>


Update: I just tried the above solution with a netNamedPipeBinding:

  <netNamedPipeBinding >
    <binding name="pipeConfig" maxConnections="1000" >
      <security mode="None"/>
    </binding>
  </netNamedPipeBinding>

It actually got 3 seconds slower (from 20 to 23 seconds). Since this particular example is all inter-process, I'm not sure why. If anyone has some insights, please comment.

这篇关于WCF性能,延迟和可扩展性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆