Anti-padrão de Instâncias ImprópriasImproper Instantiation antipattern

Pode afetar o desempenho ao criar continuamente novas instâncias de um objeto que se destina a ser criado uma vez e, em seguida, partilhado.It can hurt performance to continually create new instances of an object that is meant to be created once and then shared.

Descrição do problemaProblem description

Muitas bibliotecas oferecem abstrações de recursos externos.Many libraries provide abstractions of external resources. Internamente, estas classes normalmente gerem as suas próprias ligações ao recurso, e agem como mediadores que os clientes podem utilizar para aceder ao recurso.Internally, these classes typically manage their own connections to the resource, acting as brokers that clients can use to access the resource. Seguem-se alguns exemplos das classes de mediador que são relevantes para as aplicações do Azure:Here are some examples of broker classes that are relevant to Azure applications:

  • System.Net.Http.HttpClient.System.Net.Http.HttpClient. Comunica com um serviço Web com HTTP.Communicates with a web service using HTTP.
  • Microsoft.ServiceBus.Messaging.QueueClient.Microsoft.ServiceBus.Messaging.QueueClient. Publica e recebe mensagens para uma fila do Service Bus.Posts and receives messages to a Service Bus queue.
  • Microsoft.Azure.Documents.Client.DocumentClient.Microsoft.Azure.Documents.Client.DocumentClient. Liga a uma instância de Cosmos DBConnects to a Cosmos DB instance
  • StackExchange.Redis.ConnectionMultiplexer.StackExchange.Redis.ConnectionMultiplexer. Liga ao Redis, incluindo a Cache de Redis do Azure.Connects to Redis, including Azure Redis Cache.

Estas classes destinam-se a ser instanciadas uma vez e reutilizadas em toda a duração de uma aplicação.These classes are intended to be instantiated once and reused throughout the lifetime of an application. No entanto, é um mal entendido comum que estas classes devem ser compradas apenas conforme necessário e lançadas rapidamente.However, it's a common misunderstanding that these classes should be acquired only as necessary and released quickly. (Estas estão aqui listadas são bibliotecas .NET, mas o padrão não é exclusivo para o .NET.) O seguinte exemplo ASP.NET cria uma instância de HttpClient para comunicar com um serviço remoto.(The ones listed here happen to be .NET libraries, but the pattern is not unique to .NET.) The following ASP.NET example creates an instance of HttpClient to communicate with a remote service. Pode encontrar o exemplo completo aqui.You can find the complete sample here.

public class NewHttpClientInstancePerRequestController : ApiController
{
    // This method creates a new instance of HttpClient and disposes it for every call to GetProductAsync.
    public async Task<Product> GetProductAsync(string id)
    {
        using (var httpClient = new HttpClient())
        {
            var hostName = HttpContext.Current.Request.Url.Host;
            var result = await httpClient.GetStringAsync(string.Format("http://{0}:8080/api/...", hostName));
            return new Product { Name = result };
        }
    }
}

Numa aplicação Web, esta técnica não é dimensionável.In a web application, this technique is not scalable. Um novo objeto HttpClient é criado para cada pedido de utilizador.A new HttpClient object is created for each user request. Com muita carga, o servidor Web poderá esgotar o número de sockets disponíveis, resultando em SocketException erros.Under heavy load, the web server may exhaust the number of available sockets, resulting in SocketException errors.

Este problema não está limitado à classe HttpClient.This problem is not restricted to the HttpClient class. Outras classes que encapsulam recursos num wrapper ou são dispendiosas de criar poderão causar problemas semelhantes.Other classes that wrap resources or are expensive to create might cause similar issues. O exemplo seguinte cria uma instância da classe ExpensiveToCreateService.The following example creates an instances of the ExpensiveToCreateService class. Aqui o problema não é necessariamente esgotamento de socket, mas simplesmente quanto tempo demora para criar cada instância.Here the issue is not necessarily socket exhaustion, but simply how long it takes to create each instance. Criar e destruir continuamente instâncias desta classe poderá afetar negativamente a escalabilidade do sistema.Continually creating and destroying instances of this class might adversely affect the scalability of the system.

public class NewServiceInstancePerRequestController : ApiController
{
    public async Task<Product> GetProductAsync(string id)
    {
        var expensiveToCreateService = new ExpensiveToCreateService();
        return await expensiveToCreateService.GetProductByIdAsync(id);
    }
}

public class ExpensiveToCreateService
{
    public ExpensiveToCreateService()
    {
        // Simulate delay due to setup and configuration of ExpensiveToCreateService
        Thread.SpinWait(Int32.MaxValue / 100);
    }
    ...
}

Como resolver o problemaHow to fix the problem

Se a classe que encapsula num wrapper os recursos externos for partilhável e segura quanto a threads, crie uma instância singleton partilhada ou um conjunto de instâncias reutilizáveis da classe.If the class that wraps the external resource is shareable and thread-safe, create a shared singleton instance or a pool of reusable instances of the class.

O exemplo seguinte utiliza uma instância HttpClient estática e, por conseguinte, partilha a ligação entre todos os pedidos.The following example uses a static HttpClient instance, thus sharing the connection across all requests.

public class SingleHttpClientInstanceController : ApiController
{
    private static readonly HttpClient httpClient;

    static SingleHttpClientInstanceController()
    {
        httpClient = new HttpClient();
    }

    // This method uses the shared instance of HttpClient for every call to GetProductAsync.
    public async Task<Product> GetProductAsync(string id)
    {
        var hostName = HttpContext.Current.Request.Url.Host;
        var result = await httpClient.GetStringAsync(string.Format("http://{0}:8080/api/...", hostName));
        return new Product { Name = result };
    }
}

ConsideraçõesConsiderations

  • O elemento-chave deste anti-padrão é criar e destruir repetidamente instâncias de um objeto partilhável.The key element of this antipattern is repeatedly creating and destroying instances of a shareable object. Se uma classe não for partilhável (não seguro quanto a threads), então este anti-padrão não se aplica.If a class is not shareable (not thread-safe), then this antipattern does not apply.

  • O tipo de recurso partilhado pode ditar se deve utilizar um singleton ou criar um conjunto.The type of shared resource might dictate whether you should use a singleton or create a pool. A classe HttpClient foi concebida para ser partilhada, em vez de agrupada.The HttpClient class is designed to be shared rather than pooled. Outros objetos poderão suportar o conjunto, ao ativar o sistema para distribuir a carga de trabalho em várias instâncias.Other objects might support pooling, enabling the system to spread the workload across multiple instances.

  • Os objetos que partilha em vários pedidos têm ser seguros quanto a threads.Objects that you share across multiple requests must be thread-safe. A classe HttpClient foi concebida para ser utilizada desta forma, mas outras classes podem não suportar pedidos simultâneos, por isso, veja a documentação disponível.The HttpClient class is designed to be used in this manner, but other classes might not support concurrent requests, so check the available documentation.

  • Se cuidado em relação à definição de propriedades em objetos partilhados, dado que pode levar a condições race.Be careful about setting properties on shared objects, as this can lead to race conditions. Por exemplo, definir DefaultRequestHeaders na classe HttpClient antes de cada pedido pode criar uma condição race.For example, setting DefaultRequestHeaders on the HttpClient class before each request can create a race condition. Definir esse tipo de propriedades uma vez (por exemplo, durante o arranque) e criar instâncias separadas se precisar de configurar definições diferentes.Set such properties once (for example, during startup), and create separate instances if you need to configure different settings.

  • Alguns tipos de recursos são escassos e não devem ser mantidos.Some resource types are scarce and should not be held onto. As ligações de bases de dados são um exemplo.Database connections are an example. Ter uma ligação de base de dados aberta que não é necessária pode impedir que outros utilizadores em simultâneo tenham acesso à base de dados.Holding an open database connection that is not required may prevent other concurrent users from gaining access to the database.

  • No .NET Framework, muitos objetos que estabelecem ligações a recursos externos são criados com métodos de fábrica estáticos de outras classes que gerem estas ligações.In the .NET Framework, many objects that establish connections to external resources are created by using static factory methods of other classes that manage these connections. Estes objetos destinam-se a ser guardados e reutilizados, em vez de eliminados e recriados.These objects are intended to be saved and reused, rather than disposed and recreated. Por exemplo, no Azure Service Bus, o objeto QueueClient é criado através de um objeto MessagingFactory.For example, in Azure Service Bus, the QueueClient object is created through a MessagingFactory object. Internamente, o MessagingFactory gere ligações.Internally, the MessagingFactory manages connections. Para obter mais informações, veja as Best Practices for performance improvements using Service Bus Messaging (Melhores Práticas de melhorias do desempenho com as Mensagens do Service Bus).For more information, see Best Practices for performance improvements using Service Bus Messaging.

Como detetar o problemaHow to detect the problem

Os sintomas deste problema incluem uma redução no débito ou uma taxa de aumento de erro, juntamente com um ou mais dos seguintes acontecimentos:Symptoms of this problem include a drop in throughput or an increased error rate, along with one or more of the following:

  • Um aumento de exceções que indicam esgotamento de recursos, como sockets, ligações de base de dados, identificadores de ficheiros, etc.An increase in exceptions that indicate exhaustion of resources such as sockets, database connections, file handles, and so on.
  • Aumento da utilização da memória e da libertação da memória.Increased memory use and garbage collection.
  • Um aumento na atividade de rede, disco e base de dados.An increase in network, disk, or database activity.

Pode realizar os passos seguintes para ajudar a identificar este problema:You can perform the following steps to help identify this problem:

  1. Realizar a monitorização de processos do sistema de produção para identificar os pontos em que os tempos de resposta se tornam mais lentos ou o sistema falha devido à falta de recursos.Performing process monitoring of the production system, to identify points when response times slow down or the system fails due to lack of resources.
  2. Examine os dados telemétricos capturados nestes pontos para determinar as operações que podem estar a criar e a destruir objetos que consomem recursos.Examine the telemetry data captured at these points to determine which operations might be creating and destroying resource-consuming objects.
  3. Realize o teste de carga em cada operação suspeita num ambiente de teste controlado, em vez do sistema de produção.Load test each suspected operation, in a controlled test environment rather than the production system.
  4. Veja o código de origem e examine a forma como os objetos mediadores são geridos.Review the source code and examine the how broker objects are managed.

Veja os rastreios de pilha para as operações com uma execução lenta ou para gerar exceções quando o sistema está sob carga.Look at stack traces for operations that are slow-running or that generate exceptions when the system is under load. Estas informações podem ajudar a identificar a forma como estas operações estão a utilizar os recursos.This information can help to identify how these operations are utilizing resources. As exceções podem ajudar a determinar se os erros são causados pelos recursos partilhados se estarem a esgotar.Exceptions can help to determine whether errors are caused by shared resources being exhausted.

Diagnóstico de exemploExample diagnosis

As secções seguintes aplicam estes passos para o exemplo de aplicação descrito anteriormente.The following sections apply these steps to the sample application described earlier.

Identificar pontos de lentidão ou de falhaIdentify points of slow down or failure

A imagem seguinte mostra os resultados gerados com o New Relic APM, que mostra as operações que têm um tempo de resposta fraco.The following image shows results generated using New Relic APM, showing operations that have a poor response time. Neste caso, o método GetProductAsync no controlador NewHttpClientInstancePerRequest merece ainda mais investigação.In this case, the GetProductAsync method in the NewHttpClientInstancePerRequest controller is worth investigating further. Tenha em atenção que a taxa de erros também aumenta quando estas operações estão em execução.Notice that the error rate also increases when these operations are running.

O dashboard de monitorização New Relic mostra o exemplo de aplicação, ao criar uma nova instância de um objeto HttpClient para cada pedido

Examinar os dados telemétricos e localizar correlaçõesExamine telemetry data and find correlations

A imagem seguinte mostra os dados capturados com a criação de perfis de thread durante o mesmo período correspondente à imagem anterior.The next image shows data captured using thread profiling, over the same period corresponding as the previous image. O sistema passa muito tempo a abrir ligações de socket e ainda mais tempo a fechá-las e a processar exceções de socket.The system spends a significant time opening socket connections, and even more time closing them and handling socket exceptions.

O gerador de perfis de thread New Relic mostra o exemplo de aplicação, ao criar uma nova instância de um objeto HttpClient para cada pedido

Realizar teste de cargaPerforming load testing

Utilize o teste de carga para simular as operações habituais que os utilizadores podem realizar.Use load testing to simulate the typical operations that users might perform. Esta ação pode ajudar a identificar quais as partes de um sistema que são afetadas pelo esgotamento de recursos em diferentes cargas.This can help to identify which parts of a system suffer from resource exhaustion under varying loads. Realize estes testes num ambiente controlado, em vez do sistema de produção.Perform these tests in a controlled environment rather than the production system. O gráfico seguinte mostra o débito de pedidos processados pelo controlador NewHttpClientInstancePerRequest, à medida que a carga de utilizador aumenta para 100 utilizadores em simultâneo.The following graph shows the throughput of requests handled by the NewHttpClientInstancePerRequest controller as the user load increases to 100 concurrent users.

Débito do exemplo de aplicação ao criar uma nova instância de um objeto HttpClient para cada pedido

Em primeiro lugar, o volume de pedidos processados por segundo aumenta à medida que a carga de trabalho é aumentada.At first, the volume of requests handled per second increases as the workload increases. Em cerca de 30 utilizadores, no entanto, o volume de pedidos com êxito atinge um limite e o sistema começa a gerar exceções.At about 30 users, however, the volume of successful requests reaches a limit, and the system starts to generate exceptions. De ora em diante, o volume de exceções aumenta gradualmente de acordo com a carga do utilizador.From then on, the volume of exceptions gradually increases with the user load.

O teste de carga comunicou estas falhas como erros de HTTP 500 (Servidor Interno).The load test reported these failures as HTTP 500 (Internal Server) errors. Rever a telemetria mostrou que estes erros foram causados pela execução do sistema fora dos recursos de socket, uma vez que foram criados cada vez mais objetos HttpClient.Reviewing the telemetry showed that these errors were caused by the system running out of socket resources, as more and more HttpClient objects were created.

O gráfico seguinte mostra um teste semelhante para um controlador que cria o objeto ExpensiveToCreateService personalizado.The next graph shows a similar test for a controller that creates the custom ExpensiveToCreateService object.

Débito do exemplo de aplicação ao criar uma nova instância do ExpensiveToCreateService para cada pedido

Neste momento, o controlador não gera quaisquer exceções, mas o débito ainda atinge um patamar, enquanto o tempo de resposta médio aumenta por um fator de 20.This time, the controller does not generate any exceptions, but throughput still reaches a plateau, while the average response time increases by a factor of 20. (O gráfico utiliza uma escala logarítmica para o tempo de resposta e o débito.) A telemetria mostrou que a criação de novas instâncias do ExpensiveToCreateService foi a causa principal do problema.(The graph uses a logarithmic scale for response time and throughput.) Telemetry showed that creating new instances of the ExpensiveToCreateService was the main cause of the problem.

Implementar a solução e verificar o resultadoImplement the solution and verify the result

Após mudar o método GetProductAsync para partilhar numa única instância HttpClient, um segundo teste de carga mostrou uma melhoria do desempenho.After switching the GetProductAsync method to share a single HttpClient instance, a second load test showed improved performance. Não foram comunicados erros e o sistema foi capaz de lidar com um aumento da carga de até 500 pedidos por segundo.No errors were reported, and the system was able to handle an increasing load of up to 500 requests per second. O tempo de resposta médio foi reduzido para metade, em comparação com o teste anterior.The average response time was cut in half, compared with the previous test.

Débito do exemplo de aplicação ao reutilizar a mesma instância de um objeto HttpClient para cada pedido

Para comparação, a imagem seguinte mostra a telemetria de rastreio de pilha.For comparison, the following image shows the stack trace telemetry. Neste momento, o sistema passa a maior parte do tempo a realizar trabalho real, em vez de abrir e fechar sockets.This time, the system spends most of its time performing real work, rather than opening and closing sockets.

O gerador de perfis de thread New Relic mostra o exemplo de aplicação, ao criar uma única instância de um objeto HttpClient para todos os pedidos

O gráfico seguinte mostra um teste de carga semelhante com uma instância partilhada do objeto ExpensiveToCreateService.The next graph shows a similar load test using a shared instance of the ExpensiveToCreateService object. Novamente, o volume de pedidos processados aumenta de acordo com a carga de utilizador, enquanto o tempo de resposta médio permanece baixo.Again, the volume of handled requests increases in line with the user load, while the average response time remains low.

Débito do exemplo de aplicação ao reutilizar a mesma instância de um objeto HttpClient para cada pedido