Editar

Compartilhar via


Ajuste de desempenho - Streaming de eventos

Funções do Azure
Hub IoT do Azure
Azure Cosmos DB

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.

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

Cenário: Processe a transmissão de eventos usando o Azure Functions.

Diagram of an event streaming architecture

Nesse cenário, uma frota de drones envia dados de posição em tempo real para o Hub IoT do Azure. Um aplicativo do Functions recebe os eventos, transforma os dados no formato GeoJSON e grava os dados transformados no Azure Cosmos DB. O Azure Cosmos DB tem suporte nativo para dados geoespaciais e as coleções do Azure Cosmos DB podem ser indexadas para consultas espaciais eficientes. Por exemplo, um aplicativo cliente pode consultar todos os drones dentro de 1 km de um determinado local, ou encontrar todos os drones dentro de uma determinada área.

Esses requisitos de processamento são simples o suficiente para não exigirem um mecanismo de processamento de transmissão completo. Em particular, o processamento não une transmissões, agrega dados ou processa em janelas de tempo. Com base nesses requisitos, o Azure Functions é uma boa opção para processar as mensagens. O Azure Cosmos DB também pode ser dimensionado para oferecer suporte a uma taxa de transferência de gravação muito alta.

Monitorando a taxa de transferência

Esse cenário apresenta um desafio de desempenho interessante. A taxa de dados por dispositivo é conhecida, mas o número de dispositivos pode flutuar. Para esse cenário de negócios, os requisitos de latência não são particularmente rigorosos. A posição relatada de um drone apenas tem que ser precisa dentro de um minuto. Dito isso, o aplicativo de função deve acompanhar a taxa média de ingestão ao longo do tempo.

O Hub IoT armazena mensagens em um fluxo de log. As mensagens recebidas são anexadas à cauda do fluxo. Um leitor da transmissão — neste caso, o aplicativo de função — controla sua própria taxa de travessia da transmissão. Esse desacoplamento dos caminhos de leitura e gravação torna o Hub IoT muito eficiente, mas também significa que um leitor lento pode ficar para trás. Para detectar essa condição, a equipe de desenvolvimento adicionou uma métrica personalizada para medir o atraso da mensagem. Essa métrica registra o delta entre quando uma mensagem chega ao Hub IoT e quando a função recebe a mensagem para processamento.

var ticksUTCNow = DateTimeOffset.UtcNow;

// Track whether messages are arriving at the function late.
DateTime? firstMsgEnqueuedTicksUtc = messages[0]?.EnqueuedTimeUtc;
if (firstMsgEnqueuedTicksUtc.HasValue)
{
    CustomTelemetry.TrackMetric(
                        context,
                        "IoTHubMessagesReceivedFreshnessMsec",
                        (ticksUTCNow - firstMsgEnqueuedTicksUtc.Value).TotalMilliseconds);
}

O TrackMetric método grava uma métrica personalizada no Application Insights. Para obter informações sobre como usar TrackMetric dentro de um Azure Function, consulte Telemetria personalizada na função C#.

Se a função acompanhar o volume de mensagens, essa métrica deve permanecer em um estado estável baixo. Alguma latência é inevitável, então o valor nunca será zero. Mas se a função ficar para trás, o delta entre o tempo de enfileiramento e o tempo de processamento começará a subir.

Teste 1: Linha de base

O primeiro teste de carga mostrou um problema imediato: o aplicativo Function recebeu erros HTTP 429 consistentemente do Azure Cosmos DB, indicando que este estava limitando as solicitações de gravação.

Graph of Azure Cosmos DB throttled requests

Em resposta, a equipe dimensionou o Azure Cosmos DB aumentando o número de RUs alocadas para a coleção, mas os erros continuaram. Isso pareceu estranho, porque seu contra-cálculo mostrou que o Azure Cosmos DB não deve ter problemas para acompanhar o volume de solicitações de gravação.

Mais tarde naquele dia, um dos desenvolvedores enviou o seguinte e-mail para a equipe:

Dei uma olhada no Azure Cosmos DB no caminho quente. Tem uma coisa que eu não entendi. A chave de partição é deliveryId, no entanto, não enviamos deliveryId para o Azure Cosmos DB. Eu deixei alguma coisa passar?

Essa foi a pista. Olhando para o mapa de calor da partição, descobriu-se que todos os documentos estavam pousando na mesma partição.

Graph of Azure Cosmos DB partition heat map

O que você quer ver no mapa de calor é uma distribuição uniforme em todas as partições. Nesse caso, como todos os documentos estavam sendo gravados na mesma partição, adicionar RUs não ajudou. O problema acabou se tornando um bug no código. Embora a coleção do Azure Cosmos DB tivesse uma chave de partição, o Azure Function não incluía a chave de partição no documento. Para obter mais informações sobre o mapa de calor da partição, consulte Determinar a distribuição da taxa de transferência entre as partições.

Teste 2: Corrigir problema de particionamento

Quando a equipe implantou uma correção de código e executou novamente o teste, o Azure Cosmos DB parou de limitar. Por um tempo, tudo parecia estar bem. Mas, com uma certa carga, a telemetria mostrou que a função estava gravando menos documentos do que deveria. O gráfico a seguir mostra as mensagens recebidas do Hub IoT versus os documentos gravados no Azure Cosmos DB. A linha amarela é o número de mensagens recebidas por lote e a verde é o número de documentos gravados por lote. Estes devem ser proporcionais. Em vez disso, o número de operações de gravação de banco de dados por lote cai significativamente por volta das 07:30.

Graph of dropped messages

O gráfico a seguir mostra a latência entre quando uma mensagem chega ao Hub IoT de um dispositivo e quando o aplicativo de função processa essa mensagem. Você pode ver que, ao mesmo tempo, o atraso aumenta drasticamente, depois se estabiliza e declina.

Graph of message lateness

A razão pela qual o valor atinge o pico em 5 minutos e, em seguida, cai para zero é porque o aplicativo de função descarta mensagens com mais de 5 minutos de atraso:

foreach (var message in messages)
{
    // Drop stale messages,
    if (message.EnqueuedTimeUtc < cutoffTime)
    {
        log.Info($"Dropping late message batch. Enqueued time = {message.EnqueuedTimeUtc}, Cutoff = {cutoffTime}");
        droppedMessages++;
        continue;
    }
}

Você pode ver isso no gráfico quando a métrica de atraso cai para zero. Enquanto isso, os dados foram perdidos, porque a função estava jogando mensagens fora.

O que estava acontecendo? Para esse teste de carga específico, a coleção do Azure Cosmos DB tinha RUs de sobra, portanto, o afunilamento não estava no banco de dados. Em vez disso, o problema estava no loop de processamento de mensagens. Simplificando, a função não estava gravando documentos com rapidez suficiente para acompanhar o volume de mensagens recebidas. Com o tempo, foi ficando cada vez mais para trás.

Teste 3: Gravações paralelas

Se o tempo para processar uma mensagem for o gargalo, uma solução é processar mais mensagens em paralelo. Neste cenário:

  • Aumente o número de partições do Hub IoT. Cada partição do Hub IoT recebe uma instância de função por vez, portanto, esperamos que a taxa de transferência seja dimensionada linearmente com o número de partições.
  • Paralelize as gravações do documento dentro da função.

Para explorar a segunda opção, a equipe modificou a função para oferecer suporte a gravações paralelas. A versão original da função usava a associação de saída do Azure Cosmos DB. A versão otimizada chama o cliente do Azure Cosmos DB diretamente e executa as gravações em paralelo usando Task.WhenAll:

private async Task<(long documentsUpserted,
                    long droppedMessages,
                    long cosmosDbTotalMilliseconds)>
                ProcessMessagesFromEventHub(
                    int taskCount,
                    int numberOfDocumentsToUpsertPerTask,
                    EventData[] messages,
                    TraceWriter log)
{
    DateTimeOffset cutoffTime = DateTimeOffset.UtcNow.AddMinutes(-5);

    var tasks = new List<Task>();

    for (var i = 0; i < taskCount; i++)
    {
        var docsToUpsert = messages
                            .Skip(i * numberOfDocumentsToUpsertPerTask)
                            .Take(numberOfDocumentsToUpsertPerTask);
        // client will attempt to create connections to the data
        // nodes on Azure Cosmos DB clusters on a range of port numbers
        tasks.Add(UpsertDocuments(i, docsToUpsert, cutoffTime, log));
    }

    await Task.WhenAll(tasks);

    return (this.UpsertedDocuments,
            this.DroppedMessages,
            this.CosmosDbTotalMilliseconds);
}

Observe que as condições de corrida são possíveis com a abordagem. Suponha que duas mensagens do mesmo drone cheguem no mesmo lote de mensagens. Ao escrevê-las em paralelo, a mensagem anterior poderia substituir a mensagem posterior. Para esse cenário específico, o aplicativo pode tolerar a perda de uma mensagem ocasional. Os drones enviam novos dados de posição a cada 5 segundos, para que os dados no Azure Cosmos DB sejam atualizados continuamente. No entanto, em outros cenários, pode ser importante processar as mensagens estritamente em ordem.

Depois de implantar essa alteração de código, o aplicativo foi capaz de ingerir mais de 2500 solicitações/seg, usando um Hub IoT com 32 partições.

Considerações do lado do cliente

A experiência geral do cliente pode ser diminuída pela paralelização agressiva no lado do servidor. Considere o uso da biblioteca de executores em massa do Azure Cosmos DB (não mostrada nesta implementação), que reduz significativamente os recursos de computação do lado do cliente necessários para saturar a taxa de transferência alocada em um contêiner do Azure Cosmos DB. Um aplicativo de thread único que grava dados usando a API de importação em massa atinge quase dez vezes mais a taxa de transferência de gravação quando comparado a um aplicativo multithread que grava dados em paralelo ao saturar a CPU do computador do cliente.

Resumo

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

  • Partição de gravação a quente, devido a um valor de chave de partição ausente nos documentos que estão sendo gravados.
  • Gravação de documentos em série por partição do Hub IoT.

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

  • Solicitações limitadas no Azure Cosmos DB.
  • Mapa de calor da partição — Máximo de RUs consumidos por partição.
  • Mensagens recebidas versus documentos criados.
  • Atraso nas mensagens.

Próximas etapas

Examinar os antipadrões de desempenho