.NET

6 set, 2017

.Net Core, uma visão prática

Publicidade

Foi em 2014 que a Microsoft anunciou as tecnologias .NET Core e ASP.NET Core, assim como a exposição (read-only) do código do .NET Framework. Com isso, a empresa indicava um direcionamento para um contexto “open source” e “cross platform”, provavelmente com o objetivo de reverter a queda observada na adoção das suas tecnologias pela indústria de desenvolvimento de software.

Recentemente, o .NET vem perdendo espaço devido à evolução de novas tecnologias de desenvolvimento, principalmente das baseadas em JavaScript. Isso porque, com a diminuição da vantagem tecnológica, o foco passa a ser o custo. Se antes os profissionais optavam pelo .NET para poderem desenvolver aplicações com código assíncrono (no-block) de maneira simples (async/await), agora outras ferramentas também permitem atingir esse objetivo de maneira simplificada, como objetos do tipo Promise no JS, por exemplo, e com a vantagem de não ter de arcar com os custos de uma IDE para ter um ambiente de desenvolvimento eficiente e produtivo, nem com os custos de hospedar a aplicação em servidores SO Windows.

Após o lançamento, entre 2014 e 2015, observamos uma crescente significativa nos repositórios C# no Github, o que indica que os esforços da Microsoft surtiram efeito.

Fonte: Github

Desde o anúncio, a Microsoft dedica bastante atenção ao produto, com melhorias contínuas em confiabilidade e performance. A última versão estável do .NET Core (na data da produção deste artigo) é a 1.1.1. Hoje vou compartilhar a experiência que tive no primeiro contato com o .NET Core, bem como uma comparação entre o desempenho dela e do node.js.

Criando uma ASP.NET Core app

Neste artigo não vamos colocar um passo-a-passo para criar uma API usando o ASP.NET Core, ok? Para isso, você consegue toda a orientação necessária na documentação oficial. Se você quiser, também pode acessar neste repositório o código da aplicação que desenvolvemos para o estudo. Nela, implementamos boas práticas que consideramos importantes: injeção de dependência, configurações multi-ambientes, arquivos de segredos para gravação de informações sensíveis fora do versionamento, entre outras.

O que vamos destacar no artigo são conceitos importantes para criar uma aplicação usando o .NET Core. Bora lá?

Framework Dependent || Self Contained Application23

O .NET Core permite que você configure sua aplicação para seguir duas estratégias de publicação: “Framework Dependent” ou “Self Contained”. A estratégia a ser escolhida depende da resposta que você consegue dar à seguinte pergunta: “qual é o controle que você possui do ambiente no qual sua aplicação ficará hospedada e quão exclusivo é esse ambiente?”

Na estratégia “Framework Dependent”, o .NET CORE Runtime já está instalado no servidor, então temos como vantagem a geração de um pacote consideravelmente menor, o que otimiza a utilização do disco do servidor e agiliza o processo de publicação. A estratégia é mais indicada para os casos em que usamos servidores dedicados ou containers para se disponibilizar a aplicação. No nosso estudo, vamos seguir essa estratégia, pois disponibilizamos a API por meio de um container Docker.

Em contrapartida, na estratégia “Self Contained” não se pode garantir a existência no servidor do .NET CORE Runtime. Por isso, todos os componentes do framework necessários ao funcionamento da aplicação são embarcados no pacote gerado. Essa estratégia é mais indicada para aplicações publicadas em ambientes compartilhados ou sob os quais não se tem o total controle.

O Kestrel é rápido, mas não se garante sozinho

Kestrel é um servidor de aplicação multiplataforma otimizado para servir conteúdo dinâmico de forma rápida. É o padrão dos templates de projetos ASP.NET Core no Visual Studio e sua utilização é simples. Não é necessário se preocupar com a instalação no servidor, pois as aplicações ASP.NET Core são, por padrão, aplicações self-hosted. Logo, Kestrel e aplicação são executados no mesmo processo. Sua definição e inicialização acontecem no método “Main” da aplicação ASP.NET Core.

O Kestrel é rápido, mas possui limitações. Como é uma criação relativamente recente e com foco em performance na entrega de conteúdo dinâmico. Ele ainda não possui um conjunto de implementações de segurança satisfatório, como, por exemplo, limite de tempo de conexão e de conexões concorrentes. Por isso, é importante observar que nunca deve ficar exposto à internet e deve atender apenas ao tráfego de uma rede privada sobre a qual se tenha controle total do fluxo de requisições.

Para disponibilizar aplicações com o Kestrel para a internet, a aplicação deve ser “posicionada” atrás de um servidor de aplicação seguro e que fará a função de proxy reverso. No nosso estudo, utilizamos o NGINX.

Portanto, o Kestrel deve ser utilizado nos casos em que sua aplicação fique hospedada em um ambiente que não seja Windows. Para estes ambientes, dê preferência à utilização dos servidores de aplicação IIS ou WebListener, que são específicos e não possuem as limitações do Kestrel, então não é necessário configurar um proxy reverso para eles.

IApplicationBuilder: eu tenho um pipeline e não tenho medo de usá-lo

Com o .NET Core ficou mais fácil customizar o pipeline de requisição http. Chega de ficar se preocupando em administrar e entender as diferenças entre Loggers, Filters e Message Handlers. Agora você customiza o seu pipeline com Middlewares, classes que recebem um objeto do tipo “RequestDelegate” no construtor e possuem o seguinte método implementado: “Task Invoke(HttpContext context)”.

Após criar uma classe com as características mencionadas, você deve adicionar o comportamento implementado ao pipeline utilizando o método “UseMiddleware” do objeto “IApplicationBuilder” passado como argumento ao método “Configure” da classe “Startup” existente em seu projeto. Mas lembre-se que a ordem de chamada dos métodos UseXXXX, que está sendo realizada dentro do método Configure, importa. Essa ordem representa as camadas do Pipeline: Middleware registrado primeiro, será evocado primeiro e, consequentemente, seu método “Invoke” será terminado por último.

Show me the money! – os resultados

O que implementamos

Em nosso estudo criamos uma API com duas rotas. Executamos repetidas cargas de requisições a elas e analisamos quantidades de requisições atendidas por minuto e a média de tempo na qual as requisições foram atendidas.

Ambas as rotas executam praticamente o mesmo serviço. A diferença é que em uma gravamos no banco de dados o objeto gerado, executando um processo I/O bound, enquanto que na outra apenas fomos acumulando os objetos gerados em memória, executando um processo totalmente CPU bound.

A geração do objeto gravado conforme mencionado acima é composta das seguintes etapas:

  • Recepção de usuário e senha por meio do corpo do request;
  • Acesso à configuração para obtenção de um valor de salt;
  • Concatenação do salt com o valor de senha passado;
  • Computação do hash SHA-256 do valor “salt + senha”;
  • Persistência do objeto representando o usuário criado.

Ao seguir o fluxo mencionado, a intenção era implementar um serviço com um esforço computacional relevante representando, no máximo, um fluxo típico de uma aplicação real. Concluímos nosso estudo realizando a mesma implementação em NodeJS e confrontando os resultados.

Características do ambiente

Executamos as requisições a partir de nossa máquina Windows para a aplicação que ficou hospedada em uma VM Linux executada, também, em nossa máquina. Entendemos que a performance da aplicação é afetada pela concorrência aos recursos da máquina, mas consideramos isso como um trade off válido para não estarmos sujeitos a gargalos ou interferências da rede.

Todas as camadas do sistema (proxy reverso, aplicação e banco de dados) foram implementadas em containers Docker. A versão do Docker utilizada foi a 17.05.0-ce.

As versões de framework e SGDB utilizadas foram as seguintes:

  • .NET Core 1.1.1
  • Node 6.10.2 & Express 14.0
  • Postgres 9.5
  • Mongo 3.4.2

Os recursos de máquina utilizados seguem abaixo:

  • Máquina Windows:
    • Processador: Intel Core I7 5500U 2.4 GHz – 2 Cores – 4 Threads
    • Memória: 16 GB DDR3 – 1600 MHz
    • Disco: 1 TB – SATA – 5400 RPM – 6Gb/s
    • Sistema Operacional: Windows 10 Pro
  • VM Linux:
    • Engine de Virtualização: Virtual Box 5.1.22
    • Processador: 2 processadores sem restrição de utilização
    • Memória: 6144MB
    • Sistema Operacional: Ubuntu Trusty 14.04

Resultados observados

Foram executadas diversas baterias de requisições com duração de um minuto cada. Os resultados que melhor representam o conjunto total de resultados obtidos estão expostos nas tabelas abaixo. No fim do artigo, você pode ver os gráficos com o desempenho e a evolução de cada teste registrados segundo a segundo.

Observações:

  • Baterias com final “IO” executaram a gravação dos objetos em banco;
  • Baterias com final “CPU” mantiveram os objetos gerados em memória;
  • Coluna “Quantidade” representa requisições atendidas em um minuto;
  • Coluna “Tempo Médio” representa média de duração das requisições em milissegundos;
  • Células marcadas de verde indicam melhor valor;
  • Ao executarmos as baterias “CPU” os containers de banco estavam ativos, por isso executamos novamente para cada banco, considerando assim os recursos consumidos pelo container de cada banco.

Resultados:

Conclusão

Se você iniciar uma busca na internet, rapidamente vai encontrar links com estudos e benchmarks indicando uma performance esmagadora do .NET Core em comparação ao NodeJS. Entretanto, não confirmamos isso em nosso case. Identificamos um melhor desempenho do NodeJS na execução do serviço com processo IO bound, enquanto o .NET Core mostrou um desempenho superior na execução do serviço totalmente CPU bound.

Identificamos também que um aumento no tempo em que uma requisição é atendida impacta menos no .NET Core a quantidade de requisições atendidas em um minuto. Na bateria “Postgres, 1 – IO”, por exemplo, enquanto o tempo médio do NodeJS é 30,92% menor que o do .NET CORE a quantidade de requisições atendidas pelo NODE é apenas 4,92 % maior que a quantidade atendida pelo .NET.

Ainda precisamos de mais análises para confirmar o motivo que leva à característica mencionada, mas nossa tese é que um melhor desempenho CPU bound faz com que o .NET execute de forma mais rápida as etapas que compõem o pipeline para requisições http (principalmente o roteamento). Dessa forma, quando a thread responsável por atender as requisições é liberada, ao realizar uma chamada assíncrona consegue retirar a próxima requisição da fila mais rápido e passar com ela através do pipeline.

Quanto à quantidade de threads responsáveis por atender as requisições, em nosso case ambas as aplicações estavam com apenas uma thread no thread pool. Sabemos que essa é uma característica intrínseca ao NodeJS, enquanto o thread pool do .NET Core pode ser maior. Contudo, deixamos o thread pool com sua definição padrão que é o  [número de processadores] dividido por 2. Como nossa VM tinha 2 processadores nossa API ficou com apenas 1 thread no thread pool.

Realmente ficamos animados com o desempenho apresentado pelo .NET Core. Nosso estudo mostra que ele possui um desempenho competitivo para processos IO Bound e um excelente desempenho para processos CPU Bound. Acreditamos que  essa tecnologia merece uma chance em nossos próximos trabalhos a serem publicados em ambiente Linux. E você, o que acha? Deixe abaixo os seus comentários.

Até a próxima!

***

Gráficos com o desempenho e a evolução de cada teste:

Postgres 1 – IO, quantidade
Postgres 1 – IO, tempo médio
Postgres 2 – IO, quantidade
Postgres 2 – IO, tempo médio
Postgres 3 – IO, quantidade
Postgres 3 – IO, tempo médio
Postgres 1 – CPU, quantidade
Postgres 1 – CPU, tempo médio
Postgres 2 – CPU, quantidade
Postgres 2 – CPU, tempo médio
Postgres 3 – CPU, quantidade
Postgres 3 – CPU, tempo médio
Mongo 1 – IO, quantidade
Mongo 1 – IO, tempo médio
Mongo 2 – IO, quantidade
Mongo 2 – IO, tempo médio
Mongo 3 – IO, quantidade
Mongo 3 – IO, tempo médio
Mongo 1 – CPU, quantidade
Mongo 1 – CPU, tempo médio
Mongo 2 – CPU, quantidade
Mongo 2 – CPU, tempo médio
Mongo 3 – CPU, quantidade
Mongo 3 – CPU, tempo médio

 

***

Artigo publicado originalmente em: https://www.concrete.com.br/2017/08/23/net-core-uma-visao-pratica/