Editar

Partilhar via


Otimização do desempenho - Vários serviços de back-end

Azure Kubernetes Service (AKS)
Azure Cosmos DB

Este artigo descreve como uma equipa de desenvolvimento utilizou métricas para encontrar estrangulamentos e melhorar o desempenho de um sistema distribuído. O artigo baseia-se no teste de carga real que foi feito para uma aplicação de exemplo. A aplicação é da Linha de Base do Azure Kubernetes Service (AKS) para microsserviços, juntamente com um projeto de teste de carga do Visual Studio utilizado para gerar os resultados.

Este artigo faz parte de uma série. Leia a primeira parte aqui.

Cenário: chame vários serviços de back-end para obter informações e, em seguida, agregar os resultados.

Este cenário envolve uma aplicação de entrega por drone. Os clientes podem consultar uma API REST para obter as informações mais recentes da fatura. A fatura inclui um resumo das entregas, pacotes e utilização total de drones do cliente. Esta aplicação utiliza uma arquitetura de microsserviços em execução no AKS e as informações necessárias para a fatura são distribuídas por vários microsserviços.

Em vez de o cliente chamar cada serviço diretamente, a aplicação implementa o padrão de Agregação de Gateway . Com este padrão, o cliente faz um único pedido a um serviço de gateway. Por sua vez, o gateway chama os serviços de back-end em paralelo e, em seguida, agrega os resultados num payload de resposta única.

Diagrama a mostrar o padrão de Agregação do Gateway

Teste 1: Desempenho da linha de base

Para estabelecer uma linha de base, a equipa de desenvolvimento começou com um teste de carregamento de passos, aumentando a carga de um utilizador simulado até 40 utilizadores, durante um período total de 8 minutos. O gráfico seguinte, retirado do Visual Studio, mostra os resultados. A linha roxa mostra a carga do utilizador e a linha laranja mostra débito (pedidos médios por segundo).

Gráfico dos resultados do teste de carga do Visual Studio

A linha vermelha ao longo da parte inferior do gráfico mostra que não foram devolvidos erros ao cliente, o que é encorajador. No entanto, o débito médio atinge um pico a cerca de metade do teste e, em seguida, cai para o resto, mesmo que a carga continue a aumentar. Isto indica que o back-end não consegue acompanhar. O padrão visto aqui é comum quando um sistema começa a atingir os limites de recursos , depois de atingir um máximo, o débito realmente cai significativamente. A contenção de recursos, erros transitórios ou um aumento da taxa de exceções podem contribuir para este padrão.

Vamos analisar os dados de monitorização para saber o que está a acontecer dentro do sistema. O gráfico seguinte é retirado do Application Insights. Mostra a duração média das chamadas HTTP do gateway para os serviços de back-end.

Gráfico de durações de chamadas HTTP

Este gráfico mostra que uma operação em particular, GetDroneUtilization, demora muito mais tempo, em média, por uma ordem de magnitude. O gateway faz estas chamadas em paralelo, pelo que a operação mais lenta determina quanto tempo demora a conclusão de todo o pedido.

Claramente, o próximo passo é investigar a GetDroneUtilization operação e procurar estrangulamentos. Uma possibilidade é o esgotamento de recursos. Talvez este serviço de back-end em particular esteja a ficar sem CPU ou memória. Para um cluster do AKS, estas informações estão disponíveis no portal do Azure através da funcionalidade informações de contentor do Azure Monitor. Os gráficos seguintes mostram a utilização de recursos ao nível do cluster:

Graph of AKS node utilization (Grafo de utilização de nós do AKS)

Nesta captura de ecrã, são apresentados os valores médio e máximo. É importante analisar mais do que apenas a média, uma vez que a média pode ocultar picos nos dados. Aqui, a utilização média da CPU permanece abaixo dos 50%, mas existem alguns picos para 80%. Está perto da capacidade, mas ainda dentro das tolerâncias. Outra coisa está a causar o estrangulamento.

O gráfico seguinte revela o verdadeiro culpado. Este gráfico mostra códigos de resposta HTTP da base de dados de back-end do Serviço de entrega, que neste caso é o Azure Cosmos DB. A linha azul representa os códigos de êxito (HTTP 2xx), enquanto a linha verde representa erros HTTP 429. Um código de retorno HTTP 429 significa que o Azure Cosmos DB está a limitar temporariamente os pedidos, porque o autor da chamada está a consumir mais unidades de recursos (RU) do que o aprovisionado.

Gráfico de pedidos limitados

Para obter mais informações, a equipa de desenvolvimento utilizou o Application Insights para ver a telemetria ponto a ponto de uma amostra representativa de pedidos. Eis uma instância:

Captura de ecrã da vista de transação ponto a ponto

Esta vista mostra as chamadas relacionadas com um único pedido de cliente, juntamente com informações de temporização e códigos de resposta. As chamadas de nível superior são do gateway para os serviços de back-end. A chamada para GetDroneUtilization é expandida para mostrar chamadas para dependências externas , neste caso, para o Azure Cosmos DB. A chamada a vermelho devolveu um erro HTTP 429.

Tenha em atenção o grande intervalo entre o erro HTTP 429 e a chamada seguinte. Quando a biblioteca de cliente do Azure Cosmos DB recebe um erro HTTP 429, este recua automaticamente e aguarda para repetir a operação. O que esta vista mostra é que, durante os 672 ms que esta operação demorou, a maior parte desse tempo foi despendida à espera de repetir o Azure Cosmos DB.

Eis outro gráfico interessante para esta análise. Mostra o consumo de RUs por partição física versus RUs aprovisionadas por partição física:

Gráfico do consumo de RUs por partição

Para compreender este gráfico, tem de compreender como o Azure Cosmos DB gere as partições. As coleções no Azure Cosmos DB podem ter uma chave de partição. Cada valor de chave possível define uma partição lógica dos dados na coleção. O Azure Cosmos DB distribui estas partições lógicas por uma ou mais partições físicas . A gestão de partições físicas é processada automaticamente pelo Azure Cosmos DB. À medida que armazena mais dados, o Azure Cosmos DB pode mover partições lógicas para novas partições físicas, de modo a distribuir a carga pelas partições físicas.

Para este teste de carga, a coleção do Azure Cosmos DB foi aprovisionada com 900 RUs. O gráfico mostra 100 RU por partição física, o que implica um total de nove partições físicas. Embora o Azure Cosmos DB processe automaticamente a fragmentação de partições físicas, saber que a contagem de partições pode dar informações sobre o desempenho. A equipa de desenvolvimento irá utilizar estas informações mais tarde, uma vez que continuam a otimizar. Quando a linha azul cruza a linha horizontal roxa, o consumo de RUs excedeu as RUs aprovisionadas. É esse o ponto em que o Azure Cosmos DB começará a limitar as chamadas.

Teste 2: Aumentar unidades de recursos

Para o segundo teste de carga, a equipa aumentou horizontalmente a coleção do Azure Cosmos DB de 900 RU para 2500 RU. O débito aumentou de 19 pedidos/segundo para 23 pedidos/segundo e a latência média baixou de 669 ms para 569 ms.

Metric Teste 1 Teste 2
Débito (req/seg) 19 23
Latência média (ms) 669 569
Pedidos com êxito 9,8 K 11 K

Estes não são grandes ganhos, mas olhar para o gráfico ao longo do tempo mostra uma imagem mais completa:

Gráfico dos resultados do teste de carga do Visual Studio a mostrar um débito mais consistente.

Enquanto o teste anterior mostrou um pico inicial seguido de uma queda acentuada, este teste mostra um débito mais consistente. No entanto, o débito máximo não é significativamente maior.

Todos os pedidos para o Azure Cosmos DB devolveram um estado 2xx e os erros HTTP 429 desapareceram:

Gráfico de chamadas do Azure Cosmos DB

O gráfico de consumo de RUs versus RUs aprovisionadas mostra que há bastante espaço. Existem cerca de 275 RUs por partição física e o teste de carga atingiu o pico em cerca de 100 RUs consumidas por segundo.

Gráfico de consumo de RUs versus RUs aprovisionadas que mostra que há bastante espaço.

Outra métrica interessante é o número de chamadas para o Azure Cosmos DB por operação bem-sucedida:

Metric Teste 1 Teste 2
Chamadas por operação 11 9

Assumindo que não existem erros, o número de chamadas deve corresponder ao plano de consulta real. Neste caso, a operação envolve uma consulta entre partições que atinge todas as nove partições físicas. O valor mais elevado no primeiro teste de carga reflete o número de chamadas que devolveram um erro 429.

Esta métrica foi calculada ao executar uma consulta personalizada do Log Analytics:

let start=datetime("2020-06-18T20:59:00.000Z");
let end=datetime("2020-07-24T21:10:00.000Z");
let operationNameToEval="GET DroneDeliveries/GetDroneUtilization";
let dependencyType="Azure DocumentDB";
let dataset=requests
| where timestamp > start and timestamp < end
| where success == true
| where name == operationNameToEval;
dataset
| project reqOk=itemCount
| summarize
    SuccessRequests=sum(reqOk),
    TotalNumberOfDepCalls=(toscalar(dependencies
    | where timestamp > start and timestamp < end
    | where type == dependencyType
    | summarize sum(itemCount)))
| project
    OperationName=operationNameToEval,
    DependencyName=dependencyType,
    SuccessRequests,
    AverageNumberOfDepCallsPerOperation=(TotalNumberOfDepCalls/SuccessRequests)

Para resumir, o segundo teste de carga mostra melhorias. No entanto, a GetDroneUtilization operação ainda demora cerca de uma ordem de magnitude maior do que a operação mais lenta seguinte. Analisar as transações ponto a ponto ajuda a explicar porquê:

Captura de ecrã do segundo teste de carga a mostrar melhorias.

Conforme mencionado anteriormente, a GetDroneUtilization operação envolve uma consulta entre partições para o Azure Cosmos DB. Isto significa que o cliente do Azure Cosmos DB tem de distribuir a consulta a cada partição física e recolher os resultados. Como mostra a vista de transação ponto a ponto, estas consultas estão a ser executadas em série. A operação demora desde que a soma de todas as consultas e este problema só piore à medida que o tamanho dos dados aumenta e são adicionadas mais partições físicas.

Teste 3: Consultas paralelas

Com base nos resultados anteriores, uma forma óbvia de reduzir a latência é emitir as consultas em paralelo. O SDK de cliente do Azure Cosmos DB tem uma definição que controla o grau máximo de paralelismo.

Valor Descrição
0 Sem paralelismo (predefinição)
> 0 Número máximo de chamadas paralelas
-1 O SDK de cliente seleciona um grau ideal de paralelismo

Para o terceiro teste de carga, esta definição foi alterada de 0 para -1. A tabela seguinte resume os resultados:

Metric Teste 1 Teste 2 Teste 3
Débito (req/seg) 19 23 42
Latência média (ms) 669 569 215
Pedidos com êxito 9,8 K 11 K 20 K
Pedidos limitados 2,72 K 0 0

No gráfico de teste de carga, não só o débito geral é muito maior (a linha laranja), como o débito também mantém o ritmo com a carga (a linha roxa).

Graph of Visual Studio load test results showing higher overall débito that keeps pace with load.

Podemos verificar se o cliente do Azure Cosmos DB está a fazer consultas em paralelo ao analisar a vista de transação ponto a ponto:

Captura de ecrã da vista de transação ponto a ponto a mostrar que o cliente do Azure Cosmos DB está a fazer consultas em paralelo.

Curiosamente, um efeito colateral do aumento do débito é que o número de RUs consumidas por segundo também aumenta. Embora o Azure Cosmos DB não tenha limitado nenhum pedido durante este teste, o consumo foi próximo do limite de RU aprovisionado:

Gráfico de consumo de RU próximo do limite de RU aprovisionado.

Este gráfico pode ser um sinal para aumentar ainda mais a base de dados. No entanto, verifica-se que podemos otimizar a consulta.

Passo 4: Otimizar a consulta

O teste de carga anterior mostrou um melhor desempenho em termos de latência e débito. A latência média dos pedidos foi reduzida em 68% e o débito aumentou 220%. No entanto, a consulta entre partições é uma preocupação.

O problema das consultas entre partições é que paga por RU em todas as partições. Se a consulta só for executada ocasionalmente , digamos, uma vez por hora, poderá não importar. No entanto, sempre que vir uma carga de trabalho de leitura intensiva que envolva uma consulta entre partições, deve ver se a consulta pode ser otimizada ao incluir uma chave de partição. (Poderá ter de redesenhar a coleção para utilizar uma chave de partição diferente.)

Eis a consulta para este cenário específico:

SELECT * FROM c
WHERE c.ownerId = <ownerIdValue> and
      c.year = <yearValue> and
      c.month = <monthValue>

Esta consulta seleciona registos que correspondem a um ID de proprietário específico e mês/ano. Na estrutura original, nenhuma destas propriedades é a chave de partição. Isto requer que o cliente acione a consulta para cada partição física e reúna os resultados. Para melhorar o desempenho das consultas, a equipa de desenvolvimento alterou a estrutura para que o ID do proprietário seja a chave de partição da coleção. Desta forma, a consulta pode visar uma partição física específica. (O Azure Cosmos DB processa isto automaticamente; não tem de gerir o mapeamento entre valores de chave de partição e partições físicas.)

Depois de mudar a coleção para a nova chave de partição, houve uma melhoria dramática no consumo de RU, que se traduz diretamente em custos mais baixos.

Metric Teste 1 Teste 2 Teste 3 Teste 4
RUs por operação 29 29 29 3.4
Chamadas por operação 11 9 10 1

A vista de transação ponto a ponto mostra que, conforme previsto, a consulta lê apenas uma partição física:

Captura de ecrã da vista de transação ponto a ponto a mostrar que a consulta lê apenas uma partição física.

O teste de carga mostra um débito e latência melhorados:

Metric Teste 1 Teste 2 Teste 3 Teste 4
Débito (req/seg) 19 23 42 59
Latência média (ms) 669 569 215 176
Pedidos com êxito 9,8 K 11 K 20 K 29 K
Pedidos limitados 2,72 K 0 0 0

Uma consequência do desempenho melhorado é que a utilização da CPU do nó se torna muito elevada:

Gráfico a mostrar uma utilização elevada da CPU do nó.

No final do teste de carga, a CPU média atingiu cerca de 90%, e a CPU máxima atingiu os 100%. Esta métrica indica que a CPU é o próximo estrangulamento no sistema. Se for necessário um débito mais elevado, o próximo passo poderá ser aumentar horizontalmente o serviço de Entrega para mais instâncias.

Resumo

Para este cenário, foram identificados os seguintes estrangulamentos:

  • Pedidos de limitação do Azure Cosmos DB devido a RUs insuficientes aprovisionadas.
  • Latência elevada causada pela consulta de várias partições de base de dados em série.
  • Consulta entre partições ineficiente, porque a consulta não incluiu a chave de partição.

Além disso, a utilização da CPU foi identificada como um potencial estrangulamento numa escala mais elevada. Para diagnosticar estes problemas, a equipa de desenvolvimento analisou:

  • Latência e débito do teste de carga.
  • Erros do Azure Cosmos DB e consumo de RU.
  • A vista de transação ponto a ponto no Application Insight.
  • Utilização da CPU e da memória nas informações de contentor do Azure Monitor.

Passos seguintes

Rever anti-padrões de desempenho