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.