Um pouco de história:

Há alguns anos atrás, o consumidor tinha a necessidade de dirigir-se de loja em loja realizando pesquisa de mercado, anotando preços, anotando nomes das lojas e/ou seus respectivos endereços; isso tomava muito tempo de cada um dos consumidores que estavam, na maioria das vezes, querendo comprar um determinado produto naquele exato momento ou quem sabe no dia seguinte.

Assim, muitos de nós, nossos pais, familiares e amigos viveram por anos e anos, até chegar a internet e todos conseguirem realizar parte dessas pesquisas, mesmo que de maneira básica, através da de sites de busca muito simples. Começava ai grande evolução tecnológica sob a qual vivemos e consumimos hoje em dia. Hoje consumimos os chamados: Comércios Eletônicos ou E-Commerce! [O que não é novidade para ninguém atualmente].

Com toda esse avanço de tecnologias e infraestrutura de hardware e servidores, juntamente com a popularização do acesso a internet, dados e informações; tivemos um grande aumento no volume de usuários ativos e, com isso, acessando por várias horas os diversos comércios eletrônicos que existem atualmente. Baseando-se nesses avanços que só tendem a crescer cada vez mais, não podemos ser tão simplistas na construção de um sistema – com o qual converterá a operação do mundo real com intervenções humana, para processamento operado por softwarres e servidores – nem podemos implementar tudo que vemos, ouvimos ou lemos de novidades na internet/palestras. O somatório dessas implementações e ações poderá tornar seu site fadado ao fracasso ou ter um turn-over de usuários alto suficiente para que você conheça o que muitos e-commerces não querem conhecer: Reclame Aqui!

Cenário de um E-Commerce com alto consumo de CPU:

O que abordaremos neste artigo é como alguns pontos de um sistema – muitas vezes pequenos pontos – devem ser pensados e implementados para que não tenhamos que realizar um procedimento de Deep Memory Analyzer. Para essa situação utilizamos a ferramenta: Debug Diagnostic Tool.

Atualmente atuo em um cliente que executa uma operação de varejo online, sob a qual funciona utilizando um sistema de e-commerce. Fizemos várias implementações na aplicação, rodamos teste de carga [mas não analisamos como a aplicação tinha se comportado via PerfMon], validamos os Response Time da aplicação, consumo de memória e algumas outras variáveis. O resultado apresentado mostrava-se aceitável [devido as poucas variáveis analisadas] para realizar o deploy da aplicação em produção. Para nossa surpresa, com poucos acessos (1200 usuários/min) a aplicação apresentava o comportamento esperado. No decorrer de uma semana de operação pós-produção, começamos a notar que a aplicação começava a dar picos de processamento da ordem de 85%-95% [Temos um parque com 10 servidores, 12gb de memória e 6 vCPUs] em poucas máquinas. Fazendo o acompanhamento de 1 (um) dia, começamos a notar que em horários de pico de acesso no site (2000 usuários/min), o parque de servidores começava a entrar em colapso no processamento das 6 vCPUs, onde todos os servidores atingiam quase que o mesmo pico de processamento 85%-95%.

O cenário acima, estava gerando um turn-over de usuários e com isso não estava sendo possível converter novos usuários. Uma aplicação de comércio eletrônico – para ter sucesso – precisa focar em trazer a operação do mundo real para o mundo virtual, levando em consideração algumas premissas. Algumas delas cito abaixo:

  • Baixo tempo de resposta
  • Baixo tempo de carregamento da página
  • Confidencialidade dos dados
  • Garantia que a compra será finalizada com sucesso
  • Conversão de usuários para dentro da aplicação
  • Conversão financeira para a empresa
  • Usabilidade + Layout amigável ao usuários
  • Velocidade no processamento dos dados

Todos os itens listados acima e vários outros que poderiamos listar, levarão ao sucesso uma aplicação que se destina a virtualizar a operação de uma empresa em cliques do comércio eletrônico; mas podemos prejudicar rapidamente cada um dos itens acima, caso não levemos em consideração algumas premissas de desenvolvimento de software que vem antes dos tópicos listados acima. É o que mostrarei logo a seguir no decorrer deste artigo, com o intuito de colaborar com a performance e melhores práticas no desenvolvimento de suas aplicações.

Debug Diagnostic Tool:

Para realizar uma análise desses cenários, tivemos que atuar com ferramentas que são antigas, mas bastante eficientes, além de ser FREE. Vocês não utilizam Application Insights? Não. Mas para termos uma análise do ponto de vista de execução [Deep Inside] no código da aplicação e descobrir o que estava apresentando gargalo de processamento, tivemos que levantar relatórios utilizando o PerfMon [não será nosso foco neste artigo] e extrair contadores para alguns indicadores voltados apenas para aplicação ASP.NET, tais como: Garbage Collector, Threads, Large Objects, .NET CLR.

Outra ferramenta que utilizamos foi: Debug Diagnostic Tool, excelente ferramenta para análise de DUMP Memory e você pode fazer o download dela aqui. Foi através da ferramenta Debug Diagnostic Tool que consegui, identificar alguns pontos de gargalo dentro da aplicação, conforme segue as imagens abaixo:

Na figura abaixo, estamos obtendo o arquivo do DUMP de memória:

Obtendo DUMP de MemóriaObtendo DUMP de Memória

Ferramenta DebugDiag Tool:

Debug Diag ToolDebug Diag Tool

Processando arquivo do DUMP de memória (.dmp):*

Processando arquivo do DUMPProcessando arquivo do DUMP

Após extrair o Dump de memória do servidor [Isso só é possível quando tem-se acesso a máquina] ou mesmo de sua máquina de desenvolvimento no momento em que sua aplicação estiver apresentando pico CPU, agora é a hora de submeter o Dump para a ferramenta analisar. Apartir desse momento, o Debug Diagnostic Tool analisará o Dump de Memória e, após concluir o processamento do arquivo, abrirá o IE com um arquivo em formato HTML, apresentando um relatório listando tópicos relevantes, tais como:

  • 5 Top Thread que tem mais consumo de memória;
  • Os módulos (assemblies) com maior tempo de carga e que são mais consumidos pela aplicação;
  • Quais são os trechos de código que foram gargalo naquele momento de pico de CPU;
  • No final do arquivo .mht encontram-se a seção Call Stacks, contendo informações sobre a pilha de execução de alguns blocos de código. PS.: Essa é uma das partes do relatório que, particularmente, eu acho mais interessante para irmos direto ao ponto de gargalo do código;
  • Informações importantes sobre grandes objetos (Largest Objects);
  • HttpContex Request

Conforme imagens abaixo:

Cebaçalho do relatório:

Informações do cabeçalho do relatórioInformações do cabeçalho do relatório

Top 5 Threads com maior consumo de processador:

Top 5 threads de maior consumoTop 5 threads de maior consumo

Chamada ao método que utiliza RegEx, aguardando que o GC (Garbage Collection) termine seu processamento:

Thread RegEx com problema [RemoveAcentos]

Timeout no processamento da Threadque usou paralelismo:

HttpContext Report

Implementação da consulta utilizando paralelismo com grande volume de dados obtidos do Cache da aplicação (ASP.NET Cache):

public ActionResult Index()
{
    using (var contexto = new EFDBContext())
    {
      var produtos = contexto.Produtos.ToList();
      HttpContext.Cache.Insert("produtos", produtos);
      ConcurrentBag<Produto> bag = new ConcurrentBag<Produto>(ObterProdutosCache() ?? new List<Produto>());
      var resultado = produtos.AsParallel().Where(p => bag.Any(x => x.Modelo == p.Modelo)).ToList();
    }
    
    return View();
}

private IList<Produto> ObterProdutosCache()
{
    // O mecanismo de cache retorna os 14K (apenas para simplificar o armazenamento no cache)
    return (IList<Produto>)HttpContext.Cache["produtos"];
}

Utilização de parser RegEx em pontos alguns pontos da aplicação aplicando grandes patterns no RegEx dentro de laços (ForEach, por exemplo), aumenta o processamento da aplicação.

public static string RemoveAcentos(this string text)
{
    if (string.IsNullOrEmpty(text))
        return string.Empty;
    
    StringBuilder sbReturn = new StringBuilder();
    var arrayText = text.Normalize(NormalizationForm.FormD).ToCharArray();
    foreach (char letter in arrayText)
    {
        if (CharUnicodeInfo.GetUnicodeCategory(letter) != UnicodeCategory.NonSpacingMark) sbReturn.Append(letter);
    }
    string pattern = @"(?i)[^0-9a-záéíóúèìòùâêîôûãõçs]";
    string replacement = "";
    string result = new Regex(pattern).Replace(sbReturn.ToString(), replacement);
    return result;
}

A análise feita no trecho de código acima constatou que o problema não estava no paralelismo, mas conjunto de fatores: paralelismo + grande volume de dados + grande volume de usuários acessando a aplicação simultaneamente, gerando uma sobrecarga de processamento nos servidores devido ao paralelismo. Os picos de processamento nos núcleos das vCPUs acontecia devido os itens abaixo:

  • Grande volume de acessos simultaneos executando aquele trecho de código
  • Balanceamento de carga
  • Grande volume de dados sendo processados usando paralelismo (consumindo as vCPUs)
  • O código demorava cerca de 10-30seg para retornar, isso degradava muito a performance dos servidores
  • O código acima iniciava várias tasks para realizar a consulta e o predicado da condição, consumindo as vCPUs.

Se você chegou até esta parte do artigo deve estar se perguntando: Mas porque um desenvolvedor em sã consciencia iria armazenar 14K registros de uma tabela no cache da aplicação? Bom, para responder não seria tão fácil e simples assim; precisaria analisar o cenário em que sua aplicação e negócio estão operando, para chegarmos numa solução que melhor atenda o seu negócio. Mas onde estou atuando, no momento, essa estratégia foi necessária para não pagarmos caro com outros tipos de processamento e consumos (Não estamos usando nenhum servidor de Cache, como: Redis ou memcache):

  • Alto Throughput de rede entre servidor web e servidor de banco de dados
  • Alto Troughput de I/O de disco no processamento das consultas constantes no servidor de banco de dados
  • Aumento do tempo de resposta da página em um cenário de e-commerce
  • Latência no tráfego dos dados entre servidor de banco e servidor web
  • Alto Troughput de rede entre servidor do Redis, por exemplo, e os servidores web da aplicação (Isso deveu-se ao armazenamento de grande volume de dados sem aplicar estratégias de Cache)

Para exemplificar o quão prejudicial é um processamento paralelo em uma aplicação web com grande volume de acessos, abaixo segue as configurações da máquina que rodou essa simulação e algumas imagens de como ficou o processador da máquina:

Configurações da máquina

  • Windows 10 Pro
  • I7-2670QM 2.2GHz, com 4 núcleos físicos e mais um virtualizado em cada, somando 8 núcleos
  • 16GB de memória
  • HD 750 7200 rpm

Processador Antes:

Como pode ser observado na imagem, a seta vermelha marca o ponto de gargalo da aplicação. Ao lado está o gerenciador de tarefas apontando o número de threads, antes de a aplicação chegar no ponto de gargalo.

Inicio do teste, processador sem ação de paralelismo:

Processador AntesProcessador Antes

Análise do processamento após as primeiras iterações do paralelismo:

Processador Durante segunda seçãoProcessador Durante segunda seção

Análise após alguns minutos de processamento da linha de código do paralelismo:

Processador Durante primeira seçãoProcessador Durante primeira seção

Apenas para matar a curiosidade de alguns J, aqui estão os 8 núcleos sendo consumidos devido a programação usando paralelismo:

8 núcleos com alto consumo8 núcleos com alto consumo.

Para a simulação acima foi executado apenas um Request, ou seja, foi executado através de um F5 pela IDE do Visual Studio 2015. Caso você queira simular algo mais próximo da realidade, você pode seguir os passos abaixo:

  • Execute sua aplicação através do Visual Studio;
  • Execute a aplicação sem nenhum Breakpoint;
  • Abra o seu gerenciador de tarefas;
  • Com a aplicação rodando no browser, basta apenas teclar F5 simulando vários requests;
  • Observe que sua CPU, após vários F5 tende a chegar próximo dos 100% de processamento

A solução para o problema apresentado, deu-se através de uma implementação simples usando uma das teorias matemáticas dos conjuntos, que no caso do Linq fazemos utilizando o método .Except. Mas para utilizarmos essa implementação tivemos que criar uma nova classe que faz toda a validação se realmente cada objeto dentro da coleção é diferente um do outro, retornando apenas osobjetos que divergem da coleção de origem.

public class ProductCompare : IEqualityComparer<Produto>
{
    public bool Equals(Produto x, Produto y)
    {
        return x.ProdutoId == y.ProdutoId;
    }

    public int GetHashCode(Produto obj)
    {
        return obj.ProdutoId.GetHashCode();
    }
}

public ActionResult Index()
{
    using (var contexto = new EFDBContext())
    {
        var produtos = contexto.Produtos.ToList();
        HttpContext.Cache.Insert("produtos", produtos);
        ConcurrentBag<Produto> bag = new ConcurrentBag<Produto>(ObterProdutosCache() ?? new List<Produto>());
        //var resultado = produtos.AsParallel().Where(p => bag.Any(x => x.Modelo == p.Modelo)).ToList();

        var resultado = produtos.Except(bag, new ProductCompare()).ToList(); } return View();
}

Conclusão

Atualmente temos uma centena de caminhos para resolvermos problemas arquiteturais, recebemos uma enxurrada de informações diariamente sobre novos recursos na linguagem de programação que estamos utilizando, novas ferramentas para melhorar e otimizar a performance das aplicações web. O ponto é:

  • Você está realmente entendendo o contexto de negócio do seu cliente?
  • Você sabe utilizar os recursos da tecnologia para resolver de forma eficiente e eficaz? Em prol de aumentar o pontencial da ferramenta desenvolvida para o seu cliente?
  • O que você está buscando implementar é apenas preciosismo de usar sempre o mais novo?

Nem tudo o que ouvimos, nem tudo o que se lê é aplicado 101% em nosso dia-a-dia, não existe receita de bolo e não podemos plugar tudo e qualquer novidade em nossa arquitetura. Arquitetura de Software e Arquitetura de Sistemas é uma Arte e é necessário ter um bom entendimento sobre contexto do problema do negócio que precisa ser resolvido, analisar as várias formas, ferramentas, recursos tecnológicos que temos em mãos a fim de podermos aplica-los em prol de melhorias e sempre buscando aumentar o potencial das aplicações web, mobile e afins. Quando pensamos em arquitetura de software e sistemas estamos pensando no coração (core) de uma plataforma que vai ajudar uma empresa a acelerar sua operação, otimizar o tempo dos seus colaboradores, maximizar a conversão de clientes e potencializar o poder de venda. Sendo assim, precisamos repensar desde os pequenos detalhes até os mais complexos fluxos.

Espero que tenha sido útil esse artigo e provocado uma reflexão sobre: Como eu estou desenvolvendo meu código e quão performático o core da aplicação se encontra?

Você pode encontrar o código fonte com script da carga dos dados aqui