Na noite de 7 de maio, a refrigeração falhou em um data hall de uma zona de disponibilidade em us-east-1, os racks perderam energia, e o raio de explosão da AWS fez o que a documentação promete: o dano ficou contido dentro de use1-az4. Todas as outras zonas continuaram servindo. As empresas que caíram naquela noite caíram dentro da própria arquitetura, e sabemos disso com uma precisão incomum porque a Coinbase publicou um postmortem na segunda-feira que diz exatamente isso com as próprias palavras. A negociação ficou, na formulação deles, "unavailable or degraded for roughly eight hours, with full recovery of all systems taking another twelve", durante um evento em que a fronteira de zona da plataforma funcionou.

Esta é a Parte 1 de Nothing Fails Alone, uma série sobre a distância entre a disponibilidade que você compra de um provedor de nuvem e a disponibilidade que você de fato constrói em cima dela. Cada parte pega um incidente real, extrai a afirmação arquitetural que ele coloca à prova e entrega um pequeno lab que você pode rodar contra a sua própria conta. Os labs vivem em um repositório companheiro, nothing-fails-alone no GitHub, e cada um deles é somente leitura.

O que de fato aconteceu em use1-az4

O primeiro relatório de incidente no AWS Health Dashboard foi carimbado às 17:25 PDT de 7 de maio, e como o evento resolvido já saiu do dashboard, a formulação abaixo é a que The Register citou na época: problemas na zona de disponibilidade use1-az4 de us-east-1. Uma atualização seguinte descreveu o mecanismo sem rodeios: "EC2 instances and EBS volumes hosted on impacted hardware are affected by the loss of power during the thermal event." Às 18:47 PDT, a AWS disse que estava trabalhando para trazer as temperaturas de volta a níveis normais e avisou que outros serviços que dependiam das instâncias EC2 e volumes EBS afetados naquela zona também poderiam ser prejudicados.

O postmortem da Coinbase preenche a causa física: várias unidades de chiller falharam ao mesmo tempo em um único data hall, e a perda de refrigeração disparou um desligamento de segurança térmica dos racks afetados. É o que "loss of power during the thermal event" significa na prática. Os racks não foram destruídos; foram deliberadamente desligados, porque rodar computação sem refrigeração a destrói. As instâncias EC2 e os volumes EBS naqueles racks ficaram offline juntos, em um prédio, em uma zona.

A recuperação foi gradual e honesta em ser gradual. Às 22:11 PDT daquela noite, a AWS reportou capacidade adicional de refrigeração online e alguns racks recuperados, com mais a serem recuperados "in a controlled and safe manner". A capacidade de refrigeração voltou aos níveis pré-incidente às 13:50 PDT de 8 de maio, cerca de vinte horas após o primeiro relatório, com a recuperação das instâncias vindo atrás da restauração da refrigeração. Se o seu modelo mental de um evento de AZ é uma interrupção de quinze minutos, recalibre: um evento térmico é medido em horas porque a física é, e os racks voltam rack a rack, não todos de uma vez.

O que não aconteceu

As outras zonas de disponibilidade em us-east-1 permaneceram saudáveis. Essa é a parte deste incidente que deveria receber mais atenção do que recebe, porque é a parte que a AWS de fato prometeu. Zonas de disponibilidade são instalações fisicamente separadas com energia e refrigeração independentes, e em 7 de maio essa independência foi testada por uma falha real de instalação e aguentou. O domínio de falha foi um data hall; o raio de explosão ficou dentro de uma zona.

Houve exatamente uma ressalva honesta, e vale citá-la porque ela importa mais adiante nesta série. A AWS desviou o tráfego para longe da zona atingida, aconselhou os clientes a mover cargas de trabalho para as outras zonas de us-east-1 e então admitiu: "Customers may experience longer than usual provisioning times." Claro que podem. Quando uma zona morre, toda arquitetura multi-AZ corretamente construída naquela região começa a repor capacidade nas zonas sobreviventes ao mesmo tempo. A fronteira de zona aguentou, mas as zonas sobreviventes são um pool compartilhado, e um failover em massa é uma manada em disparada contra esse pool. Guarde essa ressalva; a Parte 3 desta série trata do que o seu plano de recuperação tem em comum com o plano de recuperação de todo mundo.

As próprias palavras da Coinbase

Postmortems de empresas de capital aberto costumam ser lixados até não sobrar nada. Este não é, e é por isso que vale a pena ler na íntegra. Duas frases carregam toda a história arquitetural. Primeira: "Our matching engine was pinned to a single building." Segunda, a explicação de como: o matching engine da Coinbase Exchange "runs as a Raft-based replicated cluster inside an AWS Cluster Placement Group". E então a frase que transforma um evento de zona em uma queda de oito horas: "There was no automated cross-zone failover."

Um cluster placement group é single-AZ por construção. Isso não é uma limitação que a AWS esconde; é o produto. O objetivo da estratégia de cluster é empacotar instâncias em hardware fisicamente próximo, no mesmo segmento de rede de alta largura de banda, para que a latência nó a nó seja tão baixa quanto o EC2 consegue fazer. Você não pode ter isso e separação de zona ao mesmo tempo, porque separação de zona é distância e distância é latência. Para um matching engine, onde microssegundos de latência nó a nó são o produto, um cluster placement group é a escolha correta, e a Coinbase diz exatamente isso: "We make this choice deliberately."

O problema é o que vivia dentro dele. O Raft existe para sobreviver à perda de uma minoria de nós. Coloque cinco nós Raft em um cluster placement group e você construiu um protocolo de consenso cujos membros compartilham um prédio, o que significa que compartilham um domínio de falha, o que significa que o protocolo não pode mais fazer a única coisa para a qual ele existe. Quando a AWS terminou instâncias EC2 dentro do placement group da Coinbase às 21:29 ET, três de cinco nós do matching engine caíram juntos e o quórum se foi. Um quórum que compartilha um data hall não é um quórum; é uma falha com cinco nomes.

Sem failover cross-zone automatizado por trás, a recuperação virou um processo humano. Exigiu uma mudança de código emergencial, entregue durante o incidente, para remover uma premissa de inicialização de que os cinco nós do cluster eram resolvíveis. O matching engine voltou em modo cancel-only às 02:25 ET de 8 de maio, a negociação plena foi retomada às 03:49 ET, o site de varejo e o app mobile ficaram totalmente recuperados às 09:53 ET, e o backlog de tópicos de event-streaming foi zerado às 14:00 ET. Cerca de oito horas fora do ar, mais doze até a recuperação plena, durante um evento que a AWS conteve em uma zona.

Multi-AZ é algo que você compra e algo que você constrói

O formato geral deste incidente não é uma história da Coinbase. É a distância entre comprar infraestrutura multi-AZ e construir um sistema multi-AZ, e essa distância tem um pequeno número de formatos recorrentes. Cada um destes é uma configuração que parece redundante no diagrama de arquitetura e não é:

  • O Auto Scaling group de subnet única. O ASG vai alegremente repor instâncias falhas para sempre, na mesma zona, porque a lista de subnets que você deu a ele resolve para uma única AZ. Quando aquela zona degrada, a capacidade desejada não pode ser atingida até que um humano edite o grupo. Auto-recuperação que se recupera para dentro da falha não é auto-recuperação.
  • RDS sem Multi-AZ. Sem standby síncrono, a perda de zona não é um failover, é um restore: de um snapshot, medido em horas, com perda de dados até o último ponto de backup. O plano de DR ao lado dele geralmente assume um failover sub-minuto, porque alguém leu a documentação de Multi-AZ e ninguém conferiu o flag no console.
  • O quórum em um cluster placement group. O padrão da Coinbase: três, cinco ou sete nós de Raft, etcd ou ZooKeeper empacotados em um grupo que é single-AZ por definição. A camada de consenso se reporta como saudável e tolerante a falhas até o exato momento em que o prédio discorda.
  • A dependência de NAT cross-AZ. Uma subnet privada na zona B cuja rota padrão aponta para um NAT gateway na zona A. A falha da zona A derruba o tráfego de saída de instâncias cuja própria zona está perfeitamente saudável. Essa é invisível em todo diagrama porque a seta aponta para "NAT", não para uma zona.

Nenhuma delas é exótica. Todas passam por um teste de carga, por um game day que só mata instâncias e pela maioria das revisões de arquitetura, porque cada uma só falha quando uma zona inteira falha, e zonas inteiras falham raro o suficiente para que a premissa fique sem teste por anos.

O lab: um detector de concentração single-AZ

Afirmações devem ser testáveis, então esta parte vem com um lab: 01-single-az-detector, um único arquivo Python que varre a conta e a região atuais em busca dos padrões acima e mais três. Ele é somente leitura no sentido estrito: toda chamada de API que ele faz é um Describe*. Ele roda sete verificações: Auto Scaling groups single-AZ, cluster placement groups, clusters de tamanho de quórum dentro de placement groups, instâncias RDS single-AZ, clusters RDS cujos membros estão todos em uma zona, replication groups do ElastiCache sem Multi-AZ, e subnets privadas cuja rota padrão cruza zonas para alcançar um NAT gateway.

$ python3 detect.py
SEVERITY | RESOURCE                      | AZ                    | WHY
--------------------------------------------------------------------------...
CRITICAL | placement-group/matching-prod | us-east-1a (use1-az4) | 3 instances in cluster placement group 'matching-prod': a quorum-sized cluster in a group that is single-AZ by construction. ...
HIGH     | asg/api-workers               | us-east-1c (use1-az2) | Auto Scaling group spans exactly one AZ. Every replacement instance lands in the same zone ...
HIGH     | rds-cluster/orders            | us-east-1a (use1-az4) | All members of DB cluster 'orders' sit in one AZ. Aurora failover needs a reader in a different zone ...
MEDIUM   | subnet-0a1b2c3d -> nat-0e4f5a6b | us-east-1b (use1-az6) -> us-east-1a (use1-az4) | Private subnet in us-east-1b (use1-az6) routes 0.0.0.0/0 through a NAT gateway in us-east-1a (use1-az4) ...

4 finding(s).

A verificação que merece a severidade CRITICAL é QUORUM_IN_ONE_AZ: um cluster placement group contendo exatamente 3, 5 ou 7 instâncias em execução. Essas contagens não são arbitrárias; são o formato de um quórum de Raft, etcd ou ZooKeeper, e uma carga em formato de quórum concentrada em uma zona é a exata configuração que derrubou a Coinbase. Um placement group com alguma outra contagem de instâncias ainda é sinalizado, mas como HIGH: é uma otimização de latência que alguém deveria confirmar ter sido escolhida de propósito.

Dois comportamentos são deliberados. Primeiro, o código de saída: o scan sai com 0 sempre que conclui, com achados ou não, porque isto é um relatório, não um portão. Se o IAM negar uma das APIs, aquela verificação é pulada com um aviso de uma linha no stderr, as verificações restantes ainda rodam, e o código de saída permanece 0; você recebe código de saída 2 apenas quando o scan não pode rodar de jeito nenhum, sem credenciais, sem região, sem boto3. Confira o stderr antes de confiar em uma tabela limpa. Segundo, todo achado reporta a zona tanto como nome quanto como ID, por exemplo us-east-1a (use1-az4). O nome é um alias que a AWS randomiza por conta; o ID é a zona física. O Health Dashboard disse use1-az4, e sem o ID você não consegue dizer se aquela era a sua us-east-1a. Essa distinção parece trivialidade hoje. A Parte 2 é sobre o incidente em que ela deixa de ser trivialidade.

Modos de falha e o que observar

O trade-off por baixo de tudo isso é legítimo, então nomeie-o em vez de fingir que ele não existe. Colocação single-AZ compra latência, transferência de dados cross-AZ custa dinheiro, e replicação síncrona entre zonas custa latência de escrita. A Coinbase não tropeçou em um placement group; eles projetaram nele e aceitaram um risco. A falha não foi a escolha, foi que a consequência da escolha, nenhum caminho automatizado para fora da zona, foi descoberta às 21:29 durante o incidente em vez de decidida em uma revisão de design antes dele.

O que observar na sua própria conta, além de rodar o detector: aritmética de quórum, não só localização do quórum, porque dois de três nós etcd em uma zona é o mesmo defeito com uma linha de tabela mais amigável. ASGs que listam várias subnets que todas resolvem para a mesma zona, e é por isso que o detector resolve as subnets do VPCZoneIdentifier em vez de confiar na lista de AZ. Caches sem réplicas: uma perda de zona deixa o ElastiCache frio, e a tempestade de misses resultante cai no seu banco de dados no pior momento possível. E provisionar capacidade durante um evento real de zona, que é a única coisa que o detector não consegue ver, porque é uma propriedade da arquitetura de todo mundo, não da sua. A própria AWS te avisou, no meio do incidente: longer than usual provisioning times. A fronteira de zona é real, e aguentou. O que você constrói em cima dela é seu.

Leia isto a seguir

Para consultoria em AWS, arquitetura de nuvem, revisões de resiliência e trabalho de plataforma, comece em ercanermis.com.

Referências