Todos nós sabemos o que fazer, certo? Depois de terminarmos de trabalhar em uma alteração de código e [esperançosamente] testá-la em nossa máquina local, levamos a alteração para o próximo estágio do ciclo. Os testes locais são muito tendenciosos e, idealmente, gostaríamos de validar a mudança em um ambiente mais estável (e também não seguir apenas o ponto de vista do engenheiro que implementou a mudança).
Uma próxima etapa parece muito natural aqui: enviar as alterações para um ambiente de teste confiável e ter parceiros (QAs, PMs, outros engenheiros) ajudando na validação antes de mover as alterações. Isso seria seguido pela correção de bugs e revalidação até acreditarmos que é bom o suficiente para enviar para produção. Ótimo!
Na maioria dos contextos, entretanto, isso simplesmente não acontece. Pode ser por vários motivos, mas independentemente do motivo, a consequência é que muitas vezes temos que promover alterações nos servidores de produção antes que eles sejam testados ou validados suficientemente bem.
O problema é… E se algo quebrar? Na verdade, como detectar problemas mais cedo? Boas notícias: é possível adotar algumas ferramentas e práticas para tornar o teste e a validação em produção não apenas uma prática segura para você e sua empresa, mas talvez até uma boa ideia.
Antes de passarmos aos testes em produção, temos que falar sobre métricas: precisamos delas para validar se a mudança que estamos enviando produz o efeito desejado, não causa efeitos colaterais indesejados, o produto ainda está estável, etc. métricas estabelecidas, ficamos basicamente cegos ao implementar as mudanças. Faremos referência a métricas em muitos dos tópicos do artigo, então vamos dar uma olhada em dois tipos diferentes de métricas que devemos ter em mente.
Métricas relacionadas aos negócios, como KPIs, metas e comportamento do usuário, devem ser monitoradas após a implementação das mudanças para avaliar o impacto. Antes de qualquer mudança, identifique as métricas que deverão ser afetadas. Igualmente importantes são as métricas de proteção, indicadores do que não deve mudar. Mudanças imprevistas nessas proteções podem significar problemas com a nova mudança, necessitando de uma revisão.
Uma vez definidas as métricas de negócios, também é importante compreender as métricas técnicas. Estes são fundamentais para garantir que os sistemas permaneçam saudáveis à medida que as alterações são introduzidas ao longo do tempo. Aqui estamos falando sobre estabilidade do sistema, taxa de erro, volume, restrições de capacidade da máquina, etc.
Boas métricas técnicas também são úteis para explicar problemas observados nas métricas de negócios ou para encontrar rapidamente a causa raiz das regressões. Por exemplo, digamos que observamos os usuários se envolvendo muito menos com um recurso específico após o lançamento da última versão. Um aumento nos tempos limite de solicitação ou nas taxas de erro pode mostrar rapidamente quais serviços/pontos de extremidade estão causando o problema.
Temos métricas comerciais e técnicas bem definidas, ainda bem! Agora, temos que monitorá-los. Há muitas maneiras de fazer isso, mas um primeiro passo comum é criar painéis que monitorem as métricas ao longo do tempo, facilitando a detecção de picos incomuns. Melhor ainda se o dashboard permitir uma filtragem rápida de dados com base em segmentos específicos que podem ser especialmente relevantes para o negócio. Monitorar ativamente os painéis é uma boa maneira de visualizar rapidamente os efeitos que uma nova mudança introduziu no sistema. Algumas empresas consideram o monitoramento ativo tão importante que até têm turnos de monitoramento 24 horas por dia, 7 dias por semana, para detectar e resolver problemas o mais cedo possível.
Outra boa forma de monitorar métricas é por meio de detecção e alertas automáticos. Para métricas importantes, os alertas podem fornecer notificações em tempo real quando algo parece errado. Digamos que começamos a implementar um recurso e, alguns minutos após o início do processo, recebemos um alerta informando que a taxa de erro está aumentando acima de um limite específico. Essa notificação antecipada pode nos impedir de propagar ainda mais a mudança na produção e nos salvar de muitos problemas!
Por último, é importante estar atento à quantidade de informação de que necessitamos e em que circunstâncias. Embora os painéis sejam muito úteis para fornecer uma visão visual do desempenho do produto e do sistema, adicionar 1.000 gráficos diferentes trará mais confusão do que clareza. Da mesma forma, se recebermos 1.000 alertas por dia, será impossível investigá-los e agir de acordo com eles, e eles acabarão sendo ignorados.
Métricas definidas, monitoramento implementado, ótimo! Agora vamos dar uma olhada em algumas ferramentas e estratégias que nos ajudam a evitar problemas, detectá-los mais cedo e minimizar impactos na produção. Dependendo de como o ambiente de produção está configurado, alguns deles serão mais difíceis de implementar do que outros e talvez nem façam muito sentido quando combinados. No entanto, cada item aqui pode nos ajudar a nos aproximar de um ambiente de produção seguro e estável.
Testes automatizados, muitas vezes deixados de lado quando os projetos saem do caminho, podem agilizar o desenvolvimento e tornar as alterações na produção mais seguras e rápidas. Quanto mais cedo os problemas forem detectados, mais rapidamente poderão ser corrigidos, reduzindo assim o tempo total gasto no processo. O processo de reverter mudanças, corrigi-las e empurrá-las novamente costuma ser muito estressante e pode consumir um tempo precioso.
Visar 100% de cobertura de testes com testes unitários, de integração e de ponta a ponta pode ser ideal para a maioria dos projetos. Em vez disso, priorize os testes com base no esforço versus benefício. As métricas podem orientar isso: cobrir os principais recursos do negócio é provavelmente mais crucial do que recursos de nicho de menor impacto, certo? Comece com os recursos principais, expandindo conforme o sistema evolui.
O processo de publicação para produção deve incluir a execução do conjunto de testes antes da implantação na produção. As falhas nos testes devem interromper a publicação, evitando problemas de produção. É preferível atrasar o lançamento de um recurso do que descobrir que ele está totalmente com defeito no dia seguinte.
Dogfooding é o processo de liberação de um recurso para testes internos antes de chegar aos usuários finais. Durante o dogfooding, o recurso é disponibilizado em produção, mas apenas para usuários internos (funcionários, membros da equipe etc). Dessa forma, podemos testar e validar se o novo recurso está funcionando conforme o esperado, utilizando dados reais de produção, sem impactar usuários externos.
Existem diferentes estratégias para dogfooding. Para uma visão geral simplificada, poderíamos agrupá-los em dois grupos maiores:
O lançamento canário é um processo de lançamento onde, em vez de implementar as alterações na produção para todos os servidores de uma vez, a alteração é disponibilizada para um pequeno subconjunto deles e monitorada por algum tempo. Somente depois de certificar que a alteração é estável, ela é enviada para o ambiente de produção.
Esta é uma das ferramentas mais poderosas para testar novos recursos e mudanças arriscadas, reduzindo assim as chances de quebrar algo na produção. Ao testar a mudança em um grupo de usuários, podemos interromper/reverter o processo de implementação se algum problema for detectado, evitando o impacto na maioria dos usuários.
Blue Green Deployment, uma prática DevOps, visa evitar tempos de inatividade usando dois clusters de servidores (Azul e Verde) e alternando o tráfego de produção entre eles. Durante a implementação do recurso, as alterações são publicadas em um conjunto (Verde) enquanto o outro (Azul) permanece inalterado. Caso surjam problemas, o tráfego pode ser rapidamente revertido para os servidores Blue, pois eles continuavam funcionando com a versão anterior.
A implantação azul verde é frequentemente contrastada com a versão Canary que discutimos anteriormente. Não entraremos nos detalhes desta discussão, mas é importante mencioná-lo para nos ajudar na hora de decidir quais ferramentas são mais adequadas para o nosso trabalho.
Os interruptores de interrupção não são originados no contexto da engenharia de software, e a melhor maneira de entender seu uso é olhando para trás, para a intenção e o design originais. Em máquinas utilizadas nas indústrias, os interruptores de interrupção são mecanismos de segurança que os desligam o mais rápido possível através de uma interação muito simples (geralmente um simples botão ou interruptor liga/desliga). Existem para situações de emergência, para evitar que um incidente (mau funcionamento da máquina, por exemplo) provoque outro ainda pior (ferimentos ou morte).
Na engenharia de software, os kill switches têm um propósito semelhante: aceitamos perder (ou eliminar) um recurso específico na tentativa de manter o sistema em funcionamento. A implementação é, em alto nível, uma verificação de condição (veja o trecho de código abaixo), geralmente adicionada no ponto de entrada de uma alteração ou recurso específico.
if (feature_is_enabled('feature_x')) {xNewBehaviour();} else {xOldBehaviour();}
Digamos, por exemplo, que estamos enviando uma migração para uma nova API de terceiros. Está tudo bem nos testes, estável no lançamento canário e então a mudança é 100% lançada em produção. Depois de algum tempo, a nova API começa a ter dificuldades com o volume e as solicitações começam a falhar (lembra das métricas técnicas?). Como temos um kill switch, as solicitações de API podem ser revertidas instantaneamente para a API antiga e não precisamos reverter para uma versão anterior ou enviar rapidamente um hotfix.
Tecnicamente falando, kill switches são, na verdade, um caso de uso específico de alternância de recursos (também conhecidos como sinalizadores de recurso). Já que estamos no assunto, vale a pena mencionar outro grande benefício das alternâncias de recursos: permitir o desenvolvimento baseado em tronco. Graças às alternâncias de recursos, o novo código pode ser enviado com segurança para produção, mesmo que esteja incompleto ou ainda não testado.
O código exemplificado acima provavelmente deixou alguns de nós se perguntando se esse é realmente um bom padrão, com comportamentos novos e antigos vivendo no aplicativo ao mesmo tempo. Concordo que este provavelmente não é o estado final que desejamos para nossa base de código, caso contrário, cada pedaço de código acabaria cercado por cláusulas if/else, tornando o código ilegível em pouco tempo.
No entanto, nem sempre devemos nos apressar em eliminar o comportamento antigo. Sim, é muito tentador limpar o código assim que ele deixa de ser usado e evitar débitos técnicos. Mas também não há problema em deixá-lo assim por algum tempo, alternando recursos. Às vezes, pode demorar um pouco até que o novo recurso seja estabilizado, e ter uma opção de backup é um mecanismo seguro caso precisemos voltar a ele, mesmo que por um curto período de tempo.
O ciclo de vida de cada versão é diferente e é uma boa prática acompanhar quando é um bom momento para se livrar do código antigo. Manter o código limpo e reduzir a sobrecarga de manutenção evitará a situação oposta em que, embora tenhamos o recurso desabilitado no código, ele provavelmente está quebrado, dado o tempo que passou desde que foi desabilitado.
Uma das minhas técnicas favoritas para implementar mudanças mais seguras é conhecida como teste de sombra ou modo sombra. Consiste em executar comportamentos antigos e novos para comparar os resultados, mas desabilitando alguns dos efeitos colaterais do novo comportamento, conforme aplicável. Vamos dar uma olhada neste exemplo simples:
int sum(int a, int b) {int currentResult = currentMathLib.sum(a, b);int newResult = newMathLib.sum(a, b);logDivergences(a, b, currentResult, newResult);return currentResult;}void logSumDivergences(int a, int b, int currentResult, int newResult) {if (currentResult != newResult) {logger.warn( 'Divergence detected when executing {0} + {1}: {2} != {3}',a, b, currentResult, newResult);}}
Embora ambas as operações de soma sejam executadas, a nova é usada apenas para comparar e registrar divergências. Esta técnica é particularmente útil para monitorar mudanças complexas no sistema e esperamos alguma paridade entre os comportamentos antigos e novos. Outro ótimo caso de uso é quando precisamos fazer alterações em produtos com os quais não estamos muito familiarizados ou quando não sabemos bem quais casos extremos podem ser afetados pela alteração pretendida.
Em cenários mais complexos, talvez seja necessário desativar alguns efeitos colaterais antes de ativar o teste de sombra. Por exemplo, digamos que estamos implementando uma nova API backend para cadastrar usuários e salvar no banco de dados, retornando o ID do usuário. Poderíamos até ter um shadow DB instalado para executar todo o processo, mas definitivamente não é uma boa ideia enviar o e-mail “Registro bem-sucedido” duas vezes, uma para cada API de back-end. Também no mesmo exemplo, precisaríamos de uma lógica de comparação mais profunda, pois simplesmente comparar os IDs de usuário retornados não seria muito útil.
Por último, é importante compreender o que precisa de ser monitorizado e testado e quais os critérios que serão aplicados caso a paridade não seja alcançada. Em alguns cenários críticos, teremos que repetir o teste de sombra até que os resultados sejam exatamente os mesmos. Noutros, pode ser aceitável ter alguma percentagem de divergência quando a nova implementação oferece benefícios adicionais que compensam a perda.
Mesmo com salvaguardas robustas, os sistemas podem falhar. Quando isso acontece, precisamos ser capazes de entender o que está acontecendo, com o nível adequado de detalhe, caso contrário, pode ser extremamente difícil conseguir uma solução eficiente. É aqui que os registros vêm para salvar o dia.
Embora o registro não seja um conceito novo e existam muitas soluções fáceis de implementar, garantir registros eficazes é um desafio. Muitas vezes, os logs não são claros, são excessivamente complexos, estão ausentes ou estão repletos de entradas irrelevantes, dificultando a solução de problemas. No entanto, os logs não servem apenas para resolver problemas. O registro adequado ajuda a verificar a eficácia de novos recursos e alterações. Ao amostrar as entradas de log, é possível rastrear as jornadas dos usuários e confirmar o funcionamento dos sistemas conforme pretendido.
Enviar código para produção às vezes é perigoso, mas temos muitas estratégias para tornar o processo muito mais seguro. Mesmo que identifiquemos um problema, também é importante saber o que é aceitável ou não. Nem todas as falhas resultam em uma reversão. E se estivermos tentando corrigir uma falha grave de segurança ou cumprir uma nova regulamentação? Ter critérios claros e compreender o quão crítica é a mudança é muito importante para determinar quando abortar ou prosseguir em caso de problemas. Voltando ao início, as principais métricas estão aí para nos ajudar no processo de decisão.
Pouso seguro, pessoal!