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.
Entendendo disponibilidade Ainda confuso sobre o significa disponibilidade, enquanto atributo de qualidade arquitetural? Acesse o tópico onde explicamos o conceito. Acessar tópico |
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.
Filas geram saturação A formação de filas, invariavelmente, gera saturação de recursos, eventualmente gerando indisponibilidades. Interessado em saber mais? Acessar tópico |
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
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. |
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. |
Métricas importantes para confiabilidade
Há dois pares de métricas fundamentais para o design arquitetural de software com disponibilidade e confiabilidade.
- Sob a perspectiva funcional, MTTR e MTBF;
- 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.
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.
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:
- Como identificar a ocorrência de falhas?
- Como prevenir a ocorrência de falhas?
- 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.
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.
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”.
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:
- 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”.
- Circuito aberto
- Nenhuma requisição do “cliente” deve ser encaminhada ao “servidor”, falhando imediatamente
- Transcorrido um determinado tempo, o circuito deve ficar “meio aberto”
- Circuito meio aberto
- Algumas requisições devem ser encaminhadas para o “servidor”, outras negadas
- Se as falhas persistirem, o circuito deverá “abrir” novamente
- 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:
- Que padrões para resiliência já adotou?
- Qual a relação entre a complexidade crescente dos sistemas e a demanda por resiliência?
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.