Leo

Sobre tecnologia...

Histórias de terror de um engenheiro de software | Episodio 1: O upgrade do legado.

1 day ago · 6 MIN READ
#PHP  #Laravel  #MongoDB  #AWS  #CloudComputing 

Introdução

Este post é baseado em um post mortem de um incidente real de tecnologia, documentado após a resolução completa do problema.
Removemos completamente quaisquer referências a pessoas reais e a empresas reais. O objetivo é focar no aprendizado e entreter com uma bela história.

Detalhamento do incidente

O incidente começou a ser identificado a partir de um aumento expressivo de exceções relacionadas a conexões com o banco de dados MongoDB.
Essas falhas apresentavam um comportamento intermitente: não derrubavam completamente o sistema, mas ocasionalmente resultavam em erros HTTP 500. Na maioria das vezes, ao atualizar a página, a aplicação voltava a funcionar normalmente.

Por esse motivo, o impacto foi classificado como baixo, já que:

  • Não houve indisponibilidade total da plataforma
  • Muitos usuários sequer perceberam o problema
  • O erro não impedia o uso contínuo da aplicação

Apesar da baixa severidade, existia uma preocupação relevante: o risco de dessincronização entre bancos de dados.
O dano de uma falhas intermitentes desse tipo tende a se agravar com o tempo, aumentando a probabilidade de inconsistências de dados. Por isso, mesmo com impacto reduzido, a resolução era urgente.


Tratamento do incidente

Começamos a investigar as centenas de exceções de timeout que aconteciam por toda a plataforma e tivemos muita dificuldade de chegar a alguma conclusão assertiva do motivo desses timeouts, fizemos diversos testes baseados nas pesquisas e até mesmo algumas coisas promissoras que foram apontadas no "Deep Search" do ChatGPT mas nada parecia ser o problema.

Sem saber exatamente o que fazer, seguimos uma abordagem de "tentativa e erro" com o objetivo de identificar a natureza do problema:

  1. Rollback de deploys recentes
    Os deploys mais recentes foram revertidos para verificar se o problema havia sido introduzido por código novo.
    O problema persistiu, descartando a possibilidade do problema ter sido introduzido pela aplicação.

  2. Revisão de permissões (ACLs)
    As regras de acesso do cluster foram removidas temporariamente para validar possíveis falhas de permissionamento.
    O erro continuou ocorrendo, eliminando a possibilidade de uma configuração recente ter causado o problema.

  3. Criação de uma infraestrutura mínima
    Uma infraestrutura nova e simplificada foi criada para isolar problemas relacionados a servidores.
    Mesmo assim, o comportamento intermitente permaneceu, descartando a possibilidade do problema estar relacionado a algum elemento de infraestrutura como load balancer ou NAT gateways. O problema persistiu em instâncias EC2 e no ECS com Fargate eliminando também a possibilidade de alguma atualização a nível de sistema operacional ter causado o problema.

Com essas possibilidades descartadas, restou a hipótese mais crítica:
A imagem Docker que executa a aplicação era baseada em uma stack extremamente antiga, utilizando PHP 7.2 com uma versão bem antiga da extensão do MongoDB também. Como em produção utilizávamos o MongoDB Atlas, a melhor aposta era que alguma alteração em algum aspecto do cluster pode ter causado algum tipo de inconsistência com uma versão muito antiga da extensão do MongoDB.

Essa hipótese foi deixada por último porque versões mais novas dessa extensão são compatíveis apenas com versões mais novas do PHP e versões mais novas do PHP são compatíveis apenas com versões mais novas do framework Laravel. Então seria necessário fazer um upgrade em massa para chegar ao objetivo final e isso tudo é só uma aposta.

Apesar de ser um tiro no escuro, essa era a única camada de toda a aplicação que ainda não tinha sido excluída nos testes, então eu estava confiante de que isso era um bom tiro no escuro e decidimos embarcar nessa odisseia.

A análise confirmou que:

  • A versão atual da extensão do MongoDB só era compatível com PHP 8.1
  • A versão mínima do framework compatível com PHP 8.1 era Laravel 6.0

Isso tornou inevitável o upgrade:

  • PHP 7.2 → PHP 8.1
  • Laravel 5.4 → Laravel 6.0

Durante o processo, surgiram diversos desafios:

Dependências abandonadas e sem suporte

Ao tentar fazer o upgrade para versões novas da linguagem e do framework, esbarramos com alguns pacotes PHP antigos que não são compatíveis com as versões novas, logo não é possível instalar essas dependências via "composer" e para solucionar isso seguimos a seguinte abordagem.

  1. Versionamos o pacote juntamente com a aplicação.
  2. Fizemos o autoload manual no composer.json da aplicação "imitando" o autoload que estava no composer.json do pacote.
  3. Adequamos o pacote à versão nova do PHP e Laravel para funcionar corretamente.

No início fiquei meio receoso com essa abordagem mas logo na primeira tentativa funcionou de maneira impressionante e não foi necessário adequar tanto assim o código para que ele se tornasse compatível com as novas versões do PHP.

Dead Code

Isso foi uma das coisas que nos confundiram demais, porque de repente estavámos investindo tempo em atualizar um código que nem era mais utilizado, no fim tudo que era necessário ajustar precisava passar por uma verificação para ver se realmente era utilizado, e como você pode imaginar não havia uma documentação. Por mais que isso pareça uma tarefa ingrata, tínhamos um membro do time que sabia exatamente os códigos que não estavam sendo utilizados então isso foi até uma oportunidade de limpar o projeto.

Ausência de testes automatizados

Bom aqui não foi possível escapar, tivemos que testar cada rota do serviço legado para verificar se tudo estava funcionando adequadamente, de fato fizemos uma espécie de "Blue Green" com a versão antiga sendo executada ao lado da nova, para ser possível fazer um "de para" do comportamento da aplicação.

A aplicação foi validada rota por rota, corrigindo exceções conforme surgiam, e como essa etapa durou tempo(alguns dias na realidade), até que todo o sistema estivesse funcional nas novas versões. Algumas dependências específicas exigiram esforço adicional para funcionar corretamente em imagens modernas, como o caso do whkhtmltopdf que é utilizado por alguns fluxos de geração de PDFs e deu um pouco de trabalho acertar todas as dependências para ele funcionar na imagem base Alpine que a imagem oficial do PHP que utilizamos foi baseada.

O que foi possível notar após o upgrade?

Após o upgrade, não houve aumento significativo no consumo de recursos. Isso abriu espaço para otimizações que antes não eram possíveis, como:

  • Ajuste fino da quantidade de processos do PHP-FPM
  • Ativação de Opcache
  • Uso de conexões persistentes para Redis e cache

O resultado foi expressivo: um serviço que antes rodava em 5 instâncias passou a rodar em apenas 1, mantendo estabilidade e performance.

Nas semanas seguintes, acabamos migrando para uma estrutura serverless com ECS + Fargate, em um container com bem menos recursos do que a instância, mas mantivemos 2 containers rodando por redundância e para comportar melhor a carga.

Todo o processo levou cerca de uma semana, com ciclos intensos de trabalho e uma mudança colossal no código. Geralmente esse tipo de upgrade assusta porque as semanas seguintes se tornam um inferno com uma pancada de detalhes que não foram levados em consideração se voltando contra nós.

Pois é mas nada disso aconteceu, acho que estavamos com tanto medo do que poderia acontecer que criamos um processo de migração muito seguro e no final tivemos apenas alguns problemas com a biblioteca whkhtmltopdf que acabou quebrando alguns PDFs mas logo foi possível ajustar a versão correta da biblioteca. Para ser justo passaram algumas exceções também que logo foram identificadas no Sentry e conseguimos ajustar rapidamente.

Como podemos avaliar a atuação do time?

Sinceramente aqui existe uma GRANDE autocrítica. Se realmente era necessário um upgrade massivo para resolver uma exceção? Esse problema parecia ser relacionado a networking, não duvido de que tenha alguma configuração que era possível apenas "ligar" e resolver o problema, no entanto realmente pesquisamos muito e testamos muitas coisas e não chegamos a uma solução fácil, não tinhamos uma pessoa especialista em networking para ajudar também então essa nos pareceu a melhor opção visto que já estávamos visando rever essa aplicação e como mostrado na seção anterior, conseguimos chegar nesse resultado. No final acredito que nos faltou assertividade mas excedemos as expectativas nos resultados.

Lições aprendidas

Alguns aprendizados ficaram muito claros após o encerramento do incidente:

  1. Sistemas legados têm custo oculto
    Ignorar um projeto legado não significa estabilidade eterna. Em algum momento a conta chega e chega inflacionada!!!

  2. Dead code é um problema real
    Código não utilizado aumenta a complexidade, confunde quem mantém o sistema e cria um terreno fértil para bugs e falhas inesperadas.

  3. Exceções ignoradas viram incidentes
    Alertas intermitentes tendem a ser negligenciados, mas quase sempre são sintomas de problemas estruturais mais profundos.


Planos de ação

A partir desse incidente, alguns planos claros foram definidos:

  1. Estratégia de descontinuação para sistemas legados
    Projetos antigos precisam de um plano claro de evolução ou desligamento, com revisões periódicas. Traçamos um plano com o time de produto para que todas as funcionalidades do legado sejam reconstruídas nos novos sistemas.

  2. Limpeza contínua de dead code
    Estabelecer processos recorrentes para remover código não utilizado, reduzindo complexidade e riscos.


···

Leonardo Lemos


comments powered by Disqus


Proudly powered by Canvas · Sign In