Considerações de design de aplicativos para cargas de trabalho de missão crítica

A arquitetura de referência de missão crítica de linha de base usa um aplicativo de catálogo on-line simples para ilustrar uma carga de trabalho altamente confiável. Os usuários podem navegar por um catálogo de itens, revisar detalhes de itens e postar classificações e comentários para itens. Este artigo se concentra nos aspetos de confiabilidade e resiliência de um aplicativo de missão crítica, como processamento assíncrono de solicitações e como obter alta taxa de transferência em uma solução.

Importante

Logótipo do GitHubUma implementação de referência de nível de produção que mostra o desenvolvimento de aplicativos de missão crítica no Azure dá suporte às orientações neste artigo. Você pode usar essa implementação como base para o desenvolvimento de soluções adicionais em seu primeiro passo para a produção.

Composição da aplicação

Para aplicativos de missão crítica de alta escala, você deve otimizar a arquitetura para escalabilidade e resiliência de ponta a ponta. É possível separar os componentes em unidades funcionais que podem operar de forma independente. Aplique essa separação em todos os níveis na pilha de aplicativos para que cada parte do sistema possa ser dimensionada independentemente e atender às mudanças na demanda. A implementação demonstra esta abordagem.

O aplicativo usa pontos de extremidade de API sem estado que separam solicitações de gravação de longa execução de forma assíncrona por meio de um agente de mensagens. A composição da carga de trabalho permite excluir e recriar clusters inteiros do Serviço Kubernetes do Azure (AKS) e outras dependências no carimbo a qualquer momento. Os principais componentes da aplicação são:

  • Interface do usuário (UI): um aplicativo Web de página única que os usuários podem acessar. A interface do usuário é hospedada na hospedagem de site estático de uma conta de Armazenamento do Azure.

  • API (CatalogService): UMA API REST que é chamada pelo aplicativo de interface do usuário, mas ainda está disponível para outros aplicativos cliente em potencial.

  • Trabalhador (BackgroundProcessor): um trabalhador em segundo plano que escuta novos eventos no barramento de mensagens e processa as solicitações de gravação no banco de dados. Este componente não expõe nenhuma API.

  • API do serviço de integridade (HealthService): uma API que relata a integridade do aplicativo verificando se os componentes críticos estão funcionando, como o banco de dados ou o barramento de mensagens.

    Diagrama que mostra o fluxo do aplicativo.

A carga de trabalho consiste na API, no trabalhador e nos aplicativos de verificação de integridade. Um namespace AKS dedicado chamado workload hospeda a carga de trabalho como contêineres. Nenhuma comunicação direta ocorre entre os pods. Os pods são apátridas e podem ser dimensionados de forma independente.

Diagrama que mostra a composição detalhada da carga de trabalho.

Outros componentes de suporte executados no cluster incluem:

  • Um controlador de entrada NGINX: roteia solicitações de entrada para a carga de trabalho e balanceamentos de carga entre pods. O controlador de entrada NGINX é exposto através do Azure Load Balancer com um endereço IP público, mas só pode ser acedido através da Porta Frontal do Azure.

  • Gerenciador de certificados: os certificados TLS (Transport Layer Security) do cert-manager Jetstack são automáticos usando Let's Encrypt para as regras de entrada.

  • Driver CSI do Repositório de Segredos: O provedor do Cofre de Chaves do Azure para o Driver CSI do Repositório de Segredos lê segredos com segurança, como cadeias de conexão do Cofre da Chave.

  • Agente de monitoramento: a configuração padrão do OMSAgentForLinux é ajustada para reduzir a quantidade de dados de monitoramento enviados para o espaço de trabalho Azure Monitor Logs.

Conexão com o banco de dados

Devido à natureza efêmera dos selos de implantação, evite ao máximo persistir o estado dentro do selo. Você deve persistir o estado em um armazenamento de dados externalizado. Para dar suporte ao SLO (objetivo de nível de serviço) de confiabilidade, crie um armazenamento de dados resiliente. Recomendamos que você use soluções gerenciadas ou de plataforma como serviço (PaaS) em combinação com bibliotecas SDK nativas que lidam automaticamente com tempos limites, desconexões e outros estados de falha.

Na implementação de referência, o Azure Cosmos DB serve como o armazenamento de dados principal para o aplicativo. O Azure Cosmos DB fornece gravações em várias regiões. Cada carimbo pode gravar na réplica do Azure Cosmos DB na mesma região, e o Azure Cosmos DB lida internamente com a replicação de dados e a sincronização entre regiões. O Azure Cosmos DB para NoSQL dá suporte a todos os recursos do mecanismo de banco de dados.

Para obter mais informações, consulte Plataforma de dados para cargas de trabalho de missão crítica.

Nota

Use o Azure Cosmos DB para NoSQL para novos aplicativos. Para aplicativos herdados que usam outro protocolo NoSQL, avalie o caminho de migração para o Azure Cosmos DB.

Para aplicativos de missão crítica que priorizam a disponibilidade em detrimento do desempenho, recomendamos a gravação em uma única região e a leitura em várias regiões com um forte nível de consistência .

Essa arquitetura usa o Armazenamento para armazenar temporariamente o estado no carimbo para o ponto de verificação dos Hubs de Eventos do Azure.

Todos os componentes de carga de trabalho usam o SDK do Azure Cosmos DB .NET Core para se comunicar com o banco de dados. O SDK inclui uma lógica robusta para manter conexões de banco de dados e lidar com falhas. As principais definições de configuração incluem:

  • Modo de conectividade direta: essa configuração é padrão para o .NET SDK v3 porque oferece melhor desempenho. O modo de conectividade direta tem menos saltos de rede em comparação com o modo Gateway, que usa HTTP.

  • Retornar resposta de conteúdo na gravação: essa abordagem é desabilitada para que o cliente do Azure Cosmos DB não possa retornar o documento das operações de criação, atualização e correção e substituição, o que reduz o tráfego de rede. O processamento adicional no cliente não requer essa configuração.

  • Serialização personalizada: este processo define a política de nomenclatura de propriedades JSON para JsonNamingPolicy.CamelCase traduzir propriedades .NET para propriedades JSON padrão. Ele também pode traduzir propriedades JSON para propriedades .NET. A condição de ignorar padrão ignora propriedades com valores nulos, como JsonIgnoreCondition.WhenWritingNull, durante a serialização.

  • ApplicationRegion: Esta propriedade é definida como a região do carimbo, o que permite que o SDK encontre o ponto de extremidade de conexão mais próximo. O parâmetro de avaliação final deve, de preferência, situar-se na mesma região.

O seguinte bloco de código aparece na implementação de referência:

//
// /src/app/AlwaysOn.Shared/Services/CosmosDbService.cs
//
CosmosClientBuilder clientBuilder = new CosmosClientBuilder(sysConfig.CosmosEndpointUri, sysConfig.CosmosApiKey)
    .WithConnectionModeDirect()
    .WithContentResponseOnWrite(false)
    .WithRequestTimeout(TimeSpan.FromSeconds(sysConfig.ComsosRequestTimeoutSeconds))
    .WithThrottlingRetryOptions(TimeSpan.FromSeconds(sysConfig.ComsosRetryWaitSeconds), sysConfig.ComsosMaxRetryCount)
    .WithCustomSerializer(new CosmosNetSerializer(Globals.JsonSerializerOptions));

if (sysConfig.AzureRegion != "unknown")
{
    clientBuilder = clientBuilder.WithApplicationRegion(sysConfig.AzureRegion);
}

_dbClient = clientBuilder.Build();

Mensagens assíncronas

Quando você implementa acoplamento flexível, os serviços não têm dependências de outros serviços. O aspeto solto permite que um serviço opere de forma independente. O aspeto de acoplamento permite a comunicação entre serviços através de interfaces bem definidas. Para uma aplicação de missão crítica, o acoplamento flexível evita que falhas a jusante se estendam em cascata para front-ends ou outros carimbos de implantação, o que proporciona alta disponibilidade.

As principais características do sistema de mensagens assíncronas incluem:

  • Os serviços não precisam usar a mesma plataforma de computação, linguagem de programação ou sistema operacional.

  • Os serviços são dimensionados de forma independente.

  • As falhas a jusante não afetam as transações dos clientes.

  • A integridade transacional é difícil de manter porque a criação e a persistência de dados ocorrem em serviços separados. A integridade transacional é um desafio em todos os serviços de mensagens e persistência. Para obter mais informações, consulte Processamento de mensagens idempotentes.

  • O rastreamento de ponta a ponta requer orquestração complexa.

Recomendamos que você use padrões de design conhecidos, como o padrão de Nivelamento de Carga Baseado em Fila e o padrão de Consumidores Concorrentes. Esses padrões distribuem a carga do produtor para os consumidores e permitem o processamento assíncrono pelos consumidores. Por exemplo, o trabalhador permite que a API aceite a solicitação e retorne rapidamente ao chamador, e o trabalhador processa uma operação de gravação de banco de dados separadamente.

Os Hubs de Eventos intermediam mensagens entre a API e o trabalhador.

Importante

Não use o agente de mensagens como um armazenamento de dados persistente por longos períodos de tempo. O serviço Hubs de Eventos suporta o recurso de captura. O recurso de captura permite que um hub de eventos grave automaticamente uma cópia das mensagens em uma conta de armazenamento vinculada. Esse processo controla o uso e serve como um mecanismo para fazer backup de mensagens.

Escrever detalhes da implementação das operações

As operações de gravação, como classificação de postagem e comentário de postagem, são processadas de forma assíncrona. A API primeiro envia uma mensagem com todas as informações relevantes, como o tipo de ação e os dados de comentário, para a fila de mensagens e retorna HTTP 202 (Accepted) imediatamente com o Location cabeçalho do objeto que será criado.

BackgroundProcessor As instâncias processam mensagens na fila e manipulam a comunicação real do banco de dados para operações de gravação. BackgroundProcessor Dimensiona e expande dinamicamente com base no volume de mensagens da fila. O limite de expansão das instâncias do processador é definido pelo número máximo de partições dos Hubs de Eventos, que é 32 para as camadas Basic e Standard, 100 para a camada Premium e 1.024 para a camada Dedicada.

Diagrama que mostra a natureza assíncrona do recurso de classificação de postagem na implementação.

A biblioteca do Processador de Hubs de Eventos do Azure usa BackgroundProcessor o Armazenamento de Blobs do Azure para gerenciar a propriedade da partição, o balanceamento de carga entre diferentes instâncias de trabalho e usar pontos de verificação para acompanhar o progresso. Os pontos de verificação não são gravados no armazenamento de blob após cada evento porque adiciona um atraso caro para cada mensagem. Em vez disso, os pontos de verificação são gravados em um loop de temporizador e você pode configurar a duração. A configuração padrão é de 10 segundos.

O seguinte bloco de código aparece na implementação de referência:

while (!stoppingToken.IsCancellationRequested)
{
    await Task.Delay(TimeSpan.FromSeconds(_sysConfig.BackendCheckpointLoopSeconds), stoppingToken);
    if (!stoppingToken.IsCancellationRequested && !checkpointEvents.IsEmpty)
    {
        string lastPartition = null;
        try
        {
            foreach (var partition in checkpointEvents.Keys)
            {
                lastPartition = partition;
                if (checkpointEvents.TryRemove(partition, out ProcessEventArgs lastProcessEventArgs))
                {
                    if (lastProcessEventArgs.HasEvent)
                    {
                        _logger.LogDebug("Scheduled checkpointing for partition {partition}. Offset={offset}", partition, lastProcessEventArgs.Data.Offset);
                        await lastProcessEventArgs.UpdateCheckpointAsync();
                    }
                }
            }
        }
        catch (Exception e)
        {
            _logger.LogError(e, "Exception during checkpointing loop for partition={lastPartition}", lastPartition);
        }
    }
}

Se o aplicativo do processador encontrar um erro ou for interrompido antes de poder processar a mensagem:

  • Outra instância pega a mensagem para reprocessamento porque ela não foi verificada corretamente no Armazenamento.

  • Um conflito ocorre se o trabalhador anterior persistiu o documento no banco de dados antes que o trabalhador falhasse. Este erro ocorre porque o mesmo ID e chave de partição são usados. O processador pode ignorar a mensagem com segurança porque o documento já está persistente.

  • Uma nova instância repete as etapas e finaliza a persistência se o trabalhador anterior tiver sido encerrado antes de gravar no banco de dados.

Leia os detalhes da implementação das operações

A API processa diretamente as operações de leitura e retorna imediatamente os dados ao usuário.

Diagrama que mostra um processo de operações de leitura.

Um método de back-channel não é estabelecido para se comunicar com o cliente se a operação for concluída com êxito. O aplicativo cliente deve sondar proativamente a API em busca de atualizações sobre o item especificado no Location cabeçalho HTTP.

Escalabilidade

Os componentes individuais da carga de trabalho devem ser dimensionados independentemente porque cada componente tem padrões de carga diferentes. Os requisitos de dimensionamento dependem da funcionalidade do serviço. Certos serviços afetam diretamente os usuários e devem ser dimensionados de forma agressiva para garantir respostas rápidas e uma experiência positiva para o usuário.

A implementação empacota os serviços como imagens de contêiner e usa gráficos Helm para implantar os serviços em cada carimbo. Os serviços são configurados para ter as solicitações e limites esperados do Kubernetes e uma regra de dimensionamento automático pré-configurada. Os CatalogService componentes e a BackgroundProcessor carga de trabalho podem ser dimensionados e dimensionados individualmente porque ambos os serviços são sem monitoração de estado.

Os usuários interagem diretamente com o CatalogService, portanto, essa parte da carga de trabalho deve responder sob qualquer carga. Há um mínimo de três instâncias para cada cluster se espalhar por três zonas de disponibilidade em uma região do Azure. O autoscaler horizontal de pods (HPA) no AKS adiciona automaticamente mais pods conforme necessário. O recurso de dimensionamento automático do Azure Cosmos DB pode aumentar e reduzir dinamicamente as unidades de solicitação (RUs) disponíveis para a coleção. O e o CatalogService Azure Cosmos DB se combinam para formar uma unidade de escala dentro de um carimbo.

O HPA é implantado com um gráfico Helm que tem um número máximo configurável e um número mínimo de réplicas. O teste de carga determinou que cada instância pode lidar com cerca de 250 solicitações por segundo com um padrão de uso padrão.

O BackgroundProcessor serviço tem requisitos diferentes e é considerado um trabalhador em segundo plano que tem um efeito limitado na experiência do usuário. Portanto, BackgroundProcessor tem uma configuração de dimensionamento automático diferente em comparação com a CatalogService, e pode ser dimensionada entre 2 e 32 instâncias. Determine esse limite com base no número de partições que você usa nos hubs de eventos. Você não precisa de mais trabalhadores do que partições.

Componente minReplicas maxReplicas
CatalogService 3 20
Processador de fundo 2 32

Cada componente da carga de trabalho que inclui dependências como ingress-nginx tem a configuração PDBs (pod disruption budgets) configurada para garantir que um número mínimo de instâncias permaneça disponível quando os clusters forem alterados.

O seguinte bloco de código aparece na implementação de referência:

#
# /src/app/charts/healthservice/templates/pdb.yaml
# Example pod distribution budget configuration.
#
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: {{ .Chart.Name }}-pdb
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: {{ .Chart.Name }}

Nota

Determine o número mínimo real e o número máximo de pods para cada componente através do teste de carga. O número de pods pode variar para cada carga de trabalho.

Instrumentação

Use a instrumentação para avaliar gargalos de desempenho e problemas de integridade que os componentes da carga de trabalho podem introduzir no sistema. Para ajudá-lo a quantificar decisões, cada componente deve emitir informações suficientes por meio de métricas e logs de rastreamento. Considere as seguintes considerações principais ao instrumentar seu aplicativo:

  • Envie logs, métricas e outras telemetrias para o sistema de log do selo.
  • Use o log estruturado em vez de texto sem formatação para que você possa consultar informações.
  • Implemente a correlação de eventos para obter uma visualização de transação de ponta a ponta. Na implementação de referência, cada resposta de API contém um ID de operação como um cabeçalho HTTP para rastreabilidade.
  • Não confie apenas no registro stdout ou no registro do console. Mas você pode usar esses logs para solucionar imediatamente um pod com falha.

Essa arquitetura implementa o rastreamento distribuído com o Application Insights e um espaço de trabalho do Azure Monitor Logs para dados de monitoramento de aplicativos. Use os Logs do Azure Monitor para logs e métricas de componentes de carga de trabalho e infraestrutura. Essa arquitetura implementa o rastreamento completo de ponta a ponta de solicitações que vêm da API, passam pelos Hubs de Eventos e, em seguida, pelo Azure Cosmos DB.

Importante

Implante recursos de monitoramento de carimbo em um grupo de recursos de monitoramento separado. Os recursos têm um ciclo de vida diferente do próprio selo. Para obter mais informações, consulte Monitoramento de dados para recursos de carimbo.

Diagrama de serviços globais separados, serviços de monitoramento e implantação de carimbo.

Detalhes da implementação do monitoramento de aplicativos

O BackgroundProcessor componente usa o Microsoft.ApplicationInsights.WorkerService pacote NuGet para obter instrumentação pronta para uso do aplicativo. Serilog também é usado para todo o registro dentro do aplicativo. O Application Insights é configurado como um coletor além do coletor de console. Uma TelemetryClient instância do Application Insights é usada diretamente apenas quando é necessário acompanhar outras métricas.

O seguinte bloco de código aparece na implementação de referência:

//
// /src/app/AlwaysOn.BackgroundProcessor/Program.cs
//
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, services) =>
    {
        Log.Logger = new LoggerConfiguration()
                            .ReadFrom.Configuration(hostContext.Configuration)
                            .Enrich.FromLogContext()
                            .WriteTo.Console(outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
                            .WriteTo.ApplicationInsights(hostContext.Configuration[SysConfiguration.ApplicationInsightsConnStringKeyName], TelemetryConverter.Traces)
                            .CreateLogger();
    }

Captura de tela do recurso de rastreamento de ponta a ponta.

Para demonstrar a rastreabilidade prática de solicitações, cada solicitação de API bem-sucedida e malsucedida retorna o cabeçalho da ID de correlação para o chamador. A equipe de suporte ao aplicativo pode pesquisar o Application Insights com esse identificador e obter uma visão detalhada da transação completa, que é ilustrada no diagrama anterior.

O seguinte bloco de código aparece na implementação de referência:

//
// /src/app/AlwaysOn.CatalogService/Startup.cs
//
app.Use(async (context, next) =>
{
    context.Response.OnStarting(o =>
    {
        if (o is HttpContext ctx)
        {
            // ... code omitted for brevity
            context.Response.Headers.Add("Server-Location", sysConfig.AzureRegion);
            context.Response.Headers.Add("Correlation-ID", Activity.Current?.RootId);
            context.Response.Headers.Add("Requested-Api-Version", ctx.GetRequestedApiVersion()?.ToString());
        }
        return Task.CompletedTask;
    }, context);
    await next();
});

Nota

A amostragem adaptável é habilitada por padrão no SDK do Application Insights. A amostragem adaptável significa que nem todas as solicitações são enviadas para a nuvem e podem ser pesquisadas por ID. As equipes de aplicativos de missão crítica precisam rastrear de forma confiável todas as solicitações, e é por isso que a implementação de referência tem a amostragem adaptativa desabilitada no ambiente de produção.

Detalhes da implementação do monitoramento do Kubernetes

Você pode usar as configurações de diagnóstico para enviar logs e métricas do AKS para os Logs do Azure Monitor. Você também pode usar o recurso de insights de contêiner com o AKS. Habilite insights de contêiner para implantar o OMSAgentForLinux por meio de um DaemonSet do Kubernetes em cada um dos nós em clusters AKS. O OMSAgentForLinux pode coletar mais logs e métricas de dentro do cluster do Kubernetes e enviá-los para seu espaço de trabalho correspondente do Azure Monitor Logs. Esse espaço de trabalho contém dados granulares sobre pods, implantações, serviços e a integridade geral do cluster.

O registro extensivo pode afetar negativamente o custo e não oferece benefícios. Por esse motivo, a coleta de logs stdout e a raspagem do Prometheus são desabilitadas para os pods de carga de trabalho na configuração de insights de contêiner porque todos os rastreamentos já são capturados por meio do Application Insights, que gera registros duplicados.

O seguinte bloco de código aparece na implementação de referência:

#
# /src/config/monitoring/container-azm-ms-agentconfig.yaml
# This is just a snippet showing the relevant part.
#
[log_collection_settings]
    [log_collection_settings.stdout]
        enabled = false

        exclude_namespaces = ["kube-system"]

Para obter mais informações, consulte o arquivo de configuração completo.

Monitoramento da integridade do aplicativo

Você pode usar o monitoramento e a observabilidade do aplicativo para identificar rapidamente problemas do sistema e informar o modelo de integridade sobre o estado atual do aplicativo. Você pode monitorar a integridade da superfície por meio de pontos de extremidade de integridade. As sondas de integridade usam dados de monitoramento de integridade para fornecer informações. O balanceador de carga principal usa essas informações para retirar imediatamente o componente não íntegro da rotação.

Essa arquitetura aplica o monitoramento de integridade nos seguintes níveis:

  • Pods de carga de trabalho que são executados no AKS. Estas cápsulas têm sondas de saúde e vivacidade, para que a AKS possa gerir o seu ciclo de vida.

  • Serviço de Saúde, que é um componente dedicado no cluster. O Azure Front Door está configurado para investigar o Serviço de Integridade em cada carimbo e remover carimbos não íntegros do balanceamento de carga automático.

Detalhes da implementação do Serviço de Saúde

HealthService é um componente de carga de trabalho que é executado junto com outros componentes, como CatalogService e BackgroundProcessor, no cluster de computação. HealthService fornece uma API REST que a verificação de integridade do Azure Front Door chama para determinar a disponibilidade de um carimbo. Ao contrário das sondas básicas de vivacidade, o Serviço de Saúde é um componente mais complexo que fornece o estado de dependências, além de seu próprio estado.

Diagrama do serviço de integridade consultando o Azure Cosmos DB, Hubs de Eventos e Armazenamento.

O Serviço de Saúde não responde se o cluster AKS estiver inativo, o que torna a carga de trabalho não íntegra. Quando o serviço é executado, ele executa verificações periódicas em relação aos componentes críticos da solução. Todas as verificações são feitas de forma assíncrona e em paralelo. Se alguma das verificações falhar, o carimbo inteiro fica indisponível.

Aviso

As sondas de integridade do Azure Front Door podem impor uma carga significativa ao Serviço de Integridade porque as solicitações vêm de vários locais de ponto de presença (PoP). Para evitar sobrecarregar os componentes downstream, implemente um cache eficaz.

O Serviço de Integridade também é usado para testes de ping de URL explicitamente configurados com o recurso Application Insights de cada selo.

Para obter mais informações sobre a HealthService implementação, consulte Serviço de integridade do aplicativo.

Próximo passo