Teste, Carga e Monitoramento – Parte 3

Uma vez que o serviço esteja em funcionamento em algum lugar, podemos nos deparar com problemas de performance, escalabilidade, memória, etc., que podem aparecer por algum descuido, como por exemplo, questões de infraestrutura, tais como a política de gerenciamento de instâncias e concorrência, throttling, funcionalidades expostas pelos bindings, e ainda problemas relacionados ao código que foi escrito dentro da operação, como por exemplo, o uso desordenado de recursos custosos, o não encerramento correto de cada um deles, dados que poderiam ser compartilhados entre várias requisições, etc.

Todos esses problemas são, na maioria das vezes, “invisíveis” enquanto estamos desenvolvendo e testando, onde não há uma carga efetiva de requisições sendo disparadas contra ele, ou seja, por pior que esteja a configuração de infraestrutura ou o código, como sempre há um único usuário/sistema acessando o serviço, dificilmente irá esgotar os recursos do servidor (máquina) que hospeda e gerencia a execução do mesmo.

Quem pode ajudar a detectar esses possíveis problemas são os testes de carga, onde determinamos um limite de acesso concorrente e verificamos se ele se comporta bem. A partir do momento em que você não consegue atender um número mínimo de requisições em intervalo de tempo que você julga necessário, é um sinal de há algum gargalo, por algum dos indícios que comentei acima.

Há no Visual Studio um conjunto de ferramentas que nos possibilita monitorar e diagnosticar a saúde de uma aplicação, analisando o consumo de processamento, memória e contenção. As versões Ultimate e Premium do Visual Studio 2010 já vem munidas destas ferramentas, que nos permite monitorar vários tipos de aplicações.

Esses monitores estão disponíveis a partir do menu Analyse. Em seguida podemos clicar em Launch Performance Wizard, que lançará um wizard para nos auxiliar na escolha do tipo de monitoramento que desejamos realizar. São apresentados quatro opções, descritas a seguir:

  • CPU Sampling: A cada intervalo de tempo (configurável), extrai da aplicação que está sendo monitorada, informações que nos dirá o quanto a mesma impacta a performance da CPU, exibindo o quanto cada método consome.
  • Instrumentation: Com esta opção, o profiler irá gravar o tempo que cada função consome, desde a sua chamada até a sua saída, contemplando ou não chamadas para outras funções que eventualmente são realizadas.
  • .NET Memory Allocation (Sampling): Coleta informações dos objetos que estão sendo criados em tempo de execução, tais como: número e instâncias, quantidade de memória alocada, etc.
  • Concurrency: Permite o monitoramento de contenção de recursos, como a espera para um objeto que está bloqueado por outra requisição. Além disso, também é possível capturar informações sobre a execução de uma determinada thread, tais como contenção de recursos, utilização de CPU, etc.

É importante dizer que essas ferramentas são bastante intrusivas, então é necessário que se tome cuidado com o que, onde e quando você precisa monitorar, para evitar overheads desnecessários durante a execução.

Além disso, o método Buscar efetua a leitura do – mesmo – arquivo a cada requisição que chega para o mesmo, ou seja, recupera o conteúdo do arquivo e armazena em uma variável (array) local, e logo em seguida, efetua a busca em cima da mesma. Para tentar diagnosticar eventuais pontos de alocação de memória, podemos recorrer ao monitor do tipo .NET Memory Allocation, que tem justamente essa finalidade. Se rodarmos o mesmo em cima do serviço que criamos na primeira parte desta série, o resultado será mais ou menos o que é demonstrado abaixo:

Analisando o resultado do profiler, podemos então voltar ao código e tentar melhorar alguns pontos para diminuir o consumo de memória. A primeira mudança a ser feito é ao invés de ler o arquivo a todo momento, podemos criar uma espécie de caching e armazenar o resultado em um membro privado da classe que representa o serviço. Dentro do construtor inicializamos a coleção, dimensionando a mesma para um número que consiga acomodar a quantidade total de itens, para evitar o redimensionamento do array interno a cada item adicionado. Em seguida, extraimos o conteúdo do arquivo e armazenamos neste membro interno, para evitar que esta tarefa custosa seja realizada a todo momento. Abaixo temos o serviço, já refletindo estas alterações:

[ServiceBehavior(
    InstanceContextMode = InstanceContextMode.Single,
    ConcurrencyMode = ConcurrencyMode.Multiple)]
public class ServicoDeBusca : IBuscador
{
    private readonly string[] linhas;

    public ServicoDeBusca()
    {
        linhas = File.ReadAllLines(@”C:TempTecnologias.txt”);
    }

    public string[] Buscar(string parametro)
    {
        Thread.Sleep(200); //Simula um processamento

        return
            (
                from s in linhas
                where s.IndexOf(parametro, StringComparison.CurrentCultureIgnoreCase) > -1
                select s
            ).ToArray();
    }
}

Ao executar novamente o teste de carga, e fazendo o monitoramento utilizando o mesmo tipo de profiler, podemos perceber que a quantidade de memória alocada caiu drasticamente, assim como podemos reparar na imagem abaixo. É importante dizer que colocar o arquivo sendo lido a todo momento na primeira versão do serviço foi proposital. A ideia foi criar um código que prejudique a execução do serviço para podemos visualizar as ferramentas que temos à nossa disposição para diagnosticar esses problemas e, posteriormente, resolvê-los.

Quando você analisa este tipo de relatório é comum você se deparar com o termo exclusive e inclusive. Exclusive refere-se ao tempo/memória gasto com o método em si, com exceção do que é gasto pelas funções que ele chama internamente; já o inclusive refere-se ao tempo/memória gasto no total geral, incluindo o que se gasta dentro das funções que ele chama internamente.

Conclusão: Vimos no decorrer deste artigo e nos anteriores, a grande gama de ferramentas que é fornecida pelo Visual Studio para nos auxiliar, fornecendo recursos extremamente úteis que nos auxiliam a detectar problemas que possam ocorrer durante a execução de um serviço, quando o mesmo está sendo disponibilizado para um grande volume de requisições.

Teste, Carga e Monitoramento – Parte 2

Na primeira parte desta série eu comentei sobre como utilizar a ferramenta WCF Load Test para gerar testes unitários para serviços WCF. Como eu havia comentado lá, o grande intuito da geração daqueles testes é justamente a integração que podemos ter com outros tipos de testes fornecidos pelo próprio Visual Studio.

O principal tipo de teste a ser utilizado por serviços WCF é o teste de carga (Load Test). Testes de carga são usados para verificar se sua aplicação/serviço irá se comportar bem enquanto sofre uma grande quantidade de acessos simultâneos. A ideia aqui será combinar com os testes unitários que fizemos no artigo anterior, e com isso reproduzir a(s) chamada(s) para o(s) serviço(s) através de N usuários concorrentes dentro de um período de tempo, e que serão configurados através deste tipo de teste.

A ideia do teste de carga é especificar a quantidade, distribuição, periodicidade dos acessos, tentando estressar ao máximo o serviço/aplicação, afim de detectar possíveis cenários, onde depois de um certo número, a aplicação/serviço possa começar a não dar mais conta de atender todas as requisições que chegam para ela.

Para criar um teste de carga para serviços WCF, podemos utilizar o mesmo projeto criado anteriormente, e incluir nele um teste chamado Load Test, assim como é mostrado na imagem abaixo:

Ao incluir este teste, um wizard será iniciado para nos conduzir nas configurações do teste de carga. O primeiro que é exigido é a escolha do think time, que determina o período de tempo que existe entre as requisições (delay), que podemos geralmente utilizar a opção padrão. Já no passo seguinte temos o modelo do teste de carga, que controla a frequência em que os testes envolvidos serão executados. Podemos escolher uma entre quatro opções disponíveis:

  • Based on the total number of tests
  • Based on the number of virtual users
  • Based on user pace
  • Based on sequential test order

Além disso, uma das principais configurações do teste de carga é justamente a quantidade de usuários que acessarão o serviço. Há duas opções disponíveis: Constant Load e Step Load. A primeira exige um número inteiro que corresponde ao número de usuários, e que serão disparadas o mais rápido possível ao aplicação/serviço. A segunda opção, Step Load, permite aumentar gradativamente o número de usuários que acessam a aplicação/serviço, até atingir um número máximo de usuários, que pode ser mais próximo ao mundo real. A imagem abaixo ilustra essas configurações:

Dentro de um teste de carga, ainda podemos criar vários cenários, onde cada um deles poderá conter testes do tipo Web Performance Test (usado por aplicações Web) ou Unit Test, e cada teste sendo executado para um cenário específico, como por exemplo, N usuários, a distribuição das requisições durante o tempo estipulado, qual navegador está sendo utilizado para acesso (útil para aplicações Web), através de qual meio de conexão (LAN ou DialUp), etc.

Ao clicar no método Add para adicionar, serão listados os Unit Tests, inclusive aquele que criamos anteriormente, para o consumo do serviço WCF. A imagem abaixo ilustra esta tela e o teste já selecionado:

Na última tela temos as configurações que determinam por quanto tempo o teste irá rodar. Através da opção Load Test Duration, podemos especificar um tempo para warm-up, que determina a partir de quanto tempo os dados começarão a serem coletados. Isso é interessante porque permite ignorarmos inicializações que o serviço eventualmente faça, para que isso não seja contemplado nos resultados; além disso, temos a opção Run Duration, que determina a quantidade de tempo que em que o teste rodará. E para finalizar, esta tela conta também com o campo chamado Sampling Rate, que determina o tempo em que as informações serão coletadas e exibidas.

Com essas configurações feitas, o teste de carga é criado no Visual Studio, e a qualquer momento permitirá a alteração de alguma configuração previamente definida. Você pode também estar interessado em monitorar contadores de performance específicos, como é o caso dos contadores do ASP.NET e do WCF, que ajudarão nos diagnósticos de eventuais gargalos. A imagem abaixo resume a configuração realizada:

Tudo o que precisamos fazer agora é rodar o teste. Neste momento, o Visual Studio irá começar a executar o teste que criamos, respeitando todas as configurações lá colocadas e, principalmente, coletando as informações que você está interessado, e que será exibida graficamente através da leitura os contadores de performance que você escolheu. A imagem abaixo exibe o resultado do processamento do teste, onde podemos visualizar a quantidade de requisições, duração de cada uma delas, instâncias criadas, etc.

O serviço que criamos no artigo anterior, está sem qualquer configuração extra no WCF, ou seja, o gerenciamento de instâncias padrão é definido como PerSession e a sincronização como sendo Single. Outra configuração que pode afetar aqui é o throttle, apesar de que na versão 4.0 do WCF, seus limites foram incrementados. Apenas para constar, abaixo temos os limites de throttle em ambas as versões:

  • WCF 3.0
    •  MaxConcurrentSessions: 10
    •  MaxConcurrentCalls: 16
    •  MaxConcurrentInstances: 26 (MaxConcurrentSessions + MaxConcurrentInstances)
  • WCF 4.0
    • MaxConcurrentSessions: 100 * Número de Processador
    • MaxConcurrentCalls: 16 * Número de Processador
    • MaxConcurrentInstances: MaxConcurrentSessions + MaxConcurrentInstances

Para o nosso caso, podemos alterar o serviço para haver uma única instância para todas as requisições, e garantir o acesso concorrente ao método Buscar, e para fazer essa alteração, devemos incluir o atributo ServiceBehaviorAttribute para customizar isso:

[ServiceBehavior(
    InstanceContextMode = InstanceContextMode.Single,
    ConcurrencyMode = ConcurrencyMode.Multiple)]
public class ServicoDeBusca : IBuscador { }

Quando temos a configuração acima, o único parâmetro que nos interessa no throttle é a quantidade máxima de chamadas concorrentes (MaxConcurrentCalls), que está limitado em 16 * número de processadores. Vamos incrementar isso para 150, incluindo um behavior de serviço, conforme é mostrado no código abaixo. Para mais detalhes sobre throttle, consulte este artigo.

<serviceThrottling maxConcurrentCalls=”150″ />

Após essa alteração aplicada ao serviço, se rodarmos o mesmo teste, veremos que a quantidade de requisições é elevada, conseguindo atender mais requisições simultâneamente.

Conclusão: Com as informações geradas por este teste, é possível diagnosticar e realizar alguns ajustes finos no WCF (throttling, por exemplo), no IIS e no pipeline de execução do ASP.NET, para conseguirmos dar mais vazão ao processamento das requisições. De qualquer forma, existem outras ferramentas que nos permite extrair informações de custo de processamento, memória e threading e que serão abordados em um futuro artigo.

Teste, Carga e Monitoramento – Parte 1

Um grupo dentro da Microsoft tem trabalhado na construção de um “novo” tipo de teste para o Visual Studio, que tem a finalidade de criar testes unitários para serviços WCF baseando-se nas requisições que chegam para ele ou através de uma análise de um arquivo de tracing, que também é gerado pelo próprio WCF.

Esses testes unitários servirão apenas como uma forma de automatizar/criar as chamadas que serão realizadas para as operações expostas pelo serviço, mas a grande utilidade será a possibilidade de criar um teste de carga para serviços WCF, incluindo esses testes unitários como uma forma de customizar e automatizar como e quantas requisições serão disparadas contra o mesmo e, consequentemente, conseguirmos mensurar o consumo de alguns recursos como memória e processamento que estarão sendo utilizados pelo mesmo. Note que o nome do projeto é WCF Load Test, que está em sua versão 3.0.

Como exemplo, vamos criar um contrato que recebe uma string como parâmetro e retorna um array, também do tipo string, representando os itens que foram encontrados baseando-se naquele parâmetro. Abaixo temos o contrato para ilustrar:

[ServiceContract]
public interface IBuscador
{
    [OperationContract]
    string[] Buscar(string parametro);
}

A implementação para este contrato é extremamente simples, onde tudo o que temos dentro do serviço é a leitura de todo o conteúdo de um arquivo texto que contém um conjunto de tecnologias Microsoft, qual faremos a busca para ver se encontramos o item dentro deste arquivo. A classe que representa o serviço está abaixo:

public class ServicoDeBusca : IBuscador
{
    public string[] Buscar(string parametro)
    {
        string[] linhas = File.ReadAllLines(@”C:TempTecnologias.txt”);
        Thread.Sleep(200); //Simula um processamento

        return
            (
                fromin linhas
                where s.IndexOf(parametro, StringComparison.CurrentCultureIgnoreCase) > -1
                select s
            ).ToArray();
    }
}

O serviço está sendo exposto através do binding BasicHttpBinding, mas a configuração do mesmo será omitida aqui, assim como a configuração do tracing de mensagens, que precisa estar habilitado para submetermos para a criação do teste. Se você quiser saber mais sobre o tracing do WCF, você pode consultar este artigo ou este vídeo.

Depois do projeto WCF Load Test instalado, você já será capaz de adicionar um teste do tipo WCF Test em seu projeto de testes. A imagem abaixo ilustra esse novo item que é adicionado após a instalação do arquivo MSI que você precisa baixar.

Quando clicar no botão OK, um wizard será inicializado, onde você poderá customizar o teste do serviço. O primeiro passo determina a fonte de onde quer extrair as informações para gerar os testes, onde a primeira opção é selecionar a aplicação cliente que consome o serviço, ou utilizar um arquivo de log previamente criado. Além disso, é necessário selecionar uma das três opções que estão logo abaixo: Client-side, Server-side e Fiddler text. As duas primeiras se referem aos arquivos de tracing gerados pelo WCF, e como estamos gerando o tracing através do serviço, temos que selecionar a opção Server-side. Além disso, ainda há a possibilidade de utilizar um arquivo de log do Fiddler, caso o serviço esteja sendo exposto através do protocolo HTTP. A imagem abaixo ilustra este passo:

Observação: Para poder gerar uma requisição para o serviço, podemos utilizar o utilitário WCF Test Client, que nos permite consumir um serviço WCF sem a necessidade de ter que criar uma aplicação para efetuar a requisição.

O próximo passo consiste em selecionar quais operações (SOAP Actions) serão envolvidas no teste. Lembrando que o serviço pode possuir várias delas, e você tem a possibilidade de envolver somente aquelas que julgar necessário para compor os testes. Podemos visualizar este passo através da imagem abaixo:

Para finalizar, precisamos informar no último passo do wizard onde estão os contratos (de serviço, de dados ou faults) que serão utilizados pelo cliente. A ideia é selecionar o assembly onde estão este tipos. E se dentro deste assembly você tiver somente os contratos ou tiver o proxy efetivamente criado, a geração do código de testes é ligeiramente diferente.

Quando o wizard é finalizado, uma classe é gerada encapsulando toda a comunicação com esse serviço, respeitando os métodos selecionados. Para cada operação do serviço, um método de teste é criado e decorado com o atributo TestMethodAttribute. Além disso, um método de inicialização (TestInitializeAttribute) é criado em uma classe parcial, que é utilizado para construir a instância o proxy. Esse método exige uma customização, que é o binding a ser utilizado e o endereço até o serviço.

Abaixo temos a classe gerada para efetuar os testes, qual o Visual Studio (MSTest) utiliza para rodar como sendo um teste unitário. Note que como temos o método Buscar, ele é colocado dentro desta classe, abastecendo o parâmetro com o valor que também foi extraído do arquivo de tracing. Depois disso, envolvemos a chamada para a operação no timer fornecido pela classe TestContext, que será utilizado mais tarde por um teste de carga.

[TestClass]
public partial class ServicoDeBuscaTests
{
    private Servicos.IBuscador buscadorClient;

    public TestContext TestContext { get; set; }
        
    [TestMethod]
    public void ServicoDeBusca()
    {
        this.Buscar();
    }
        
    private string[] Buscar()
    {
        string parametro = “Windows”;
        this.CustomiseBuscar(ref parametro);
        this.TestContext.BeginTimer(“ServicoDeBusca_Buscar”);
        try
        {
            return buscadorClient.Buscar(parametro);
        }
        finally
        {
            this.TestContext.EndTimer(“ServicoDeBusca_Buscar”);
        }
    }
}

Como já mencionei acima, uma configuração necessária para o teste funcionar corretamente, é a necessidade de definir o binding que será utilizado e o endereço até o serviço. Para isso, você pode utilizar o construtor da classe ChannelFactory<TChannel>, que está localizada dentro da classe parcial que foi criada automaticamente através dos passos acima. Você poderá analisar essa configuração no projeto que está anexado no final deste artigo.

Finalmente você pode rodar o projeto de teste através do Visual Studio para se certificar de que a chamada seja realizada, e com isso visualizar o andamento através da janela que sumariza os testes, indicando o status da execução e se sucedeu ou não:

Conclusão: Neste artigo exploramos uma ferramenta que podemos integrá-la no Visual Studio para podermos gerar testes unitários se baseando em serviços WCF. O grande intuito dele é ser utilizado dentro de um outro teste, como é o caso de um teste de carga, que nos permite adicionar um teste previamente criado para compor a carga que será disparada contra o serviço, assunto qual será abordado em um futuro artigo.

WCFLoadTesting.zip (24.22 kb)