Programador do Defender mais forte que a entropia

© Dragon Ball. Goku.

O programador-defensor a qualquer momento e em qualquer lugar do código espera o aparecimento de possíveis problemas e grava o código de forma a se proteger deles com antecedência. E se você não pode se defender de um problema, certifique-se de que as consequências e o impacto sobre os usuários sejam mínimos.

Lembro-me do efeito FlashForward dos blockbusters de Hollywood quando o protagonista vê a catástrofe iminente e permanece extremamente calmo, porque ele sabe de antemão que isso vai acontecer e tem proteção contra ela. A idéia por trás da programação defensiva é proteger-se de problemas difíceis ou impossíveis de prever. Um programador de segurança espera que ocorram erros em qualquer lugar do sistema e a qualquer momento para evitá-los antes que causem danos. No entanto, o objetivo não é criar um sistema que nunca trava, ainda é impossível. O objetivo é criar um sistema que trava graciosamente no caso de qualquer problema imprevisto.

Vamos entender com mais detalhes o que está incluído no conceito de "cair graciosamente".

  • Cair rápido. No caso de um erro inesperado, todas as operações devem ser concluídas imediatamente, especialmente se os cálculos subsequentes forem difíceis ou puderem causar corrupção de dados.
  • Cair ordenadamente. Se ocorrer um erro, o programa deve liberar todos os recursos, remover bloqueios, excluir arquivos temporários e semi-gravados, fechar conexões. Aguarde a conclusão de operações críticas, cuja interrupção pode levar a resultados imprevisíveis. Ou uma maneira segura de travar essas operações.
  • Caindo clara e lindamente. Se algo estiver quebrado, a mensagem de erro deve ser simples, concisa e conter detalhes importantes do contexto do sistema em que o erro ocorreu. Isso ajudará a equipe responsável pelo sistema a descobrir o problema o mais rápido possível e corrigi-lo.

Mas você pode ter uma pergunta.

Por que perder tempo com problemas que possam surgir no futuro? Agora eles não estão lá, o código funciona perfeitamente. Além disso, os problemas podem nunca acontecer. Afinal, os profissionais não fazem engenharia por causa da engenharia ( YAGNI - Você não vai precisar disso)!

O principal é o pragmatismo


Andrew Hunt no livro "Programador-pragmatista" fornece a seguinte definição de programação defensiva - " paranóia pragmática ".

Proteja seu código de:

  • próprios erros;
  • erros de outras pessoas;
  • erros e falhas em outros sistemas com os quais você está integrado;
  • erros de ferro, ambientes e plataformas nas quais seu aplicativo trabalha.

Vamos discutir vários métodos táticos e estratégicos de programação defensiva, a seguir os quais criarão um sistema confiável e previsível, resistente a falhas arbitrárias.

Algumas dicas podem parecer "do capitão", mas na prática, muitos desenvolvedores nem as seguem. Mas se você seguir práticas e abordagens simples, isso aumentará significativamente a estabilidade do seu sistema.

Não confie em ninguém


Os dados do usuário não são confiáveis ​​por padrão. Os usuários geralmente entendem mal o que parece óbvio para nós (como desenvolvedores do sistema). Espere entrada incorreta e sempre verifique.

Verifique também a quantidade de entrada. Pode ser que o usuário envie muitos deles. Ao mesmo tempo, do ponto de vista da lógica de negócios, este é o cenário correto. Mas isso pode levar a um processamento muito longo. O que pode ser feito com isso? Por exemplo, execute-o de forma assíncrona, se a quantidade de dados de entrada exceder um determinado limite e as especificidades da empresa permitirem processar os dados em segundo plano.

As configurações do aplicativo (por exemplo, arquivos de configuração) também estão sujeitas à aparência de dados incorretos. Freqüentemente, as configurações do programa são armazenadas em JSON, YAML, XML, INI e outros formatos. Como todos esses são arquivos de texto, é de se esperar que mais cedo ou mais tarde alguém mude algo neles e seu programa comece a funcionar incorretamente. Pode ser um usuário final ou alguém da sua equipe.

Bancos de dados, arquivos, armazenamento centralizado de configurações, registro - todos esses locais podem ser acessados ​​por outras pessoas e, mais cedo ou mais tarde, eles mudarão algo lá ( lei de Murphy ).

Entrada de lixo → entrada de lixo


As entradas que passam na validação e começam a ser processadas devem estar limpas se você deseja que seu código faça exatamente o que você espera dele.

No entanto, é uma boa prática fazer verificações adicionais de validação de dados, inclusive quando elas já começaram a ser processadas. Em locais críticos (cobrança, autorização, dados pessoais e confidenciais, etc.), isso é quase um requisito obrigatório. Isso é necessário para que, no caso de erros no código ou problemas com o validador de dados de entrada, pare o fluxo de execução o mais rápido possível. É difícil fazer a validação de alta qualidade com a verificação de todos os cenários de erro possíveis, para que você possa usar maneiras mais simples de validar se o programa ainda está sendo executado corretamente - asserções e exceções.

Paranoidismo saudável é uma característica de todos os desenvolvedores profissionais. Mas é muito importante buscar o equilíbrio ideal e entender quando a solução já é boa o suficiente.

Configurações separadas em torno dos ambientes


Uma causa comum de problemas é a separação insuficiente de configurações entre ambientes ou a ausência dessa separação.

Isso pode levar a muitos problemas, por exemplo:

  • o ambiente de teste começa a ler e / ou gravar dados da produção, bancos de dados, filas e outros recursos;
  • o ambiente de teste usa integrações e serviços externos com uma conta de produção;
  • misturando estatísticas, métricas, erros de diferentes ambientes;
  • violação de segurança (desenvolvedores, testadores e outros membros da equipe obtêm acesso aos recursos de produção);
  • erros difíceis de investigar na produção (por exemplo, parte das mensagens na fila é perdida devido ao ambiente de teste começar a lê-lo).

Estes são apenas exemplos, uma lista completa de problemas que podem ser causados ​​por uma separação de configurações insuficientemente responsável é quase infinita e depende das especificidades do projeto.

A separação responsável dos dados de configuração por ambiente pode reduzir significativamente a probabilidade de imediatamente toda uma classe de problemas associados a:

  • segurança
  • confiabilidade;
  • suporte e implantação (os engenheiros do DevOps agradecerão).

Além disso, é uma boa prática armazenar dados secretos (chaves, tokens, senhas) em um local separado, especialmente projetado para armazenar e processar segredos. Esses sistemas criptografam com segurança os dados, possuem meios flexíveis para gerenciar os direitos de acesso e também permitem que você altere rapidamente as chaves, caso tenham sido comprometidas. Nesse caso, você não precisa fazer alterações no código e reimplantar o aplicativo. Isso é especialmente importante para sistemas que trabalham com transações financeiras, dados confidenciais ou pessoais.

Lembre-se do efeito em cascata


Uma causa comum da queda de sistemas grandes e complexos é o efeito em cascata. A quebra ou degradação da funcionalidade de uma das partes do sistema ocorre e, um a um, os outros subsistemas associados a ele começam a falhar. Em cascata até que todo o sistema se torne completamente inacessível.

Alguns truques de proteção:

  • use tempos limite progressivos (exponenciais) com um elemento aleatório;
  • defina valores razoáveis ​​para o tempo limite da conexão e o tempo limite do soquete;
  • prever antecipadamente o fallback em caso de falha de serviços individuais. É melhor degradar temporariamente algumas das funcionalidades, desativar completamente os serviços, mas não corre o risco de quebrar todo o sistema. Mas imagine que, nesse caso, o usuário receba uma mensagem compreensível e não assustadora, e a equipe de suporte e desenvolvimento o mais rápido possível descubra o problema.

Relatar problemas rapidamente


Todos os sistemas falham. Às vezes, acontece algo estranho que os criadores esperam "uma vez a cada 10 anos". As integrações e APIs externas periodicamente ficam indisponíveis ou respondem incorretamente. Fazer fallback para todos esses casos geralmente é difícil, longo ou simplesmente impossível. Antecipe esta situação com antecedência e relate-a o mais rápido possível. Log para o nível de ERRO ou para o sistema de monitoramento - por garantido. Adicionar validação adicional à verificação de saúde é ainda melhor. Enviar uma mensagem do código para o Slack, Telegram, PagerDuty ou outro serviço que notificará sua equipe instantaneamente sobre o problema é o ideal.

Mas é importante entender claramente quando faz sentido enviar mensagens diretamente. Somente se um erro, uma situação suspeita ou atípica estiver associada aos processos de negócios, é importante que uma pessoa ou grupo específico de pessoas em uma equipe receba uma notificação o mais rápido possível e possa responder.

Todos os outros problemas e desvios técnicos devem ser tratados por meios padrão - monitoramento, alerta e registro.

Cache usado frequentemente e / ou dados recentes


Programas e pessoas têm uma coisa em comum - eles tendem a reutilizar dados que são frequentemente usados ​​ou encontrados recentemente. Em sistemas altamente carregados, lembre-se sempre disso e armazene em cache os dados nos locais mais quentes do sistema.

A estratégia de armazenamento em cache é altamente dependente das especificidades do projeto e dos dados. Se os dados forem mutáveis, é necessário invalidar o cache. Portanto, considere com antecedência como você fará isso. E também pense em quais riscos podem existir se dados desatualizados aparecerem no cache, se o cache estiver fora de ordem, etc.

Substitua operações caras por operações baratas


Trabalhar com strings é uma das operações mais comuns em qualquer programa. E se isso não for feito da melhor maneira, pode ser uma operação cara. Em diferentes linguagens de programação, as especificidades do trabalho com seqüências de caracteres podem variar, mas você deve sempre se lembrar disso.

Em aplicativos grandes com uma grande base de códigos, é frequentemente encontrado um código escrito há muitos anos que funciona sem erros, mas não é o ideal em termos de desempenho. Freqüentemente, uma mudança banal na estrutura de dados de uma matriz / lista para uma tabela de hash dá um grande impulso (mesmo que apenas em um local no código).

Às vezes, você pode melhorar o desempenho reescrevendo o algoritmo para usar operações bit a bit. Mas mesmo naqueles casos raros em que se justifica, o código é muito complexo. Portanto, ao tomar uma decisão, considere a legibilidade do código e o fato de que ele precisará ser suportado. O mesmo vale para outras otimizações complicadas: quase sempre, esse código se torna difícil de ler e muito difícil de manter. Se você ainda optar por otimizações complicadas, não se esqueça de escrever comentários descrevendo o que você deseja que esse código faça e por que é escrito dessa maneira.

Ao mesmo tempo, a otimização deve ser tratada com pragmatismo saudável:

  • se você leva, como desenvolvedor, por alguns segundos ou minutos - faz sentido fazê-lo imediatamente;
  • se for o caso, é razoável fazê-lo imediatamente somente quando você tiver 100% de certeza de sua necessidade. Em todos os outros casos, faz sentido adiá-lo, escrever no código TODO, coletar mais informações, consultar colegas, etc.

Otimização prematura é a raiz de todo mal (Donald Knuth)

Reescrever em um idioma de nível inferior


Esta é uma medida extrema. Os idiomas de baixo nível são quase sempre mais rápidos em comparação aos idiomas de nível superior. Mas essa solução tem um preço - desenvolver esse programa é mais longo e mais difícil. Às vezes, reescrevendo partes críticas do sistema em um idioma de baixo nível, você pode obter um aumento sério na produtividade. Mas há efeitos colaterais - geralmente essas soluções perdem na plataforma cruzada e seu suporte é mais caro. Portanto, tome uma decisão com cuidado.

Sozinho no campo não é um guerreiro


Concluindo, gostaria de observar mais uma coisa importante, talvez a mais importante. As medidas que consideramos nos parágrafos anteriores funcionarão apenas se todos os membros da equipe aderirem a eles e todos entenderem quem é responsável por o que e o que precisa ser feito em caso de uma situação crítica. É importante que, depois de corrigir o problema, realize uma reunião (Post Mortem) com todas as pessoas interessadas e descubra por que esse problema surgiu e o que pode ser feito para impedir que esse problema ocorra no futuro. Em muitos casos, são necessárias alterações técnicas e de processo. A cada novo Post Mortem, seu sistema se tornará mais confiável, a equipe será mais experiente e coesa e a entropia no universo será um pouco menor;)

O artigo usa parcialmente os materiais Por que a programação defensiva é a melhor maneira de codificação robusta (Ravi Shankar Rajan).

Source: https://habr.com/ru/post/pt472922/


All Articles