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.