WCF 性能、延迟和可扩展性
我正在尝试将 F# 中的简单异步 TCP 服务器移植到 C# 4。服务器接收连接、读取单个请求并在关闭连接之前流回一系列响应。
C# 4 中的异步看起来很乏味且容易出错,所以我想我应该尝试使用 WCF。该服务器不太可能在野外看到 1,000 个并发请求,因此我认为吞吐量和延迟都很重要。
我用 C# 编写了一个最小的双工 WCF Web 服务和控制台客户端。尽管我使用 WCF 而不是原始套接字,但这已经是 175 行代码,而原始代码为 80 行。但我更关心性能和可扩展性:
- 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 客户端:
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
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
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
WCF 为其几乎所有默认值选择非常安全的值。这遵循了“不要让新手开发者开枪自杀”的理念。但是,如果您知道要更改的限制和要使用的绑定,则可以获得合理的性能和扩展。
在我的核心 i5-2400(四核,无超线程,3.10 GHz)上,下面的解决方案将运行 1000 个客户端,每个客户端 1000 个回调,平均总运行时间为 20 秒。这相当于 20 秒内 1,000,000 个 WCF 调用。
不幸的是,我无法运行您的 F# 程序来进行直接比较。如果您在您的机器上运行我的解决方案,您能否发布一些 F# 与 C# WCF 性能比较数据?
免责声明:以下内容旨在作为概念证明。其中一些设置对于生产没有意义。
我做了什么:
服务主机接收回调。这本质上就是一个
双工绑定是在幕后进行的。 (这也是普拉蒂克的
建议)
maxConcurrentInstances 全部设置为 1000
请注意,在此原型中所有服务和客户端位于同一个应用程序域并共享同一个线程池。
我学到了什么:
在 core i5-2400 上运行的程序输出。
请注意,计时器的使用方式与原始问题中的不同(请参阅代码)。
将所有代码集成到一个控制台应用程序文件中:
app.config:
更新:
我刚刚使用 netNamedPipeBinding 尝试了上述解决方案:
实际上慢了 3 秒(从 20 秒到 23 秒)。由于这个特定的示例都是进程间的,所以我不确定为什么。如果有人有一些见解,请发表评论。
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:
service hosts to receive the callbacks. This is essentially what a
duplex binding is doing under the hood. (It’s also Pratik’s
suggestion)
maxConcurrentInstances all to 1000
Note that in this prototype all services and clients are in the same App Domain and sharing the same thread pool.
What I learned:
Program output running on a core i5-2400.
Note the timers are used differently than in the original question (see the code).
Code all in one console application file:
app.config:
Update:
I just tried the above solution with a 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 始终会产生开销。但与原始套接字相比,它具有大量功能(如安全性、可靠性、互操作性、多种传输协议、跟踪等),您是否可以接受这种权衡取决于您的场景。看起来您正在做一些金融交易应用程序,而 WCF 可能不适合您的情况(尽管我不在金融行业,无法以经验来证明这一点)。
对于您的第一个问题,不要尝试在客户端中托管一个单独的 WCF 服务,而不是双重 http 绑定,以便客户端本身可以成为一个服务,并在可能的情况下使用 netTCP 绑定。调整服务行为中 serviceThrotdling 元素中的属性。在 .Net 4 之前,默认值较低。
To answer your second question first, WCF will always have a overhead when compared to raw sockets. 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).
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. Tweak the attributes in serviceThrottling element in service behavior. The defaults were lower prior to .Net 4.
我想说这取决于你的目标。如果您想尽可能地推动您的硬件,那么当然可以轻松获得 10,000 多个连接的客户端,秘诀是最大限度地减少垃圾收集器上花费的时间并有效地使用套接字。
我在这里有一些关于 F# 中的套接字的帖子:http://moiraesoftware.com
我正在使用一个名为 的库进行一些正在进行的工作Fracture-IO 在这里:https://github.com/fractureio/fracture
你可能想看看这些对于想法...
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.
I have a few posts on Sockets in F# here: http://moiraesoftware.com
Im doing some ongoing work with a library called Fracture-IO here: https://github.com/fractureio/fracture
You might want to check those out for ideas...