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 estratégias como replicações (eventualmente em mais de uma região) e clusters. A ideia é que a escala horizontal previne a saturação de recursos e cria um “backup” operante se algo vai mal. Entretanto, em sistemas cada vez mais distribuídos, com componentes cada vez menores, esta estratégia parece não ter mais efetividade. Parece fazer mais sentido implementar táticas para resiliência do que replicação.
0
Considerações?x

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.

Definições para disponibilidade e confiabilidade

Disponibilidade é um atributo de qualidade, mensurável, que indica a relação entre o tempo em que um sistema ou componente está disponível e o tempo em que ele “poderia” (ou “deveria”) estar. Entendendo-se, nesse contexto, que um sistema está disponível quando funciona com a performance projetada.

Confiabilidade, também um atributo de qualidade mensurável, implica que, além de disponível, um sistema ou componente opere sem erros nem defeitos.

Para ser confiável, um sistema ou componente precisa se manter disponível. Entretanto, nem todo sistema disponível é confiável.

Estratégia para disponibilidade

Historicamente, disponibilidade é obtida através de clusterização e replicação. A ideia é tentar reduzir o impacto de saturação de alguma instância de componente, distribuindo cargas.

Estratégia para confiabilidade

Devido aos custos de escala e a fragmentação crescente de componentes (como em arquiteturas baseadas em microsserviços), ultimamente tem-se adotado, outra estratégia: a resiliência.

Para ser resiliente, um componente precisa adaptar seu comportamento, reconhecendo eventuais erros ou defeitos nos demais, tolerando problemas com latência, eventualmente implantando mecanismos de retentativas, circuit-breaking, reinicialização automática, etc. Sistemas resilientes não são, necessariamente, mais disponíveis, mas, sem dúvidas, são mais confiáveis.

Sobre falhas, erros e defeitos

O que você pensa quando vê uma rachadura pequena na tela de um celular? Eu sempre penso sobre em quanto tempo  la irá se propagar e se há algo que possa ser feito para contenção. 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 forem contidas, eventualmente, “espalhadas”, convertem-se em defeito.

Pequenas falhas (faults) – como bugs ou validações insuficientes em “entradas” fornecidas por usuários – deixam 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.

Falhas provocam erros que provocam defeitos. Por exemplo, 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.

Reconhecendo que 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 os riscos a confiabilidade.

Estatisticamente, é fácil demonstrar que quanto mais componentes formam um sistema, maiores são as chances da ocorrência de incidentes com potencial para gerar instabilidade. Em termos simples, 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.

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.

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. Quando menor for esta janela, geralmente maior é o RTO.

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

Táticas para identificar 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.

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.

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.
0
Considerações?x

Táticas para impedir 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

Já tratamos de bulkheads quando falamos sobre estratégias para suportar escalabilidade. Entretanto, o padrão também é útil para criação de sistemas resilientes.

A ideia é criar instâncias dedicadas de determinados componentes para alguns cenários de uso. Dessa forma, impedindo que falhas ou eventos em um contexto de consumo propaguem para os demais.

Táticas para previnir 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.
0
Consideraçõesx
 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.

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
3 Comentários
Oldest
Newest Most Voted
Feedbacks interativos
Ver todos os comentários
Cassio Eskelsen
Cassio Eskelsen
2 anos atrás

Artigo interessante relacionado com esse assunto: Autonomous Computing https://pathelland.substack.com/p/autonomous-computing-short-version

Marcelo Amaral
Marcelo Amaral
2 anos atrás

Dentre as “Táticas para impedir a propagação de falhas”, podemos colocar também a Poison Queue.

Anderson Konzen
Anderson Konzen
2 anos atrás

Elemar, ao ler a seção “Táticas para impedir a propagação de falhas” me lembrei de Erlang/Elixir onde uma das prerrogativas é o “let it crash”, ou seja, deixar o processo com bug/falha morrer e ser reiniciado com um estado correto de modo que a falha não se propague. O mecanismo de supervisão e modelo de atores – entre outros – propicia a criação de sistemas resilientes (vide Akka para o .NET).

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

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