Editar

Compartilhar via


Ajuste de desempenho de transações comerciais distribuídas

AKS (Serviço de Kubernetes do Azure)
Cache do Azure para Redis

Este artigo descreve como uma equipe de desenvolvimento usou métricas para encontrar gargalos e melhorar o desempenho de um sistema distribuído. O artigo é baseado no teste de carga real que foi feito para um aplicativo de exemplo. O aplicativo é originado da Linha de base do Serviço de Kubernetes do Azure (AKS) para microsserviços.

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

Cenário: um aplicativo cliente inicia uma transação comercial que envolve várias etapas.

Esse cenário envolve um aplicativo de entrega de drones que é executado no AKS. Os clientes usam um aplicativo Web para agendar entregas por drone. Cada transação requer várias etapas que são executadas por microsserviços separados no back-end:

  • O serviço Entrega gerencia as entregas.
  • O serviço Agendador de Drone agenda drones para retirada.
  • O serviço Pacote gerencia pacotes.

Há dois outros serviços: um serviço de ingestão que aceita solicitações de clientes e as coloca em uma fila para processamento e um serviço de Fluxo de Trabalho que coordena as etapas no fluxo de trabalho.

Diagram showing the distributed workflow

Para obter mais informações sobre esse cenário, consulte Projetar uma arquitetura de microsserviços.

Teste 1: Linha de base

Para o primeiro teste de carga, a equipe criou um cluster do AKS de seis nós e implantou três réplicas de cada microsserviço. O teste de carga foi um teste de carga em etapas, começando com dois usuários simulados e aumentando para 40.

Configuração Valor
Nós de cluster 6
Pods 3 por serviço

O gráfico a seguir mostra os resultados do teste de carga, conforme mostrado no Visual Studio. A linha roxa plota a carga do usuário, e a linha laranja, o total de solicitações.

Graph of Visual Studio load test results

A primeira coisa a perceber sobre esse cenário é que as solicitações de cliente por segundo não são uma métrica útil de desempenho. Isso ocorre porque o aplicativo processa as solicitações de forma assíncrona, para que o cliente obtenha uma resposta imediatamente. O código de resposta é sempre HTTP 202 (Aceito), o que significa que a solicitação foi aceita, mas o processamento não está concluído.

Na verdade, queremos saber se o back-end está acompanhando a taxa de solicitação. A fila do Barramento de Serviço pode absorver picos, mas se o back-end não conseguir lidar com uma carga sustentada, o processamento ficará cada vez mais aquém.

Aqui está um gráfico mais informativo. Ele plota o número de mensagens de entrada e saída na fila do Barramento de Serviço. As mensagens recebidas são mostradas em azul claro, e as de saída são mostradas em azul escuro:

Graph of incoming and outgoing messages

Esse gráfico mostra que a taxa de mensagens recebidas aumenta, atingindo um pico e, em seguida, caindo para zero no final do teste de carga. Mas o número de mensagens enviadas atinge o pico no início do teste e depois cai. Isso significa que o serviço de Fluxo de Trabalho, que lida com as solicitações, não está acompanhando. Mesmo após o término do teste de carga (por volta das 9h22 no gráfico), as mensagens ainda estão sendo processadas à medida que o serviço de Fluxo de Trabalho continua drenando fila.

O que está atrasando o processamento? Primeiro, procure ou exceções que possam indicar um problema sistemático. O Mapa de Aplicativo no Azure Monitor mostra o gráfico de chamadas entre componentes e é uma maneira rápida de identificar problemas e clicar para obter mais detalhes.

O Mapa do Aplicativo mostra que o serviço de Fluxo de Trabalho está recebendo erros do serviço de Entrega:

Screenshot of Application Map

Para ver mais detalhes, você pode selecionar um nó no gráfico e clicar em uma exibição de transação de ponta a ponta. Nesse caso, ele mostra que o serviço de Entrega está retornando erros HTTP 500. As mensagens de erro indicam que uma exceção está sendo lançada devido a limites de memória no Cache do Azure para Redis.

Screenshot of end-to-end transaction view

Você pode notar que essas chamadas para Redis não aparecem no Mapa do Aplicativo. Isso ocorre porque a biblioteca .NET para Application Insights não tem suporte interno para controlar o Redis como uma dependência. (Para obter uma lista do que é suportado de forma imediata, consulte Coleta automática de dependência.) Como fallback, você pode usar a API TrackDependency para controlar qualquer dependência. O teste de carga geralmente revela esses tipos de lacunas na telemetria, que podem ser corrigidas.

Teste 2: Aumento do tamanho do cache

Para o segundo teste de carga, a equipe de desenvolvimento aumentou o tamanho do cache no Cache do Azure para Redis. (Veja Como dimensionar o Cache do Azure para Redis.) Essa alteração resolveu as exceções de falta de memória, e agora o Mapa do Aplicativo mostra zero erros:

Screenshot of Application Map showing that increasing the cache size resolved the out-of-memory exceptions.

No entanto, ainda há um atraso grande no processamento de mensagens. No pico do teste de carga, a taxa de mensagens de entrada é superior a 5× a taxa de saída:

Graph of incoming and outgoing messages showing the incoming message rate is more than 5x the outgoing rate.

O gráfico a seguir mede a taxa de transferência em termos de conclusão de mensagens, ou seja, a taxa na qual o serviço de Fluxo de Trabalho marca as mensagens do Barramento de Serviço como concluídas. Cada ponto no gráfico representa 5 segundos de dados, mostrando cerca de 16/seg de taxa de transferência máxima.

Graph of message throughput

Este gráfico foi gerado executando uma consulta no workspace do Log Analytics usando a linguagem de consulta Kusto:

let start=datetime("2020-07-31T22:30:00.000Z");
let end=datetime("2020-07-31T22:45:00.000Z");
dependencies
| where cloud_RoleName == 'fabrikam-workflow'
| where timestamp > start and timestamp < end
| where type == 'Azure Service Bus'
| where target has 'https://dev-i-iuosnlbwkzkau.servicebus.windows.net'
| where client_Type == "PC"
| where name == "Complete"
| summarize succeeded=sumif(itemCount, success == true), failed=sumif(itemCount, success == false) by bin(timestamp, 5s)
| render timechart

Teste 3: Expandir os serviços de back-end

Parece que o back-end é o gargalo. Uma próxima etapa fácil é escalar horizontalmente os serviços de negócios (Pacote, Entrega e Agendador de Drone) e ver se a taxa de transferência melhora. Para o próximo teste de carga, a equipe dimensionou esses serviços de três réplicas para seis réplicas.

Configuração Valor
Nós de cluster 6
Serviço de ingestão 3 réplicas
Serviço de Fluxo de Trabalho 3 réplicas
Serviços de Pacote, Entrega, Agendador de Drone 6 réplicas cada

Infelizmente, este teste de carga mostra apenas uma melhoria modesta. As mensagens de saída ainda não estão acompanhando as mensagens recebidas:

Graph of incoming and outgoing messages showing that outgoing messages are still not keeping up with incoming messages.

A taxa de transferência é mais consistente, mas o máximo alcançado é próximo ao do teste anterior:

Graph of message throughput showing that the maximum achieved is about the same as the previous test.

Além disso, analisando os insights de contêiner do Azure Monitor, parece que o problema não é causado pelo esgotamento de recursos no cluster. Primeiro, as métricas no nível do nó mostram que a utilização da CPU permanece abaixo de 40%, mesmo no percentil 95, e a utilização da memória é de cerca de 20%.

Graph of AKS node utilization

Em um ambiente Kubernetes, é possível que pods individuais sejam limitados por recursos, mesmo quando os nós não são. Mas a exibição no nível do pod mostra que todos os pods estão íntegros.

Graph of AKS pod utilization

A partir desse teste, parece que adicionar mais pods ao back-end não ajudará. A próxima etapa é examinar mais de perto o serviço de Fluxo de Trabalho para entender o que está acontecendo quando ele processa mensagens. O Application Insights mostra que a duração média da operação Process do serviço de Fluxo de Trabalho é de 246 ms.

Screenshot of Application Insights

Também podemos executar uma consulta para obter métricas sobre as operações individuais dentro de cada transação:

destino percentile_duration_50 percentile_duration_95
https://dev-i-iuosnlbwkzkau.servicebus.windows.net/ | dev-i-iuosnlbwkzkau 86.66950203 283.4255578
entrega contínua 37 57
package 12 17
dronescheduler 21 41

A primeira linha desta tabela representa a fila do Barramento de Serviço. As outras linhas são as chamadas para os serviços de back-end. Para referência, aqui está a consulta do Log Analytics para esta tabela:

let start=datetime("2020-07-31T22:30:00.000Z");
let end=datetime("2020-07-31T22:45:00.000Z");
let dataset=dependencies
| where timestamp > start and timestamp < end
| where (cloud_RoleName == 'fabrikam-workflow')
| where name == 'Complete' or target in ('package', 'delivery', 'dronescheduler');
dataset
| summarize percentiles(duration, 50, 95) by target

Screenshot of Log Analytics query result

Essas latências parecem razoáveis. Mas aqui está o principal insight: se o tempo total de operação for de cerca de 250 ms, isso inclui um limite superior estrito na velocidade em que as mensagens podem ser processadas em série. A chave para melhorar a taxa de transferência, portanto, é um paralelismo maior.

Isso deve ser possível nesse cenário, por dois motivos:

  • Essas são chamadas de rede, portanto, a maior parte do tempo é gasto aguardando a conclusão da E/S
  • As mensagens são independentes e não precisam ser processadas em ordem.

Teste 4: Aumentar o paralelismo

Para esse teste, a equipe concentrou-se em aumentar o paralelismo. Para isso, eles ajustaram duas configurações no cliente do Barramento de Serviço usado pelo serviço de Fluxo de Trabalho:

Configuração Descrição Padrão Novo valor
MaxConcurrentCalls O número máximo de mensagens a serem processadas simultaneamente. 1 20
PrefetchCount Quantas mensagens o cliente buscará antecipadamente no cache local. 0 3000

Para saber mais informações sobre essas configurações, consulte Práticas recomendadas para melhorias de desempenho usando o Sistema de Mensagens do Barramento de Serviço. A execução do teste com essas configurações produziu o seguinte gráfico:

Graph of incoming and outgoing messages showing the number of outgoing messages actually exceeding the total number of incoming messages.

Lembre-se de que as mensagens recebidas são mostradas em azul claro e as enviadas em azul escuro.

À primeira vista, esse é um gráfico muito estranho. Por um tempo, a taxa de mensagens de saída rastreia exatamente a taxa de entrada. Mas então, por volta da marca de 2:03, a taxa de mensagens recebidas se estabiliza, enquanto o número de mensagens de saída continua a aumentar, excedendo o número total de mensagens recebidas. Isso parece impossível.

A pista para esse mistério pode ser encontrada na visualização Dependências no Application Insights. Este gráfico resume todas as chamadas que o serviço de Fluxo de Trabalho fez para o Barramento de Serviço:

Graph of dependency calls

Observe a entrada para DeadLetter. Essas chamadas indicam que as mensagens estão indo para a fila de mensagens mortas do Barramento de Serviço.

Para entender o que está acontecendo, você precisa entender a semântica Peek-Lock no Barramento de Serviço. Quando um cliente usa o Peek-Lock, o Barramento de Serviço recupera e bloqueia atomicamente uma mensagem. Enquanto o bloqueio é mantido, a mensagem não é entregue a outros receptores. Se o bloqueio expirar, a mensagem ficará disponível para outros receptores. Após um número máximo de tentativas de entrega (que é configurável), o Barramento de Serviço colocará as mensagens em uma fila de mensagens mortas, onde poderá ser examinada posteriormente.

Lembre-se de que o serviço Fluxo de Trabalho está fazendo uma pré-busca de grandes lotes de mensagens — 3.000 mensagens por vez). Isso significa que o tempo total para processar cada mensagem é maior, resultando em mensagens que atingem o tempo limite, voltam para a fila e, eventualmente, vão para a fila de mensagens mortas.

Você também pode ver esse comportamento nas exceções, onde várias exceções MessageLostLockException são registradas:

Screenshot of Application Insights exceptions showing numerous MessageLostLockException exceptions.

Teste 5: Aumentar a duração do bloqueio

Para esse teste de carga, a duração do bloqueio de mensagens foi definida como 5 minutos para evitar tempos limite de bloqueio. O gráfico de mensagens recebidas e enviadas agora mostra que o sistema está acompanhando a taxa de mensagens recebidas:

Graph of incoming and outgoing messages showing that the system is keeping up with the rate of incoming messages.

Durante todo o teste de carga de 8 minutos, o aplicativo completou 25 mil operações, com uma taxa de transferência de pico de 72 operações/seg, representando um aumento de 400% na taxa de transferência máxima.

Graph of message throughput showing a 400% increase in maximum throughput.

No entanto, a execução do mesmo teste com uma duração mais longa mostrou que o aplicativo não poderia sustentar essa taxa:

Graph of incoming and outgoing messages showing that the application could not sustain this rate.

As métricas de contêiner mostram que a utilização máxima da CPU foi próxima de 100%. Neste ponto, o aplicativo parece estar vinculado à CPU. O dimensionamento do cluster pode melhorar o desempenho agora, ao contrário da tentativa anterior de expansão.

Graph of AKS node utilization showing that maximum CPU utilization was close to 100%.

Teste 6: Dimensionar os serviços de back-end (novamente)

Para o teste de carga final da série, a equipe dimensionou o cluster e os pods do Kubernetes da seguinte maneira:

Configuração Valor
Nós de cluster 12
Serviço de ingestão 3 réplicas
Serviço de Fluxo de Trabalho 6 réplicas
Serviços de Pacote, Entrega, Agendador de Drone 9 réplicas cada

Esse teste resultou em uma taxa de transferência sustentada mais alta, sem atrasos significativos no processamento de mensagens. Além disso, a utilização da CPU do nó permaneceu abaixo de 80%.

Graph of message throughput showing higher sustained throughput, with no significant lags in processing messages.

Resumo

Para esse cenário, foram identificados os seguintes gargalos:

  • Exceções de falta de memória no Cache do Azure para Redis.
  • Falta de paralelismo no processamento de mensagens.
  • Duração insuficiente do bloqueio de mensagens, levando a tempos limite de bloqueio e mensagens sendo colocadas na fila de mensagens mortas.
  • Esgotamento de CPU.

Para diagnosticar esses problemas, a equipe de desenvolvimento baseou-se nas seguintes métricas:

  • A taxa de mensagens do Barramento de Serviço de entrada e saída.
  • Mapa de Aplicativos no Application Insights.
  • Erros e exceções.
  • Consultas personalizadas do Log Analytics.
  • Utilização da CPU e da memória nos insights de contêiner do Azure Monitor.

Próximas etapas

Para obter mais informações sobre o design desse cenário, consulte Projetar uma arquitetura de microsserviços.