Uso da redundância geográfica para criar aplicativos altamente disponíveis

Infraestruturas baseadas em nuvem, como o Armazenamento do Azure, fornecem uma plataforma altamente disponível e durável para hospedar dados e aplicativos. Os desenvolvedores de aplicativos baseados em nuvem devem considerar cuidadosamente como aproveitar essa plataforma para maximizar as vantagens para seus usuários. O Armazenamento do Azure oferece opções de redundância geográfica para garantir alta disponibilidade, mesmo durante uma interrupção regional. As contas de armazenamento configuradas para replicação com redundância geográfica são replicadas de forma síncrona na região primária e replicadas assincronamente em uma região secundária que está a centenas de quilômetros de distância.

O Armazenamento do Azure oferece duas opções para replicação com redundância geográfica: GRS (armazenamento com redundância geográfica) e GZRS (armazenamento com redundância de zona geográfica). Para usar as opções de redundância geográfica do Armazenamento do Azure, verifique se sua conta de armazenamento está configurada para RA-GRS (armazenamento com redundância geográfica com acesso de leitura) ou RA-GZRS (armazenamento com redundância de zona geográfica com acesso de leitura). Em caso negativo, você pode saber mais sobre como alterar o tipo de replicação da conta de armazenamento.

Este artigo mostra como criar um aplicativo que continuará funcionando, embora com uma capacidade limitada, mesmo quando houver uma interrupção significativa na região primária. Se a região primária ficar indisponível, o aplicativo poderá alternar perfeitamente para executar operações de leitura na região secundária até que a região primária responda novamente.

Considerações sobre o design de aplicativo

Você pode criar seu aplicativo para resolver problemas transitórios ou interrupções significativas usando a leitura da região secundária quando houver um problema que interfere na leitura da região primária. Quando a região primária estiver disponível novamente, o aplicativo poderá retornar com a leitura da região primária.

Tenha em mente estas considerações fundamentais ao desenvolver seu aplicativo para disponibilidade e resiliência usando RA-GRS ou RA-GZRS:

  • Uma cópia somente leitura dos dados armazenados na região primária é replicada de forma assíncrona em uma região secundária. Essa replicação assíncrona significa que a cópia somente leitura na região secundária é eventualmente consistente com os dados na região primária. O serviço de armazenamento determina o local da região secundária.

  • Você pode usar as bibliotecas de clientes do Armazenamento do Azure para realizar solicitações de leitura e atualização no ponto de extremidade da região primária. Se a região primária estiver indisponível, é possível redirecionar automaticamente as solicitações de leitura para a região secundária. Também é possível configurar o aplicativo para enviar solicitações de leitura diretamente para a região secundária, se desejado, mesmo quando a região primária estiver disponível.

  • Se a região primária ficar indisponível, você pode iniciar um failover de conta. Quando você executa o failover para a região secundária, as entradas DNS que apontam para a região primária são alteradas para apontar para a região secundária. Após o failover estar concluído, o acesso de gravação é restaurado para as contas GRS e RA-GRS. Para saber mais, confira Recuperação de desastre e failover da conta de armazenamento.

Trabalhar com dados eventualmente consistentes

A solução proposta pressupõe que é aceitável retornar dados potencialmente obsoletos para o aplicativo de chamada. Como os dados na região secundária são eventualmente consistentes, é possível que a região primária se torne inacessível antes que uma atualização da região secundária tenha concluído a replicação.

Por exemplo, suponha que o cliente envie uma atualização com êxito, mas a região primária falhe antes que a atualização seja propagada para a região secundária. Quando o cliente solicita a leitura dos dados novamente, ele recebe os dados obsoletos da região secundária, em vez dos dados atualizados. Ao criar seu aplicativo, você deve decidir se esse comportamento é aceitável ou não. Se positivo, você também precisará considerar como notificar o usuário.

Posteriormente neste artigo, você aprenderá mais sobre como lidar com dados eventualmente consistentes e como verificar a propriedade Hora da Última Sincronização para avaliar quaisquer discrepâncias entre os dados nas regiões primária e secundária.

Tratamento de serviços separadamente ou em conjunto

Embora não seja provável, é possível que um serviço (blobs, filas, tabelas ou arquivos) fique indisponível enquanto os outros serviços ainda permaneçam totalmente funcionais. É possível tratar as tentativas para cada serviço separadamente ou você pode tratá-las de forma genérica para todos os serviços de armazenamento em conjunto.

Por exemplo, se usar filas e blobs no seu aplicativo, você poderá optar por incluir código separado para lidar com erros de nova tentativa para cada serviço. Dessa forma, um erro do serviço de blob afetará apenas a parte do aplicativo que lida com blobs, deixando as filas continuarem em execução normalmente. No entanto, se você decidir tratar todas as tentativas de serviço de armazenamento em conjunto, as solicitações para os serviços de blobs e filas serão afetadas se qualquer serviço retornar um erro com nova tentativa.

Em última análise, essa decisão depende da complexidade do aplicativo. Talvez você prefira tratar falhas por serviço para limitar o impacto das tentativas. Ou talvez você decida redirecionar as solicitações de leitura a todos os serviços de armazenamento para a região secundária quando detectar um problema em qualquer serviço de armazenamento na região primária.

Executando o aplicativo no modo somente leitura

Para se preparar eficazmente para uma interrupção na região primária, seu aplicativo deve ser capaz de lidar com solicitações de leitura com falha e solicitações de atualização com falha. Se a região primária falhar, as solicitações de leitura poderão ser redirecionadas para a região secundária. No entanto, as solicitações de atualização não podem ser redirecionadas porque os dados replicados na região secundária são somente leitura. Por esse motivo, é necessário criar o aplicativo para ser executado no modo somente leitura.

Por exemplo, você pode definir um sinalizador que é verificado antes que todas as solicitações de atualização sejam enviadas ao Armazenamento do Azure. Quando uma solicitação de atualização chega, você pode ignorar a solicitação e retornar uma resposta apropriada para o usuário. Você pode até optar por desabilitar totalmente determinados recursos até que o problema seja resolvido e notificar os usuários de que esses recursos estão temporariamente indisponíveis.

Se decidir tratar os erros em cada serviço separadamente, você também precisará lidar com a capacidade de executar o aplicativo no modo somente leitura por serviço. Por exemplo, você pode configurar sinalizadores somente leitura para cada serviço. Em seguida, você pode habilitar ou desabilitar os sinalizadores no código, conforme necessário.

Ser capaz de executar o aplicativo no modo somente leitura também oferece a capacidade de garantir funcionalidade limitada durante uma atualização importante do aplicativo. Você pode disparar o aplicativo para ser executado no modo somente leitura e apontar para o data center secundário, garantindo que ninguém acessa os dados na região primária enquanto você faz atualizações.

Tratamento de atualizações ao executar no modo somente leitura

Há várias maneiras de lidar com solicitações de atualização durante a execução no modo somente leitura. Esta seção se concentra em alguns padrões gerais a serem considerados.

  • Você pode responder ao usuário e notificá-lo de que as solicitações de atualização não estão sendo processadas no momento. Por exemplo, um sistema de gerenciamento de contatos pode permitir que usuários acessem informações de contato, mas não façam atualizações.

  • Você pode enfileirar as atualizações em outra região. Nesse caso, você gravaria as solicitações de atualização pendentes em uma fila em uma região diferente e teria uma maneira de processar essas solicitações depois que o data center primário ficasse online novamente. Nesse cenário, é necessário informar ao usuário que a solicitação de atualização está na fila para processamento posterior.

  • Você pode gravar as atualizações em uma conta de armazenamento em outra região. Em seguida, quando a região primária ficar online novamente, será possível mesclar essas atualizações nos dados principais, dependendo da estrutura dos dados. Por exemplo, se estiver criando arquivos separados com um carimbo de data/hora no nome, você poderá copiar esses arquivos de volta para a região primária. Essa solução pode ser aplicada a cargas de trabalho, como registro em log e dados de IoT.

Lidar com repetições

Os aplicativos que se comunicam com serviços em execução na nuvem devem ser sensíveis a eventos não planejados e falhas que podem ocorrer. Essas falhas podem ser transitórias ou persistentes, variando desde uma perda momentânea de conectividade até uma interrupção significativa devido a um desastre natural. É importante criar aplicativos de nuvem com tratamento de repetição apropriado para maximizar a disponibilidade e melhorar a estabilidade geral do aplicativo.

Solicitações de leitura

Se a região primária ficar indisponível, as solicitações de leitura poderão ser redirecionadas para o armazenamento secundário. Conforme observado anteriormente, deve ser aceitável que o aplicativo possa ler dados obsoletos. A biblioteca de clientes do Armazenamento do Azure oferece opções para tratar tentativas e redirecionar solicitações de leitura para uma região secundária.

Neste exemplo, o tratamento de repetição para armazenamento de Blobs é configurado na classe BlobClientOptions e será aplicado ao objeto BlobServiceClient que criamos usando essas opções de configuração. Essa configuração é uma abordagem primária e secundária, em que as tentativas de solicitação de leitura da região primária são redirecionadas para a região secundária. Essa abordagem é melhor quando se espera que as falhas na região primária sejam temporárias.

string accountName = "<YOURSTORAGEACCOUNTNAME>";
Uri primaryAccountUri = new Uri($"https://{accountName}.blob.core.windows.net/");
Uri secondaryAccountUri = new Uri($"https://{accountName}-secondary.blob.core.windows.net/");

// Provide the client configuration options for connecting to Azure Blob storage
BlobClientOptions blobClientOptions = new BlobClientOptions()
{
    Retry = {
        // The delay between retry attempts for a fixed approach or the delay
        // on which to base calculations for a backoff-based approach
        Delay = TimeSpan.FromSeconds(2),

        // The maximum number of retry attempts before giving up
        MaxRetries = 5,

        // The approach to use for calculating retry delays
        Mode = RetryMode.Exponential,

        // The maximum permissible delay between retry attempts
        MaxDelay = TimeSpan.FromSeconds(10)
    },

    // If the GeoRedundantSecondaryUri property is set, the secondary Uri will be used for 
    // GET or HEAD requests during retries.
    // If the status of the response from the secondary Uri is a 404, then subsequent retries
    // for the request will not use the secondary Uri again, as this indicates that the resource 
    // may not have propagated there yet.
    // Otherwise, subsequent retries will alternate back and forth between primary and secondary Uri.
    GeoRedundantSecondaryUri = secondaryAccountUri
};

// Create a BlobServiceClient object using the configuration options above
BlobServiceClient blobServiceClient = new BlobServiceClient(primaryAccountUri, new DefaultAzureCredential(), blobClientOptions);

Se você determinar que a região primária provavelmente ficará indisponível por um longo período de tempo, poderá configurar que todas as solicitações de leitura sejam apontadas para a região secundária. Essa configuração é uma abordagem somente secundária. Conforme discutido anteriormente, você precisará de uma estratégia para tratar solicitações de atualização durante esse período e uma maneira de informar aos usuários que somente as solicitações de leitura serão processadas. Neste exemplo, criamos uma nova instância de BlobServiceClient que usa o ponto de extremidade da região secundária.

string accountName = "<YOURSTORAGEACCOUNTNAME>";
Uri primaryAccountUri = new Uri($"https://{accountName}.blob.core.windows.net/");
Uri secondaryAccountUri = new Uri($"https://{accountName}-secondary.blob.core.windows.net/");

// Create a BlobServiceClient object pointed at the secondary Uri
// Use blobServiceClientSecondary only when issuing read requests, as secondary storage is read-only
BlobServiceClient blobServiceClientSecondary = new BlobServiceClient(secondaryAccountUri, new DefaultAzureCredential(), blobClientOptions);

Saber quando alternar para o modo somente leitura e solicitações somente secundárias é parte de um padrão de design de arquitetura chamado padrão de Disjuntor, que será discutido em uma seção posterior.

Solicitações de atualização

As solicitações de atualização não podem ser redirecionadas para o armazenamento secundário, que é somente leitura. Conforme descrito anteriormente, seu aplicativo precisa ser capaz de tratar solicitações de atualização quando a região primária não estiver disponível.

O padrão de Disjuntor também pode ser aplicado a solicitações de atualização. Para tratar erros de solicitação de atualização, é possível definir um limite no código, como 10 falhas consecutivas, e acompanhar o número de falhas de solicitações para a região primária. Depois que o limite for atingido, é possível alternar o aplicativo para o modo somente leitura de maneira que as solicitações de atualização para a região primária não sejam mais emitidas.

Como implementar o padrão de Disjuntor

Tratar falhas que podem levar um tempo variável para a recuperação faz parte de um padrão de design de arquitetura chamado padrão de Disjuntor. A implementação adequada desse padrão pode impedir que um aplicativo tente executar repetidamente uma operação que provavelmente falhará, melhorando assim a estabilidade e a resiliência do aplicativo.

Um aspecto do padrão de Disjuntor é identificar quando há um problema contínuo com um ponto de extremidade primário. Para fazer essa determinação, é possível monitorar com que frequência o cliente encontra erros com nova tentativa. Como cada cenário é diferente, é necessário determinar um limite apropriado a ser usado para a decisão de alternar para o ponto de extremidade secundário e executar o aplicativo no modo somente leitura.

Por exemplo, você pode decidir executar a alternância se houver 10 falhas consecutivas na região primária. É possível acompanhar esse caso mantendo uma contagem das falhas no código. Se houver êxito antes de atingir o limite, redefina a contagem como zero. Se a contagem atingir o limite, alterne o aplicativo para usar a região secundária para solicitações de leitura.

Como uma abordagem alternativa, você pode implementar um componente de monitoramento personalizado no aplicativo. Esse componente pode executar ping contínuo no ponto de extremidade do armazenamento primário com solicitações de leitura triviais (como ler um blob pequeno) para determinar sua integridade. Essa abordagem exige alguns recursos, mas não uma quantidade significativa. Quando é descoberto um problema que atinge o limite, é possível alternar para solicitações de leitura somente secundárias e o modo somente leitura. Para esse cenário, quando a execução de ping no ponto de extremidade do armazenamento primário tiver êxito novamente, é possível alternar de volta para a região primária e continuar permitindo atualizações.

O limite de erros usado para determinar quando alternar pode variar dependendo do serviço dentro do aplicativo. Portanto, você deve considerar torná-los parâmetros configuráveis.

Outra consideração é como lidar com várias instâncias de um aplicativo e o que fazer ao detectar erros com nova tentativa em cada instância. Por exemplo, você pode ter 20 VMs em execução com o mesmo aplicativo carregado. Você lida com cada instância separadamente? Se uma instância começar a ter problemas, você quer limitar a resposta a apenas uma instância? Ou você quer que todas as instâncias respondam da mesma forma quando uma instância tiver um problema? Tratar as instâncias separadamente é muito mais simples do que tentar coordenar a resposta entre elas, mas sua abordagem dependerá da arquitetura do aplicativo.

Manipulação de dados eventualmente consistentes

O armazenamento com redundância geográfica funciona por meio da replicação de transações da região primária para a secundária. O processo de replicação garante que os dados na região secundária sejam eventualmente consistentes. Isso significa que todas as transações na região primária eventualmente aparecerão na região secundária, mas pode haver um atraso antes de aparecerem. Também não há garantia de que as transações chegarão à região secundária na mesma ordem em que foram aplicadas originalmente na região primária. Se as transações chegarem na região secundária fora de ordem, você poderá considerar que os dados na região secundária estão em um estado inconsistente até que o serviço restabeleça a ordem.

O exemplo a seguir para a Tabela do Azure mostra o que pode acontecer quando você atualiza os detalhes de um funcionário para torná-lo membro da função Administrador. Para este exemplo, isso requer que você atualize a entidade funcionário e atualize uma entidade função de administrador com uma contagem do número total de administradores. Observe como as atualizações são aplicadas fora de ordem na região secundária.

Hora Transação Replicação Hora da Última Sincronização Resultado
T0 Transação A:
Inserir funcionário
entidade no principal
Transação A inserida no primário,
ainda não replicada.
T1 Transação A
replicada para
secundário
T1 A transação A foi replicada para o secundário.
Hora da Última Sincronização atualizada.
T2 Transação B:
Atualizar
entidade de funcionário
no principal
T1 Transação B gravada no principal,
ainda não replicada.
T3 Transação C:
Atualizar
administrator
entidade de função em
primary
T1 Transação C gravada no principal,
ainda não replicada.
T4 Transação C
replicada para
secundário
T1 Transação C replicada para o secundário.
LastSyncTime não atualizado porque
a transação B ainda não foi replicada.
T5 Ler entidades
de secundário
T1 Você obtém o valor obsoleto para a entidade de funcionário
porque a transação B não foi
replicada ainda. Você obtém o novo valor para
a entidade de função de administrador porque C foi
replicada. A Hora da Última Sincronização ainda não
foi atualizada porque a transação B
não foi replicada. Você pode ver que a
entidade da função de administrador está inconsistente
porque a data/hora da entidade é posterior
à Hora da Última Sincronização.
T6 Transação B
replicada para
secundário
T6 T6 – todas as transações até C
foram replicadas; a Hora da Última Sincronização
foi atualizada.

Neste exemplo, suponha que o cliente alterne para leitura da região secundária em T5. Ele pode ler com êxito a entidade função Administrador nesse momento, mas a entidade contém um valor para a contagem de administradores que não é consistente com o número de entidades funcionário que estão marcadas como administradores na região secundária nesse momento. O cliente pode exibir esse valor, com o risco de que as informações sejam inconsistentes. Como alternativa, o cliente pode tentar determinar que a função de administrador está em um estado potencialmente inconsistente porque as atualizações ocorreram fora de ordem e informar esse fato ao usuário.

Para determinar se uma conta de armazenamento tem dados potencialmente inconsistentes, o cliente pode verificar o valor da propriedade Hora da Última Sincronização. Hora da Última Sincronização indica a hora em que os dados na região secundária estiveram consistentes pela última vez e quando o serviço aplicou todas as transações antes desse ponto no tempo. No exemplo mostrado acima, após o serviço inserir a entidade funcionário na região secundária, a Hora da Última Sincronização é definida como T1. Ela permanece em T1 até que o serviço atualize a entidade funcionário na região secundária, quando ela será definida como T6. Se o cliente recupera a hora da última sincronização ao ler a entidade em T5, pode compará-la ao carimbo de data/hora na entidade. Se o carimbo de data/hora na entidade for posterior à hora da última sincronização, a entidade está em um estado potencialmente inconsistente, e você poderá executar a ação apropriada. Usar esse campo requer que você saiba quando a última atualização do primário foi concluída.

Para saber como verificar o horário da última sincronização, consulte a propriedade Verificar horário da última sincronização de uma conta de armazenamento.

Testando

É importante testar se o aplicativo se comporta conforme o esperado ao encontra erros com nova tentativa. Por exemplo, é necessário testar se o aplicativo alterna para a região secundária ao detectar um problema e alterna de volta quando a região primária ficar disponível novamente. Para testar esse comportamento corretamente, você precisa de uma maneira de simular erros com nova tentativa e controlar com que frequência eles ocorrem.

Uma opção é usar o Fiddler para interceptar e modificar respostas HTTP em um script. Esse script pode identificar as respostas que vêm do ponto de extremidade primário e alterar o código de status HTTP de forma que a biblioteca de clientes do Armazenamento o reconheça como um erro com nova tentativa. Este snippet de código mostra um exemplo simples de um script do Fiddler que intercepta as respostas para ler as solicitações em relação à tabela employeedata para retornar um status 502:

static function OnBeforeResponse(oSession: Session) {
    ...
    if ((oSession.hostname == "\[YOURSTORAGEACCOUNTNAME\].table.core.windows.net")
      && (oSession.PathAndQuery.StartsWith("/employeedata?$filter"))) {
        oSession.responseCode = 502;
    }
}

Você pode estender esse exemplo para interceptar uma maior gama de solicitações e alterar apenas o responseCode em alguns deles para simular melhor um cenário do mundo real. Para obter mais informações sobre como personalizar os scripts do Fiddler, confira Modificando uma solicitação ou resposta na documentação do Fiddler.

Se você tiver definido limites configuráveis para alternar o aplicativo para o modo somente leitura, será mais fácil testar o comportamento com volumes de transações de não produção.


Próximas etapas

Para obter um exemplo completo que mostra como fazer a alternância entre os pontos de extremidade primário e secundário, confira Exemplos do Azure – usando o padrão de Disjuntor com o armazenamento de RA-GRS.