Antipadrão E/S chattyChatty I/O antipattern

O efeito cumulativo de um elevado número de pedidos de E/S pode ter um impacto significativo no desempenho e na capacidade de resposta.The cumulative effect of a large number of I/O requests can have a significant impact on performance and responsiveness.

Descrição do problemaProblem description

As chamadas de rede e outras operações de E/S são inerentemente lentas em comparação com as tarefas de computação.Network calls and other I/O operations are inherently slow compared to compute tasks. Normalmente, cada pedido de E/S tem custos gerais significativos e o efeito cumulativo de muitas operações de E/S pode abrandar o sistema.Each I/O request typically has significant overhead, and the cumulative effect of numerous I/O operations can slow down the system. Seguem-se algumas causas comuns de E/S chatty.Here are some common causes of chatty I/O.

Leitura e escrita de registos individuais numa base de dados como pedidos distintosReading and writing individual records to a database as distinct requests

O exemplo seguinte lê a partir de uma base de dados de produtos.The following example reads from a database of products. Existem três tabelas, Product, ProductSubcategory e ProductPriceListHistory.There are three tables, Product, ProductSubcategory, and ProductPriceListHistory. O código obtém todos os produtos numa subcategoria, juntamente com as informações de preços, ao executar uma série de consultas:The code retrieves all of the products in a subcategory, along with the pricing information, by executing a series of queries:

  1. Consulte a subcategoria a partir da tabela ProductSubcategory.Query the subcategory from the ProductSubcategory table.
  2. Localize todos os produtos nessa subcategoria ao consultar a tabela Product.Find all products in that subcategory by querying the Product table.
  3. Para cada produto, consulte os dados de preços a partir da tabela ProductPriceListHistory.For each product, query the pricing data from the ProductPriceListHistory table.

A aplicação utiliza o Entity Framework para consultar a base de dados.The application uses Entity Framework to query the database. Pode encontrar o exemplo completo aqui.You can find the complete sample here.

public async Task<IHttpActionResult> GetProductsInSubCategoryAsync(int subcategoryId)
{
    using (var context = GetContext())
    {
        // Get product subcategory.
        var productSubcategory = await context.ProductSubcategories
                .Where(psc => psc.ProductSubcategoryId == subcategoryId)
                .FirstOrDefaultAsync();

        // Find products in that category.
        productSubcategory.Product = await context.Products
            .Where(p => subcategoryId == p.ProductSubcategoryId)
            .ToListAsync();

        // Find price history for each product.
        foreach (var prod in productSubcategory.Product)
        {
            int productId = prod.ProductId;
            var productListPriceHistory = await context.ProductListPriceHistory
                .Where(pl => pl.ProductId == productId)
                .ToListAsync();
            prod.ProductListPriceHistory = productListPriceHistory;
        }
        return Ok(productSubcategory);
    }
}

Este exemplo mostra o problema explicitamente, mas, por vezes, um O/RM pode mascarar o problema, se obtiver implicitamente os registos subordinados um de cada vez.This example shows the problem explicitly, but sometimes an O/RM can mask the problem, if it implicitly fetches child records one at a time. Isto é conhecido como o "problema N+1".This is known as the "N+1 problem".

Implementar uma única operação lógica como uma série de pedidos HTTPImplementing a single logical operation as a series of HTTP requests

Isto acontece frequentemente quando os programadores tentam seguir um paradigma orientado para objetos e tratam os objetos remotos como se fossem objetos locais na memória.This often happens when developers try to follow an object-oriented paradigm, and treat remote objects as if they were local objects in memory. Isto pode resultar em demasiados percursos de ida e volta na rede.This can result in too many network round trips. Por exemplo, a API da Web seguinte expõe as propriedades individuais de objetos User através de métodos GET HTTP individuais.For example, the following web API exposes the individual properties of User objects through individual HTTP GET methods.

public class UserController : ApiController
{
    [HttpGet]
    [Route("users/{id:int}/username")]
    public HttpResponseMessage GetUserName(int id)
    {
        ...
    }

    [HttpGet]
    [Route("users/{id:int}/gender")]
    public HttpResponseMessage GetGender(int id)
    {
        ...
    }

    [HttpGet]
    [Route("users/{id:int}/dateofbirth")]
    public HttpResponseMessage GetDateOfBirth(int id)
    {
        ...
    }
}

Enquanto não existe nada tecnicamente errado com esta abordagem, a maioria dos clientes precisa provavelmente de obter várias propriedades para cada User, o que resulta num código de cliente semelhante ao seguinte.While there's nothing technically wrong with this approach, most clients will probably need to get several properties for each User, resulting in client code like the following.

HttpResponseMessage response = await client.GetAsync("users/1/username");
response.EnsureSuccessStatusCode();
var userName = await response.Content.ReadAsStringAsync();

response = await client.GetAsync("users/1/gender");
response.EnsureSuccessStatusCode();
var gender = await response.Content.ReadAsStringAsync();

response = await client.GetAsync("users/1/dateofbirth");
response.EnsureSuccessStatusCode();
var dob = await response.Content.ReadAsStringAsync();

Ler e escrever num ficheiro no discoReading and writing to a file on disk

A E/S de ficheiros envolve abrir um ficheiro e mover para o ponto adequado antes de ler ou escrever dados.File I/O involves opening a file and moving to the appropriate point before reading or writing data. Quando a operação estiver concluída, o ficheiro pode estar fechado para poupar recursos do sistema operativo.When the operation is complete, the file might be closed to save operating system resources. Uma aplicação que lê e escreve continuamente pequenas quantidades de informações num ficheiro irá gerar custos gerais de E/S significativos.An application that continually reads and writes small amounts of information to a file will generate significant I/O overhead. Pequenos pedidos de escrita também podem originar uma fragmentação de ficheiros, o que abranda ainda mais as operações de E/S subsequentes.Small write requests can also lead to file fragmentation, slowing subsequent I/O operations still further.

O exemplo seguinte utiliza FileStream para escrever um objeto Customer num ficheiro.The following example uses a FileStream to write a Customer object to a file. A criação de FileStream abre o ficheiro e a eliminação do mesmo fecha o ficheiro.Creating the FileStream opens the file, and disposing it closes the file. (A instrução using elimina automaticamente o objeto FileStream.) Se a aplicação chamar este método repetidamente à medida que são adicionados novos clientes, os custos gerais de E/S podem acumular-se rapidamente.(The using statement automatically disposes the FileStream object.) If the application calls this method repeatedly as new customers are added, the I/O overhead can accumulate quickly.

private async Task SaveCustomerToFileAsync(Customer cust)
{
    using (Stream fileStream = new FileStream(CustomersFileName, FileMode.Append))
    {
        BinaryFormatter formatter = new BinaryFormatter();
        byte [] data = null;
        using (MemoryStream memStream = new MemoryStream())
        {
            formatter.Serialize(memStream, cust);
            data = memStream.ToArray();
        }
        await fileStream.WriteAsync(data, 0, data.Length);
    }
}

Como resolver o problemaHow to fix the problem

Reduza o número de pedidos de E/S ao empacotar os dados num menor número de pedidos maiores.Reduce the number of I/O requests by packaging the data into larger, fewer requests.

Obtenha os dados a partir de uma base de dados como uma única consulta, em vez de várias consultas mais pequenas.Fetch data from a database as a single query, instead of several smaller queries. Segue-se uma versão revista do código que obtém as informações do produto.Here's a revised version of the code that retrieves product information.

public async Task<IHttpActionResult> GetProductCategoryDetailsAsync(int subCategoryId)
{
    using (var context = GetContext())
    {
        var subCategory = await context.ProductSubcategories
                .Where(psc => psc.ProductSubcategoryId == subCategoryId)
                .Include("Product.ProductListPriceHistory")
                .FirstOrDefaultAsync();

        if (subCategory == null)
            return NotFound();

        return Ok(subCategory);
    }
}

Siga os princípios de conceção REST para APIs da Web.Follow REST design principles for web APIs. Segue-se uma versão revista da API da Web do exemplo anterior.Here's a revised version of the web API from the earlier example. Em vez de métodos GET separados para cada propriedade, existe um único método GET que devolve User.Instead of separate GET methods for each property, there is a single GET method that returns the User. Isto resulta num corpo de resposta maior por pedido, mas é provável que cada cliente efetue menos chamadas de API.This results in a larger response body per request, but each client is likely to make fewer API calls.

public class UserController : ApiController
{
    [HttpGet]
    [Route("users/{id:int}")]
    public HttpResponseMessage GetUser(int id)
    {
        ...
    }
}

// Client code
HttpResponseMessage response = await client.GetAsync("users/1");
response.EnsureSuccessStatusCode();
var user = await response.Content.ReadAsStringAsync();

Para a E/S de ficheiros, considere colocar os dados na memória intermédia e, em seguida, escrevê-los num ficheiro como uma única operação.For file I/O, consider buffering data in memory and then writing the buffered data to a file as a single operation. Esta abordagem reduz os custos gerais de abrir e fechar repetidamente o ficheiro e ajuda a reduzir a fragmentação do ficheiro no disco.This approach reduces the overhead from repeatedly opening and closing the file, and helps to reduce fragmentation of the file on disk.

// Save a list of customer objects to a file
private async Task SaveCustomerListToFileAsync(List<Customer> customers)
{
    using (Stream fileStream = new FileStream(CustomersFileName, FileMode.Append))
    {
        BinaryFormatter formatter = new BinaryFormatter();
        foreach (var cust in customers)
        {
            byte[] data = null;
            using (MemoryStream memStream = new MemoryStream())
            {
                formatter.Serialize(memStream, cust);
                data = memStream.ToArray();
            }
            await fileStream.WriteAsync(data, 0, data.Length);
        }
    }
}

// In-memory buffer for customers.
List<Customer> customers = new List<Customers>();

// Create a new customer and add it to the buffer
var cust = new Customer(...);
customers.Add(cust);

// Add more customers to the list as they are created
...

// Save the contents of the list, writing all customers in a single operation
await SaveCustomerListToFileAsync(customers);

ConsideraçõesConsiderations

  • Os primeiros dois exemplos efetuam menos chamadas de E/S, mas cada uma delas obtém mais informações.The first two examples make fewer I/O calls, but each one retrieves more information. Tem de considerar o compromisso entre estes dois fatores.You must consider the tradeoff between these two factors. A resposta adequada irá depender dos padrões de utilização reais.The right answer will depend on the actual usage patterns. Por exemplo, no exemplo da API da Web, é possível os clientes precisem frequentemente apenas do nome de utilizador.For example, in the web API example, it might turn out that clients often need just the user name. Nesse caso, poderá ser aconselhável expô-la como uma chamada de API separada.In that case, it might make sense to expose it as a separate API call. Para obter mais informações, veja o antipadrão Obtenção Externa.For more information, see the Extraneous Fetching antipattern.

  • Ao ler dados, não crie pedidos de E/S demasiado grandes.When reading data, do not make your I/O requests too large. Uma aplicação deve obter apenas as informações que é provável que utilize.An application should only retrieve the information that it is likely to use.

  • Por vezes, ajuda criar partições das informações para um objeto em dois segmentos, os dados acedidos com frequência (que ocorrem na maioria dos pedidos) e os dados acedidos com menos frequência (raramente utilizados).Sometimes it helps to partition the information for an object into two chunks, frequently accessed data that accounts for most requests, and less frequently accessed data that is used rarely. Muitas vezes, os dados acedidos com mais frequência são uma parte relativamente pequena do total de dados para um objeto, pelo que devolver apenas essa parte pode evitar custos gerais de E/S significativos.Often the most frequently accessed data is a relatively small portion of the total data for an object, so returning just that portion can save significant I/O overhead.

  • Ao escrever dados, evite bloquear os recursos mais tempo do que o necessário, para reduzir as possibilidades de contenção durante uma operação demorada.When writing data, avoid locking resources for longer than necessary, to reduce the chances of contention during a lengthy operation. Se uma operação de escrita abranger vários arquivos de dados, ficheiros ou serviços, adote uma abordagem eventualmente consistente.If a write operation spans multiple data stores, files, or services, then adopt an eventually consistent approach. Veja Orientações de Consistência de Dados.See Data Consistency guidance.

  • Se colocar os dados na memória intermédia antes de os escrever, os dados ficam vulneráveis se o processo falhar.If you buffer data in memory before writing it, the data is vulnerable if the process crashes. Se a taxa de dados tiver normalmente picos ou for relativamente dispersa, poderá ser mais seguro colocar os dados na memória intermédia numa fila durável externa, como Hubs de Eventos.If the data rate typically has bursts or is relatively sparse, it may be safer to buffer the data in an external durable queue such as Event Hubs.

  • Considere colocar em cache os dados obtidos de um serviço ou base de dados.Consider caching data that you retrieve from a service or a database. Isto pode ajudar a reduzir o volume de E/S ao evitar pedidos repetidos para os mesmos dados.This can help to reduce the volume of I/O by avoiding repeated requests for the same data. Para obter mais informações, veja Melhores práticas para colocação em cache.For more information, see Caching best practices.

Como detetar o problemaHow to detect the problem

Os sintomas de E/S chatty incluem elevada latência e baixo débito.Symptoms of chatty I/O include high latency and low throughput. É provável que os utilizadores finais reportem tempos de resposta prolongados ou falhas causadas por tempo limite excedido, devido ao aumento da contenção de recursos de E/S.End users are likely to report extended response times or failures caused by services timing out, due to increased contention for I/O resources.

Pode efetuar os passos seguintes para ajudar a identificar as causas de qualquer problema:You can perform the following steps to help identify the causes of any problems:

  1. Efetue a monitorização de processos do sistema de produção para identificar as operações com fracos tempos de resposta.Perform process monitoring of the production system to identify operations with poor response times.
  2. Efetue testes de carga de cada operação identificada no passo anterior.Perform load testing of each operation identified in the previous step.
  3. Durante os testes de carga, recolha dados de telemetria sobre os pedidos de acesso a dados efetuados por cada operação.During the load tests, gather telemetry data about the data access requests made by each operation.
  4. Recolha estatísticas detalhadas de cada pedido enviado para um arquivo de dados.Gather detailed statistics for each request sent to a data store.
  5. Crie o perfil da aplicação no ambiente de teste para estabelecer onde podem ocorrer possíveis estrangulamentos de E/S.Profile the application in the test environment to establish where possible I/O bottlenecks might be occurring.

Procure por qualquer um destes sintomas:Look for any of these symptoms:

  • Um elevado número de pequenos pedidos de E/S efetuados para o mesmo ficheiro.A large number of small I/O requests made to the same file.
  • Um elevado número de pequenos pedidos de rede efetuados por uma instância de aplicação para o mesmo serviço.A large number of small network requests made by an application instance to the same service.
  • Um elevado número de pequenos pedidos efetuados por uma instância de aplicação para o mesmo arquivo de dados.A large number of small requests made by an application instance to the same data store.
  • As aplicações e os serviços tornam-se vinculados por E/S.Applications and services becoming I/O bound.

Diagnóstico de exemploExample diagnosis

As secções seguintes aplicam estes passos ao exemplo apresentado anteriormente que consulta uma base de dados.The following sections apply these steps to the example shown earlier that queries a database.

Testar a carga da aplicaçãoLoad test the application

Este gráfico mostra os resultados do teste de carga.This graph shows the results of load testing. O tempo de resposta mediano é medido em dezenas de segundos por pedido.Median response time is measured in tens of seconds per request. O gráfico mostra a latência muito elevada.The graph shows very high latency. Com uma carga de 1000 utilizadores, um utilizador pode ter de aguardar durante quase um minuto para ver os resultados de uma consulta.With a load of 1000 users, a user might have to wait for nearly a minute to see the results of a query.

Principais indicadores dos resultados do teste de carga para a aplicação de exemplo E/S chatty

Nota

A aplicação foi implementada como uma aplicação Web do Serviço de Aplicações do Azure através de uma Base de Dados SQL do Azure.The application was deployed as an Azure App Service web app, using Azure SQL Database. O teste de carga utilizou uma carga de trabalho com passos simulados superior a 1000 utilizadores em simultâneo.The load test used a simulated step workload of up to 1000 concurrent users. A base de dados foi configurada com um conjunto de ligações que suporta até 1000 ligações simultâneas para reduzir a probabilidade de a contenção de ligações afetar os resultados.The database was configured with a connection pool supporting up to 1000 concurrent connections, to reduce the chance that contention for connections would affect the results.

Monitorizar a aplicaçãoMonitor the application

Pode utilizar um pacote de monitorização de desempenho de aplicações (APM) para capturar e analisar as métricas-chave que podem identificar E/S chatty.You can use an application performance monitoring (APM) package to capture and analyze the key metrics that might identify chatty I/O. As métricas que são importantes irão depender da carga de trabalho de E/S.Which metrics are important will depend on the I/O workload. Neste exemplo, os pedidos de E/S interessantes foram as consultas da base de dados.For this example, the interesting I/O requests were the database queries.

A imagem seguinte mostra os resultados gerados através do APM New Relic.The following image shows results generated using New Relic APM. O tempo de resposta médio da base de dados teve um pico aproximadamente nos 5,6 segundos por pedido durante a carga de trabalho máxima.The average database response time peaked at approximately 5.6 seconds per request during the maximum workload. O sistema conseguiu suportar uma média de 410 pedidos por minuto durante o teste.The system was able to support an average of 410 requests per minute throughout the test.

Descrição geral do tráfego que atinge a base de dados AdventureWorks2012

Recolher informações de acesso a dados detalhadasGather detailed data access information

A análise mais aprofundada dos dados de monitorização mostra que a aplicação executa três instruções SELECT SQL diferentes.Digging deeper into the monitoring data shows the application executes three different SQL SELECT statements. Estas correspondem aos pedidos gerados pelo Entity Framework para obter dados a partir das tabelas ProductListPriceHistory, Product e ProductSubcategory.These correspond to the requests generated by Entity Framework to fetch data from the ProductListPriceHistory, Product, and ProductSubcategory tables. Além disso, a consulta que obtém os dados da tabela ProductListPriceHistory é de longe a instrução SELECT executada com mais frequência por uma ordem de magnitude.Furthermore, the query that retrieves data from the ProductListPriceHistory table is by far the most frequently executed SELECT statement, by an order of magnitude.

Consultas efetuadas pela aplicação de exemplo no teste

Parece que o método GetProductsInSubCategoryAsync, apresentado anteriormente, efetua 45 consultas SELECT.It turns out that the GetProductsInSubCategoryAsync method, shown earlier, performs 45 SELECT queries. Cada consulta faz com que a aplicação abra uma nova ligação SQL.Each query causes the application to open a new SQL connection.

Estatísticas das consultas da aplicação de exemplo no teste

Nota

Esta imagem mostra as informações de rastreio para a instância mais lenta da operação GetProductsInSubCategoryAsync no teste de carga.This image shows trace information for the slowest instance of the GetProductsInSubCategoryAsync operation in the load test. Num ambiente de produção, é útil examinar os rastreios das instâncias mais lentas, para ver se existe um padrão que sugira um problema.In a production environment, it's useful to examine traces of the slowest instances, to see if there is a pattern that suggests a problem. Se observar os valores médios, pode detetar problemas que irão ser significativamente piores em carga.If you just look at the average values, you might overlook problems that will get dramatically worse under load.

A imagem seguinte mostra as instruções SQL reais que foram emitidas.The next image shows the actual SQL statements that were issued. A consulta que obtém as informações de preços é executada para cada produto individual na subcategoria de produtos.The query that fetches price information is run for each individual product in the product subcategory. A utilização de uma associação reduziria significativamente o número de chamadas da base de dados.Using a join would considerably reduce the number of database calls.

Detalhes das consultas da aplicação de exemplo no teste

Se estiver a utilizar um O/RM, como o Entity Framework, rastrear as consultas SQL pode fornecer informações sobre como o O/RM traduz as chamadas programáticas para instruções SQL e indica as áreas onde o acesso a dados pode ser otimizado.If you are using an O/RM, such as Entity Framework, tracing the SQL queries can provide insight into how the O/RM translates programmatic calls into SQL statements, and indicate areas where data access might be optimized.

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

A reescrita da chamada ao Entity Framework produziu os seguintes resultados.Rewriting the call to Entity Framework produced the following results.

Principais indicadores dos resultados do teste de carga para a API de segmentos na aplicação de exemplo E/S chatty

Este teste de carga foi efetuado na mesma implementação com o mesmo perfil de carga.This load test was performed on the same deployment, using the same load profile. Desta vez, o gráfico mostra uma latência muito menor.This time the graph shows much lower latency. O tempo médio de pedidos em 1000 utilizadores situa-se entre 5 e 6 segundos, menos quase um minuto.The average request time at 1000 users is between 5 and 6 seconds, down from nearly a minute.

Desta vez, o sistema suportou uma média de 3970 pedidos por minuto, em comparação com os 410 do teste anterior.This time the system supported an average of 3,970 requests per minute, compared to 410 for the earlier test.

Descrição geral das transações da API de segmentos

O rastreio da instrução SQL mostra que todos os dados são obtidos numa única instrução SELECT.Tracing the SQL statement shows that all the data is fetched in a single SELECT statement. Embora esta consulta seja consideravelmente mais complexa, é executada apenas uma vez por operação.Although this query is considerably more complex, it is performed only once per operation. Apesar de as associações complexas se poderem tornar dispendiosas, os sistemas de base de dados relacionais estão otimizados para este tipo de consulta.And while complex joins can become expensive, relational database systems are optimized for this type of query.

Detalhes das consultas da API de segmentos