“Conteúdo originalmente criado por Manuel Pais, a partir da apresentação “Testing in Production - Quality Software Faster” de Michael Bryzek, e traduzido por Mayra Michels.”
Os testes em produção nos fornecem uma ótima ferramenta para acelerar o desenvolvimento e a produção de software com qualidade superior e maior agilidade.
Pense no último recurso no qual trabalhou e implantou em produção. Passou por todo o processo de desenvolvimento e revisão de código. Após rever todo o código, se sentiu muito bem e foi para casa. Esse recurso está funcionando em produção agora? Como você sabe?
Talvez sua caixa de entrada não esteja recebendo spam com alertas do Nagios ou Graphite. Mas isso é suficiente? Como ter certeza absoluta de que o recurso está realmente funcionando, não apenas durante a implantação e verificação, mas uma semana depois, um mês depois, um ano depois? Se souber disso, poderá reduzir um pouco da ansiedade e concentrar a energia no que está por vir.
O maior medo é ter como resultado, as reclamações vindas pelo serviço ao cliente. O telefone toca e percebemos que alguma coisa não está executando como deveria. Não é uma sensação boa. Por fim, testar em produção é remover essa ansiedade e saber que o software que construímos está funcionando.
A qualidade do software é difícil
Alguns anos atrás, foi publicado um incrível artigo sobre as pessoas que escrevem software para os ônibus espaciais. Trabalharam entre 5 e 9 pessoas nos projetos. Eram consistentes. Quando um bug era encontrado, era corrigido e posteriormente examinavam o motivo dele ter acontecido. Com isso, outros lugares da base de código era vistoriada em busca de condições para que um bug semelhante pudesse ocorrer, então todos eram corrigidos.
Quando estão em preparação para uma nova versão do software, dados são usados para avaliar a qualidade do software. Se cada nova versão possuir 120 bugs em média, e só foram encontrados 50 até determinado momento na versão atual, não a liberarão até encontrarem os demais bugs desconhecidos.
Produzem alguns dos softwares da mais alta qualidade do mundo, mas isso têm um custo. Este é o software mais caro já produzido no mundo.
Alguns anos atrás, a salesforce.com fez um grande investimento para testar seu software em nuvem e escalar todos os testes de integração entre suas APIs. Eles fizeram aproximadamente 50.000 testes e, se fossem executados em série, seriam executados por uma década ou mais. Foi feito um investimento maciço em ferramentas e tecnologia de escala.
No Facebook, milhares de engenheiros de software trabalham para detectar alterações no processo automático de revisão de código, antes de se tornarem problemas para os clientes. Tudo isso é apenas um sinal de que é difícil garantir a qualidade do software.
Fornecendo software de qualidade
Quando pensamos na qualidade do software, tendemos a pensar no processo de desenvolvimento até o momento em que disponibilizamos o código em produção. Mas, se realmente pensarmos no ciclo de vida do codigo, todo o software está dentro da qualidade, desde a concepção de um recurso ou ideia, até o software ser desativado. Queremos que o software mantenha uma alta qualidade durante todo o ciclo de vida.
A produção é uma parte realmente importante de nossas vidas como desenvolvedores e engenheiros. Testar em produção traz certo preconceito à tona, mas é uma técnica incrivelmente poderosa para nos ajudar a criar software de qualidade. Isso nos permite provar que nosso software funciona e, curiosamente, nos leva a testar o software de diversas maneiras diferentes.
Na Flow, a verdadeira entrega contínua é praticada. Isso significa que, quando é realizado um merge através do pull request, esse código irá para produção - não há nada mais no caminho. A integração contínua ocorre antes do commit do merge. É uma distinção importante, porque queriam garantir que o processo usado para promover o código para produção fosse o mesmo usado nos momentos em que não há estresse ou nos momentos urgentes em que um bug precisa ser corrigido rapidamente. O pipeline é exatamente o mesmo.
Não temos ambientes de homologação na Flow. Não temos ambientes de desenvolvimento. Não temos ambientes de controle de qualidade. Não temos ambientes de pré-produção.
Agora, qual é o desejo de um desenvolvedor? Ele deseja que seu código esteja em produção, sem causar problemas. Se não estiver confortável ao pressionar o botão de deploy, escreva alguns testes. Se, ainda assim, estiver desconfortável, escreva mais alguns testes. Eventualmente, escreverá testes suficientes para sentir que está seguro quanto ao código e então o enviará para produção. Agora, podemos falar sobre qual parte desses testes é seguro para continuar sendo executado em produção e como podemos fazer isso.
A verdadeira entrega contínua
Não é possível entender todos os benefícios da entrega contínua até praticá-la de verdade. Quando realizamos o merge do código, ele é enviado para produção, sempre com segurança e previsibilidade, essa é uma maneira fantástica de operar. Quando suas implantações são realmente simples e confiáveis, a entrega contínua abre novas maneiras de produzir código.
Se queremos que nossos grupos de engenharia realmente invistam na criação de bons testes automatizados, precisamos remover algumas proteções. Se a única maneira de sabermos que nosso software funcionará em produção é com um bom teste automatizado, estaremos motivados a escrevê-lo.
Além disso, quando a liberação do software é simples e previsível, podemos realizar pequenas releases o tempo todo. Isso também significa que podemos liberar alteração de configuração exatamente da mesma maneira que o código e com a mesma visibilidade. Os sistemas de monitoramento que sinalizam implantação em produção farão o mesmo para a configuração. Ao verificar o que aconteceu em uma implantação, veremos que um estado de configuração foi alterado.
Na Flow, escrevemos uma pequena ferramenta de entrega contínua e código aberto, chamada Delta. A Delta enxerga os webhooks do GitHub, criando imagens do Docker no Travis e dimensiona os clusters na AWS ECS além de inspecionar a saúde quando necessário. Quando a ferramenta detecta uma alteração na master, ela cria uma nova tag e implanta com segurança a alteração em produção. É uma ferramenta extremamente leve. O ponto chave aqui é que ter um processo de entrega contínua antigamente, era algo bem difícil e não ferramental.
Sem ambientes de homologação
"Adoro o meu ambiente homologação", dito por ninguém, nunca.
Os ambientes de homologação parecem adequados para implantar mudanças e inspecioná-las visualmente, ou para que outra equipe execute cenários de teste. Mas, à medida que avançamos nas arquiteturas de microservices, algumas coisas começam a acontecer. Primeiro, o ambiente de homologação começa a mudar durante o teste. Não podemos serializar alterações para verificação; caso contrário, não poderemos alterar muito em um dia. Esses gargalos são ineficientes e nos atrasam.
Em segundo lugar, é muito difícil tratar um ambiente de homologação como um ambiente de produção. Se os testes dependem de um serviço fora de nosso controle, ele poderá ser bloqueado por diversos motivos: o serviço pode não estar disponível, não responder, estar configurado incorretamente, estar faltando dados corretos ou pode estar com falta de memória. Mesmo que haja alertas para essas situações, eles provavelmente não serão tratados pela equipe responsável da mesma maneira que um problema de produção. Portanto, a homologação torna-se inconfiável e não possuímos meios para corrigi-la.
Muitas empresas começam com apenas um ambiente de homologação e, quando mudam para uma arquitetura distribuída, ramificam-se e começam a criar um ambiente de homologação para cada equipe ou serviço (por exemplo, o ambiente de homologação da equipe de pagamentos, o ambiente de homologação da equipe de checkout, o ambiente de teste para login e assim por diante). O que acaba acontecendo é que começam a ter diversas falhas - às vezes funciona, às vezes não.
As equipes normalmente estão sob pressão para liberar suas features. Então, começam a ocupar o ambiente de homologação até que seja consistente o suficiente para que possam testar o recurso que precisam liberar. Isso garante que o recurso funcione na produção. Mas realmente não sabem até que ele seja no ambiente quente.
Quando ocorre um problema, os ambientes de homologação são frágeis e difíceis de serem depurados. É necessário gastar um esforço considerável para avaliar as falhas em um ambiente de homologação. Por exemplo, uma vez escrevi um pequeno script em Ruby para encontrar todos os arquivos de log no sistema. A cada três segundos, é exibido os cinco logs mais alterados, provavelmente devido a erros.
As empresas nas quais vi ambientes de homologação funcionando, investem mais da metade do orçamento neles. Vendo esse número, a maioria das empresas tem o que chamo de ambientes de teste disfuncionais que exigem grandes esforços em toda a empresa para fazê-lo funcionar.
Finalmente, os ambientes de teste criam incentivos errados pedindo às pessoas que repitam as coisas várias vezes, na maioria dos casos sem automação. Se puder liberar e inspecionar um ambiente de homologação, o incentivo para criar uma automação diminui, porque é difícil escrever bons testes automatizados que sejam repetíveis.
Aprenda a confiar nos testes
Se remover a homologação, e ainda desejar que seja liberado um código de boa qualidade em produção, então a questão realmente se resume aos testes. Caso não se sinta confortável com o funcionamento do código, é só escrever mais testes.
Com um alto nível de confiança nos testes, é possível aplicar práticas como "dia das dependências". Na Flow, decidimos que todo código dependerá de versões mais recentes de tudo, como: nosso próprio código, nossas próprias bibliotecas, as bibliotecas de código aberto que usamos, AWS, etc. Durante o dia das dependências, encontramos as últimas versões de todas as dependências que usamos e enviamos pull requests a todos os serviços para atualização. Se houver sucesso no build resultante, realizamos o merge e implantamos essa alteração em produção, porque conquistamos confiança suficiente em nossos testes.
Agora, não precisamos nos preocupar com incompatibilidades ou atualizações de versão nos serviços que executavam versões antigas por semanas, meses ou até anos.
Qualidade através da arquitetura
Existem diversos elementos possíveis de obter qualidade através da arquitetura. Um deles é a ideia de isolamento extremo. Se o serviço não estiver funcionando porque algo o interrompeu, a pergunta a ser feita é:
"Como executar um serviço confiável se variáveis fora do meu controle podem interromper o serviço?"
Uma abordagem é o isolamento extremo do serviço. Por exemplo, cada serviço na Flow têm seu próprio DNS, balanceador de carga e banco de dados privado. Nenhum outro serviço tem permissão para se conectar ao banco de dados e não há estado ou console compartilhado. Isso interrompe falhas em cascata e evita que serviços externos causem um problema em outro serviço.
Os engenheiros podem considerar isso caro, mas o custo de entender a falha é muito maior que o custo adicional da execução de balanceadores de carga ou infraestrutura extras. Os benefícios de recuperar a confiança nos serviços rapidamente e gerar qualidade são muito maiores que o custo.
Essa abordagem significa que a rede de microservices na Flow é muito silenciosa. Os serviços não conversam diretamente entre si - mas consomem dados por meio de fluxos de eventos sofisticados que contêm mensagens relevantes publicadas por outros serviços. Existem apenas alguns casos de uso que realmente exigem que os serviços sejam síncronos. Por exemplo, ao enviar um pedido, desejamos a garantia de que recebemos o pagamento do cliente. Mas essas são as minorias, e tudo o mais é assíncrono.
Com o streaming de eventos, se um serviço do qual dependemos tem um problema, o efeito em serviço é um pequeno atraso no recebimento dos dados pelos fluxos de eventos, e isso é muito mais fácil de gerenciar do que as falhas em cascata.
Exemplo: Saiba que o checkout funciona
Vejamos alguns exemplos reais de testes bem-sucedidos em produção.
Na Gilt, uma empresa de e-commerce, os desenvolvedores desejavam garantir que a funcionalidade para enviar um pedido funcionasse o tempo todo. A abordagem comum é testar em homologação, implantar em produção e talvez configurar um alerta que dispara se nenhum pedido for realizado nos últimos 5 ou 10 minutos.
E se repensássemos isso enviando um pedido (usando um bot) a cada minuto ou a cada cinco minutos? O que teria que acontecer para que isso se tornasse uma realidade?
Isso significaria encontrar uma maneira de identificar um usuário de teste em produção. Também é possível verificar se o registro e o login estão funcionando quando criamos esse novo usuário de teste. Foi acordado sobre como identificar os usuários de teste por um nome de domínio (por exemplo, gilttest.com) e qualquer pedido neste domínio será cancelado. Isso significa apenas uma alteração de três linhas no processamento de pedidos:
if user.emaildomain=gilttest.com
order.cancel
end
Isso nos leva ao próximo requisito. Os pedidos cancelados ainda estão afetando o estoque por um curto período de tempo e podem impedir que usuários reais comprem os itens dos pedidos de teste. Para resolver isso, consultamos o inventário quanto a itens com, digamos, mais de 1.000 unidades disponíveis e usamo-os em nossos testes de produção.
Agora, é possível escrever um alerta que dispara quando não há pedidos do usuário de teste nos últimos três minutos, por exemplo. É um alerta de alta fidelidade indicando a existência de um problema em produção. Poderemos investigar e corrigir o problema rapidamente, de preferência antes que qualquer cliente perceba. Nesse processo, aprendemos com o problema e melhoramos o processo.
O teste de carga em produção também é muito poderoso. Na Gilt, o modelo de negócios era todos os dias às 9h (fuso do Pacífico), colocar a venda uma nova seleção de estoques a preços excelentes e tudo se esgotar. Foi observado um aumento de 50x no tráfego entre as 8:59 e às 9h. Nos primeiros dias da Gilt, 80% da receita ocorreu entre as 9h e às 10h. O custo de um erro durante esse período seria extremamente alto.
Mas, e se pudéssemos realizar um teste de carga à 1h (fuso do Pacífico) quando o tráfego estiver super silencioso? Se identificarmos um problema à 1h, ainda temos oito horas para corrigi-lo antes da hora do rush. Parece assustador no começo e provavelmente as coisas irão falhar em produção (mesmo que passem pela homologação). Mas com o ciclo de feedback da execução dos testes de carga em produção foi possível corrigir os problemas quando ocorrerem, com isso aumentamos acentuadamente a qualidade geral do sistema. Agora, as falhas são raras e, quando acontecem, ainda temos uma boa chance de proteger a receita dos negócios.
Exemplo: Testes de Integração de Ponta a Ponta
Temos um teste de integração que é executado uma vez por dia através da feature cron do TravisCI. O teste cria um pedido, faz o pagamento usando um token de cartão de crédito e captura os recursos enviados na autorização desse cartão de crédito.
Investimos tempo (cerca de três semanas para encontrar e corrigir diferentes tipos de problemas durante a execução de produção) para garantir que esse teste seja confiável (permaneceu sem falhas por um ano inteiro após as primeiras semanas). Agora, se esse teste falhar, sabemos imediatamente que há um problema real.
Sabemos todos os dias se é possível aceitar pedidos e processar pagamentos. Isso é extremamente poderoso. Sabemos que, quando implantamos alterações em nosso sistema de pagamento, podemos acionar esse teste para garantir que ele funcione com as alterações no sistema de pagamento. Sabemos que, quando alteramos as rotas da API, podemos executar este teste e garantir que o coração do sistema continue funcionando. Em outras palavras, sabemos que os processamentos de pagamento estão funcionando como esperado.
Exemplo: Verificando se o servidor proxy funciona como esperado
Na Flow, criamos nosso próprio proxy reverso para permitir que todas as APIs de microservice tenham tratamento de autenticação comum. Essa é uma parte essencial da infraestrutura para nós. Se houver um erro no proxy da API, todas as APIs ficarão indisponíveis - é uma falha em cascata - uma emergência para nós.
Anteriormente, foi mencionado um isolamento extremo nos serviços. Nesse caso, o proxy da API é o único serviço sem isolamento. Esse proxy da API tem seus próprios testes, mas como garantir que uma alteração na configuração ou um novo recurso continue funcionando?
O que fazemos é executar testes com a configuração de perfil da AWS em nossa conta flowvault
em nosso ambiente PCI:
$ eval $(AWS_ PROFILE=flowvault dev sbt --env production)
O dev
é um script que lida com tudo para os desenvolvedores, portanto, o comando acima recupera efetivamente o ambiente de produção para o proxy reverso, mas o executa localmente.
Agora tenho um proxy API reverso executado localmente, mas conversando com nossos serviços de produção. Então, é possível executar testes locais, mas ao invés de acessar a API de produção, acessamos o proxy reverso no localhost. Essencialmente, para cada recurso que adicionamos ao proxy reverso, também adicionamos um pequeno teste para verificar o funcionamento.
Agora, é possível executar testes não apenas antes de implantar as alterações de proxy, mas também agendá-los para execução todos os dias.
Um outro benefício adicional é a facilidade em escrever testes HTTP para proxies de API, ou seja, inspecionar de um ponto de vista externo e não internamente. Ao introduzir essa maneira de testar, conseguimos simplificar bastante os testes executados no proxy.
O que fazer quando as coisas dão errado
Vejamos quatro aspectos a serem considerados para reduzir a chance das coisas darem errado. Antes de tudo, torne explícito o acesso à produção. Precisamos começar a pensar em como testar mudanças em produção sem acesso ao ambiente na fase de design.
Página com a documentação de um exemplo de resposta retirado de um teste de produção
O segundo é usar caminhos definidos. Se possui uma API, use chamadas de API. Não basta invadir um banco de dados de produção e alterar alguns dados para ativar um caso de uso específico. O objetivo aqui é testar usando os mesmos caminhos que todo o resto. No entanto, o software funciona em produção, então use-o. Aqui está um conto de um amigo: Quando a empresa estava chegando à alta temporada, decidiram aumentar o número de servidores e realizar deploys com menos frequência. Duas semanas depois, os sistemas começam a falhar. Acontece que tiveram um vazamento de memória por anos, mas como estavam fornecendo software de maneira contínua, isso não importava. Mudaram a forma de gerenciar produção e surgiram novos problemas.
Restringir dados confidenciais é outra consideração importante, especialmente ao lidar com padrões de conformidade como HIPAA, PCI ou GDPR na Europa. O exemplo anterior mostra que é possível executar um teste de produção que realmente crie um cartão de crédito de teste, verifica o cartão e autoriza a captura de fundos. Isso exige pensar e corrigir problemas inesperados, mas vale o esforço no final.
Por fim, é importante projetar antecipadamente os efeitos colaterais. Problemas como testes A/B afetados pela ordem do teste, não conseguindo entender o resultado. É importante pensar sobre essas possibilidades antecipadamente. Descobrir como limpar os dados criados e/ou garantir que os dados sejam aleatórios e normais, tendo um impacto insignificante nos negócios.
Benefícios inesperados dos testes em produção
Além das vantagens mencionadas, também existem alguns benefícios inesperados dos testes em produção.
Uma é a documentação perfeita. Todos sabemos que a documentação estática fica desatualizada rapidamente. Ao invés disso, podemos capturar a solicitação e a resposta como parte de nossos testes de produção. No final do teste, se for bem-sucedido, ele envia as informações de captura para o S3. Temos demonstrações com todos os nossos exemplos. Quando lançamos nosso site de documentação, simplesmente baixamos o tarball mais recente. Sabemos que os exemplos são reais e funcionais.
O segundo benefício inesperado são as demos mais atraentes. Somos uma empresa de software empresarial e uma das coisas que gostamos de demonstrar é a nossa página de análise. Ao executar os testes automatizados em produção com a conta demo, também coletamos dados reais que alimentam essa demonstração e a tornam muito mais atraente do que antes.
Ferramentas
Fazemos muitos mocks em nossos testes de integração. Desenvolvemos uma ferramenta chamada API Builder que pode fornecer uma simulação de uma interface de qualquer serviço ou fluxo de eventos. O mock sabe gerar dados válidos e então podemos substituir as partes necessárias, por exemplo, tendo um teste de geolocalização com respostas específicas para determinados locais, pois são gerados a partir dos serviços ativos, respeitando os mesmos contratos. Portanto, não temos situações em que os testes funcionam com os mocks, mas falham em produção.
No lado do banco de dados, usamos uma ferramenta chamada VividCortex, que fornece feedback em tempo real sobre o que está acontecendo nos bancos de dados. Muito rapidamente, é possível ver se uma determinada consulta ao banco de dados está tendo lentidão. A chave aqui é considerar o lado dos dados para que, se ocorrer um problema inesperado, o conhecemos o mais rápido possível.
Independentemente de qual ferramenta use, agregar ao log é essencial para ter uma visão consolidada e totalmente pesquisável de cada arquivo de log de cada serviço. Isso permite definir alertas em tempo real, nos quais, qualquer mensagem com um determinado prefixo (por exemplo, "FlowAlertError") aciona um email de notificação ou o PagerDuty, por exemplo.
Conclusão
Confie nos testes e execute-os constantemente em produção (ou pelo menos um subconjunto) para garantir que o software esteja funcionando conforme o esperado. Precisará investir também na entrega contínua, facilitar a implantação de pequenas alterações e reverter rapidamente, se necessário.
Existem muitas técnicas para ajudar nos testes de produção, de contas de sandbox a mocks. O objetivo final é saber que os recursos estão funcionando como esperado em produção (não apenas no momento da implantação) vale a pena investir nessas técnicas.
Sobre o autor
Michael Bryzek é CTO e co-fundador do Gilt Groupe, um inovador no ramo de compras on-line que oferece a seus membros acesso especial às mercadorias mais inspiradoras, ofertas culinárias e experiências todos os dias, muitas a preços privilegiados. Ele construiu a plataforma de tecnologia da Gilt para suportar enormes explosões de tráfego, à medida que as vendas começam todos os dias ao meio-dia.