DEV Community

Luan Andryl
Luan Andryl

Posted on

Quando a arquitetura fala mais alto que o código

Crônica de uma madrugada em que a beleza do design não salvou o sistema

Estava eu vivendo minha vida tranquilamente até que um alerta me tirou da inércia: “Failover automático disparado: primário caiu, standby promovido.”

O sistema era herdado, cheio de camadas históricas, e minha primeira reação foi quase otimista. “Perfeito, o Multi-AZ está funcionando. Tudo volta sozinho.”

Mas, alguns minutos depois, o otimismo evaporou. As exceções começaram a pipocar no monitoramento, os consumidores de Kafka acumularam lag e a fila de processamento travou por completo.

Abri os logs e lá estava a enxurrada: read_only_sql_transaction.

Quem já viu esse erro sabe o que ele significa — a aplicação está tentando escrever em uma réplica de leitura. O failover aconteceu, o banco se recuperou, mas o sistema continuou falando com o nó antigo, acreditando que ele ainda era o primário.

Reiniciei os pods e, como num passe de mágica, tudo voltou ao normal.

Kafka fluindo, jobs executando, alertas desaparecendo.

Mas o alívio durou pouco. Assim que o sistema estabilizou, veio a pergunta inevitável: e as mensagens perdidas durante o downtime?

A princípio, confiávamos que havia um mecanismo de resiliência cuidando disso. E havia — mas, para minha surpresa, ele estava apoiado no próprio banco de dados.

Foi nesse instante que percebi o real problema. O incidente não expôs apenas o failover, mas a fragilidade arquitetural de um processo essencial diante de falhas de infraestrutura.

Em um cenário ideal, talvez usar o banco como fallback para questões de negócio até funcione.

Mas acreditar que “nunca vai haver problema de rede ou infraestrutura” é o tipo de premissa que destrói sistemas em produção.

A verdade é que rede não é confiável e mensageria deve ser tratada com mensageria.

O que nos faltava era um Dead Letter Topic (DLT) um canal seguro para mensagens que não puderam ser processadas.

Sem isso, cada falha virava esquecimento, e a arquitetura, que deveria proteger o sistema, acabou se tornando parte do problema.

Aquela madrugada deixou claro que os incidentes mais caros não nascem de uma grande falha, mas de uma sequência de pequenas negligências: um DNS cacheado, um pool de conexões teimoso, um DLT inexistente e a falta de idempotência.


O desastre invisível: quando o DNS dita o destino

Quando o primário de um banco cai, o RDS faz o que promete:

promove a réplica standby e atualiza o endpoint DNS. Tecnicamente, o sistema “voltou”. Mas, na prática, a aplicação não sabe disso.

O DNS — aquele detalhe que raramente recebe atenção — é o elo mais frágil entre o banco e o cliente. E sim, vale lembrar: sua aplicação fala com o banco pela rede. Não há mágica aqui.

Se o IP resolvido pelo sistema operacional está cacheado, ou se o pool mantém conexões antigas, sua aplicação vai continuar escrevendo no lugar errado. É como mudar de casa, mas continuar indo ao endereço antigo. O nome (endpoint) está certo, mas o caminho ficou preso na memória.

O RDS troca o IP em segundos, mas o sistema pode levar minutos, às vezes horas, para entender que o mundo mudou. Enquanto isso, o Kafka empilha mensagens, o banco rejeita gravações e o time corre atrás de fantasmas.


O cenário Elixir/Ecto: o falso conforto do “restart resolve”

Quem trabalha com Elixir conhece bem o padrão: Ecto e Postgrex mantêm conexões persistentes no pool. Quando o failover ocorre e o primário vira réplica, essas conexões continuam vivas, apontando para o nó errado. Resultado: read_only_sql_transaction.

A correção existe e é elegante ... basta configurar :disconnect_on_error_codes no Repo, incluindo :read_only_sql_transaction. Assim, quando o erro ocorre, o pool descarta a conexão e força uma reconexão limpa.

Mas isso só resolve metade do problema. Se o cache DNS do sistema ainda aponta para o IP antigo, o Ecto vai reconectar… no mesmo lugar errado. Em ambientes Kubernetes, esse cache pode durar o TTL configurado ... ou eternamente, dependendo da base da imagem.

Reiniciar o pod resolve, sim, mas é uma solução de sorte. É como consertar um vazamento fechando o registro geral ... funciona, mas não é engenharia, é improviso.


O mesmo problema, outro runtime: a JVM e o DNS eterno

Nos últimos meses venho explorando mais a JVM, e ao encontrar o mesmo comportamento em produção, fui investigar como ela lida com esse tipo de falha.

E a descoberta é, no mínimo, curiosa: por padrão, a JVM cacheia resoluções DNS para sempre.

O parâmetro networkaddress.cache.ttl vem indefinido, e o comportamento padrão é infinito. Uma vez resolvido o endpoint, ele nunca mais é atualizado ... mesmo que o RDS já tenha mudado de IP várias vezes.

Por isso, aplicações Spring Boot que dependem de failover precisam definir explicitamente -Dsun.net.inetaddr.ttl=60 (ou menos)

e garantir que o pool (geralmente HikariCP) valide conexões antes de reutilizá-las. Além disso, é fundamental tratar o erro SQLState 25006 como gatilho para encerrar e recriar conexões.

É o equivalente JVM do disconnect_on_error_codes do Ecto.

Foi aí que ficou claro: o problema não era o banco.

Era a confiança cega na infraestrutura sem entender o comportamento do runtime que a consome.


O elo perdido: quando o DLT não existe

O que me tirou o sono naquela madrugada não foi o failover em si, mas o que veio depois: as mensagens que não puderam ser gravadas simplesmente sumiram.

Sem um Dead Letter Topic (DLT), não havia para onde elas irem.

O processo de reprocessamento dependia do Oban que, ironicamente, também persistia no mesmo banco afetado. Quando o banco cai, a fila de recuperação cai junto.

E o Kafka? Avançou o offset. Pronto. Dados no limbo.

O DLT é a fronteira entre um incidente e uma tragédia.

Ele é o arquivo de quarentena das mensagens que falharam, mas ainda têm valor. Sem ele, o sistema não apenas perde dados .. perde memória.

O DLT não é um detalhe técnico; é uma escolha ética. É o “não esqueci o que deu errado”.


Idempotência: o antídoto contra o caos

DLT sozinho não basta. Ele te avisa o que falhou, mas não repara o dano. É aí que entra a idempotência.

Durante a recuperação, reprocessar as mensagens foi um exercício de paciência: algumas já haviam sido aplicadas parcialmente, outras não.

Sem uma chave única, não havia forma segura de distinguir uma duplicata de uma nova execução.

Esse é o tipo mais caro de incidente evitável ... horas de trabalho manual, avaliando cada registro e decidindo o que reaplicar.

Essa experiência reforçou algo que considero inegociável:
idempotência é um princípio de arquitetura, não uma conveniência de código.

Em Elixir, isso significa projetar cada operação para ser reconhecida de forma única ... por uma chave natural ou por estados determinísticos no banco.

Em Kotlin, passa por deduplicação lógica, seja via banco, Redis ou hashing, garantindo que um mesmo evento não produza efeitos colaterais duas vezes.

Sem idempotência, o DLT deixa de ser ferramenta de resiliência e vira apenas um cemitério de dúvidas.


Para fechar: arquitetura é o que te salva às três da manhã

Depois daquela madrugada, algo ficou cristalino: de nada adianta o design do seu código ser perfeito se a arquitetura não funciona.

É preferível um módulo feio que cumpre seu papel do que uma obra-prima de desacoplamento que não entrega o básico: disponibilidade e integridade.

O primeiro caso se resolve com uma PR de refactor. O segundo consome uma sprint inteira e, às vezes, a confiança da equipe.

Problemas de arquitetura não são detalhes são o que mais consome tempo, esforço e dinheiro. E, paradoxalmente, são também o que mais pode salvar um sistema quando tudo dá errado.

Naquela noite, percebi que o verdadeiro inimigo não era o RDS, nem o Kafka. Era a ilusão de que alta disponibilidade é algo que “se configura”.

O failover promove, o DNS muda, mas se sua aplicação não entende o que aconteceu: se o pool não reconecta, se o DNS não se renova, se o DLT não existe ... Então você não tem resiliência, tem sorte.

Hoje vejo o failover não como um evento de infraestrutura, mas como um teste de maturidade arquitetural.

O hardware faz seu papel mas a aplicação é quem precisa entender o que mudou.

E isso exige fundamentos: *TCP, DNS, cache, mensageria, idempotência. *

Resiliência não é reiniciar o pod e torcer pra voltar. É projetar pra que, quando o chão se mover, o sistema saiba dançar sozinho.

Top comments (4)

Collapse
 
eduardoklosowski profile image
Eduardo Klosowski

Além de pensar e projetar como o sistema vai funcionar normalmente, é sempre bom lembrar também de outras questões: Como tratar quando uma decisão automática não funcionou como o esperado? Se um processamento deveria ter resultado X, mas a resposta foi Y, o sistema permite sobrescrever o resultado do processamento de forma manual? É possível corrigir o registro e mandar processar novamente? Quando se projeta a operação do sistema de quando as coisas não saem como o esperado evita muitas surpresas.

Collapse
 
romuloscampini profile image
Romulo Scampini

Exato Luan e Edu. De fato, reiniciar o pod as vezes resolve, mas é uma solução de sorte. Isso só mostra que, além da resiliência técnica, precisamos investir nessa "lacuna" que pode ser gerada, ou seja: monitoramento que detecta não só que algo caiu, mas que algo ficou "em estado degradado".
Parabéns pelo artigo Luan. Esse tipo de conteúdo prova a importância do conhecimento e da arquitetura, e que com isso, não precisa contar com a "sorte" somente, rs.
👏

Collapse
 
gissandrogama profile image
Gissandro Gama

Excelente postagem!

Isso levanta uma dica crucial sobre a importância de testar nossa arquitetura para validar se a abordagem precisa ser reestruturada. Uma prática essencial para isso nos times de engenharia é o Chaos Engineering (Testes de Falha).

Precisamos simular ativamente o failover do banco em staging (ou produção controlada) e verificar na prática se o DNS atualiza, se o DLT captura as mensagens e se os Circuit Breakers atuam como esperado.

A sua conclusão é perfeita: arquitetura é projetar ativamente para o fracasso, não apenas para o sucesso.

Collapse
 
andryl_ profile image
Luan Andryl

arquitetura é projetar ativamente para o fracasso

Exatamente!!!