WCF性能、延迟和可伸缩性

本文关键字:可伸缩性 延迟 性能 WCF | 更新日期: 2023-09-27 18:10:54

我正在尝试将一个简单的f#异步TCP服务器移植到c# 4。服务器接收到一个连接,读取一个请求,并在关闭连接之前返回一系列响应。

c# 4中的Async看起来乏味且容易出错,所以我想我会尝试使用WCF代替。这个服务器不太可能同时看到1000个请求,所以我认为吞吐量和延迟都是值得关注的。 我用c#写了一个最小的双工WCF web服务和控制台客户端。虽然我使用WCF代替原始套接字,但这已经是175行代码,而原始代码是80行。但是我更关心性能和可伸缩性:
  • 延迟是154×
  • 吞吐量为54×
  • TCP可以轻松处理1,000个同时连接,但WCF仅阻塞20个。

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

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

这是我用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>

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

这是我在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性能、延迟和可伸缩性

WCF为几乎所有默认值选择非常安全的值。这遵循了不要让新手开发者开枪自杀的原则。但是,如果您知道要更改的限制和要使用的绑定,则可以获得合理的性能和可伸缩性。

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

不幸的是,我不能让你的f#程序运行直接比较。如果你在你的机器上运行我的解决方案,你能发布一些f#和c# WCF性能比较数字吗?


免责声明 :下面是一个概念证明。有些设置对生产环境没有意义。

我做了:

  • 删除了双工绑定,并让客户端创建自己的双工绑定接收回调的服务主机。这就是a双面绑定是在幕后进行的。(这也是普拉提的建议)
  • 将绑定更改为netTcpBinding。
  • 更改的节流值:
      WCF: maxConcurrentCalls, maxconcurrentssessions,maxConcurrentInstances全部设置为1000
  • TCP绑定:maxConnections=1000
  • 线程池:最小工作线程数= 1000,最小IO线程数= 2000
  • 新增IsOneWay服务操作
  • 请注意,在这个原型中,所有的服务和客户端都在同一个App Domain中,并且共享同一个线程池。

    我的收获:

    • 当客户端收到"无法建立连接,因为目标机器主动拒绝它"异常时
      • 可能的原因:
        1. WCF已达到限值
        2. TCP限制已达到
        3. 没有可用的I/O线程来处理调用。
    • #3的解决方案是:
      1. 增加最小IO线程数- or -
      2. 让StockService在工作线程上做回调(这确实增加了总运行时间)
  • 添加IsOneWay将运行时间减半(从40秒减少到20秒)。
  • 在i5-2400内核上运行的程序输出。请注意,计时器的使用方式与原始问题中的不同(请参阅代码)。

    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:

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

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

    首先回答您的第二个问题,与原始套接字相比,WCF总是有开销。但与原始套接字相比,它具有大量的功能(如安全性、可靠性、互操作性、多种传输协议、跟踪等),您是否可以接受这种权衡取决于您的场景。看起来你正在做一些金融交易应用程序,WCF可能不适合你的情况(尽管我不是在金融行业,没有经验来证明这一点)。

    对于你的第一个问题,尝试在客户端托管一个单独的WCF服务,而不是双重http绑定,这样客户端可以自己成为一个服务,如果可能的话,使用netttcp绑定。调整服务行为中的serviceThrottling元素的属性。在。net 4之前,默认值更低。

    我想说这取决于你的目标。如果你想把你的硬件推到尽可能远的地方,那么很容易获得10000 +连接的客户端当然是可能的,秘诀是最小化花在垃圾收集器上的时间,并有效地使用套接字。

    我有一些关于f#中socket的帖子:http://moiraesoftware.com

    我正在做一些正在进行的工作与一个库称为骨折io在这里:https://github.com/fractureio/fracture

    你可能想看看这些想法…