gpt4 book ai didi

c# - WCF 性能、延迟和可伸缩性

转载 作者:可可西里 更新时间:2023-11-01 02:29:47 26 4
gpt4 key购买 nike

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

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

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

  • WCF 的延迟是原来的 154 倍。
  • WCF 的吞吐量降低了 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 客户端:

程序.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 几乎为其所有默认值选择了非常安全的值。这遵循了不要让新手开发人员自杀的理念。但是,如果您知道要更改的限制和要使用的绑定(bind),您可以获得合理的性能和缩放。

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

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


免责声明:以下内容旨在证明概念。其中一些设置对生产没有意义。

我做了什么:

  • 删除了双工绑定(bind)并让客户创建他们自己的服务主机接收回调。这本质上是一个双面绑定(bind)在引擎盖下进行。 (这也是 Pratik 的建议)
  • 将绑定(bind)更改为 netTcpBinding。
  • 更改了限制值:
    • WCF:maxConcurrentCalls、maxConcurrentSessions、 maxConcurrentInstances全部到 1000
    • TCP binding : 最大连接数=1000
    • 线程池:最小工作线程数 = 1000,最小 IO 线程数 = 2000
  • 已添加 IsOneWay服务运营

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

我学到了什么:

  • 当客户端收到“无法建立连接,因为目标机器主动拒绝”异常时
    • 可能的原因:
      1. 已达到 WCF 限制
      2. 已达到 TCP 限制
      3. 没有可用于处理调用的 I/O 线程。
    • #3 的解决方案是:
      1. 增加最小 IO 线程数 - 或 -
      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();
}
}
}
}

应用程序配置:

<?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 秒)。由于这个特定的例子都是进程间的,我不确定为什么。如果有人有一些见解,请发表评论。

关于c# - WCF 性能、延迟和可伸缩性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7710220/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com