Um colapso de um sistema distribuído pode começar com um erro que parece limitado, mas transformar-se rapidamente em uma cadeia de falhas quando muitos componentes dependem uns dos outros. Em uma apresentação feita por Sam Newman, o consultor independente de engenharia de software relaciona o conceito de colapso progressivo (Progressive Collapse) na engenharia civil aos problemas de resiliência nos sistemas digitais e conclui que é impossível evitar todos os erros, mas é possível reduzir as chances de sua propagação e seu impacto.
Do edifício Ronan Point aos serviços em nuvem
Newman retoma o caso de um colapso parcial na torre Ronan Point, na região de Canning Town, em Londres, em 1968. Uma explosão de gás limitada dentro do apartamento de Mrs. Ivy Hodge arrancou uma parede externa que sustentava parte do edifício. Os quatro andares acima do local da explosão desabaram, o que levou à queda de parte do canto da torre em um efeito progressivo. Quatro pessoas morreram, e o horário do acidente, pouco antes das seis da manhã, ajudou a limitar o número de vítimas.
O significado para o software não está na semelhança física entre um edifício e um serviço digital, mas no padrão da falha: uma pequena falha inicial interrompe um componente do qual outros componentes dependem, ampliando o círculo de danos. Essa cadeia é difícil de compreender em sistemas distribuídos porque as relações entre serviços, bancos de dados, balanceadores de carga e sistemas DNS nem sempre são visíveis como uma fileira de peças de dominó.
Exemplos digitais de falha em cascata
Durante a interrupção da região us-east-1 da AWS em outubro, um subcomponente do DynamoDB enfrentou um problema ao atualizar rotas DNS dentro da infraestrutura da AWS. Uma falha no processo de atualização levou à exclusão de rotas no Route 53; em seguida, os serviços que dependiam dessas rotas começaram a falhar, incluindo balanceadores de carga de rede, computação, filas e EKS. Como esses serviços básicos são utilizados por outros serviços, o problema chegou a produtos e empresas voltados aos usuários, entre eles Alexa, Ring, Slack, Snapchat, Zoom e Shopify, com alguns afetados parcialmente e outros totalmente.
Segundo a explicação de Newman baseada no relatório da AWS, o sistema incluía um planejador que criava planos de alterações de DNS e vários executores que os aplicavam. Um dos planos levou mais tempo que o normal; depois, outro plano foi criado e executado rapidamente por um executor diferente. Quando a execução do plano antigo foi concluída mais tarde, ele removeu as rotas que o novo plano havia criado. O resultado foi uma condição de corrida (Race Condition) que levou à exclusão de entradas DNS.
O segundo exemplo foi um site de venda de carros usados, motocicletas e trailers, que funcionava por meio de um aplicativo com o nome de código Sauron em dez servidores. O aplicativo costumava lidar com 30 a 60 solicitações simultâneas, mas enfrentou mais de 800 solicitações. Um dos sites dependentes aceitava as conexões e depois travava sem responder, enquanto o aplicativo aguardava 30 segundos antes de encerrar a solicitação. O pool de conexões foi esgotado; em seguida, as threads se acumularam e os processadores passaram a consumir seu tempo gerenciando-as, fazendo o sistema parar completamente. Os usuários aumentaram a pressão ao clicar repetidamente no botão de atualização.
Três caminhos para reduzir o colapso
Newman considera que concentrar-se em uma única «causa raiz» leva a uma simplificação inadequada. Impedir a faísca pode ser útil, mas não resolve todas as condições que permitem que o incêndio se espalhe. Por isso, a engenharia de resiliência concentra-se em aceitar a possibilidade de falhas e preparar-se para limitar seus efeitos por meio de três categorias inter-relacionadas:
- Reduzir os riscos: remover as fontes de perigo ou limitar sua capacidade de esgotar recursos. No caso da AWS, isso incluiu interromper temporariamente o gerenciamento automatizado de DNS até que o problema fosse resolvido e adicionar testes melhores para o componente. No aplicativo Sauron, seria possível usar o descarte de carga (Load Shedding) para aceitar um número limitado de solicitações e rejeitar as que excedem a capacidade do sistema, em vez de permitir que se acumulassem até o colapso.
- Fortalecer os componentes: aumentar a capacidade do serviço de suportar a falha de uma parte dele, por meio de redundância, monitoramento, testes e melhoria da implementação. Isso pode significar executar várias cópias do serviço atrás de um balanceador de carga, mas a decisão depende da importância e do custo do componente; o aplicativo Sauron estava em fase de aposentadoria e representava apenas uma parte da receita, o que tornou a execução de cópias de backup adicionais uma opção comercialmente pouco atraente naquele momento.
- Reduzir a interdependência: impedir que a falha de um componente passe para o restante do sistema. Isso inclui o uso de isolamentos (Bulkheads), limites de tempo, caminhos alternativos e designs locais primeiro, quando possível. Quanto menor a dependência de uma parte do sistema em relação a outra, mais fácil será conter a falha em vez de propagá-la.
O que muda, na prática, para as equipes de tecnologia?
Resiliência não significa automaticamente operar o sistema em duas regiões ou duas nuvens. Newman explica que as opções vão do backup e da restauração à arquitetura Pilot Light, que replica os dados sem executar toda a infraestrutura, passando pela recuperação em modo warm até chegar à execução simultânea de dois sites ativos. Essas opções reduzem gradualmente o tempo de inatividade e a perda de dados, mas aumentam o custo e a complexidade.
Ele também alerta que considerar a sincronização bidirecional entre sites algo simples é um erro, especialmente quando ela é adicionada a uma aplicação existente que não foi projetada para isso. O mesmo se aplica à utilização de múltiplas nuvens; ela pode reduzir a dependência de um único provedor, mas exige competências e operações diferentes para cada plataforma, e não basta implantar o mesmo código na AWS e no Azure para garantir independência, pois o mesmo defeito de software pode interromper os dois ambientes.
Conclusão
A principal lição é que resiliência não consiste em buscar um ambiente que nunca falhe, mas em projetar o sistema para que uma parte dele continue operando quando outra falhar. Por isso, é necessário avaliar os riscos, os limites de capacidade, os pontos de estrangulamento e os caminhos de dependência, e então equilibrar redundância, isolamento e custo. Decisões como operar em múltiplas regiões ou em múltiplas nuvens continuam sendo opções de engenharia e de negócio relacionadas à importância do serviço, não uma receita geral aplicável a todos os sistemas.