Ответ 1
WCF выбирает очень безопасные значения почти для всех своих значений по умолчанию. Это следует за философией, позволяющей начинающему разработчику стрелять в себя. Однако, если вы знаете, что дроссели для изменения и привязки используются, вы можете получить разумную производительность и масштабирование.
На моем ядре i5-2400 (четырехъядерный процессор, без гиперпотоков, 3,10 ГГц) в приведенном ниже решении будут выполняться 1000 клиентов с 1000 обратными вызовами каждый для среднего общего времени работы 20 секунд. Thats 1,000,000 вызовов WCF через 20 секунд.
К сожалению, я не смог запустить вашу программу F # для прямого сравнения. Если вы запустите мое решение на своем поле, не могли бы вы разместить некоторые номера сравнения производительности F # vs С# WCF?
Отказ от ответственности. Ниже приведено доказательство концепции. Некоторые из этих настроек не имеют смысла для производства.
Что я сделал:
- Удалено привязку дуплекса, и клиенты создали свои собственные для получения обратных вызовов. Это, по сути, то, что дуплексное связывание выполняется под капотом. (Его также Пратикс предложение)
- Изменена привязка к netTcpBinding.
- Измененные значения дросселирования:
- WCF: maxConcurrentCalls, maxConcurrentSessions, maxConcurrentInstances все до 1000
- Связывание TCP: maxConnections = 1000
- Threadpool: Мин рабочих потоков = 1000, Мин IO потоков = 2000
- Добавлен IsOneWay в сервисные операции
Обратите внимание, что в этом прототипе все службы и клиенты находятся в одном домене приложений и используют один и тот же пул потоков.
Что я узнал:
- Когда клиент получил "Нет соединения, которое может быть сделано, потому что целевая машина активно отказалась от него" исключение
- Возможные причины:
- Достигнут лимит WCF
- Достигнут предел TCP
- Для обработки вызова не было потока ввода-вывода.
- Решение для № 3 было либо:
- Увеличьте минимальный счетчик ввода-вывода -OR -
- Попросите 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 секунд). Поскольку этот конкретный пример - это весь процесс, я не уверен, почему. Если у кого-то есть некоторые идеи, прокомментируйте.