简体   繁体   English

WCF性能,延迟和可伸缩性

[英]WCF performance, latency and scalability

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. 我正在尝试将F#中的简单异步TCP服务器移植到C#4。服务器接收连接,读取单个请求并在关闭连接之前回送一系列响应。

Async in C# 4 looks tedious and error prone so I thought I'd try using WCF instead. C#4中的异步看起来很乏味且容易出错,所以我想我会尝试使用WCF。 This server is not unlikely to see 1,000 simultaneous requests in the wild so I think both throughput and latency are of interest. 该服务器不太可能在野外看到1,000个同时发出的请求,因此我认为吞吐量和延迟都很重要。

I've written a minimal duplex WCF web service and console client in C#. 我在C#中编写了一个最小的双工WCF Web服务和控制台客户端。 Although I'm using WCF instead of raw sockets, this is already 175 lines of code compared to 80 lines for the original. 虽然我使用的是WCF而不是原始套接字,但这已经是175行代码,而原始版本只有80行。 But I'm more concerned about the performance and scalability: 但我更关注性能和可扩展性:

  • Latency is 154× worse with WCF. WCF的延迟是154倍。
  • Throughput is 54× worse with WCF. WCF的吞吐量低54倍。
  • TCP handles 1,000 simultaneous connections easily but WCF chokes on just 20. 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? 首先,我正在使用所有内容的默认设置,所以我想知道是否有任何我可以调整来改善这些性能数据?

Secondly, I'm wondering if anyone is using WCF for this kind of thing or if it is the wrong tool for the job? 其次,我想知道是否有人正在使用WCF进行此类事情,或者它是否是错误的工具?

Here's my WCF server in C#: 这是我在C#中的WCF服务器:

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: 这是C#WCF客户端:

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#: 这是我在F#中的异步TCP客户端和服务器代码:

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. WCF几乎为所有默认值选择非常安全的值。 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. 在我的核心i5-2400(四核,无超线程,3.10 GHz)上,下面的解决方案将运行1000个客户端,每个客户端有1000个回调,平均总运行时间为20秒。 That's 1,000,000 WCF calls in 20 seconds. 这是20秒内的1,000,000次WCF呼叫。

Unfortunately I couldn't get your F# program to run for a direct comparison. 不幸的是,我无法让你的F#程序直接进行比较。 If you run my solution on your box, could you please post some F# vs C# WCF performance comparison numbers? 如果你在你的盒子上运行我的解决方案,你能否发布一些F#vs C#WCF性能比较数字?


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) (这也是Pratik的建议)
  • Changed the binding to netTcpBinding. 将绑定更改为netTcpBinding。
  • Changed throttling values: 更改了限制值:
    • WCF: maxConcurrentCalls, maxConcurrentSessions, maxConcurrentInstances all to 1000 WCF:maxConcurrentCalls,maxConcurrentSessions, maxConcurrentInstances all to 1000
    • TCP binding : maxConnections=1000 TCP绑定 :maxConnections = 1000
    • Threadpool: Min worker threads = 1000, Min IO threads = 2000 Threadpool:最小工作线程数= 1000,最小IO线程数= 2000
  • Added IsOneWay to the service operations IsOneWay添加到服务操作中

Note that in this prototype all services and clients are in the same App Domain and sharing the same thread pool. 请注意,在此原型中,所有服务和客户端都位于同一App Domain中并共享相同的线程池。

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 已达到WCF限制
      2. TCP limit had been reached 已达到TCP限制
      3. There was no I/O thread available to handle the call. 没有可用于处理呼叫的I / O线程。
    • The solution for #3 was either to: #3的解决方案是:
      1. Increase the min IO thread count -OR- 增加最小IO线程数 - 或 -
      2. Have the StockService do its callbacks on a worker thread (this does increase total runtime) 让StockService在工作线程上执行其回调(这会增加总运行时间)
  • Adding IsOneWay cut the running time in half (from 40 sec to 20 sec). 添加IsOneWay可将运行时间减半(从40秒到20秒)。

Program output running on a core i5-2400. 程序输出在核心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: 的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尝试了上述解决方案:

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

It actually got 3 seconds slower (from 20 to 23 seconds). 它实际上慢了3秒(从20秒到23秒)。 Since this particular example is all inter-process, I'm not sure why. 由于这个特定的例子都是进程间的,我不知道为什么。 If anyone has some insights, please comment. 如果有人有一些见解,请发表评论。

To answer your second question first, WCF will always have a overhead when compared to raw sockets. 要先回答第二个问题,与原始套接字相比,WCF总是会有开销。 But it has a ton of functionality (like security, reliablity, interoperability, multiple transport protocols, tracing etc.) compared to raw sockets, whether the trade-off is acceptable to you is based on your scenario. 但与原始套接字相比,它具有大量功能(如安全性,可靠性,互操作性,多种传输协议,跟踪等),无论您是否接受权衡取决于您的方案。 It looks like you are doing some financial trading application and WCF is possibly not fit for your case (although I am not in finance industry to qualify this with experience). 看起来你正在做一些金融交易应用程序而且WCF可能不适合你的情况(虽然我不是在金融行业用经验来证明这一点)。

For your first question, instead of dual http binding try hosting a separate WCF service in the client so that the client can be a service by itself, and use the netTCP binding if possible. 对于您的第一个问题,请尝试在客户端中托管单独的WCF服务,而不是双http绑定,以便客户端可以自己成为服务,并在可能的情况下使用netTCP绑定。 Tweak the attributes in serviceThrottling element in service behavior. 调整服务行为中serviceThrottling元素中的属性。 The defaults were lower prior to .Net 4. .Net 4之前的默认值较低。

I would say it depends on your goals. 我会说这取决于你的目标。 If you want to push your hardware as far as possible then it is certainly possible to get 10,000+ connected clients easily, the secret is minimising time spent in the garbage collector and using sockets efficiently. 如果你想尽可能地推动你的硬件,那么当然可以轻松地获得10,000多个连接的客户端,秘诀是最大限度地减少在垃圾收集器中花费的时间并有效地使用套接字。

I have a few posts on Sockets in F# here: http://moiraesoftware.com 我在F#的Sockets上有一些帖子: http//moiraesoftware.com

Im doing some ongoing work with a library called Fracture-IO here: https://github.com/fractureio/fracture 我正在做一个名为Fracture-IO的库的正在进行的工作: https//github.com/fractureio/fracture

You might want to check those out for ideas... 您可能想要检查那些想法......

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM