Considerações de design de aplicativo 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 online simples para ilustrar uma carga de trabalho altamente confiável. Os usuários podem navegar por um catálogo de itens, revisar detalhes deles e postar classificações e comentários sobre os itens. Este artigo foca nos aspectos de confiabilidade e resiliência de um aplicativo de missão crítica, como o processamento assíncrono de solicitações e como obter alta taxa de transferência em uma solução.

Importante

Logotipo do GitHub A implementação de referência de nível de produção que mostra o desenvolvimento de aplicativos de missão crítica no Azure suporta as diretrizes neste aplicativo. Você pode usar essa implementação como base para o desenvolvimento de soluções futuras na sua primeira etapa rumo à produção.

Composição do aplicativo

Para aplicativos de missão crítica de alta escala, você deve otimizar a arquitetura para escalabilidade e resiliência de ponta a ponta. Você pode separar componentes em unidades funcionais capazes de 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 de forma independente e atenda às mudanças na demanda. A implementação demonstra essa abordagem.

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

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

  • API (CatalogService): uma API REST chamada pelo aplicativo de IU, mas ainda disponível para outras aplicações cliente em potencial.

  • Trabalhador (BackgroundProcessor): um trabalhador em segundo plano que ouve novos eventos no barramento de mensagens e processa 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 informa a integridade do aplicativo verificando se os componentes críticos estão funcionando, como o banco de dados e o barramento de mensagens.

    Diagrama mostrando o fluxo do aplicativo.

A carga de trabalho consiste nos aplicativos API, trabalhador e verificação de integridade. Um namespace AKS dedicado chamado workload hospeda a carga de trabalho como contêineres. Não ocorre comunicação direta entre os pods. Os pods são sem estado 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 os balanceamentos de carga entre pods. O controlador de entrada NGINX é exposto por meio do Azure Load Balancer com um endereço IP público, mas pode ser acessado somente por meio do Azure Front Door.

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

  • Driver CSI do repositório de segredos: o provedor do Azure Key Vault para o driver CSI do repositório de segredos lê segredos com segurança, como cadeias de conexão do Azure Key Vault.

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

Conexão de banco de dados

Devido à natureza efêmera dos carimbos de implantação, evite ao máximo a persistência do estado dentro do carimbo. O estado deve persistir no 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 usar soluções gerenciadas, ou plataforma como serviço (PaaS), combinadas com bibliotecas nativas do SDK 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 de várias regiões. Cada carimbo pode gravar na réplica do Azure Cosmos DB na mesma região e o Azure Cosmos DB manipula internamente a replicação de dados e a sincronização entre regiões. O Azure Cosmos DB for NoSQL oferece 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.

Observação

Use o Azure Cosmos DB for 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 de uma região e a leitura de várias regiões com um nível de consistência forte.

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 .NET Core do Azure Cosmos DB para se comunicar com o banco de dados. O SDK inclui 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.

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

  • Serialização personalizada: esse processo define a diretiva de nomenclatura de propriedade JSON para JsonNamingPolicy.CamelCase converter propriedades .NET em propriedades JSON padrão. Ele também pode converter 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: essa propriedade é definida como a região do carimbo, o que permite que o SDK localize o ponto de extremidade de conexão mais próximo. O ponto de extremidade deve estar preferencialmente na mesma região.

O bloco de código a seguir 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 o acoplamento flexível, os serviços não têm dependências de outros serviços. O aspecto solto permite que um serviço opere de forma independente. O aspecto de acoplamento permite a comunicação entre serviços por meio de interfaces bem definidas. Para um aplicativo de missão crítica, o acoplamento flexível evita que as falhas de downstream caiam em cascata para front-ends ou outros carimbos de implantação, o que fornece 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 downstream não afetam as transações do cliente.

  • 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 entre os serviços de mensagens e persistência. Para obter mais informações, consulte Processamento de mensagens Idempotent.

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

Recomendamos o uso de padrões de design bem 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 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. O serviço Hubs de Eventos oferece suporte ao 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 o backup de mensagens.

Detalhes de implementação para operações de gravação

As operações de gravação, como postagem de classificação e comentários, 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 imediatamente HTTP 202 (Accepted) com o Location cabeçalho adicional do objeto que será criado.

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

Diagrama que mostra a natureza assíncrona do recurso de pós-classificação na implementação.

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

O bloco de código a seguir 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 processar a mensagem:

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

  • Ocorrerá um conflito se o trabalhador anterior tiver persistido o documento no banco de dados antes da falha do trabalhador. Esse 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á é persistente.

  • Uma nova instância repetirá as etapas e finalizará a persistência se o trabalhador anterior foi encerrado antes de gravar no banco de dados.

Detalhes de implementação para operações de leitura

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 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 para obter atualizações sobre o item especificado no cabeçalho HTTP Location.

Escalabilidade

Os componentes individuais da carga de trabalho devem ser expandidos independentemente porque cada componente possui 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 expandidos agressivamente para garantir respostas rápidas e uma experiência de usuário positiva.

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 componentes do CatalogService e da carga de trabalho BackgroundProcessor podem ser reduzidos e expandidos individualmente porque os serviços são sem estado.

Os usuários interagem diretamente com CatalogService, portanto, essa parte da carga de trabalho deve responder sob qualquer carga. Há no mínimo 3 instâncias por cluster para dividir entre três zonas de disponibilidade em uma região do Azure. O pod de escala automática horizontal (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 CatalogService e o Azure Cosmos DB se combinam para formar uma unidade de escala dentro de um carimbo.

O HPA é implantado com um pacote Helm que possui um número máximo e mínimo configurável 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 serviço BackgroundProcessor tem requisitos diferentes e é considerado um trabalhador em segundo plano que tem um efeito limitado na experiência do usuário. Portanto, o BackgroundProcessor tem uma configuração de dimensionamento automático diferente em comparação com o CatalogService, e pode ser dimensionado 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
BackgroundProcessor 2 32

Cada componente da carga de trabalho, incluindo dependências, como ingress-nginx, tem a configuração do orçamentos de interrupção de pod (PDBs) configurada para garantir que um número mínimo de instâncias esteja sempre disponível quando os clusters mudam.

O bloco de código a seguir 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 }}

Observação

Determine o número mínimo real e o número máximo de pods para cada componente por meio de testes de carga. O número de pods pode ser diferente para cada carga de trabalho.

Instrumentação

Use a instrumentação para avaliar gargalos de desempenho e problemas de integridade que os componentes de carga de trabalho podem introduzir no sistema. Para ajudar a quantificar as 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 outra telemetria para o sistema de log do carimbo.
  • Use o log estruturado em vez de texto sem formatação para que você possa consultar as informações.
  • Implemente a correlação de eventos para ter a visualização completa da transação. Na referência de implementação, cada resposta de API contém um ID de operação como um cabeçalho HTTP para rastreabilidade.
  • Não confie apenas no registro em log de stdout ou registro de log de console. Mas você pode usar esses logs para solucionar imediatamente problemas de um pod com falha.

Essa arquitetura implementa o rastreamento distribuído com o Application Insights e um espaço de trabalho dos Logs do Azure Monitor para o monitoramento de dados de aplicativos. Use os Logs do Azure Monitor para logs e métricas dos 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, para o 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 carimbo. Para obter mais informações, consulte Monitoramento de dados para recursos de carimbo.

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

Detalhes de implementação para monitoramento de aplicativos

O componente BackgroundProcessor usa o pacote NuGet Microsoft.ApplicationInsights.WorkerService para obter instrumentação pronta para uso do aplicativo. O Serilog também é usado para todos os registros dentro do aplicativo. O Application Insights é configurado como um coletor além do coletor do console. Uma TelemetryClient instância para o Application Insights é usada diretamente somente quando necessário para controlar métricas adicionais.

O bloco de código a seguir 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 da solicitação, cada solicitação de API, bem-sucedida ou não, retorna o cabeçalho de ID de Correlação para o chamador. A equipe de suporte do aplicativo pode pesquisar o Application Insights com seu identificador e obter uma visão detalhada da transação completa, o que é ilustrado no diagrama anterior.

O bloco de código a seguir 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();
});

Observação

A amostragem adaptável está habilitada por padrão no SDK do Application Insights. Amostragem adaptável significa que nem todas as solicitações são enviadas para a nuvem e pesquisáveis por ID. As equipes de aplicativos de missão crítica precisam rastrear de forma confiável cada solicitação, por isso a implementação de referência tem a amostragem adaptável 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. Ative os Insights do contêiner para implantar o OMSAgentForLinux por meio de um DaemonSet do Kubernetes em cada um dos nós em clusters do 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 dos Logs do Azure Monitor. Esse espaço de trabalho contém dados mais 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 nenhum benefício. Por esse motivo, a coleta de logs stdout e a extração do Prometheus estão desabilitadas para os pods de carga de trabalho na configuração de insights do contêiner, pois todos os rastreamentos já são capturados por meio do Application Insights, gerando registros duplicados.

O bloco de código a seguir 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, confira o Arquivo de configuração completa.

Monitoramento de 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 exibir o monitoramento de integridade por meio de pontos de extremidade de integridade. As investigações de integridade usam dados de monitoramento de integridade para fornecer informações. O balanceador de carga principal usa essas informações para tirar 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. Esses pods têm investigação de atividade e de integridade para que o AKS seja capaz de gerenciar seu ciclo de vida.

  • Serviço de integridade, que é um componente dedicado no cluster. O Azure Front Door está configurado para investigar os serviços 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 integridade

O HealthService é um componente de carga de trabalho que é executado junto com outros componentes, como CatalogService e BackgroundProcessor, no cluster de cálculo. 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 investigações básicas de atividade, o serviço de integridade é um componente mais complexo que fornece o estado das dependências além do seu próprio estado.

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

O serviço de integridade não responde se cluster do AKS estiver inativo, tornando a carga de trabalho não íntegra. Quando o serviço é executado, ele realiza verificações periódicas em componentes críticos da solução. Todas as verificações são feitas de forma assíncrona e em paralelo. Se qualquer uma das verificações falhar, todo o carimbo não estará disponível.

Aviso

As investigações de integridade do Azure Front Door podem impor uma carga significativa no serviço de integridade, porque as solicitações são originadas de vários locais PoP (ponto de presença). Para evitar a sobrecarga dos 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 carimbo.

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

Próxima etapa