Objetivo
Nesta parte faremos a discussão do código da comunicação através de Web Service e net.tcp.
Servidor
O servidor foi escrito como um serviço WCF simples, com dois endpoints configurados, um net.tcp, e um soap. O código para o serviço segue:
public class IntegrationTestsService : IIntegrationTestsService
{
private static string connString;
private void GetConnString()
{
connString = System.Configuration.ConfigurationManager.AppSettings["ConnString"];
}
public ServiceTable GetServiceTable(int ServiceTableID)
{
if (String.IsNullOrEmpty(connString))
GetConnString();
return DAO.GetServiceTable(connString, ServiceTableID);
}
public List<ServiceTable> GetServiceTables(int IDInicial, int IDFinal)
{
if (String.IsNullOrEmpty(connString))
GetConnString();
List<ServiceTable> l = new List<ServiceTable>();
for (int i = IDInicial; i <= IDFinal; i++)
{
l.Add(DAO.GetServiceTable(connString, i));
}
return l;
}
}
A connection string está sendo recuperada do web.config, e os mesmos métodos da classe DAO são usados. O primeiro método, GetServiceTable é utilizado na chamada “simples”, pergunta e resposta.
O método GetServiceTables, retorna uma lista baseado num ID inicial e final. Ele será utilizado nas chamadas em lote.
WCF em pequenos requests
Este teste também foi implementado em MSBuild, na task WCFSmallRequestsTest. A mesma task pode ser usada tanto para http quanto para net.tcp, dependendo da configuração. Segue o código:
public override bool Execute()
{
Binding binding;
if (this.EndpointType == "http")
{
binding = new BasicHttpBinding();
((BasicHttpBinding)binding).MessageEncoding = WSMessageEncoding.Text;
((BasicHttpBinding)binding).TextEncoding = Encoding.UTF8;
((BasicHttpBinding)binding).TransferMode = TransferMode.Buffered;
((BasicHttpBinding)binding).Security.Mode = BasicHttpSecurityMode.None;
}
else if (this.EndpointType == "nettcp")
{
binding = new NetTcpBinding();
((NetTcpBinding)binding).MaxReceivedMessageSize = 1024 * 1024;
((NetTcpBinding)binding).Security.Mode = SecurityMode.None;
((NetTcpBinding)binding).CloseTimeout = new TimeSpan(0, 1, 0);
((NetTcpBinding)binding).OpenTimeout = new TimeSpan(0, 1, 10);
((NetTcpBinding)binding).ReceiveTimeout = new TimeSpan(0, 1, 10);
((NetTcpBinding)binding).SendTimeout = new TimeSpan(0, 1, 10);
}
else
throw new ArgumentException("Invalid value for EndpointType. Expected: http, nettcp");
EndpointAddress address = new EndpointAddress(new Uri(WebServiceUri));
IntegrationTestsService.IntegrationTestsServiceClient client = new IntegrationTestsService.IntegrationTestsServiceClient(binding, address);
Log.LogMessage("Doing " + TotalRequests.ToString() + " calls");
Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 1; i <= TotalRequests; i++)
{
ServiceTable t = null;
bool tryAgain = true;
while (tryAgain)
{
try
{
t = client.GetServiceTable(i);
tryAgain = false;
}
catch (EndpointNotFoundException)
{
Thread.Sleep(100);
t = client.GetServiceTable(i);
tryAgain = true;
}
}
ServiceTable t2 = new ServiceTable();
t2.ServiceTableID = t.ServiceTableID;
t2.DescServiceTable = t.DescServiceTable;
t2.Value = t.Value;
t2.CreationDate = t.CreationDate;
t2.StringField1 = t.StringField1;
t2.StringField2 = t.StringField2;
DAO.ProcessServiceTable(ConnString, t2);
}
watch.Stop();
Log.LogMessage("Total processing time: " + watch.Elapsed.TotalSeconds.ToString("0.00") + " seconds");
return true;
}
Neste teste, o serviço foi importado, gerando o namespace IntegrationTestsService. O método GetServiceTable é chamado de um em um para concluir a transferência de todo o lote de informações.
WCF em pequenos lotes
Este teste foi implementado na task MSBuild WCFSmallBatchesTest. Segue o código:
public override bool Execute()
{
Binding binding;
if (this.EndpointType == "http")
{
binding = new BasicHttpBinding();
((BasicHttpBinding)binding).MaxReceivedMessageSize = 2048 * 1024;
}
else if (this.EndpointType == "nettcp")
{
binding = new NetTcpBinding();
((NetTcpBinding)binding).MaxReceivedMessageSize = 1024 * 1024;
((NetTcpBinding)binding).Security.Mode = SecurityMode.None;
((NetTcpBinding)binding).CloseTimeout = new TimeSpan(0, 0, 10);
((NetTcpBinding)binding).OpenTimeout = new TimeSpan(0, 0, 10);
((NetTcpBinding)binding).ReceiveTimeout = new TimeSpan(0, 0, 10);
((NetTcpBinding)binding).SendTimeout = new TimeSpan(0, 0, 10);
}
else
throw new ArgumentException("Invalid value for EndpointType. Expected: http, nettcp");
EndpointAddress address = new EndpointAddress(new Uri(WebServiceUri));
IntegrationTestsService.IntegrationTestsServiceClient client = new IntegrationTestsService.IntegrationTestsServiceClient(binding, address);
Log.LogMessage("Doing " + TotalBatches.ToString() + " batch calls with " + BatchSize.ToString() + " itens each");
Stopwatch watch = new Stopwatch();
watch.Start();
int count = 1;
for (int i = 0; i < TotalBatches; i++)
{
ServiceTable[] stArray = client.GetServiceTables(count, count + (BatchSize - 1));
foreach (ServiceTable t in stArray)
{
ServiceTable t2 = new ServiceTable();
t2.ServiceTableID = t.ServiceTableID;
t2.DescServiceTable = t.DescServiceTable;
t2.Value = t.Value;
t2.CreationDate = t.CreationDate;
t2.StringField1 = t.StringField1;
t2.StringField2 = t.StringField2;
DAO.ProcessServiceTable(ConnString, t2);
}
count += BatchSize;
}
watch.Stop();
Log.LogMessage("Total processing time: " + watch.Elapsed.TotalSeconds.ToString("0.00") + " seconds");
return true;
}
A idéia deste código é invés de fazer uma chamada para cada registro desejado (o que resulta num request), empacotamos o request em pequenos lotes, ou seja, colocamos o ID inicial e final do request e recebemos a resposta numa lista de objetos ServiceTable.
A sugestão inicial era de 1000 em 1000, mas fiz uma melhoria no código para que o tamanho do batch possa ser configurado. Abaixo, veremos as diferenças de tempo.
Estatísticas e Conclusão
Abaixo observamos algumas estatísticas de execução (usando duas máquinas, numa rede local):
| Método | Tempos net.tcp (segundos) | Tempos soap (segundos) |
| 20.000 chamadas | 201,2 | 193,65 |
| 2000 chamadas em lotes de 10 | 40,87 | 39,75 |
| 200 chamadas em lotes de 100 | 24,46 | 25,2 |
| 20 chamadas em lotes de 1000 | 22,39 | 19,84 |
| 10 chamadas em lotes de 2000 | 20,23 | 18,82 |
| 5 chamadas em lotes de 4000 | 18,44 | 19 |
| 4 chamadas em lotes de 5000 | 18,69 | 17,94 |
Essas estatísticas obviamente sofrem variações em diferentes execuções, porém, percebemos que enviando as chamas em lotes obtemos ganhos significativos quando passamos para lotes de 10, depois lotes de 100. A partir daí, o ganho passa a ser muito pequeno (apesar de existir).
O trade-off aqui é que quanto maior o request, maior o custo de reprocessamento no caso de uma perda de request. Quanto mais aumentamos o request mais fugimos da característica do protocolo http, e consequentemente dependemos de configurações específicas no cliente e no servidor (aumentar o tamanho do request http, por exemplo).
Outra conclusão que podemos chegar é que o uso do net.tcp em relação ao soap, apresenta muito ganho no cenário de pequenso requests (20.000 chamadas). A medida que o request aumenta, a diferença já não é tão significativa.