Fundamentos para arquiteturas de sistemas resilientes

Este capítulo possui uma versão mais recente:

Ninguém tropeça em montanhas. É a pedra pequena que faz você tropeçar. Passe por todas as pedrinhas em seu caminho e você descobrirá que você atravessou a montanha.
Autor Desconhecido

Com a transformação digital, sistemas estão cada vez mais conectados. Clientes, parceiros e fornecedores têm cada vez mais acesso a sistemas que até bem pouco tempo eram acessados apenas “dentro de casa”. As “janelas para downtime” estão ficando cada vez menores. Nunca disponibilidade e confiabilidade foram tão importantes.

Sob o ponto de arquitetura de software, é cada vez mais relevante identificar que funcionalidades dos sistemas “não devem parar”, assim como quais podem ficar eventualmente indisponíveis e em que “janelas”. A partir dessas informações, é possível criar propostas de design de componentes que colaborem com a solução, sem custos desnecessários.
0
Considerações?x

Há tempos, a indústria de software tem mantido sistemas disponíveis adotando redundância, com replicações (eventualmente em mais de uma região) e clusters de processamento. A ideia é que a escala horizontal previne a saturação de recursos e cria um “backup” operante se algo vai mal. Entretanto, essa não pode ser a única medida adotada
0
Considerações?x

Definição: Resiliência

Resiliência, como propriedade da física, implica é a propriedade que alguns corpos apresentam de retornar à forma original após terem sido submetidos a uma deformação elástica. Informalmente, é a capacidade de se recobrar facilmente ou se adaptar à má sorte ou às mudanças.

Redundância ainda é fundamento para resiliência

Considere um sistema que precise suportar, consistentemente, 1.000 requisições por segundo. Cada servidor de aplicação deste sistema demonstra capacidade para suportar 300 requisições por segundo. Daí, infere-se a necessidade de quatro instâncias.

O problema é que uma eventual indisponibilidade em um dos servidores implica em sobrecarga para as demais instâncias.

Com o traffic intensity desfavorável (arrival rate > departure rate) ocorre, então, provável saturação de recursos (falha) levando a erros e defeitos (indisponibilidade do serviço) em, provavelmente, pouco tempo.

A estratégia para resiliência, no exemplo, consiste em adicionar redundância. Em termos simples, um quinto servidor “sobrando” seria a forma evidente para tolerar falhas.

Redundância, essencial para resiliência, implica em aumento de custos.

Não há resiliência sem redundância. Entretanto, esta é apenas a primeira medida e, provavelmente, a mais cara.

Diferenciando falhas, erros e defeitos

O que você pensa quando vê uma rachadura pequena na tela de um celular? Geralmente, é questão de pouco tempo  para que ela se propague tornando o dispositivo inútil (defeituoso). Se há algo que possa ser feito para contenção, deve ser feito. Algo semelhante acontece com sistemas de software.
0
Concorda?x

Em sistemas de software, toda falha é uma espécie de rachadura pequena que, se não for contida, eventualmente, irá se espalhar até deixar o sistema o sistema defeituoso.

Pequenas falhas (faults) – como bugs ou validações insuficientes em “entradas” fornecidas por usuários – podem deixar o sistema em estado inconsistente, o que, ocasionalmente, dá origem a erros (errors), ou seja, comportamentos indesejados do software que, eventualmente, culminam em defeitos (failures), geralmente indisponibilidades.

Sistemas resilientes são tolerantes a falhas. Isso significa, impedir que elas se convertam em erros que, eventualmente, se convertem em defeitos.

Falhas provocam erros que provocam defeitos. Consultas SQL mal escritas são falhas que, eventualmente, geram lentidão no banco para responder outras consultas que, por sua vez, causam o aumento nas filas de requisições que, se prolongadas, “topam” memória que, finalmente, tornam um sistema indisponível.

Relação com confiabilidade

Esperança não é estratégia.

Frase popular entre praticantes de SRE

Definição: Confiabilidade

Confiabilidade trata da capacidade de um sistema desempenhar uma determinada função, sem erros ou defeitos, sob condições pré-estabelecidas por um período de tempo.

Tolerar falhas é, também, fundamento para a confiabilidade.

A ênfase em adicionar resiliência permite velocity para a adição de mudanças no software, mitigando riscos de comprometer os objetivos de negócio, por isso são importantes. Manter preocupação com resiliência é comportamento pró-ativo e preventivo.

Site Reliability Engineering: How Google Runs Production Systems

Este livro já pode ser considerado um clássico. Ele formaliza SRE – conjunto de práticas para resiliência da Google.

Acessar livro

Falhas são inevitáveis (e cada vez mais comuns)

Sistemas estão ficando maiores, logo, mais complexos nos últimos anos. Esse “crescimento” associado com tecnologias como a nuvem, conduziram ao desenvolvimento de soluções com cada vez mais componentes, cada vez menores, mais fáceis de manter e distribuir. Entretanto, “não há almoço grátis”, maior a fragmentação, maiores as probabilidades de ocorrência de falhas.

Em termos simples, em sistemas cada vez mais distribuídos, falhas, além de inevitáveis, devem ocorrer com frequência cada vez maior. Por isso, implementações ingênuas resultam em sistemas cada vez menos confiáveis.

Abordagens arquiteturais “ingênuas” ignoram que sistemas distribuídos enfrentam mais falhas, permitindo maior incidência de erros e defeitos.

Resiliência implica em contenção de falhas

A estratégia mais efetiva para evitar defeitos é impedir que falhas se convertam em erros. Para isso, é importante, além de tentar minimizá-las, impedir que seus efeitos “se espalhem”.

Em sistemas complexos, sem o devido cuidado, falhas em um componente “se espalham” rapidamente gerando erros em componentes com acoplamento eferente mais alto. Por isso, sob ponto de vista arquitetural, é importante cuidar dos integration points adotando estratégias que mitiguem impactos de falhas, erros ou defeitos de um componente nos demais.

Release It!

Sem dúvidas, um dos melhores livros já escritos sobre projeto de sistemas resilientes.

Acessar livro

As falhas em componentes remotos podem assumir diversas formas, incluindo falhas de comunicação ou comportamento. Componentes remotos podem se tornar inesperadamente indisponíveis ou, o que é muito pior, incrivelmente lentos.
0
Considerações?x
Por isso, é essencial que práticas defensivas sejam adotadas.

Métricas importantes para confiabilidade

A definição de métricas é de extrema importância para o projeto arquitetural. Afinal, as decisões que serão tomadas irão na direção de obter melhores resultados para estas métricas. Assim, a escolha da métrica errada pode comprometer a qualidade das decisões.
0
Considerações?x

Há dois pares de métricas fundamentais para o design arquitetural de software com disponibilidade e confiabilidade.

  1. Sob a perspectiva funcional, MTTR e MTBF;
  2. Sob a perspectiva de dados, RPO e RTO.

MTTR e MTBF

MTTR (Mean time to recover – tempo médio para recuperação) tem relação com o tempo em que uma falha destrói valor em produção. MTBF (Mean time between failures – tempo médio entre falhas) tem relação com o intervalo de tempo até falhas serem que destroem valor serem observadas em produção.

Historicamente, é mais comum dar ênfase a medidas como MTBF, principalmente, quando se adota práticas mais focadas em disponibilidade do que em confiabilidade. Entretanto, para ser efetiva, demanda sistemas menos complexos e menos fragmentados. Quando a ênfase é confiabilidade, a métrica mais apropriada é MTTR.

Métricas criam “tensões” na operação que direcionam decisões e padrões operacionais. A “tensão” gerada pelo MTBF, que é uma métrica bem intencionada, é espaçar problemas em produção o máximo possível, evitando prejuízos percebidos. O problema é que, implicitamente, essa métrica acaba encorajando, também, o prolongamento dos intervalos entre deploys, afinal, em um sistema que está funcionando bem, toda mudança é fonte potencial de problemas. Indiretamente, em função de menos entregas em produção, há aumento do lead time e também do tamanho de cada entrega, gerando, curiosamente, aumento nas chances de falhas no ambiente produtivo, mais difíceis de identificar, prejudicando o MTTR.
0
Considerações?x
Os riscos do MTBF podem ser bastante mitigados se o deploy acontecer em “anéis” e a métrica ficar restrita apenas ao anel mais amplo.

RPO e RTO

Pensar sobre disponbilidade para dados é mais desafiador do que para outros artefatos. Afinal, quando não há perda de dados, soluções mais críticas tem relação, apenas, com replicação ou reinicialização.

As métricas associadas com dados são, respectivamente RPO (recovery point objective) e RTO (recovery time objective).

RPO tem relação com o volume de dados que podem ser perdidos no caso de ocorrência de uma falha. RTO define quanto tempo é tolerado para recuperar dados para devolver o sistema ao RPO, impactando diretamente o MTTR.

Geralmente o RPO é indicado pela “janela de tempo” máxima onde perdas são toleradas.

Uma estratégia para reduzir impactos no RPO e RTO é estabelecer práticas de gestão de dados conforme temperatura, mantendo apenas os dados mais “quentes”, ou seja, usados com mais frequência, próximos do ambiente produtivo onde os defeitos acontecem. Nessa linha, dados mais “frios” (menos utilizados) podem ser movidos para estruturas de armazenamento menos flexíveis.
0
Considerações?x

Abordagens genéricas para lidar com falhas

Boa parte dos esforços arquiteturais para desenvolvimento de sistemas resilientes têm, então, vinculação direta com a capacidade de suportar falhas. Daí, nascem três preocupações inerentes:

  1. Como identificar a ocorrência de falhas?
  2. Como prevenir a ocorrência de falhas?
  3. Como evitar a propagação dos efeitos de uma falha?

Identificando a ocorrência de falhas

Health Checks (verificação de integridade)

A melhor forma de saber que um componente está funcionando bem é “perguntando” para ele. Sob o ponto de vista arquitetural, isto implica em adicionar uma função de verificação para a “saúde” em cada componente, geralmente acessível através de um endpoint específico.

A ideia é fazer com que cada componente execute alguma rotina de auto-verificação, geralmente alguma atividade sem efeitos colaterais duradouros, retornando um valor que indique seu “nível de saúde”. Obviamente, caso o componente não consiga processar a requisição, isso indica problema.
0
Consideraçõesx

As health checks devem ser utilizadas tanto por load balancers, orquestrados de contêineres e por ferramentas de monitoramento.

Readiness e Liveness

A adoção de tecnologias como Kubernetes levaram a uma ampliação do conceito de health checking. Modernamente admite-se dois tipos diferentes de verificação: readiness e liveness.

Readiness trata da “prontidão” de um pod para tratar workload. Tal prontidão é impactada pelo pod em si, mas, também, por suas dependências.

Liveness trata da saúde de um pod. É indicativo para a necessidade de “reciclagem”.

Eventualmente, as verificações podem incluir as principais dependências dos componentes, como bancos de dados, serviços remotos, etc.

É importante configurar adequadamente mecanismos de health check com intervalos e alguma estrutura de caching para evitar sobrecargas desnecessárias.

Considere ter duas versões de healthcheckers: para consumo interno e para consumo externo.

Watchdogs

Enquanto health checks operam passivamente, fornecendo informações sobre a “saúde” dos componentes, watchdogs atuam ativamente, muitas vezes acionando health checks, para, sob determinadas circunstâncias, disparar algum tipo de ação.

Um watchdog é um programa, frequentemente associado a ferramentas de APM e métricas de infraestrutura. Seu objetivo é detectar automaticamente possíveis problemas de aplicativo e infraestrutura, observando continuamente tendências e padrões nas métricas e procurando comportamento atípico.

Watchdogs devem ser planejados na arquitetura, mas raramente devem ser implementados “dentro de casa”. Todos os fornecedores do nuvem oferecem alternativas altamente configuráveis e flexíveis.

Poison Queues

Em sistemas baseados em mensageria, eventualmente, o processamento de determinadas mensagens acabam gerando falhas (e reciclagem) de maneira recorrente. Essas mensagens são “envenenadas”.

O ideal é que sistemas tenham condições de identificar tais mensagens e direciona-las para processamento apartado.

Mitigando a propagação de falhas

Comunicação assíncrona

Substituir chamadas diretas por trocas de mensagens é, provavelmente, a medida mais eficiente para aumentar a resiliência de sistemas de software. A ideia é substituir chamadas a componentes potencialmente instáveis por mecanismos de mensageria comprovadamente sólidos e estáveis.

A abordagem mais simples é utilizar filas point-to-point. Uma alternativa mais sofisticada (e menos acoplada) é a adoção de  pub/sub.

A alternativa tradicional é utilizar estratégias de alta-disponibilidade com replicações e load balancers.

Bulkheads

Considere um sistema com três componentes: “A”, “B” e “C”. Onde “A” e “B” dependem sincronamente de “C” para funcionar.

Caso “C” fique instável, “A” e “B” podem ter dificuldades para continuar funcionando bem, o que pode espalhar falhas em “efeito dominó”. Ou seja, a instabilidade de “C” pode acabar gerando instabilidades em “A” e “B” que, eventualmente, irão causar instabilidades em outros componentes.


A maioria dos projetos de sistemas onde há dependência forte entre componentes têm contingência para falhas mais graves. No exemplo, provavelmente “A” e “B” estão preparados para cenários onde “C” simplesmente pare de responder. Entretanto, poucas vezes encontramos arquiteturas preparadas para lidar com response times mais altos. Geralmente, lentidão, mais do que indisponibilidade total, é a causa raiz da maioria dos problemas mais críticos em produção.

Uma forma comum de tentar mitigar os impactos de instabilidades em um componente é torná-lo escalável na horizontal para, então, levantar várias instâncias com demanda distribuída por um load balancer.

O problema, entretanto, é que, com frequência, instabilidades, sobretudo de lentidão, são ocasionadas por dificuldades de um componente para atender “requisições envenenadas” provenientes de um “ofensor externo”. Em nosso exemplo, caso “A” gere “requisições envenenadas” para “C”, estas podem acabar afetando todas as instâncias do serviço, mesmo escalados na horizontal, apenas retardando os efeitos ruins, sem evitá-los.

Uma alternativa eficiente para mitigar o risco dessa situação é compartimentar instâncias para as diversas origens de requisição. Ou seja, “levantar” instâncias específicas para cada natureza de solicitação, mantendo-as apartadas.

Dessa forma, a “pressão” de “A” sobre “C” não impactará “B”. Nesse mesmo raciocínio, a “pressão” de “B” sobre “C” também não causará problemas para “A”.

No “mundo real”, já vimos essa solução ser aplicada, por exemplo, em grandes varejistas para apartar o tratamento de requisições de aplicações móveis, site, robôs de comparação de preços e o Google (como você pode imaginar, para um varejista, é tremendamente prejudicial ser “penalizado” pelo gigante das buscas).

O nome dessa técnica de design é bulkhead, em alusão a forma como navios eram projetados para impedir que danos em uma parte do cascos causassem um naufrágio.

Bulkheads obviamente, introduzem ociosidade desnecessária (leia-se desperdício) de recursos a uma arquitetura, tornando-a economicamente menos interessante. Entretanto, essa ineficiência pode ser necessária e, geralmente, é mais do que compensada com a minimização e isolamento de downtimes. Um de seus principais atrativos é que pode ser adotada, frequentemente, com poucas alterações (ou até nenhuma) de código.

Prevenindo falhas

Back pressure

Sempre o traffic intensity for desfavorável para um componente, o ideal é que este passe a recusar novas demandas (usando load shedding ou rate limiting), devolvendo a “pressão” para o cliente que deverá implementar alguma estratégia de retentivas ou, até mesmo, reduzir o volume de demandas com alguma estratégia de gracefully degradation.

Do ponto de vista do componente que está adotando back pressure, a implementação é restrita a algum mecanismo de sinalização (talvez retornando 429 – Too many requests) para o cliente indicando a condição. Caberá ao cliente adotar estratégia apropriada, conforme sinalização.

Load shedding (Governor pattern)

Assim como um rate limiter, um componente de load shedding opera como um middleware que monitora os recursos computacionais necessários para um componente, bem como das dependências, e recusa ativamente novas requisições até que níveis saudáveis sejam restaurados.

Timeout

Pior do que componentes que param de responder são aqueles que passam a operar com lentidão incomum. Estabelecer timeouts é importante para impedir que componentes clientes esperem “tempo demais”, seja para solicitações síncronas quanto assíncronas.


Embora não haja uma “receita de bolo” para escolher um timeout correto, eles devem ser relativamente curtos. A recomendação segura é 150% do tempo médio de resposta do serviço.

Use Proxies para componentes que não estão sob controle

Todo componente que não está sob controle do time de desenvolvimento interno e que precisa estar em conformidade com as táticas de resiliência, deve estar “envelopada” por um proxy.

O proxy (Envoy, PgBound, HAProxy, etc) consegue resolver regras como load shedding, time outs, entre outros.

Transaction Outbox

Comandos – requisições que modificam o estado do servidor – demandam, atomicamente, atualizações em bancos de dados e envio de mensagens (via algum mecanismo de mensageria).

Em termos simples, em uma transação, quando um commit acontece, mensagens devem ser enviadas. Entretanto, se for necessário um rollback,

A saída simples é adicionar uma tabela adicional, no banco de dados onde as transações estão sendo processadas, para “registrar” a demanda do envio de uma mensagem. Ao executar uma transação, essa tabela deve receber, também uma inclusão. Outro agente fica responsável por “ler a tabela” e efetivar o envio de mensagens.

Circuit breaker

Circuit breaker é uma instância de máquina de estados implementada entre dois componentes, um “cliente” e o outro “servidor”. O objetivo de um Circuit breaker é proteger o “servidor” de requisições enquanto este estiver enfrentando dificuldades (potencial saturação).

O funcionamento da máquina de estados é a seguinte:

  1. Circuito fechado
    • Toda requisição do “cliente” deve ser encaminhada ao “servidor”
    • Se houverem mais falhas do que o “aceitável” dentro de um intervalo de tempo, então o circuito deve “abrir”.
  2. Circuito aberto
    • Nenhuma requisição do “cliente” deve ser encaminhada ao “servidor”, falhando imediatamente
    • Transcorrido um determinado tempo, o circuito deve ficar “meio aberto”
  3. Circuito meio aberto
    1. Algumas requisições devem ser encaminhadas para o “servidor”,  outras negadas
    2. Se as falhas persistirem, o circuito deverá “abrir” novamente
    3. Se as falhas não ocorrerem mais, o circuito deverá “fechar”.

Sumarizando

Estratégias tradicionais para disponibilidade, baseadas em replicação e clusters, não são mais suficientes para manter sistemas confiáveis.

Tão importante quanto garantir que há recursos suficientes para atender as demandas de performance, sem saturação, é também importante adotar práticas e cuidados arquiteturais que identifiquem, previnam e interrompam a propagação de falhas, para que elas não se tornem erros e, finalmente, defeitos.

// TODO

Antes de avançar para o próximo capítulo, recomendo as seguintes reflexões:

  1. Que padrões para resiliência já adotou?
  2. Qual a relação entre a complexidade crescente dos sistemas e a demanda por resiliência?

Referências bibliográficas

ERDER, Murat; PUREUR, Pierre; WOODS, Eoin. Continuous Architecture in Practice: software architecture in the age of agility and devops. Boston, Ma: Addison-Wesley, 2021. (Vaughn Vernon signature).

HANMER, Robert S.. Patterns for Fault Tolerant Software. San Francisco, Ca: John Wiley & Sons Ltd, 2007.

NYGARD, Michael T.. Release It!: design and deploy production-ready software. 2. ed. Gravenstein Highway North, Ca: Pragmatic Booksheld, 2018. 518 p.

Compartilhe este capítulo:

Compartilhe:

Comentários

Participe da construção deste capítulo deixando seu comentário:

Inscrever-se
Notify of
guest
1 Comentário
Oldest
Newest Most Voted
Feedbacks interativos
Ver todos os comentários
Gabriel Casemiro
Gabriel Casemiro
2 anos atrás

A explicação sobre MTBF tem problemas de ortográficos. A frase “tem relação com o intervalo de tempo até falhas serem que destroem valor serem observadas em produção” está incorreta.

Fundador e CEO da EximiaCo, atua como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

Mentoria

para arquitetos de software

Imersão, em grupo, supervisionada por Elemar Júnior, onde serão discutidos tópicos avançados de arquitetura de software, extraídos de cenários reais, com ênfase em systems design.

Consultoria e Assessoria em

Arquitetura de Software

EximiaCo oferece a alocação de um Arquiteto de Software em sua empresa para orientar seu time no uso das melhores práticas de arquitetura para projetar a evolução consistente de suas aplicações.

ElemarJúnior

Fundador e CEO da EximiaCo, atua como tech trusted advisor ajudando diversas empresas a gerar mais resultados através da tecnologia.

+55 51 99942-0609 |  contato@eximia.co

+55 51 99942-0609  contato@eximia.co

1
0
Quero saber a sua opinião, deixe seu comentáriox
()
x