Integrações entre Sistemas – Parte 4 – WCF (Soap e net.tcp)

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.

Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s