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

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

问题描述

我正在尝试将 F# 中的简单异步 TCP 服务器移植到 C# 4.服务器接收连接、读取单个请求并在关闭连接之前将响应序列流回.

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.

C# 4 中的异步看起来很乏味而且容易出错,所以我想我会尝试使用 WCF.该服务器不太可能同时看到 1,000 个并发请求,因此我认为吞吐量和延迟都值得关注.

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.

我用 C# 编写了一个最小的双工 WCF Web 服务和控制台客户端.尽管我使用的是 WCF 而不是原始套接字,但与原始的 80 行相比,这已经是 175 行代码.但我更关心性能和可扩展性:

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:

  • WCF 的延迟是 154 倍.
  • WCF 的吞吐量是 54 倍.
  • TCP 可以轻松处理 1,000 个同时连接,但 WCF 仅处理 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?

其次,我想知道是否有人将 WCF 用于这种事情,或者它是否是完成这项工作的错误工具?

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

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

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>

这是 C# WCF 客户端:

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();
     }
   }
 }

这是我在 F# 中的异步 TCP 客户端和服务器代码:

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 为其几乎所有默认值选择了非常安全的值.这遵循了不要让新手开发人员自杀"的理念.但是,如果您知道要更改的节流阀和要使用的绑定,则可以获得合理的性能和扩展.

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.

在我的核心 i5-2400(四核,无超线程,3.10 GHz)上,以下解决方案将运行 1000 个客户端,每个客户端有 1000 个回调,平均总运行时间为 20 秒.即 20 秒内 1,000,000 个 WCF 调用.

很遗憾,我无法让您的 F# 程序运行以进行直接比较.如果你在你的机器上运行我的解决方案,你能不能发布一些 F# 与 C# WCF 的性能比较数据?

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.

我做了什么:

  • 删除了双工绑定并让客户创建自己的服务主机接收回调.这本质上是一个双面装订在幕后进行.(这也是 Pratik 的建议)
  • 将绑定更改为 netTcpBinding.
  • 更改了限制值:
    • WCF:maxConcurrentCalls、maxConcurrentSessions、maxConcurrentInstances 全部为 1000
    • TCP 绑定:maxConnections=1000
    • 线程池:最小工作线程数 = 1000,最小 IO 线程数 = 2000
    • 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

      请注意,在此原型中,所有服务和客户端都在同一个应用程序域中并共享同一个线程池.

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

      我学到了什么:

      • 当客户端收到无法建立连接,因为目标机器主动拒绝它"异常时
        • 可能的原因:
        • When a client got a "No connection could be made because the target machine actively refused it" exception
          • Possible causes:
          1. 已达到 WCF 限制
          2. 已达到 TCP 限制
          3. 没有可用于处理调用的 I/O 线程.

        • #3 的解决方案是:

        • The solution for #3 was either to:

          1. 增加最小 IO 线程数 -OR-
          2. 让 StockService 在工作线程上执行回调(这确实会增加总运行时间)

        • 在 i5-2400 核心上运行的程序输出.请注意,计时器的使用方式与原始问题中的不同(请参阅代码).

          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
          

          在一个控制台应用程序文件中编写所有代码:

          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>
          

          <小时>

          更新:我刚刚用 netNamedPipeBinding 尝试了上述解决方案:


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

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

          它实际上慢了 3 秒(从 20 秒到 23 秒).由于这个特定的例子都是进程间的,我不确定为什么.如果有人有一些见解,请发表评论.

          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天全站免登陆