Microsserviço 12
À medida que sistemas se tornam cada vez mais presentes na vida de nossos usuários, precisamos melhorar continuamente a qualidade dos serviços que oferecemos, já que a falha em um sistema pode ter um impacto significativo na vida das pessoas.
Temos cada vez mais a responsabilidade de criar softwares confiáveis, sendo cada vez menos tolerável o downtime por conta de manutenções. Assim, microsserviços vêm sendo adotados por empresas do mundo todo como uma oportunidade de melhorar a resiliência de seus serviços. Entretanto, adotar microsserviços como uma forma de atingir a resiliência é apenas parte do processo: é necessário um conjunto de práticas arquiteturais e operacionais a fim de atingir esse objetivo.
Resiliência
O termo resiliência advém de uma área mais ampla, a engenharia de resiliência, que define aspectos que podem ser classificados em quatro conceitos:
- robustez;
- recuperação;
- extensibilidade com elegância;
- adaptabilidade sustentável;
Robustez
Com robustez, queremos incluir métodos e processos em nosso software a fim de acomodar problemas inesperados, de forma que, quando esses problemas surgirem, o software consiga lidar com eles de maneira controlada. Aplicar robustez exige um conhecimento prévio do contexto de execução, para sabermos como reagir a perturbações já conhecidas.
O grande desafio é que, ao aumentar a robustez, também aumentamos a complexidade do sistema, o que pode, eventualmente, gerar novos problemas. Assim, qualquer tentativa de aumentar a robustez de um sistema deve ser considerada não apenas por meio de uma análise simples de custo-benefício, mas também avaliando se a complexidade adicional realmente se justifica.
Recuperação
A forma como o software se recupera após um distúrbio é essencial para que ele seja resiliente. Devemos fazer o máximo para nos proteger de eventos adversos, mas, à medida que o software aumenta em escala e complexidade, eliminar todos os problemas em potencial se torna inviável.
Podemos melhorar nossa capacidade de recuperação de incidentes adotando medidas com antecedência, por exemplo:
- ter backups disponíveis e testados;
- possuir manuais/procedimentos para situações de indisponibilidade;
- treinar a equipe para seguir esses procedimentos.
Tentar pensar em como lidar com uma indisponibilidade enquanto estiver andamento é inviável já que o estresse e o caos são inerentes à situação. O ideal é ter um plano de ação definido, conhecido e ensaiado com antecedência.
Extensibilidade com elegância
Recuperação e robustez estão relacionadas a lidar com eventos que muitas vezes podemos prever; todavia, existem situações que acontecerão e que são imprevisíveis, ou seja, seremos pegos de surpresa. Para isso, precisamos ser capazes de estender o sistema de forma elegante a fim de lidar com novos cenários. Isso depende de termos pessoas disponíveis com as habilidades, experiências e grau de autonomia apropriados para atuar quando essas situações surgirem.
Uma arquitetura que favorece acoplamento fraco, boas fronteiras de contexto e componentes bem definidos tende a ser mais fácil de estender sem gerar efeitos colaterais caóticos.
Adaptabilidade sustentável
O fato de ainda não termos sofrido uma indisponibilidade não significa que ela não possa ocorrer. Precisamos estar constantemente adaptando o que fazemos para garantir que tenhamos resiliência no futuro.
Técnicas como engenharia do caos, quando aplicadas de forma cuidadosa, são úteis para promover essa adaptabilidade sustentável, revelando fragilidades ocultas antes que gerem incidentes reais.
Além disso, ter uma cultura na qual as pessoas possam compartilhar informações sem medo de retaliações é essencial para incentivar o aprendizado após um incidente. Uma cultura orientada a aprendizado facilita a evolução contínua das práticas de resiliência.
A ideia aqui é, em grande parte, descobrir o que ainda não sabemos.
Sabemos que falhas podem acontecer. Em larga escala, a falha deixa de ser uma possibilidade e passa a ser uma certeza estatística. Com isso, podemos gastar menos tempo tentando impedir o inevitável e um pouco mais estruturando como lidar com a situação de forma elegante.
Não será possível evitar o fato de que algo pode (e vai) falhar, mas podemos incorporar esse raciocínio em tudo o que fazemos e nos planejar para as falhas. Dessa forma, conseguimos fazer avaliações de custo-benefício mais bem fundamentadas.
O nível de falhas que podemos tolerar ou a rapidez com que o sistema deve se recuperar será determinado pelos usuários e pelo contexto de negócio. Entretanto, os usuários nem sempre saberão explicar quais são exatamente os requisitos. Cabe a nós, então, partir de requisitos genéricos e refiná-los para casos de uso específicos.
Já quando pensamos em escalar sistemas para lidar melhor com carga ou falhas, alguns requisitos típicos são:
-
response time / latência:
Tempo que as operações podem demorar. Podemos fazer medições com diferentes quantidades de usuários para entender como o aumento da carga impacta a latência. Definir metas para um dado percentil (p. ex., p95) das respostas monitoradas costuma ser uma abordagem eficiente. Também queremos saber quantos usuários concorrentes o sistema deve suportar; -
disponibilidade:
Períodos e tempos de downtime aceitáveis, muitas vezes expressos em percentuais de uptime (por exemplo, 99,9% por trimestre); -
durabilidade dos dados:
Nível aceitável de perda de dados e tempo mínimo pelo qual os dados devem ser mantidos (por exemplo, RPO/RTO em cenários de desastre).
Uma parte essencial no desenvolvimento de sistemas resilientes é a capacidade de fazer com que as funcionalidades degradem com segurança. A inatividade de um serviço não deve, necessariamente, afetar a disponibilidade de outro. Para isso, precisamos entender o impacto de cada indisponibilidade e projetar como as funcionalidades irão se degradar de forma controlada.
A definição do comportamento em caso de indisponibilidade de uma funcionalidade será guiada pelo contexto de negócios. Tecnicamente saberemos o que é possível fazer, mas o negócio decidirá qual atitude tomar. Em essência, a pergunta que precisamos responder é: “O que deve acontecer se esta funcionalidade estiver inativa?”. A partir disso, saberemos qual comportamento implementar.
Ao refletirmos sobre a importância de cada funcionalidade em relação aos requisitos não funcionais (latência, disponibilidade, consistência, etc.), estaremos em melhor posição para tomar decisões de arquitetura.
Pior do que um serviço não responder é responder de forma lenta. Se um serviço está indisponível, queremos identificar isso rapidamente. Mas, se estiver apenas lento, o acúmulo de requisições em espera pode tornar todo o sistema lento, provocando contenção de recursos e, por consequência, falhas em cascata.
Existem alguns padrões que podem ser usados para reduzir o risco de efeitos em cascata, como:
- timeouts;
- bulkheads;
- circuit breaker.
Timeouts
Com timeouts, queremos definir um limite de tempo de espera para uma chamada a um serviço downstream. Devemos definir um timeout padrão para toda chamada externa ao processo e observar os tempos de resposta “saudáveis” considerados normais para cada serviço downstream. Esses valores podem orientar a definição de timeouts específicos.
Além disso, como alguns problemas em serviços downstream podem ser temporários, fazer novas tentativas (retries) para uma chamada pode fazer sentido, levando em consideração o código HTTP retornado e aplicando padrões como exponential backoff para evitar sobrecarga.
Bulkheads
O padrão bulkhead é uma forma de isolar falhas, por exemplo, separando responsabilidades entre microsserviços distintos, de modo a reduzir as chances de uma falha em uma área afetar outras. Outro ponto prático é implementar pools independentes de conexões para cada dependência downstream, evitando que um serviço problematico consuma todos os recursos de I/O.
Circuit breaker
No padrão circuit breaker, análogo aos disjuntores residenciais, queremos não apenas proteger o consumidor contra problemas em um serviço downstream, mas também proteger esse serviço de receber um volume excessivo de chamadas enquanto está em falha.
A ideia é falhar rápido em chamadas síncronas, permitindo que o serviço upstream tome alguma ação alternativa (como retornar um fallback ou uma mensagem de indisponibilidade controlada). Já em cenários de chamadas assíncronas, enfileirar requisições para tentativa posterior pode ser uma opção.
Essa abordagem de circuit breaker, quando há possibilidade de acionamento manual, também é útil como parte de uma manutenção de rotina, permitindo “desligar” temporariamente integrações sem derrubar o sistema inteiro. Falhar rápido é sempre melhor do que falhar lentamente.
De modo geral, queremos isolamento entre serviços, para que um não comprometa a execução do outro. Podemos ter cenários em que dois serviços têm seus próprios bancos de dados separados logicamente, mas hospedados na mesma infraestrutura física. Uma falha nessa infraestrutura causaria impacto em ambos.
O ideal, do ponto de vista de resiliência, é termos recursos independentes para cada instância ou domínio crítico, mas devemos considerar o custo-benefício em relação ao aumento de complexidade. O custo pode se tornar tão alto que inviabilize uma separação física completa.
Idempotência
Operações idempotentes são aquelas em que o resultado não muda após a primeira execução da operação, mesmo sendo executada várias vezes com a mesma entrada.
Isso é muito útil para repetir o envio de mensagens quando não temos certeza se foram processadas. Basicamente, adicionamos um identificador (por exemplo, um correlation_id) ao envio de todas as mensagens e, caso o serviço seja idempotente, a mesma mensagem enviada diversas vezes não causará inconsistência de estado.
Teorema CAP
O teorema CAP diz que, em um sistema distribuído, temos três propriedades que precisam ser balanceadas:
- consistência (consistency);
- disponibilidade (availability);
- tolerância à partição (partition tolerance) ou tolerância à partição de rede.
De forma simplificada:
- Consistência: todos os nós retornam a mesma resposta para a mesma requisição, após uma atualização ser confirmada;
- Disponibilidade: toda requisição válida recebe uma resposta (não necessariamente a mais atual), mesmo na presença de falhas;
- Tolerância à partição: o sistema continua operando, de alguma forma, mesmo com falhas de comunicação entre partes da rede.
Estudo de caso
Cenário: 1 load balancer + 2 instâncias em data centers distintos + 2 bancos de dados distintos com replicação.
-
Um sistema AP é aquele que continua disponível e atende às requisições, mas pode apresentar dados inconsistentes quando a replicação falha. Esses sistemas são conhecidos como eventualmente consistentes, já que esperamos que, em algum momento no futuro, todos os nós vejam os dados atualizados, mas isso não acontece ao mesmo tempo. Precisamos conviver com a possibilidade de usuários verem dados antigos.
-
Um sistema CP é aquele em que os dados permanecem consistentes, pois cada nó do banco de dados sabe se os dados que possui são iguais aos do outro nó, e atende às requisições enquanto existe conectividade. Entretanto, se os nós do banco de dados não puderem se comunicar, eles não conseguem se coordenar para garantir consistência. Nesse caso, a opção passa a ser recusar requisições. O desafio aqui é a complexidade de implementar consistência forte em ambientes distribuídos.
-
Sobre sistemas CA, se o sistema não tem tolerância à partição, ele não pode ser considerado verdadeiramente distribuído, pois pressupõe que a rede nunca falha. Na prática, sistemas CA “puros” não existem em ambientes distribuídos reais.
De modo geral, sistemas AP acabam sendo uma opção adequada em muitas situações, especialmente quando a aplicação tolera consistência eventual e prioriza alta disponibilidade.
Engenharia do caos
Engenharia do caos é a disciplina de fazer experimentos controlados em um sistema a fim de aumentar a confiança na capacidade desse sistema de suportar condições turbulentas em produção. “Sistema” aqui abrange não apenas software e hardware, mas também pessoas, processos e a cultura organizacional.
Um precursor da engenharia do caos é a prática de Game Days, que consiste em testar o nível de preparação das pessoas para determinados eventos. São cenários planejados com antecedência, mas iniciados de surpresa, com exercícios que oferecem uma chance de testar pessoas e processos diante de situações realistas, porém fictícias.
Aplicada em sua forma mais restrita, a engenharia do caos é uma atividade útil para melhorar a robustez dos sistemas, verificando até que ponto eles são capazes de lidar com problemas esperados e inesperados.
Do ponto de vista cultural, é crucial que a organização não tenha uma cultura de acusação, isto é, de buscar “culpados” por incidentes. Isso cria um ambiente de medo em que as pessoas não se sentem à vontade para relatar problemas ou admitir erros. O resultado é a perda de capacidade de aprender com os erros e o aumento da probabilidade de reincidência dos mesmos problemas.
Ter uma cultura em que as pessoas se sintam seguras para admitir falhas é essencial para criar um ambiente de aprendizado, o que contribui para sistemas mais robustos e um local de trabalho mais saudável e satisfatório. O foco deve ser sempre entender a causa raiz, e não apenas o erro humano mais visível.
Ferramentas
Algumas ferramentas que podem ser usadas em cenários de engenharia do caos e resiliência:
- Chaos Toolkit;
- Reliably;
- Gremlin.
Riscos e pontos de atenção arquiteturais
-
Complexidade excessiva em nome da robustez
Adicionar muitos mecanismos de resiliência (retries, circuit breakers, timeouts complexos, filas, replicações) sem um desenho coerente pode criar sistemas difíceis de entender e operar, aumentando o risco de falhas acidentais. -
Configuração inadequada de timeouts e retries
Timeouts muito altos e retries agressivos podem piorar incidentes, amplificando a carga sobre serviços já degradados e causando falhas em cascata. É fundamental calibrar timeouts e políticas de retry com base em medições reais. -
Isolamento incompleto entre serviços
Ter serviços com bancos independentes, mas hospedados na mesma infraestrutura (mesmo cluster, mesmo storage) pode criar pontos únicos de falha. Em domínios críticos, é importante avaliar isolamento físico ou lógico adicional. -
Uso incorreto de idempotência
Supor que uma operação é idempotente quando não é pode levar a duplicidade de lançamentos financeiros, envios de e-mail em excesso ou inconsistências sutis. É necessário projetar e testar explicitamente o comportamento idempotente. -
Escolhas de CAP desalinhadas ao negócio
Optar por um modelo AP ou CP sem considerar as necessidades de consistência do domínio (por exemplo, finanças versus conteúdo estático) pode gerar problemas graves de confiança nos dados. A decisão deve ser tomada por domínio, não de forma genérica. -
Engenharia do caos sem governança
Executar experimentos de caos em produção sem hipóteses claras, sem limites de impacto e sem plano de rollback pode agravar incidentes em vez de preveni-los. É fundamental ter critérios de sucesso, monitoramento forte e escopo bem definido. -
Ausência de métricas de resiliência
Falar em resiliência sem medir indicadores como MTTR, taxa de erros sob carga, latência em cenários de falha parcial e impacto real de indisponibilidades dificulta a evolução arquitetural baseada em dados.
Conclusão
Resiliência em sistemas distribuídos não é apenas resultado da adoção de microsserviços, mas da combinação de boas decisões arquiteturais, processos bem definidos e uma cultura organizacional voltada ao aprendizado. Conceitos como robustez, recuperação, extensibilidade elegante e adaptabilidade sustentável fornecem uma base para raciocinar sobre como o sistema reage a falhas, tanto previstas quanto inesperadas.
Ao alinhar requisitos de latência, disponibilidade e durabilidade dos dados com padrões de resiliência (timeouts, bulkheads, circuit breakers, idempotência), escolhas conscientes de CAP e práticas de engenharia do caos, conseguimos construir sistemas capazes de falhar de forma controlada, se recuperar rapidamente e continuar evoluindo. Isso aumenta a confiança dos usuários, reduz o impacto de incidentes e torna a arquitetura mais preparada para crescer em escala e complexidade ao longo do tempo.
