Projeto do sistema: prepare-se para o pior

Nov 28 2022
Ao desenvolver um novo aplicativo ou um novo recurso, uma das coisas importantes que você precisa prestar atenção é a capacidade de seu aplicativo se auto-recuperar em casos de falhas. Seu código pode ser ótimo e atender aos requisitos de negócios, MAS, é bom o suficiente para fazê-lo também em momentos de falhas inesperadas? A resiliência do seu aplicativo é suficiente para superar automaticamente essas falhas? Às vezes, você não será instruído implicitamente a implementar mecanismos de resiliência e fallback como parte dos requisitos de negócios, pois os cenários que ele supõe manipular podem ser muito técnicos e direcionados ao desenvolvimento.
Foto de David Pupaza no Unsplash

Ao desenvolver um novo aplicativo ou um novo recurso, uma das coisas importantes que você precisa prestar atenção é a capacidade de seu aplicativo se auto-recuperar em casos de falhas. Seu código pode ser ótimo e atender aos requisitos de negócios, MAS, é bom o suficiente para fazê-lo também em momentos de falhas inesperadas? A resiliência do seu aplicativo é suficiente para superar automaticamente essas falhas?

Às vezes, você não será instruído implicitamente a implementar mecanismos de resiliência e fallback como parte dos requisitos de negócios, pois os cenários que ele supõe manipular podem ser muito técnicos e direcionados ao desenvolvimento. Esses mecanismos relacionados à família de requisitos não funcionais.

O que são essas “falhas inesperadas” de que estamos falando?

Bem, existem vários cenários que podem levar seu aplicativo a um estado de falha/instável. Por exemplo:

  • Timeout/Serviço indisponível de uma API externa que seu aplicativo está usando
  • Bugs em terceiros que estão sendo usados ​​pelo seu aplicativo
  • Falha nas operações do banco de dados
  • Carga inesperada no aplicativo que causa degradação no desempenho e, portanto, negação de solicitações recebidas
  • Erro inesperado que causa perda de dados
  • processos de vida longa que são gerenciados pelo aplicativo estão parados em seu estado devido à perda de dados

Ter um aplicativo sem os mecanismos relevantes de auto-recuperação e fallback não durará muito tempo. Essas “falhas inesperadas” eventualmente acontecerão, isso prejudicará seu SLA e a experiência do cliente. Isso pode causar perda de dados que será difícil de recuperar e exigirá que VOCÊ intervenha manualmente e faça algumas mágicas para corrigir o problema. Agora pense em aplicativos que lidam com centenas de milhares de solicitações por minuto, pequenas falhas podem causar um grande impacto que pode ser difícil de corrigir manualmente e custará muito tempo e dinheiro para sua organização.
Portanto, você deve se preparar para o pior.

Vamos examinar alguns cenários comuns e fornecer soluções que podem aumentar o nível de resiliência do aplicativo e as habilidades de fallback para que você possa dormir em paz à noite :)

Mecanismo de repetição

Especialmente ao lidar com operações de E/S, como envio de solicitações http, leitura/gravação de banco de dados ou arquivo, etc., a chance de encontrar um erro é comum. Por exemplo: Serviço indisponível, Erro interno do servidor, Tempo limite de operação e muito mais.
Muitos desses erros podem ser transitórios e parar em questão de segundos, o que significa que há uma grande chance de que a próxima tentativa após alguns segundos funcione.
Então… por que interromper o fluxo de negócios apenas por causa de um pequeno problema temporário? Tente novamente! Use um mecanismo de repetição na operação que garantirá que, se o problema for temporário, seu aplicativo o superará automaticamente.
Preste atenção ao seguinte:

  • Ser capaz de configurar e limitar o número de novas tentativas que seu aplicativo tenta executar. Número infinito / alto de tentativas pode fazer com que seu aplicativo fique preso e negue outros solicitados, pois estará ocupado em tentativas infinitas
  • implementar o cozimento exponencial. Aumente exponencialmente o intervalo entre as tentativas de repetição.
    Isso aumentará a chance de que o problema temporário (no servidor/banco de dados/terceiro) tenha sido resolvido e sua próxima tentativa seja bem-sucedida. Além disso, usar o intervalo exponencial também dá graça para terceiros, que podem estar sob carga e, portanto, não podem atender seus clientes. Executar novas tentativas imediatas sem demora pode piorar o problema.
  • Certifique-se de entender quais erros devem ser repetidos e quais não. Nem todo erro pode ser resolvido por outra tentativa de repetição. Por exemplo, digamos que sua solicitação http falhou com o código de status 'Bad request' (400). Significa que algo com os dados da requisição está errado e foi rejeitado pelo servidor. Não importa quantas tentativas seu aplicativo fará, ele terminará com o mesmo resultado - falha.
    Fazer novas tentativas nesse tipo de erro é redundante e afetará o desempenho do aplicativo. Você deve distinguir entre os diferentes erros e decidir qual nova tentativa pode ser feita.

Existem várias bibliotecas de código aberto que já fornecem todos os recursos relevantes do mecanismo de repetição, para que você não precise escrevê-lo do zero. Por exemplo, no .NET você tem uma biblioteca chamada Polly.

Não se esqueça de fazer a pergunta, o que devo fazer se todas as novas tentativas falharem? Esta é uma pergunta de negócios e a resposta pode ser alterada entre os cenários.

Defina timeout para seus processos

Alguns aplicativos gerenciam processos de longa duração. O processo tem um estado que indica o progresso. O progresso do processo às vezes depende de operações longas ou de uma resposta assíncrona de terceiros. Como as coisas podem dar errado (e sempre dão errado), seu processo pode ficar preso para sempre em um estado não final e permanecer “aberto”. Por exemplo, a terceira parte teve uma falha ao enviar de volta a resposta.

Ok, então eles vão ficar abertos. Qual é o problema?
O mais provável é que outros sistemas dependam de seus processos e estejam pendentes de seu resultado. Seu aplicativo irá travar todo o fluxo de negócios, impactando o SLA de outros serviços e a experiência do usuário.
Além disso, esses processos abertos dificultarão as investigações e análises de dados, pois se comportam de maneira diferente e podem prejudicar suas estatísticas.

Eu não quero isso! Como resolvo isso?
Primeiro, defina qual é o tempo adequado e aceitável que seu processo pode ficar em um estado nenhum final. Essa é uma questão de negócios e deve ser determinada pelo SLA a que você está obrigado e pela avaliação de qual é o tempo total razoável que deve levar para as operações dentro do processo. Por exemplo, se meu processo depende de um terceiro cujo tempo médio de resposta é de 10 minutos, você pode definir o tempo limite de 20 a 30 minutos.
Agora você pode pular para a implementação. A implementação básica pode ser a criação de uma operação agendada que é executada a cada X minutos e procura por processos abertos por mais de Y minutos (valor de tempo limite). Se encontrado, basta mover o processo para um estado final, pode ser um estado de falha/cancelar.
Desta forma, você tem a capacidade de falhar/cancelar normalmente o processo. Isso está sendo feito de forma gerenciável e fornece ao seu aplicativo a capacidade de notificar o resultado relevante para seus clientes pendentes.

Perceber! o padrão não resolveu o problema em si, MAS forneceu a capacidade de gerenciá-lo automaticamente e executar o fluxo de fallback. Ser capaz de comunicar que há um problema e cumprir o SLA prometido.
Vejamos um cenário da vida real que usa este padrão:
Minha equipe no Payoneer é responsável pelo processo de aprovação automática de documentos de clientes. O processo de aprovação de um documento é um processo off-line sendo construído em várias etapas e envolve integração com fornecedores externos que fornecem resultados de análise por meio de comunicação assíncrona.
Nosso sistema implementa o padrão de timeout e após o timeout, o sistema passa os documentos do cliente para serem revisados ​​manualmente pelo representante ao invés de aguardar o resultado automático.
Desta forma não estamos impactando o SLA que temos diante do cliente quanto ao tempo que leva para revisar seus documentos.

Não retransmita apenas no retorno de chamada/webhook - Implemente o mecanismo de votação

Ao lidar com comunicação assíncrona, a resposta está sendo retornada ao cliente como um retorno de chamada. Pode ser o servidor acionando um webhook com os dados ou o servidor publicando um evento por meio de um barramento de evento/agente de mensagem.
Existem cenários em que a mensagem não foi recebida pelo cliente ou foi recebida, mas não foi tratada corretamente e, portanto, foi descartada, o que levou à perda de dados. Nesses cenários, o cliente ainda está aguardando a resposta, mas o servidor já a enviou e não a enviará novamente.

na seção anterior, mencionamos o padrão de tempo limite que não resolve o problema, mas permite que você falhe normalmente e habilite o fluxo de fallback gerenciável. Ao implementar o mecanismo de pesquisa, você poderá resolver automaticamente alguns dos problemas e permanecer no “caminho feliz” do fluxo de negócios.

Em vez de apenas esperar que o servidor forneça a resposta de forma assíncrona por push, implemente um processo agendado que será executado a cada X minutos e procurará solicitações pendentes por mais de Y minutos. Para essas solicitações, seu aplicativo enviará uma solicitação síncrona ao servidor para obter os dados de resposta. Se a solicitação original acabou de ser processada pelo servidor, ele poderá enviar o resultado para você. Nesse caso, sua aplicação continuará com o fluxo de negócios da mesma forma que era feito se os dados fossem recebidos pelo webhook. Caso contrário, a solicitação original ainda está sendo processada pelo servidor e não há como enviar de volta. Você só precisa continuar esperando.

Ao implementar este mecanismo, você foi capaz de resolver automaticamente problemas inesperados relacionados à comunicação assíncrona e, assim, evitar falhas no fluxo ou intervir manualmente para restaurar os dados do servidor.

Perceber! Nem todos os serviços com os quais você se integrará terão a capacidade de fornecer pontos de extremidade para pesquisa, além da comunicação assíncrona que eles oferecem. Se não tiverem, vale a pena perguntar se isso pode ser feito no futuro.

É verdade que há sobrecarga de desempenho com esta solução, MAS, com a escolha dos intervalos certos, ela será perfeita e o benefício que ela oferece em momentos de falhas não tem preço.

Evite o bloqueio do fornecedor

É comum que o aplicativo use fornecedores externos para atingir seus objetivos de negócios. Não é possível que toda organização desenvolva toda a lógica internamente, especialmente se a lógica for complexa e distante do negócio principal da organização. Portanto, você provavelmente vai preferir a integração com um fornecedor que poderá lhe fornecer parte da lógica necessária para completar seu fluxo de negócios.

Então, após uma pesquisa e POC, você encontrou um fornecedor adequado para suas necessidades.
A integração ocorreu sem problemas e… VOILA! você colocou seu negócio em funcionamento atendendo a centenas de milhares de clientes por dia.

Agora deixa eu te fazer uma pergunta. O que acontecerá se um dia esse fornecedor externo parar de funcionar? o que acontecerá se ele for à falência? Se for hackeado e o serviço não estiver disponível por um longo período de tempo? Ou talvez você simplesmente não fique satisfeito com o desempenho do fornecedor ou talvez com a forma como ele está sendo gerenciado, pois todos os meses há novas alterações importantes que fazem com que sua equipe de desenvolvimento trabalhe sem parar para ajustar a integração?

Principalmente quando se trata de fluxos de negócios principais e sensíveis, esse cenário é crítico e pode paralisar parte de sua organização e ter um grande impacto em seus clientes.
De volta ao processo de aprovação automática dos documentos do cliente gerenciados por minha equipe no Payoneer, como parte do processo, estamos usando fornecedores externos para extrair o texto relevante do documento e processá-lo. Optamos por integrar com 2 fornecedores que nos fornecem esse resultado e têm prioridade entre eles, portanto, se um falhar ou tiver muitos falsos positivos, podemos mudar para o outro sem tempo de inatividade do fluxo de negócios. É claro que isso também nos dá a confiança de que, se algo acontecer a um deles, sempre teremos outro para contar.

Claro que você nem sempre precisa criar esse backup. É uma questão de valor para o dinheiro e quão crítico é o fluxo de negócios com o qual você está lidando.

Padrão do disjuntor

Ao contrário do padrão de repetição, que supõe resolver falhas transitórias, o padrão de interrupção de circuito está aqui para impedir que o aplicativo invoque uma operação com alta possibilidade de falha.
Pense que você está usando um terceiro por meio da API REST, que atualmente possui um bug que faz com que todas as solicitações falhem. Digamos que o ETA para a correção seja de 4 horas.
Com o padrão de repetição, o aplicativo tentará várias vezes invocar a solicitação, que finalmente falhará. Essas tentativas inúteis de repetição afetarão o desempenho do aplicativo e do terceiro.
Em vez disso, o padrão de interrupção de circuito permitirá que o aplicativo falhe rapidamente e tente se recuperar gradualmente quando o problema for resolvido. Isso evita o surgimento de erros e a degradação do desempenho.

Então, como isso funciona?
O mecanismo está agindo como um proxy para a operação que você deseja executar.
Possui 3 estados:

  • Fechado- Significa que a operação pode ser executada regularmente.
    O número de falhas está sendo contado neste estado. Se o número de falhas ultrapassar o limite definido em um intervalo de tempo definido, o mecanismo passará para o estado aberto.
  • Aberto - Significa que o mecanismo impedirá que a operação seja executada, pois sofreu muitas falhas e, portanto, preferirá falhar rapidamente sem tentar executá-la. Neste estado, temos um tempo limite. Quando alcançado, o mecanismo está se movendo para o estado Meio aberto.
  • Meio aberto- Significa que uma requisição limitada para executar a operação é permitida e está sendo passada. Este estado é para verificar se o problema foi resolvido. Se essa quantidade limitada de operações estiver sendo executada com sucesso, o mecanismo assume que o problema foi resolvido e move o estado do mecanismo para fechado. Caso contrário, ele está voltando para Open, pois o problema ainda existe.

Endpoints dedicados para intervenção manual fácil e eficiente

Dito tudo isso, às vezes não estamos preparados com solução automatizada para qualquer caso que possa surgir. Às vezes, nossa solução automatizada virá somente depois que encontrarmos o cenário pela primeira vez e entendermos como resolvê-lo pela automação. Portanto, nesses casos, a intervenção manual é inevitável. Na maioria das vezes, essas intervenções manuais exigem que você passe pelas “entranhas” do serviço, como: alterar valores diretamente pelo banco de dados ou reestruturar manualmente uma mensagem e enviá-la diretamente pelo agente de mensagens.

Também aqui podemos estar pré-preparados para que esta intervenção manual seja gerenciável, segura e eficiente. Basta criar uma API interna em seu aplicativo que permitirá essas operações manuais comuns.
Por exemplo, crie um endpoint que permitirá cancelar um processo que seu aplicativo está gerenciando. Ou crie um endpoint que permita republicar o resultado de um processo antigo para que outro sistema possa consumi-lo novamente caso tenha sido perdido ou nunca tenha sido acionado.

Ao criar esta API interna, embora exija que alguém a acione, ela traz muitos benefícios:

  • A operação é gerenciável pelo aplicativo. O aplicativo é o proprietário da alteração e está ciente disso.
  • A operação foi testada pelo controle de qualidade, pois fazia parte do desenvolvimento. Assim você reduz as chances de erros
  • às vezes a operação não é tão simples quanto apenas atualizar o valor no banco de dados. Às vezes, também há ações secundárias que precisam ser feitas. Todas essas ações secundárias podem ser gerenciadas na API interna para facilitar o uso.
  • Economia de tempo

Concluir

Sua inscrição DEVE estar sempre preparada para o pior!
Quanto mais seu aplicativo souber se auto recuperar e ter soluções de fallback, mais seu desempenho aumentará, o impacto na experiência do cliente será baixo, mais rápido você poderá superar falhas e economizar tempo e dinheiro valiosos para sua organização.

Durante o design do seu aplicativo / recurso, você deve prestar atenção a isso. Você deve fazer as perguntas certas e identificar os pontos fracos para preparar o tratamento adequado e evitar surpresas desagradáveis ​​na produção.